Platform: Code4rena
Start Date: 24/03/2023
Pot Size: $49,200 USDC
Total HM: 20
Participants: 246
Period: 6 days
Judge: Picodes
Total Solo HM: 1
Id: 226
League: ETH
Rank: 11/246
Findings: 1
Award: $734.24
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: adriro
Also found by: 0x52, T1MOH, anodaram, cloudjunky, hassan-truscova
734.235 USDC - $734.24
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L241
Protocol fails at higher maxAmount
s
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L241
If the protocol is configured with a single derivative of Reth and the maxDeposit
is set to 2000 ETH staking fails for some deposit amounts. Derivatives can be added or subtracted at any time by the owner of the protocol.
In the proof of concept code below setMaxAmount
is set to 2000 ETH which is a plausible amount for a protocol of this kind. Restricting derivates to Reth allowed me to test the code swap when Rocket Pool is not able to be used directly so UniswapV3 is used to swap WETH for RETH.
The issue is caused by an overflow in poolPrice()
L241 which causes the staking operation to revert. Following the execution path;
SafEth::stake(1596045421752747910003)
on L63 is called with a value such as 1596045421752747910003 wei (~1596 ETH).deposit{value: 1596045421752747910003}()
on L91.ethPerDerivative(1483574263903969556156)
on L92 is called to get the value of StakedETH (RETH in this case). This passes through to the Reth.sol contract ethPerDerivative()
function on L211.poolPrice()
L228 is used to get the ethPerDerivative value via UniswapV3.return (sqrtPriceX96 * (uint(sqrtPriceX96)) * (1e18)) >> (96 * 2);
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "forge-std/Vm.sol"; import "forge-std/console.sol"; import "../contracts/SafEth/SafEth.sol"; import "../contracts/SafEth/derivatives/Reth.sol"; import "../contracts/SafEth/derivatives/SfrxEth.sol"; import "../contracts/SafEth/derivatives/WstEth.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "../contracts/interfaces/uniswap/IUniswapV3Factory.sol"; import "../contracts/interfaces/uniswap/IUniswapV3Pool.sol"; contract SafEthTest is Test { Reth rethImp; SfrxEth sfrxImp; WstEth wstImp; SafEth safethImp; ERC1967Proxy safeEthproxy; ERC1967Proxy rethProxy; ERC1967Proxy wstProxy; ERC1967Proxy sfrxProxy; address public constant ROCKET_STORAGE_ADDRESS = 0x1d8f8f00cfa6758d7bE78336684788Fb0ee0Fa46; address public constant W_ETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address public constant UNISWAP_ROUTER = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; address public constant UNI_V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; function setUp() public { safethImp = new SafEth(); safeEthproxy = new ERC1967Proxy(address(safethImp), abi.encodeWithSelector(SafEth.initialize.selector,"Asymmetry Finance ETH","safETH")); rethImp = new Reth(); rethProxy = new ERC1967Proxy(address(rethImp), abi.encodeWithSelector(Reth.initialize.selector, address(safeEthproxy))); SafEth(payable(address(safeEthproxy))).addDerivative(address(rethProxy), 1 ether); wstImp = new WstEth(); wstProxy = new ERC1967Proxy(address(wstImp), abi.encodeWithSelector(WstEth.initialize.selector, address(safeEthproxy))); //SafEth(payable(address(safeEthproxy))).addDerivative(address(wstProxy), 1 ether); sfrxImp = new SfrxEth(); sfrxProxy = new ERC1967Proxy(address(sfrxImp), abi.encodeWithSelector(SfrxEth.initialize.selector, address(safeEthproxy))); //SafEth(payable(address(safeEthproxy))).addDerivative(address(sfrxProxy), 1 ether); } function testStaking(uint256 amount_in) public { SafEth s = SafEth(payable(address(safeEthproxy))); s.setMaxAmount(2000 ether); vm.assume(amount_in >= s.minAmount()); vm.assume(amount_in <= s.maxAmount()); vm.deal(address(1337), amount_in); vm.startPrank(address(1337)); s.stake{value: amount_in}(); vm.stopPrank(); assert(s.balanceOf(address(1337)) > 0); vm.startPrank(address(1337)); s.unstake(s.balanceOf(address(1337))); vm.stopPrank(); }
An example remappings.txt is;
@chainlink/=node_modules/@chainlink/ @ensdomains/=node_modules/@ensdomains/ @eth-optimism/=node_modules/@eth-optimism/ @openzeppelin/=node_modules/@openzeppelin/ ds-test/=lib/forge-std/lib/ds-test/src/ eth-gas-reporter/=node_modules/eth-gas-reporter/ forge-std/=lib/forge-std/src/ hardhat-deploy/=node_modules/hardhat-deploy/ hardhat/=node_modules/hardhat/ v3-core/=lib/v3-core/
To run the test above you can execute the following steps in the project directory;
forge init --no-commit --force
test/
directory and name the file SafEthTest.t.sol.forge test -vvvvv --fork-url=$FORK_URL --match-test=testStaking
poolPrice()
.The UniswapV3 TWAP should be used instead of reading directly from pool.slot0
. This is beneficial from a price perspective and will also help mitigate some price manipulation where slot0 is manipulated via Flashloans.
#0 - c4-pre-sort
2023-03-31T19:56:24Z
0xSorryNotSorry marked the issue as high quality report
#1 - c4-pre-sort
2023-04-04T14:41:53Z
0xSorryNotSorry marked the issue as primary issue
#2 - c4-sponsor
2023-04-07T16:52:15Z
toshiSat marked the issue as sponsor disputed
#3 - c4-judge
2023-04-21T16:38:53Z
Picodes marked issue #593 as primary and marked this issue as a duplicate of 593
#4 - c4-judge
2023-04-21T16:40:05Z
Picodes marked the issue as satisfactory