Renzo - cu5t0mpeo's results

A protocol that abstracts all staking complexity from the end-user and enables easy collaboration with EigenLayer node operators and a Validated Services (AVSs).

General Information

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

Renzo

Findings Distribution

Researcher Performance

Rank: 65/122

Findings: 3

Award: $3.15

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

0.4071 USDC - $0.41

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
:robot:_22_group
duplicate-326

External Links

Lines of code

https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/RestakeManager.sol#L270-L358

Vulnerability details

Impact

The user will receive less money than they should have.

Proof of Concept

The user's withdrawal process involves two steps:

  1. Calling withdraw to initiate a withdraw request and calculate the amount to be received using the formula share * totalAsset / totalShare.
  2. Calling claim to burn ezETH (share) and receive the desired collateral token.

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:

https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/RestakeManager.sol#L270-L358

Here's an example:

  1. User A calls withdraw to initiate a withdrawal request.
  2. After claimReserve seconds, User A still does not call claim to receive the amount.
  3. Another day passes, and User B initiates a withdrawal request. However, when calculating the amount to be received, TVL still includes the amount waiting to be claimed by User A and totalSupply. In this process, rewards generated by RENZO continue to include User A, but User A cannot claim them (because User A's claimable amount has already been calculated), resulting in User B receiving less reward than expected when claiming the total amount, as User A is included in this process.

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

Tools Used

Manual Review

When calculating TVL, subtract the total value of claimReserve.

Assessed type

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

Awards

2.6973 USDC - $2.70

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
sufficient quality report
:robot:_06_group
duplicate-569

External Links

Lines of code

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

Vulnerability details

Impact

When the WithdrawQueue contract is paused, the amount can still be claimed

Proof of Concept

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.

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

This shows that DEPOSIT_WITHDRAW_PAUSER cannot pause the withdraws operation

Tools Used

Manual Review

Add related modifications to WithdrawQueue::withdraw and WithdrawQueue::claim to check whether it is paused

Assessed type

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

Awards

0.0402 USDC - $0.04

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
sufficient quality report
:robot:_20_group
duplicate-198

External Links

Lines of code

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

Vulnerability details

Impact

User cannot deposit normally

Proof of Concept

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:

https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/RestakeManager.sol#L546-L562

OperatorDelegator::deposit

https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Delegation/OperatorDelegator.sol#L147-L148

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()

Tools Used

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().

Assessed type

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)

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter