Stader Labs - rvierdiiev'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: 1/75

Findings: 4

Award: $12,289.77

🌟 Selected for report: 2

🚀 Solo Findings: 2

Findings Information

🌟 Selected for report: rvierdiiev

Labels

bug
2 (Med Risk)
satisfactory
selected for report
sponsor disputed
M-11

Awards

6119.6969 USDC - $6,119.70

External Links

Lines of code

https://github.com/code-423n4/2023-06-stader/blob/main/contracts/ValidatorWithdrawalVault.sol#L29-L83

Vulnerability details

Impact

ValidatorWithdrawalVault.distributeRewards can be called to make operator slashable. Attacker can call distributeRewards right before settleFunds to make operatorShare < penaltyAmount. As result validator will face loses.

Proof of Concept

ValidatorWithdrawalVault.distributeRewards can be called by anyone. It's purpose is to distribute validators rewards among stakers protocol and operator. After the call, balance of ValidatorWithdrawalVault becomes 0.

ValidatorWithdrawalVault.settle is called when validator is withdrawn from beacon chain. In this case balance of contract is used to find operatorShare and in case if it's less than accrued penalty by validator, then operator is slashed.

Because distributeRewards is permissionless, then next situation is possible. 1.Operator decided to withdraw validator. At the moment of that call, balance of ValidatorWithdrawalVault is not 0 and operatorShare is 1 eth. Also validator accrued 4.5 eth of penalty. 2.Malicious user sees when 32 eth of validator's deposit is sent to the ValidatorWithdrawalVault and frontruns it with distributeRewards call. This makes balance to be 32 eth. 3.operatorShare will be 4 eth in this time(permisssionless) and penalty is 4.5, so user is slashed. 4.In case if malicious user didn't call distributeRewards, then slash would not occur.

Also in same way, permissioned operator can call distributeRewards to get his rewards, when he is going to be slashed. As permissioned validators are not forced to have collateral to be slashed. By this move he rescued his eraning as otherwise they would be sent to pool.

Tools Used

VsCode

Maybe think about restrict access to distributeRewards function.

Assessed type

Access Control

#0 - manoj9april

2023-06-19T06:29:09Z

This is intended as we want to slash SD for operator if penalty is greater than operator share. Rewards share of vaults is going to be distributed by Stader every so often to enforce this.

#1 - c4-sponsor

2023-06-19T06:29:15Z

manoj9april marked the issue as sponsor disputed

#2 - Picodes

2023-07-02T21:56:52Z

This report shows how the fact that an operator can be slashed depends on the order of actions. It seems a valid Medium severity issue to me as front-running / changing a sequence of action could lead to a loss of funds compared to what the operator and users thoughts they should get.

Wouldn't it solve this to also fetch and solve penaltyAmount during distributeRewards ?

#3 - c4-judge

2023-07-02T21:56:59Z

Picodes marked the issue as satisfactory

#4 - Co0nan

2023-07-05T01:03:39Z

Not sure if I'm following the impact here.

At the moment of that call, balance of ValidatorWithdrawalVault is not 0 and operatorShare is 1 eth. Also validator accrued 4.5 eth of penalty.

This case means the validator has to be slashed, I mean in both cases the user must be slashed according to the design. Can't see the exact root cause, also distributeRewards can be called at any time no need to make a front-run so the system behaves as intended.

#5 - rvierdiyev

2023-07-07T13:32:16Z

@Co0nan the problem here is that once distributeRewards is called, then all balance of vault becomes 0 as i described in the example, user had 1 eth inside the vault before the distributeRewards call and he has 4.5 eth of penalty

so when withdrawals 32 eth should come, then user's balance will be 1 + 4 eth, which is more than penalty

but because malicious user calls distributeRewards before 32 eth will come, then user in the end has only 4 eth which is less than penalty

Findings Information

🌟 Selected for report: rvierdiiev

Labels

bug
2 (Med Risk)
satisfactory
selected for report
sponsor acknowledged
M-12

Awards

6119.6969 USDC - $6,119.70

External Links

