## 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 });
}
}
```