Try   HackMD

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

// 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 });
    }
}