Lines of code

https://github.com/code-423n4/2023-06-stader/blob/main/contracts/ValidatorWithdrawalVault.sol#L53-L83

Vulnerability details

Impact

ValidatorWithdrawalVault.settleFunds doesn't check amount that user has inside NodeELRewardVault to pay for penalty. That value can increase operator's earned amount which can avoid slashing for him.

Proof of Concept

When validator withdraws from beacon chain then ValidatorWithdrawalVault.settleFunds function is called. This function calculates amount that validator has earned for attestations as validator. So only balance of this contract is considered.

Then function fetches penalty amount. This penalty amount contains of 3 points: _mevTheftPenalty, _missedAttestationPenalty and _missedAttestationPenalty.

In case if penalty amount is bigger than validator's earning on ValidatorWithdrawalVault, then SD collateral is slashed.

Now we need to understand how validator receives funds in this system. All attestation payments comes to ValidatorWithdrawalVault, while mev/block proposal funds are coming to SocializingPool or NodeELRewardVault(depends on user's choice). So actually _missedAttestationPenalty is responding to ValidatorWithdrawalVault earning, while _mevTheftPenalty is responding to NodeELRewardVault earnings.

That means that NodeELRewardVault balance should also be checked in order to find out how mane earning validator has there and they should be also counted when applying penalty.

Simple example. 1.Validator wants to exit. 2.Operator earning is 0.1 eth inside ValidatorWithdrawalVault. 3.Accrued penalty is 0.11, which means that user will be slashed. 4.Operator also has NodeELRewardVault where his operator's reward is 0.05 eth 5.As result user had enough balance to cover penalty, but he still was penalizied.

Tools Used

VsCode

As you accrued _mevTheftPenalty inside ValidatorWithdrawalVault, then you also should calculate operator's rewards inside NodeELRewardVault.

Assessed type

Error

#0 - manoj9april

2023-06-19T06:10:31Z

Rewards are treated separately between CL and EL. We will take this suggestion into account for next upgrades.

#1 - c4-sponsor

2023-06-19T06:10:38Z

manoj9april marked the issue as sponsor acknowledged

#2 - c4-judge

2023-07-02T22:01:49Z

Picodes marked the issue as satisfactory

#3 - Picodes

2023-07-02T22:02:49Z

Keeping Medium severity as this report shows how an operator could be slashed despite having earned more than its penalty if we combine EL and CL rewards, which could lead to a loss of funds.

Awards

31.7954 USDC - $31.80

Labels

bug
2 (Med Risk)
satisfactory
duplicate-15

External Links

Lines of code

https://github.com/code-423n4/2023-06-stader/blob/main/contracts/StaderOracle.sol#L637-L651

Vulnerability details

Impact

StaderOracle.getPORFeedData doesn't check prices are up to date. As result prices can be invalid, which will affect system.

Proof of Concept

StaderOracle.getPORFeedData fetches prices from chainlink price feeds. https://github.com/code-423n4/2023-06-stader/blob/main/contracts/StaderOracle.sol#L637-L651

    function getPORFeedData()
        internal
        view
        returns (
            uint256,
            uint256,
            uint256
        )
    {
        (, int256 totalETHBalanceInInt, , , ) = AggregatorV3Interface(staderConfig.getETHBalancePORFeedProxy())
            .latestRoundData();
        (, int256 totalETHXSupplyInInt, , , ) = AggregatorV3Interface(staderConfig.getETHXSupplyPORFeedProxy())
            .latestRoundData();
        return (uint256(totalETHBalanceInInt), uint256(totalETHXSupplyInInt), block.number);
    }

As you can see, it doesn't do any checks after that. The problem is that oracle price can be stale in case if price feed wasn't updated for some time. As result protocol will use this stale prices.

Tools Used

VsCode

Ensure that price is not 0 and that it was provided not long time ago.

Assessed type

Oracle

#0 - c4-judge

2023-06-11T09:38:00Z

Picodes marked the issue as duplicate of #15

#1 - c4-judge

2023-07-02T10:49:50Z

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