Running 1 test for test/integrations/StethHyperdrive.t.sol:StethHyperdriveTest
[PASS] test__DOSStethHyperdriveCloseLong() (gas: 676273)
Logs:
###########################################################################
#### TEST: Denial of Service when LIDO's `TotalPooledEther` decreases. ####
###########################################################################
# Bob calls openLong() #
Bob paid basePaid == 986426442084099043440
Bob received longAmount == 1032749606381098778932
# Taking a Snapshot of the state #
sharePrice: 1126467900855209627
before _updateLiquidity
_shareProceeds 438468873966310397654
_shareReservesDelta 438468873966310397654
_shareProceeds >= _shareReservesDelta true
# Bob calls closeLong() #
Bob received baseProceeds == 493921112047177146800 for closing 516374803190549389466
# Reverting to the saved state Snapshot #
# Manipulating Lido's totalPooledEther : removing only 1e18 #
LIDO.CL_BALANCE_POSITION Before: 6840256058235313000000000
LIDO.getTotalPooledEther() Before: 6911982135515374435895912
Bob's Long Bonds balanceBefore: 1032749606381098778932
# Writing to storage... #
# ...End writing to storage #
LIDO.CL_BALANCE_POSITION After: 6840255058235313000000000
LIDO.CL_BALANCE_POSITION Before - After: 1000000000000000000
LIDO.getTotalPooledEther() After: 6911981135515374435895912
LIDO.getTotalPooledEther() Before - After: 1000000000000000000
Bob's Long Bonds balanceAfter: 1032749606381098778932
# Bob now calls closeLong() after LIDO's balance update, but this will revert #
Bob tries to call closeLong() with 516374803190549389466
sharePrice: 1126467737882001831
before _updateLiquidity
_shareProceeds 438468873900985721708
_shareReservesDelta 438468937337049174978
_shareProceeds >= _shareReservesDelta false
Bob tries to call closeLong() with 1000000000
sharePrice: 1126467737882001831
before _updateLiquidity
_shareProceeds 850334867
_shareReservesDelta 850334991
_shareProceeds >= _shareReservesDelta false
Bob tries to call closeLong() with 1000000000000000000
sharePrice: 1126467737882001831
before _updateLiquidity
_shareProceeds 850324443020858426
_shareReservesDelta 850324566042670313
_shareProceeds >= _shareReservesDelta false
Bob tries to call closeLong() with 1032749606381098778932
sharePrice: 1126467737882001831
before _updateLiquidity
_shareProceeds 875680914682503795660
_shareReservesDelta 875681041372796710638
_shareProceeds >= _shareReservesDelta false
Test result: ok. 1 passed; 0 failed; finished in 2.65s
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.19;
import { IERC20 } from "contracts/src/interfaces/IERC20.sol";
import { ERC20Mintable } from "contracts/test/ERC20Mintable.sol";
import { StethHyperdriveDeployer } from "contracts/src/factory/StethHyperdriveDeployer.sol";
import { StethHyperdriveFactory } from "contracts/src/factory/StethHyperdriveFactory.sol";
import { StethHyperdrive } from "contracts/src/instances/StethHyperdrive.sol";
import { StethHyperdriveDataProvider } from "contracts/src/instances/StethHyperdriveDataProvider.sol";
import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol";
import { ILido } from "contracts/src/interfaces/ILido.sol";
import { AssetId } from "contracts/src/libraries/AssetId.sol";
import { Errors } from "contracts/src/libraries/Errors.sol";
import { FixedPointMath } from "contracts/src/libraries/FixedPointMath.sol";
import { HyperdriveMath } from "contracts/src/libraries/HyperdriveMath.sol";
import { ForwarderFactory } from "contracts/src/token/ForwarderFactory.sol";
import { HyperdriveTest } from "test/utils/HyperdriveTest.sol";
import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol";
import { Lib } from "test/utils/Lib.sol";
import "forge-std/console.sol";
import "forge-std/Test.sol";
contract StethHyperdriveTest is HyperdriveTest {
using FixedPointMath for uint256;
using Lib for *;
using stdStorage for StdStorage;
uint256 internal constant FIXED_RATE = 0.05e18;
// The Lido storage location that tracks buffered ether reserves. We can
// simulate the accrual of interest by updating this value.
bytes32 internal constant BUFFERED_ETHER_POSITION = keccak256("lido.Lido.bufferedEther");
ILido internal constant LIDO = ILido(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84);
address internal STETH_WHALE = 0x1982b2F5814301d4e9a8b0201555376e62F82428;
address internal ETH_WHALE = 0x00000000219ab540356cBB839Cbe05303d7705Fa;
StethHyperdriveFactory factory;
function setUp() public override __mainnet_fork(17_376_154) {
super.setUp();
// Deploy the StethHyperdrive deployer and factory.
vm.startPrank(deployer);
StethHyperdriveDeployer simpleDeployer = new StethHyperdriveDeployer(LIDO);
address[] memory defaults = new address[](1);
defaults[0] = bob;
forwarderFactory = new ForwarderFactory();
factory = new StethHyperdriveFactory(alice, simpleDeployer, bob, bob, IHyperdrive.Fees(0, 0, 0), defaults, address(forwarderFactory), forwarderFactory.ERC20LINK_HASH(), LIDO);
// Alice deploys the hyperdrive instance.
vm.stopPrank();
vm.startPrank(alice);
IHyperdrive.PoolConfig memory config = IHyperdrive.PoolConfig({
baseToken: IERC20(ETH),
initialSharePrice: LIDO.getTotalPooledEther().divDown(LIDO.getTotalShares()),
positionDuration: POSITION_DURATION,
checkpointDuration: CHECKPOINT_DURATION,
timeStretch: HyperdriveUtils.calculateTimeStretch(0.05e18),
governance: governance,
feeCollector: feeCollector,
fees: IHyperdrive.Fees({ curve: 0, flat: 0, governance: 0 }),
oracleSize: ORACLE_SIZE,
updateGap: UPDATE_GAP
});
uint256 contribution = 10_000e18;
hyperdrive = factory.deployAndInitialize{ value: contribution }(config, new bytes32[](0), contribution, FIXED_RATE);
// Ensure that Alice has the correct amount of LP shares.
assertApproxEqAbs(hyperdrive.balanceOf(AssetId._LP_ASSET_ID, alice), contribution.divDown(config.initialSharePrice), 1e5);
// Fund the test accounts with stETH and ETH.
address[] memory accounts = new address[](3);
accounts[0] = alice;
accounts[1] = bob;
accounts[2] = celine;
fundAccounts(address(hyperdrive), IERC20(LIDO), STETH_WHALE, accounts);
// Start recording event logs.
vm.recordLogs();
}
function test_attack_long_stEth() external {
// Get some balance information before the deposit.
uint256 hyperdriveSharesBefore = LIDO.sharesOf(address(hyperdrive));
// Bob opens a long by depositing ETH.
uint256 basePaid = HyperdriveUtils.calculateMaxLong(hyperdrive);
(uint256 maturityTime, uint256 longAmount) = openLong(bob, basePaid);
// Get some balance information before the withdrawal.
uint256 totalPooledEtherBefore = LIDO.getTotalPooledEther();
uint256 totalSharesBefore = LIDO.getTotalShares();
AccountBalances memory bobBalancesBefore = getAccountBalances(bob);
AccountBalances memory hyperdriveBalancesBefore = getAccountBalances(address(hyperdrive));
// Bob closes his long with stETH as the target asset.
uint256 baseProceeds = closeLong(bob, maturityTime, longAmount, false);
// Ensure that Lido's aggregates and the token balances were updated
// correctly during the trade.
verifyStethWithdrawal(bob, baseProceeds, totalPooledEtherBefore, totalSharesBefore, bobBalancesBefore, hyperdriveBalancesBefore);
}
function test__DOSStethHyperdriveCloseLong() external {
console.log("\n###########################################################################");
console.log("\n#### TEST: Denial of Service when LIDO's `TotalPooledEther` decreases. ####");
console.log("\n###########################################################################");
// Ensure that the share price is the expected value.
uint256 totalPooledEther = LIDO.getTotalPooledEther();
uint256 totalShares = LIDO.getTotalShares();
uint256 sharePrice = hyperdrive.getPoolInfo().sharePrice;
assertEq(sharePrice, totalPooledEther.divDown(totalShares));
// Ensure that the share price accurately predicts the amount of shares
// that will be minted for depositing a given amount of ETH. This will
// be an approximation since Lido uses `mulDivDown` whereas this test
// pre-computes the share price.
uint256 basePaid = HyperdriveUtils.calculateMaxLong(hyperdrive) / 10;
uint256 hyperdriveSharesBefore = LIDO.sharesOf(address(hyperdrive));
console.log("\n# Bob calls openLong() #\n");
(uint256 maturityTime, uint256 longAmount) = openLong(bob, basePaid);
console.log("Bob paid basePaid == ", basePaid);
console.log("Bob received longAmount == ", longAmount);
assertApproxEqAbs(LIDO.sharesOf(address(hyperdrive)), hyperdriveSharesBefore + basePaid.divDown(sharePrice), 1e4);
// Get some balance information before the withdrawal.
uint256 totalPooledEtherBefore = LIDO.getTotalPooledEther();
uint256 totalSharesBefore = LIDO.getTotalShares();
AccountBalances memory bobBalancesBefore = getAccountBalances(bob);
AccountBalances memory hyperdriveBalancesBefore = getAccountBalances(address(hyperdrive));
uint256 snapshotId = vm.snapshot();
console.log("\n# Taking a Snapshot of the state #\n");
// Bob closes his long with stETH as the target asset.
uint256 baseProceeds = closeLong(bob, maturityTime, longAmount / 2, false);
console.log("\n# Bob calls closeLong() #\n");
console.log("Bob received baseProceeds == %s for closing %s", baseProceeds, longAmount / 2);
// Ensure that Lido's aggregates and the token balances were updated
// correctly during the trade.
verifyStethWithdrawal(bob, baseProceeds, totalPooledEtherBefore, totalSharesBefore, bobBalancesBefore, hyperdriveBalancesBefore);
console.log("\n# Reverting to the saved state Snapshot #\n");
vm.revertTo(snapshotId);
console.log("\n# Manipulating Lido's totalPooledEther : removing only 1e18 #\n");
bytes32 balanceBefore = vm.load(address(LIDO), bytes32(0xa66d35f054e68143c18f32c990ed5cb972bb68a68f500cd2dd3a16bbf3686483));
console.log("LIDO.CL_BALANCE_POSITION Before: ", uint(balanceBefore));
uint beforeTotalPooledEther = uint(LIDO.getTotalPooledEther());
console.log("LIDO.getTotalPooledEther() Before: ", beforeTotalPooledEther);
uint256 bondBalanceBefore = hyperdrive.balanceOf(AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), bob);
console.log("Bob's Long Bonds balanceBefore: ", bondBalanceBefore);
console.log("\n# Writing to storage... #\n");
vm.store(address(LIDO), bytes32(uint256(0xa66d35f054e68143c18f32c990ed5cb972bb68a68f500cd2dd3a16bbf3686483)), bytes32(uint256(balanceBefore) - 1e18));
console.log("\n# ...End writing to storage #\n");
// Avoid Stack too deep
uint256 maturityTime_ = maturityTime;
uint256 longAmount_ = longAmount;
bytes32 balanceAfter = vm.load(address(LIDO), bytes32(uint256(0xa66d35f054e68143c18f32c990ed5cb972bb68a68f500cd2dd3a16bbf3686483)));
console.log("LIDO.CL_BALANCE_POSITION After: ", uint(balanceAfter));
console.log("LIDO.CL_BALANCE_POSITION Before - After: ", uint(balanceBefore) - uint(balanceAfter));
console.log("LIDO.getTotalPooledEther() After: ", uint(LIDO.getTotalPooledEther()));
console.log("LIDO.getTotalPooledEther() Before - After: ", beforeTotalPooledEther - uint(LIDO.getTotalPooledEther()));
// Bob closes his long with stETH as the target asset.
uint256 bondBalanceAfter = hyperdrive.balanceOf(AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime_), bob);
console.log("Bob's Long Bonds balanceAfter: ", bondBalanceAfter);
console.log("\n# Bob now calls closeLong() after LIDO's balance update, but this will revert #\n");
console.log("\n Bob tries to call closeLong() with %s \n", longAmount_ / 2);
vm.expectRevert();
uint256 baseProceeds2 = closeLong(bob, maturityTime_, longAmount_ / 2, false);
console.log("\n Bob tries to call closeLong() with %s \n", 1e9);
vm.expectRevert();
uint256 baseProceeds3 = closeLong(bob, maturityTime_, 1e9, false);
console.log("\n Bob tries to call closeLong() with %s \n", 1e18);
vm.expectRevert();
uint256 baseProceeds4 = closeLong(bob, maturityTime_, 1e18, false);
console.log("\n Bob tries to call closeLong() with %s \n", longAmount_);
vm.expectRevert();
uint256 baseProceeds5 = closeLong(bob, maturityTime_, longAmount_, false);
}
function verifyDeposit(
address trader,
uint256 basePaid,
bool asUnderlying,
uint256 totalPooledEtherBefore,
uint256 totalSharesBefore,
AccountBalances memory traderBalancesBefore,
AccountBalances memory hyperdriveBalancesBefore
) internal {
if (asUnderlying) {
// Ensure that the amount of pooled ether increased by the base paid.
assertEq(LIDO.getTotalPooledEther(), totalPooledEtherBefore + basePaid);
// Ensure that the ETH balances were updated correctly.
assertEq(address(hyperdrive).balance, hyperdriveBalancesBefore.ETHBalance);
assertEq(bob.balance, traderBalancesBefore.ETHBalance - basePaid);
// Ensure that the stETH balances were updated correctly.
assertApproxEqAbs(LIDO.balanceOf(address(hyperdrive)), hyperdriveBalancesBefore.stethBalance + basePaid, 1);
assertEq(LIDO.balanceOf(trader), traderBalancesBefore.stethBalance);
// Ensure that the stETH shares were updated correctly.
uint256 expectedShares = basePaid.mulDivDown(totalSharesBefore, totalPooledEtherBefore);
assertEq(LIDO.getTotalShares(), totalSharesBefore + expectedShares);
assertEq(LIDO.sharesOf(address(hyperdrive)), hyperdriveBalancesBefore.stethShares + expectedShares);
assertEq(LIDO.sharesOf(bob), traderBalancesBefore.stethShares);
} else {
// Ensure that the amount of pooled ether stays the same.
assertEq(LIDO.getTotalPooledEther(), totalPooledEtherBefore);
// Ensure that the ETH balances were updated correctly.
assertEq(address(hyperdrive).balance, hyperdriveBalancesBefore.ETHBalance);
assertEq(trader.balance, traderBalancesBefore.ETHBalance);
// Ensure that the stETH balances were updated correctly.
assertApproxEqAbs(LIDO.balanceOf(address(hyperdrive)), hyperdriveBalancesBefore.stethBalance + basePaid, 1);
assertApproxEqAbs(LIDO.balanceOf(trader), traderBalancesBefore.stethBalance - basePaid, 1);
// Ensure that the stETH shares were updated correctly.
uint256 expectedShares = basePaid.mulDivDown(totalSharesBefore, totalPooledEtherBefore);
assertEq(LIDO.getTotalShares(), totalSharesBefore);
assertEq(LIDO.sharesOf(address(hyperdrive)), hyperdriveBalancesBefore.stethShares + expectedShares);
assertEq(LIDO.sharesOf(trader), traderBalancesBefore.stethShares - expectedShares);
}
}
function verifyStethWithdrawal(
address trader,
uint256 baseProceeds,
uint256 totalPooledEtherBefore,
uint256 totalSharesBefore,
AccountBalances memory traderBalancesBefore,
AccountBalances memory hyperdriveBalancesBefore
) internal {
// Ensure that the total pooled ether and shares stays the same.
assertEq(LIDO.getTotalPooledEther(), totalPooledEtherBefore);
assertApproxEqAbs(LIDO.getTotalShares(), totalSharesBefore, 1);
// Ensure that the ETH balances were updated correctly.
assertEq(address(hyperdrive).balance, hyperdriveBalancesBefore.ETHBalance);
assertEq(trader.balance, traderBalancesBefore.ETHBalance);
// Ensure that the stETH balances were updated correctly.
assertApproxEqAbs(LIDO.balanceOf(address(hyperdrive)), hyperdriveBalancesBefore.stethBalance - baseProceeds, 1);
assertApproxEqAbs(LIDO.balanceOf(trader), traderBalancesBefore.stethBalance + baseProceeds, 1);
// Ensure that the stETH shares were updated correctly.
uint256 expectedShares = baseProceeds.mulDivDown(totalSharesBefore, totalPooledEtherBefore);
assertApproxEqAbs(LIDO.sharesOf(address(hyperdrive)), hyperdriveBalancesBefore.stethShares - expectedShares, 1);
assertApproxEqAbs(LIDO.sharesOf(trader), traderBalancesBefore.stethShares + expectedShares, 1);
}
/// Helpers ///
function advanceTime(uint256 timeDelta, int256 variableRate) internal override {
// Advance the time.
vm.warp(block.timestamp + timeDelta);
// Accrue interest in Lido. Since the share price is given by
// `getTotalPooledEther() / getTotalShares()`, we can simulate the
// accrual of interest by multiplying the total pooled ether by the
// variable rate plus one.
uint256 bufferedEther = variableRate >= 0
? LIDO.getBufferedEther() + LIDO.getTotalPooledEther().mulDown(uint256(variableRate))
: LIDO.getBufferedEther() - LIDO.getTotalPooledEther().mulDown(uint256(variableRate));
vm.store(address(LIDO), BUFFERED_ETHER_POSITION, bytes32(bufferedEther));
}
struct AccountBalances {
uint256 stethShares;
uint256 stethBalance;
uint256 ETHBalance;
}
function getAccountBalances(address account) internal view returns (AccountBalances memory) {
return AccountBalances({ stethShares: LIDO.sharesOf(account), stethBalance: LIDO.balanceOf(account), ETHBalance: account.balance });
}
}
Certora Summary This document describes the specification and verification of OpenZeppelin's contracts using the Certora Prover. The work was undertaken from May 9th to June 10th. The latest commit that was reviewed and run through the Certora Prover was commit 109778c. The scope of our verification was the following contracts: Initializable.sol ( Verification Result ) GovernorPreventLateQuorum.sol ( Verification Result ) ERC1155Burnable.sol ( Verification Result )
Jan 7, 2023Certora Summary This document describes the specification and verification of Aave's V3 protocol using the Certora Prover. The work was undertaken from Nov. 12, 2021 to Jan. 24, 2022. The latest commit that was reviewed and ran through the Certora Prover was a87d546. The scope of this verification is Aave's governance system, particularly the following contracts: StableDebtToken.sol ( Verification Results ) VariableDebtToken.sol ( Verification Results ) Atoken.sol (Verification Results)
Jan 4, 2023June 20, 2022 Certora is building a developer tool for smart contracts that supports finding bugs, reasoning about correctness, and producing formally verified proofs. The framework consists of two main components: (1) a specification language for describing intended smart contract behaviors symbolically and (2) an engine for analyzing the smart contracts against the specification. The technology automatically locates critical bugs that even the best auditor can miss and increases confidence in code security by proving that certain rules hold. On a high level, the Certora prover is essentially a sophisticated compiler translating the bytecode and the properties into a mathematical formula that concisely defines the exact conditions under which the program may violate the properties. This formula is fed into existing open source tools which locate bugs and produce proofs. This document describes the main techniques implemented in the Certora Prover from a high-level perspective. We assume that the readers are familiar with smart contracts. The examples used in this document are also accessible at Certora's demo page. Design Principles The Certora prover takes a low-level EVM bytecode program and a specification written in Certora's specification language, CVL.[^1] The prover analyzes the code and the spec together to identify scenarios where the code deviates from the specification.
Jun 30, 2022Summary This document describes the specification and verification of OpenZeppelin's contracts using the Certora Prover. The work was undertaken from March 2 to April 6, 2022. The latest commit that was reviewed and ran through the Certora Prover was 4088540a. The scope of this verification is OpenZeppelin's governance system, particularly the following contracts: ERC20Votes.sol ERC20FlashMint.sol ERC20Wrapper.sol TimelockController.sol draft-ERC721Votes.sol
Jun 6, 2022or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up