Stader Labs - sces60107's results

Decentralized ETH liquid staking protocol with 4 ETH bond for anyone to be a node operator.

General Information

Platform: Code4rena

Start Date: 02/06/2023

Pot Size: $100,000 USDC

Total HM: 15

Participants: 75

Period: 7 days

Judge: Picodes

Total Solo HM: 5

Id: 249

League: ETH

Stader Labs

Findings Distribution

Researcher Performance

Rank: 10/75

Findings: 2

Award: $2,328.85

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: bin2chen

Also found by: sces60107

Labels

bug
2 (Med Risk)
satisfactory
sponsor disputed
duplicate-175

Awards

2118.3566 USDC - $2,118.36

External Links

Lines of code

https://github.com/code-423n4/2023-06-stader/blob/main/contracts/StaderStakePoolsManager.sol#L215 https://github.com/code-423n4/2023-06-stader/blob/main/contracts/PoolSelector.sol#L93 https://github.com/code-423n4/2023-06-stader/blob/main/contracts/PoolSelector.sol#L99-L104

Vulnerability details

Impact

StaderStakePoolsManager.depositETHOverTargetWeight helps stake all deposited ETH to the capacity available across any pool without disabling deposits. This feature is designed to be fair to all pools over time and enables Stader to stake all deposited ETH as long as the validator supply exists on any pool.

However, if StaderStakePoolsManager.depositETHOverTargetWeight is called when availableETHForNewDeposit > 0 && availableETHForNewDeposit < poolDepositSize, poolIdArrayIndexForExcessDeposit move forward but no ETH would be deposited.

A malicious user can use it to move poolIdArrayIndexForExcessDeposit and causes unfairness

Proof of Concept

StaderStakePoolsManager.depositETHOverTargetWeight calls PoolSelector.poolAllocationForExcessETHDeposit(availableETHForNewDeposit) to get the selected pool capacity. https://github.com/code-423n4/2023-06-stader/blob/main/contracts/StaderStakePoolsManager.sol#L215

    function depositETHOverTargetWeight() external override nonReentrant {
        if (block.number < lastExcessETHDepositBlock + excessETHDepositCoolDown) {
            revert CooldownNotComplete();
        }
        …
        if (availableETHForNewDeposit == 0) {
            revert InsufficientBalance();
        }
        (uint256[] memory selectedPoolCapacity, uint8[] memory poolIdArray) = IPoolSelector(
            staderConfig.getPoolSelector()
        ).poolAllocationForExcessETHDeposit(availableETHForNewDeposit);

        uint256 poolCount = poolIdArray.length;
        for (uint256 i = 0; i < poolCount; i++) {
            uint256 validatorToDeposit = selectedPoolCapacity[i];
            if (validatorToDeposit == 0) {
                continue;
            }
            …
        }
    }

In PoolSelector.poolAllocationForExcessETHDeposit, it calculate the selectedPoolCapacity and move poolIdArrayIndexForExcessDeposit. https://github.com/code-423n4/2023-06-stader/blob/main/contracts/PoolSelector.sol#L93 https://github.com/code-423n4/2023-06-stader/blob/main/contracts/PoolSelector.sol#L99-L104

    function poolAllocationForExcessETHDeposit(uint256 _excessETHAmount)
        external
        override
        returns (uint256[] memory selectedPoolCapacity, uint8[] memory poolIdArray)
    {
        UtilLib.onlyStaderContract(msg.sender, staderConfig, staderConfig.STAKE_POOL_MANAGER());
        uint256 ethToDeposit = _excessETHAmount;
        …
        uint256 ETH_PER_NODE = staderConfig.getStakedEthPerNode();
        …
        for (uint256 j; j < poolCount; ) {
            uint256 poolCapacity = poolUtils.getQueuedValidatorCountByPool(poolIdArray[i]);
            uint256 poolDepositSize = ETH_PER_NODE - poolUtils.getCollateralETH(poolIdArray[i]);
            uint256 remainingValidatorsToDeposit = ethToDeposit / poolDepositSize;
            selectedPoolCapacity[i] = Math.min(
                poolAllocationMaxSize - selectedValidatorCount,
                Math.min(poolCapacity, remainingValidatorsToDeposit)
            );
            selectedValidatorCount += selectedPoolCapacity[i];
            ethToDeposit -= selectedPoolCapacity[i] * poolDepositSize;
            i = (i + 1) % poolCount;
            //For ethToDeposit < ETH_PER_NODE, we will be able to at best deposit one more validator
            //but that will introduce complex logic, hence we are not solving that
            if (ethToDeposit < ETH_PER_NODE || selectedValidatorCount >= poolAllocationMaxSize) {
                poolIdArrayIndexForExcessDeposit = i;
                break;
            }
            unchecked {
                ++j;
            }
        }
    }

Suppose ethToDeposit is smaller than poolDepositSize. The following things happen.

remainingValidatorsToDeposit = 0 //  ethToDeposit / poolDepositSize = 0
selectedPoolCapacity[i] = 0 //  Math.min(poolCapacity, remainingValidatorsToDeposit)
ethToDeposit -= 0;
i = (i + 1) % poolCount;
poolIdArrayIndexForExcessDeposit = i; // ethToDeposit < poolDepositSize  <= ETH_PER_NODE
break;

We can find out that i moves forward when ethToDeposit is smaller than poolDepositSize. And poolIdArrayIndexForExcessDeposit is set to the new i. In conclusion, a malicious user can call StaderStakePoolsManager.depositETHOverTargetWeight to move poolIdArrayIndexForExcessDeposit when availableETHForNewDeposit > 0 && availableETHForNewDeposit < poolDepositSize. It could cause unfairness. It is said that depositETHOverTargetWeight is designed to be fair to all pools. https://blog.staderlabs.com/ethx-deposits-bda0f62d8ed8

Tools Used

Manual Review

It is hard to get the poolDepositSize in StaderStakePoolsManager.depositETHOverTargetWeight. Uses ETH_PER_NODE as the lower bound of availableETHForNewDeposit

    function depositETHOverTargetWeight() external override nonReentrant {
        if (block.number < lastExcessETHDepositBlock + excessETHDepositCoolDown) {
            revert CooldownNotComplete();
        }
        …
+       uint256 ETH_PER_NODE = staderConfig.getStakedEthPerNode();
+       if (availableETHForNewDeposit < ETH_PER_NODE ) {
-       if (availableETHForNewDeposit == 0) {
            revert InsufficientBalance();
        }
    }

Assessed type

Other

#0 - manoj9april

2023-06-20T06:38:53Z

This is a duplicate issue of #175

#1 - c4-sponsor

2023-06-20T06:39:03Z

manoj9april marked the issue as sponsor disputed

#2 - c4-judge

2023-07-02T12:15:07Z

Picodes marked the issue as duplicate of #175

#3 - c4-judge

2023-07-02T23:04:25Z

Picodes marked the issue as satisfactory

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