Platform: Code4rena
Start Date: 30/04/2024
Pot Size: $112,500 USDC
Total HM: 22
Participants: 122
Period: 8 days
Judge: alcueca
Total Solo HM: 1
Id: 372
League: ETH
Rank: 65/122
Findings: 3
Award: $3.15
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: guhu95
Also found by: 0rpse, 0x007, 0x73696d616f, 0xCiphky, 0xabhay, Audinarey, Bauchibred, Fassi_Security, GalloDaSballo, GoatedAudits, KupiaSec, LessDupes, MSaptarshi, OMEN, Ocean_Sky, RamenPeople, SBSecurity, Tendency, WildSniper, aslanbek, bill, blutorque, crypticdefense, cu5t0mpeo, d3e4, gjaldon, grearlake, gumgumzum, honey-k12, ilchovski, jokr, josephdara, kennedy1030, p0wd3r, peanuts, stonejiajia, t0x1c, tapir, underdog, zzykxx
0.4071 USDC - $0.41
The user will receive less money than they should have.
The user's withdrawal process involves two steps:
share * totalAsset / totalShare
.share: user own ezETH amount
totalAsset: TVL
totalShare: ezETH totalSupply
In this process, the amount received includes the funds from rewards. The calculation of TVL
does not take into account the amount waiting to be claimed stored in the claimReserve
state variable. Therefore, if a user initiates a withdrawal request but does not call the claim
function, the TVL will always include the amount waiting to be claimed.
TVL:
Here's an example:
withdraw
to initiate a withdrawal request.claimReserve
seconds, User A still does not call claim
to receive the amount.poc coding :
mock/mockOracle:
pragma solidity 0.8.19; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; contract TestOracle is AggregatorV3Interface{ constructor() { } function decimals() external override view returns (uint8) { return 18; } function description() external override view returns (string memory) { return "none"; } function version() external override view returns (uint256) { return 1; } function getRoundData( uint80 _roundId ) external override view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) { return (0, 0, 0, 0, 0); } function latestRoundData() external override view returns ( uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound ){ return (0, 1000, 0, block.timestamp, 0); } }
mock/mockStrategyManager.sol:
pragma solidity 0.8.19; import "../../contracts/EigenLayer/interfaces/IStrategyManager.sol"; contract MockStrategyManager { constructor() { } }
attack.t.sol:
pragma solidity 0.8.19; import "forge-std/Test.sol"; import "forge-std/console.sol"; import {MockERC20} from "forge-std/mocks/MockERC20.sol"; import "./mockTests/mockOracle.sol"; import "./mockTests/mockStrategyManager.sol"; import "../contracts/Oracle/RenzoOracle.sol"; import "../contracts/Oracle/IRenzoOracle.sol"; import "../contracts/Permissions/RoleManager.sol"; import "../contracts/Permissions/IRoleManager.sol"; import {RoleManagerStorageV1} from "../contracts/Permissions/RoleManagerStorage.sol"; import "../contracts/EigenLayer/interfaces/IStrategy.sol"; import "../contracts/EigenLayer/interfaces/IStrategyManager.sol"; import "../contracts/EigenLayer/interfaces/IDelegationManager.sol"; import "../contracts/Deposits/DepositQueue.sol"; import "../contracts/Deposits/IDepositQueue.sol"; import "../contracts/Withdraw/WithdrawQueue.sol"; import {WithdrawQueueStorageV1} from "../contracts/Withdraw/WithdrawQueueStorage.sol"; import "../contracts/Withdraw/IWithdrawQueue.sol"; import "../contracts/RestakeManager.sol"; import "../contracts/IRestakeManager.sol"; import "../contracts/token/EzEthToken.sol"; import "../contracts/token/IEzEthToken.sol"; contract testAttack is Test { address admin; address alice; address bob; address joy; IEzEthToken public ezETH; address public stETH; address public cbETH; RoleManager public roleManager; address public renzoOracle; address public mockStrategyManager; IRestakeManager public restakeManager; DepositQueue public depositQueue; WithdrawQueue public withdrawQueue; function setUp() public { admin = makeAddr("Admin"); alice = makeAddr("Alice"); bob = makeAddr("bob"); joy = makeAddr("joy"); roleManager = deployRoleManager(); vm.startPrank(admin); roleManager.grantRole(keccak256("RESTAKE_MANAGER_ADMIN"), address(admin)); roleManager.grantRole(keccak256("RX_ETH_MINTER_BURNER"), address(admin)); ezETH = IEzEthToken(deployEzETH(roleManager)); (stETH,cbETH) = deployToken(); renzoOracle = deployOracle(roleManager, IERC20(stETH), IERC20(cbETH)); mockStrategyManager = address(new MockStrategyManager()); depositQueue = deployDepositQueue(roleManager); restakeManager = IRestakeManager(deployRestakeManager(roleManager, ezETH, IRenzoOracle(renzoOracle), IStrategyManager(mockStrategyManager), IDelegationManager(address(0)), depositQueue)); WithdrawQueueStorageV1.TokenWithdrawBuffer[] memory _withdrawalBufferTarget = new WithdrawQueueStorageV1.TokenWithdrawBuffer[](2); _withdrawalBufferTarget[0] = WithdrawQueueStorageV1.TokenWithdrawBuffer(stETH, 5 * 10**18); _withdrawalBufferTarget[1] = WithdrawQueueStorageV1.TokenWithdrawBuffer(address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), 5 * 10**18); withdrawQueue = deployWithdraw(roleManager, restakeManager, ezETH, IRenzoOracle(renzoOracle), 1, _withdrawalBufferTarget); DepositQueue(depositQueue).setWithdrawQueue(IWithdrawQueue(address(withdrawQueue))); roleManager.grantRole(keccak256("RX_ETH_MINTER_BURNER"), address(withdrawQueue)); roleManager.grantRole(keccak256("RX_ETH_MINTER_BURNER"), address(depositQueue)); vm.stopPrank(); } function deployRoleManager() public returns(RoleManager) { RoleManager role = new RoleManager(); role.initialize(admin); return role; } function deployEzETH(IRoleManager _roleManager) public returns(address) { EzEthToken _ezETH = new EzEthToken(); _ezETH.initialize(_roleManager); return address(_ezETH); } function deployToken() public returns(address,address){ MockERC20 _stETH = new MockERC20(); MockERC20 _cbETH = new MockERC20(); _stETH.initialize("Staked ETH", "stETH", 18); _cbETH.initialize("Coinbase ETH", "cbETH", 18); return (address(_stETH), address(_cbETH)); } function deployOracle(IRoleManager _roleManager, IERC20 _tokenA, IERC20 _tokenB) public returns(address) { RenzoOracle _renzoOracle = new RenzoOracle(); roleManager.grantRole(keccak256("ORACLE_ADMIN"), admin); _renzoOracle.initialize(_roleManager); TestOracle tokenAOracle = new TestOracle(); TestOracle tokenBOracle = new TestOracle(); _renzoOracle.setOracleAddress(_tokenA, tokenAOracle); _renzoOracle.setOracleAddress(_tokenB, tokenBOracle); return address(_renzoOracle); } function deployRestakeManager(IRoleManager _roleManager, IEzEthToken _ezETH, IRenzoOracle _renzoOracle, IStrategyManager _strategyManager, IDelegationManager _delegationManager, IDepositQueue _depositQueue) public returns(address){ RestakeManager _restakeManager = new RestakeManager(); _restakeManager.initialize(_roleManager, _ezETH, _renzoOracle, _strategyManager, _delegationManager, _depositQueue); return address(_restakeManager); } function deployOperatorDelegators() public { } function deployDepositQueue(IRoleManager _roleManager) public returns(DepositQueue){ DepositQueue _depositQueue = new DepositQueue(); _depositQueue.initialize(_roleManager); return _depositQueue; } function deployWithdraw( IRoleManager _roleManager, IRestakeManager _restakeManager, IEzEthToken _ezETH, IRenzoOracle _renzoOracle, uint256 _coolDownPeriod, WithdrawQueueStorageV1.TokenWithdrawBuffer[] memory _withdrawalBufferTarget ) public returns(WithdrawQueue){ WithdrawQueue _withdrawQueue = new WithdrawQueue(); _withdrawQueue.initialize(_roleManager, _restakeManager, _ezETH, _renzoOracle, _coolDownPeriod, _withdrawalBufferTarget); return _withdrawQueue; } function testReceiveLess1() external { vm.deal(address(withdrawQueue), 100 * 10**18); vm.startPrank(admin); ezETH.mint(alice, 10 * 10**18); ezETH.mint(bob, 10 * 10**18); ezETH.mint(joy, 10 * 10**18); console.log("start..."); (, , uint256 totalTVL) = restakeManager.calculateTVLs(); console.log("Before tvl: %d", totalTVL); console.log("Before alice ezETH: %d", ezETH.balanceOf(alice)); console.log("Before bob ezETH: %d", ezETH.balanceOf(bob)); console.log("Before joy ezETH: %d", ezETH.balanceOf(joy)); vm.stopPrank(); vm.startPrank(alice); ezETH.approve(address(withdrawQueue), 10 * 10 ** 18); withdrawQueue.withdraw(10 * 10 ** 18, address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)); vm.stopPrank(); vm.warp(10); vm.deal(address(withdrawQueue), 200 * 10 ** 18); vm.startPrank(bob); ezETH.approve(address(withdrawQueue), 10 * 10 ** 18); withdrawQueue.withdraw(10 * 10 ** 18, address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)); vm.stopPrank(); vm.warp(20); vm.startPrank(alice); ( , , uint256 amount, , ) = withdrawQueue.withdrawRequests(alice, 0); console.log("Step 1 --- alice: %d", amount); vm.stopPrank(); vm.startPrank(bob); ( , , amount, , ) = withdrawQueue.withdrawRequests(bob, 0); console.log("Step 1 --- bob: %d", amount); vm.stopPrank(); } function testReceiveLess2() external { vm.deal(address(withdrawQueue), 100 * 10**18); vm.startPrank(admin); ezETH.mint(alice, 10 * 10**18); ezETH.mint(bob, 10 * 10**18); ezETH.mint(joy, 10 * 10**18); console.log("start..."); (, , uint256 totalTVL) = restakeManager.calculateTVLs(); console.log("Before tvl: %d", totalTVL); console.log("Before alice ezETH: %d", ezETH.balanceOf(alice)); console.log("Before bob ezETH: %d", ezETH.balanceOf(bob)); console.log("Before joy ezETH: %d", ezETH.balanceOf(joy)); vm.stopPrank(); vm.startPrank(alice); ezETH.approve(address(withdrawQueue), 10 * 10 ** 18); withdrawQueue.withdraw(10 * 10 ** 18, address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)); vm.stopPrank(); vm.warp(10); vm.startPrank(admin); // Simulate Alice's status after withdrawing money vm.deal(address(withdrawQueue), 200 * 10**18 - 16666666666666666666); ezETH.burn(address(withdrawQueue), 10 * 10 ** 18); vm.stopPrank(); vm.startPrank(bob); ezETH.approve(address(withdrawQueue), 10 * 10 ** 18); withdrawQueue.withdraw(10 * 10 ** 18, address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)); vm.stopPrank(); vm.startPrank(alice); ( , , uint256 amount, , ) = withdrawQueue.withdrawRequests(alice, 0); console.log("Step 1 --- alice: %d", amount); vm.stopPrank(); vm.startPrank(bob); ( , , amount, , ) = withdrawQueue.withdrawRequests(bob, 0); console.log("Step 1 --- bob: %d", amount); vm.stopPrank(); } /* struct WithdrawRequest { address collateralToken; uint256 withdrawRequestID; uint256 amountToRedeem; uint256 ezETHLocked; uint256 createdAt; } */ }
Results:
Logs: start... Before tvl: 100000000000000000000 Before alice ezETH: 10000000000000000000 Before bob ezETH: 10000000000000000000 Before joy ezETH: 10000000000000000000 Step 1 --- alice: 33333333333333333333 Step 1 --- bob: 66666666666666666666 [PASS] testReceiveLess2() (gas: 542203) Logs: start... Before tvl: 100000000000000000000 Before alice ezETH: 10000000000000000000 Before bob ezETH: 10000000000000000000 Before joy ezETH: 10000000000000000000 Step 1 --- alice: 33333333333333333333 Step 1 --- bob: 91666666666666666667
run forge test -vvv
Manual Review
When calculating TVL, subtract the total value of claimReserve.
Payable
#0 - C4-Staff
2024-05-15T14:15:21Z
CloudEllie marked the issue as duplicate of #544
#1 - c4-judge
2024-05-16T10:14:33Z
alcueca marked the issue as not a duplicate
#2 - alcueca
2024-05-16T10:15:26Z
Ultimately, root cause is the withdrawal amount being calculated at wthidrawal, not at claim.
#3 - c4-judge
2024-05-16T10:15:35Z
alcueca marked the issue as duplicate of #326
#4 - c4-judge
2024-05-16T10:16:04Z
alcueca marked the issue as satisfactory
🌟 Selected for report: zigtur
Also found by: 0x73696d616f, 0xBeastBoy, 0xCiphky, Aymen0909, FastChecker, LessDupes, NentoR, Sathish9098, TECHFUND, TheFabled, ak1, bigtone, cu5t0mpeo, eeshenggoh, guhu95, ilchovski, josephdara, ladboy233, mt030d, oakcobalt, rbserver, t0x1c, tapir, xg
2.6973 USDC - $2.70
https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Withdraw/WithdrawQueue.sol#L206-L206 https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Withdraw/WithdrawQueue.sol#L279-L279
When the WithdrawQueue contract is paused, the amount can still be claimed
The document mentions: "DEPOSIT_WITHDRAW_PAUSER role allows the admin to pause/unpause deposits and withdraws."
https://code4rena.com/audits/2024-04-renzo#toc-2-all-trusted-roles-in-the-protocol
However, the WithdrawQueue::withdraw and WithdrawQueue::claim functions do not check whether the contract is paused when paused, and there is no modifier in the contract to check whether the contract is paused.
This shows that DEPOSIT_WITHDRAW_PAUSER cannot pause the withdraws operation
Manual Review
Add related modifications to WithdrawQueue::withdraw and WithdrawQueue::claim to check whether it is paused
Access Control
#0 - c4-judge
2024-05-16T10:48:13Z
alcueca changed the severity to 2 (Med Risk)
#1 - c4-judge
2024-05-24T10:13:29Z
alcueca marked the issue as satisfactory
🌟 Selected for report: 0xCiphky
Also found by: 0rpse, 0x007, 0xAadi, 14si2o_Flint, ADM, Aamir, Aymen0909, BiasedMerc, DanielArmstrong, Fassi_Security, FastChecker, KupiaSec, LessDupes, MaslarovK, Neon2835, RamenPeople, SBSecurity, Shaheen, Tendency, ZanyBonzy, adam-idarrha, araj, b0g0, baz1ka, bigtone, bill, blutorque, carrotsmuggler, cu5t0mpeo, fyamf, gesha17, gumgumzum, hunter_w3b, inzinko, jokr, josephdara, kennedy1030, kinda_very_good, lanrebayode77, m_Rassska, mt030d, mussucal, tapir, underdog, xg, zzykxx
0.0402 USDC - $0.04
https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/RestakeManager.sol#L546-L562 https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Delegation/OperatorDelegator.sol#L147-L148
User cannot deposit normally
When the deposit
function is called, it checks whether the withdraw buffer is below the buffer target. If it is below, a portion of the deposit amount will be stored in the withdrawQueue contract. If the user's deposit amount is less than or equal to bufferToFill
, _amount
will be equal to 0, causing the entire transaction to revert. This is because the operatorDelegator.deposit
function does not allow a tokenAmount
value of 0.
RestakeManager::deposit:
OperatorDelegator::deposit
poc coding:
mock/mockOperatorDelegator.sol
pragma solidity 0.8.19; import "../../contracts/Delegation/IOperatorDelegator.sol"; import "../../contracts/Errors/Errors.sol"; import "../../contracts/EigenLayer/interfaces/IEigenPod.sol"; contract mockOperatorDelegator is IOperatorDelegator { constructor() {} function getTokenBalanceFromStrategy(IERC20 token) external override view returns (uint256) { return 10 * 10 ** 18; } function deposit(IERC20 _token, uint256 _tokenAmount) external override returns (uint256 shares) { // Simulation judgment conditions if (_tokenAmount == 0) { revert InvalidZeroInput(); } return 10 * 10 ** 18; } // Note: Withdraws disabled for this release // function startWithdrawal(IERC20 _token, uint256 _tokenAmount) external returns (bytes32); // function completeWithdrawal( // IStrategyManager.DeprecatedStruct_QueuedWithdrawal calldata _withdrawal, // IERC20 _token, // uint256 _middlewareTimesIndex, // address _sendToAddress // ) external; function getStakedETHBalance() external override view returns (uint256) { return 10 * 10 ** 18; } function stakeEth( bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot ) external override payable { return; } function eigenPod() external override view returns (IEigenPod) { return IEigenPod(address(0x123)); } function pendingUnstakedDelayedWithdrawalAmount() external override view returns (uint256) { return 10 * 10 ** 18; } }
mock/mockOracle.sol
pragma solidity 0.8.19; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; contract TestOracle is AggregatorV3Interface{ constructor() { } function decimals() external override view returns (uint8) { return 18; } function description() external override view returns (string memory) { return "none"; } function version() external override view returns (uint256) { return 1; } function getRoundData( uint80 _roundId ) external override view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) { return (0, 0, 0, 0, 0); } function latestRoundData() external override view returns ( uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound ){ return (0, 1000, 0, block.timestamp, 0); } }
mock/mockStrategyManager.sol
pragma solidity 0.8.19; import "../../contracts/EigenLayer/interfaces/IStrategyManager.sol"; contract MockStrategyManager { constructor() { } }
mock/mockToken.sol
pragma solidity 0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract ERCToken is IERC20 { /*////////////////////////////////////////////////////////////// METADATA STORAGE //////////////////////////////////////////////////////////////*/ string internal _name; string internal _symbol; uint8 internal _decimals; function name() external view returns (string memory) { return _name; } function symbol() external view returns (string memory) { return _symbol; } function decimals() external view returns (uint8) { return _decimals; } /*////////////////////////////////////////////////////////////// ERC20 STORAGE //////////////////////////////////////////////////////////////*/ uint256 internal _totalSupply; mapping(address => uint256) internal _balanceOf; mapping(address => mapping(address => uint256)) internal _allowance; function totalSupply() external view override returns (uint256) { return _totalSupply; } function balanceOf(address owner) external view override returns (uint256) { return _balanceOf[owner]; } function allowance(address owner, address spender) external view override returns (uint256) { return _allowance[owner][spender]; } /*////////////////////////////////////////////////////////////// EIP-2612 STORAGE //////////////////////////////////////////////////////////////*/ uint256 internal INITIAL_CHAIN_ID; bytes32 internal INITIAL_DOMAIN_SEPARATOR; mapping(address => uint256) public nonces; /*////////////////////////////////////////////////////////////// INITIALIZE //////////////////////////////////////////////////////////////*/ /// @dev A bool to track whether the contract has been initialized. bool private initialized; /// @dev To hide constructor warnings across solc versions due to different constructor visibility requirements and /// syntaxes, we add an initialization function that can be called only once. function initialize(string memory name_, string memory symbol_, uint8 decimals_) public { require(!initialized, "ALREADY_INITIALIZED"); _name = name_; _symbol = symbol_; _decimals = decimals_; INITIAL_CHAIN_ID = _pureChainId(); INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); initialized = true; } /*////////////////////////////////////////////////////////////// ERC20 LOGIC //////////////////////////////////////////////////////////////*/ function approve(address spender, uint256 amount) public virtual override returns (bool) { _allowance[msg.sender][spender] = amount; emit Approval(msg.sender, spender, amount); return true; } function transfer(address to, uint256 amount) public virtual override returns (bool) { _balanceOf[msg.sender] = _sub(_balanceOf[msg.sender], amount); _balanceOf[to] = _add(_balanceOf[to], amount); emit Transfer(msg.sender, to, amount); return true; } function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) { uint256 allowed = _allowance[from][msg.sender]; // Saves gas for limited approvals. if (allowed != ~uint256(0)) _allowance[from][msg.sender] = _sub(allowed, amount); _balanceOf[from] = _sub(_balanceOf[from], amount); _balanceOf[to] = _add(_balanceOf[to], amount); emit Transfer(from, to, amount); return true; } /*////////////////////////////////////////////////////////////// EIP-2612 LOGIC //////////////////////////////////////////////////////////////*/ function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public virtual { require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); address recoveredAddress = ecrecover( keccak256( abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR(), keccak256( abi.encode( keccak256( "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" ), owner, spender, value, nonces[owner]++, deadline ) ) ) ), v, r, s ); require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER"); _allowance[recoveredAddress][spender] = value; emit Approval(owner, spender, value); } function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { return _pureChainId() == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); } function computeDomainSeparator() internal view virtual returns (bytes32) { return keccak256( abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256(bytes(_name)), keccak256("1"), _pureChainId(), address(this) ) ); } /*////////////////////////////////////////////////////////////// INTERNAL MINT/BURN LOGIC //////////////////////////////////////////////////////////////*/ function _mint(address to, uint256 amount) internal virtual { _totalSupply = _add(_totalSupply, amount); _balanceOf[to] = _add(_balanceOf[to], amount); emit Transfer(address(0), to, amount); } function mint(address to, uint256 amount) external { _mint(to, amount); } function _burn(address from, uint256 amount) internal virtual { _balanceOf[from] = _sub(_balanceOf[from], amount); _totalSupply = _sub(_totalSupply, amount); emit Transfer(from, address(0), amount); } function burn(address from, uint256 amount) external { _burn(from, amount); } /*////////////////////////////////////////////////////////////// INTERNAL SAFE MATH LOGIC //////////////////////////////////////////////////////////////*/ function _add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; require(c >= a, "ERC20: addition overflow"); return c; } function _sub(uint256 a, uint256 b) internal pure returns (uint256) { require(a >= b, "ERC20: subtraction underflow"); return a - b; } /*////////////////////////////////////////////////////////////// HELPERS //////////////////////////////////////////////////////////////*/ // We use this complex approach of `_viewChainId` and `_pureChainId` to ensure there are no // compiler warnings when accessing chain ID in any solidity version supported by forge-std. We // can't simply access the chain ID in a normal view or pure function because the solc View Pure // Checker changed `chainid` from pure to view in 0.8.0. function _viewChainId() private view returns (uint256 chainId) { // Assembly required since `block.chainid` was introduced in 0.8.0. assembly { chainId := chainid() } address(this); // Silence warnings in older Solc versions. } function _pureChainId() private pure returns (uint256 chainId) { function() internal view returns (uint256) fnIn = _viewChainId; function() internal pure returns (uint256) pureChainId; assembly { pureChainId := fnIn } chainId = pureChainId(); } }
Attack.t.sol
pragma solidity 0.8.19; import "forge-std/Test.sol"; import "forge-std/console.sol"; import "./mockTests/mockOracle.sol"; import "./mockTests/mockStrategyManager.sol"; import "./mockTests/mockOperatorDelegator.sol"; import "./mockTests/mockToken.sol"; import "../contracts/Oracle/RenzoOracle.sol"; import "../contracts/Oracle/IRenzoOracle.sol"; import "../contracts/Permissions/RoleManager.sol"; import "../contracts/Permissions/IRoleManager.sol"; import {RoleManagerStorageV1} from "../contracts/Permissions/RoleManagerStorage.sol"; import "../contracts/EigenLayer/interfaces/IStrategy.sol"; import "../contracts/EigenLayer/interfaces/IStrategyManager.sol"; import "../contracts/EigenLayer/interfaces/IDelegationManager.sol"; import "../contracts/Deposits/DepositQueue.sol"; import "../contracts/Deposits/IDepositQueue.sol"; import "../contracts/Withdraw/WithdrawQueue.sol"; import {WithdrawQueueStorageV1} from "../contracts/Withdraw/WithdrawQueueStorage.sol"; import "../contracts/Withdraw/IWithdrawQueue.sol"; import "../contracts/RestakeManager.sol"; import "../contracts/IRestakeManager.sol"; import "../contracts/token/EzEthToken.sol"; import "../contracts/token/IEzEthToken.sol"; contract testAttack is Test { address admin; address alice; address bob; address joy; IEzEthToken public ezETH; ERCToken public stETH; ERCToken public cbETH; RoleManager public roleManager; address public renzoOracle; address public mockStrategyManager; RestakeManager public restakeManager; DepositQueue public depositQueue; WithdrawQueue public withdrawQueue; function setUp() public { admin = makeAddr("Admin"); alice = makeAddr("Alice"); bob = makeAddr("bob"); joy = makeAddr("joy"); roleManager = deployRoleManager(); vm.startPrank(admin); roleManager.grantRole(keccak256("RESTAKE_MANAGER_ADMIN"), address(admin)); roleManager.grantRole(keccak256("RX_ETH_MINTER_BURNER"), address(admin)); ezETH = IEzEthToken(deployEzETH(roleManager)); (stETH,cbETH) = deployToken(); renzoOracle = deployOracle(roleManager, IERC20(address(stETH)), IERC20(address(cbETH))); mockStrategyManager = address(new MockStrategyManager()); depositQueue = deployDepositQueue(roleManager); restakeManager = deployRestakeManager(roleManager, ezETH, IRenzoOracle(renzoOracle), IStrategyManager(mockStrategyManager), IDelegationManager(address(0)), depositQueue); depositQueue.setRestakeManager(IRestakeManager(restakeManager)); WithdrawQueueStorageV1.TokenWithdrawBuffer[] memory _withdrawalBufferTarget = new WithdrawQueueStorageV1.TokenWithdrawBuffer[](2); _withdrawalBufferTarget[0] = WithdrawQueueStorageV1.TokenWithdrawBuffer(address(stETH), 5 * 10**18); _withdrawalBufferTarget[1] = WithdrawQueueStorageV1.TokenWithdrawBuffer(address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), 5 * 10**18); withdrawQueue = deployWithdraw(roleManager, restakeManager, ezETH, IRenzoOracle(renzoOracle), 1, _withdrawalBufferTarget); DepositQueue(depositQueue).setWithdrawQueue(IWithdrawQueue(address(withdrawQueue))); roleManager.grantRole(keccak256("RX_ETH_MINTER_BURNER"), address(withdrawQueue)); roleManager.grantRole(keccak256("RX_ETH_MINTER_BURNER"), address(depositQueue)); vm.stopPrank(); } function deployRoleManager() public returns(RoleManager) { RoleManager role = new RoleManager(); role.initialize(admin); return role; } function deployEzETH(IRoleManager _roleManager) public returns(address) { EzEthToken _ezETH = new EzEthToken(); _ezETH.initialize(_roleManager); return address(_ezETH); } function deployToken() public returns(ERCToken,ERCToken){ ERCToken _stETH = new ERCToken(); ERCToken _cbETH = new ERCToken(); _stETH.initialize("Staked ETH", "stETH", 18); _cbETH.initialize("Coinbase ETH", "cbETH", 18); return (_stETH, _cbETH); } function deployOracle(IRoleManager _roleManager, IERC20 _tokenA, IERC20 _tokenB) public returns(address) { RenzoOracle _renzoOracle = new RenzoOracle(); roleManager.grantRole(keccak256("ORACLE_ADMIN"), admin); _renzoOracle.initialize(_roleManager); TestOracle tokenAOracle = new TestOracle(); TestOracle tokenBOracle = new TestOracle(); _renzoOracle.setOracleAddress(_tokenA, tokenAOracle); _renzoOracle.setOracleAddress(_tokenB, tokenBOracle); return address(_renzoOracle); } function deployRestakeManager(IRoleManager _roleManager, IEzEthToken _ezETH, IRenzoOracle _renzoOracle, IStrategyManager _strategyManager, IDelegationManager _delegationManager, IDepositQueue _depositQueue) public returns(RestakeManager){ RestakeManager _restakeManager = new RestakeManager(); _restakeManager.initialize(_roleManager, _ezETH, _renzoOracle, _strategyManager, _delegationManager, _depositQueue); return _restakeManager; } function deployOperatorDelegators() public { } function deployDepositQueue(IRoleManager _roleManager) public returns(DepositQueue){ DepositQueue _depositQueue = new DepositQueue(); _depositQueue.initialize(_roleManager); return _depositQueue; } function deployWithdraw( IRoleManager _roleManager, IRestakeManager _restakeManager, IEzEthToken _ezETH, IRenzoOracle _renzoOracle, uint256 _coolDownPeriod, WithdrawQueueStorageV1.TokenWithdrawBuffer[] memory _withdrawalBufferTarget ) public returns(WithdrawQueue){ WithdrawQueue _withdrawQueue = new WithdrawQueue(); _withdrawQueue.initialize(_roleManager, _restakeManager, _ezETH, _renzoOracle, _coolDownPeriod, _withdrawalBufferTarget); return _withdrawQueue; } function testCannotDeposit() public { vm.warp(86400 + 60); vm.deal(address(withdrawQueue), 100 * 10**18); stETH.mint(alice, 10 * 10 ** 18); stETH.mint(bob, 10 * 10 ** 18); vm.startPrank(admin); restakeManager.addOperatorDelegator(new mockOperatorDelegator(), 10000); restakeManager.addCollateralToken(stETH); vm.stopPrank(); vm.startPrank(alice); stETH.approve(address(restakeManager), 10 * 10 ** 18); restakeManager.deposit(IERC20(address(stETH)), 1 * 10 ** 18); } }
Results:
│ ├─ [373] mockOperatorDelegator::deposit(ERCToken: [0x49c3486EC9f488230dD85FAc098929AeA9a3A898], 0) │ │ └─ ← [Revert] InvalidZeroInput() │ └─ ← [Revert] InvalidZeroInput() └─ ← [Revert] InvalidZeroInput()
Manual Review
Add an additional conditional check to see if the user has excess funds available for deposit. If not, skip the call to operatorDelegator.deposit()
.
Error
#0 - c4-judge
2024-05-20T05:08:17Z
alcueca marked the issue as satisfactory
#1 - c4-judge
2024-05-24T10:26:23Z
alcueca changed the severity to 2 (Med Risk)