Reward index is not settled before stake changes
sources/staking_pool.move:L118–L156
- Likelihood
- High
- Impact
- Medium
- Weakness
- CWE-682
- Description
stakeincreases a position’s shares without first paying out pending rewards or movingreward_debtto the current index. Pending rewards are later computed asshares × (index − reward_debt), so the newly added shares earn every increment of the index since the position was last settled.The same defect exists in the reverse direction in
request_unstake, where shares removed before settlement forfeit their accrued rewards.- Preconditions
- An existing position with an old reward_debt
- Rewards distributed since that position was last settled
- Exploit scenario
- 1.Attacker opens a tiny position early, so its reward_debt is very low.
- 2.Months later, just after a large distribution, they stake a large amount into the same position.
- 3.They claim; the large stake is paid as if it had been staked since the beginning.
- Impact
Rewards owed to long-term stakers are captured by late stakers; the pool becomes unable to pay all claims.
- Recommendation
Call an internal
settle(position)at the start of every function that changes shares: pay or credit pending rewards, then setreward_debt = index.- Fix
- sources/staking_pool.movediff
public entry fun stake(user: &signer, amount: u64) acquires Pool, Position { let pos = borrow_global_mut<Position>(signer::address_of(user));+ rewards::settle(pos); let shares = shares_for(amount); pos.shares = pos.shares + shares; - Resolution
settle() is called first in stake, request_unstake, split and merge. MP-02 proved.
