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
Rank: 1/75
Findings: 4
Award: $12,289.77
🌟 Selected for report: 2
🚀 Solo Findings: 2
🌟 Selected for report: rvierdiiev
6119.6969 USDC - $6,119.70
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.
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.
VsCode
Maybe think about restrict access to distributeRewards
function.
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
🌟 Selected for report: rvierdiiev
6119.6969 USDC - $6,119.70
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.
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.
VsCode
As you accrued _mevTheftPenalty
inside ValidatorWithdrawalVault
, then you also should calculate operator's rewards inside NodeELRewardVault
.
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.
🌟 Selected for report: Madalad
Also found by: Aymen0909, Bauchibred, Breeje, DadeKuma, Hama, LaScaloneta, Madalad, MohammedRizwan, bin2chen, dwward3n, erictee, etherhood, kutugu, peanuts, piyushshukla, rvierdiiev, saneryee, tallo, turvy_fuzz, whimints
31.7954 USDC - $31.80
https://github.com/code-423n4/2023-06-stader/blob/main/contracts/StaderOracle.sol#L637-L651
StaderOracle.getPORFeedData doesn't check prices are up to date. As result prices can be invalid, which will affect system.
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.
VsCode
Ensure that price is not 0 and that it was provided not long time ago.
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