Fancy Rusty Camel
High
The TokenRewards::depositRewards
is vulnerable to sandwich attack via staking and unstaking leading to unfair rewards distribution
The StakingPool::stake
function allows to users to stake their LP tokens and StakingPool::unstake
allows them to unstake. However, this functionality is vulnerable to a malicious user staking just before rewards are deposited via TokenRewards::depositRewards
and unstaking right after, effectively claiming a part of the deposited rewards and stealing rewards from long-term and honest stakers.
The protocol distributes rewards based on current staking positions without any time-based vesting or lockup period. This allows users to temporarily stake large amounts to capture disproportionate rewards. Staking: https://github.com/sherlock-audit/2025-01-peapods-finance/blob/d28eb19f4b39d3db7997477460f9f9c76839cb0c/contracts/contracts/StakingPoolToken.sol#L67 Depositing Rewards: https://github.com/sherlock-audit/2025-01-peapods-finance/blob/d28eb19f4b39d3db7997477460f9f9c76839cb0c/contracts/contracts/TokenRewards.sol#L180 Unstaking: https://github.com/sherlock-audit/2025-01-peapods-finance/blob/d28eb19f4b39d3db7997477460f9f9c76839cb0c/contracts/contracts/StakingPoolToken.sol#L77
- StakingPoolToken must have existing stakers
- TokenRewards contract must be ready to distribute rewards
- No minimum staking period implemented
- Ability to monitor mempool for incoming reward distributions
- Sufficient capital to execute large stakes
- Monitor mempool for
TokenRewards.depositRewards
transactions - Front-run with large stake:
vm.startPrank(attacker);
stakingToken.approve(address(stakingPool), largeAmount);
stakingPool.stake(attacker, largeAmount);
- Reward distribution occurs:
tokenRewards.depositRewards(rewardToken, rewardAmount);
- Back-run with unstake:
stakingPool.unstake(largeAmount);
tokenRewards.claimReward(attacker);
vm.stopPrank();
- Honest long-term stakers lose significant portions of rewards
- Reduced incentive for legitimate long-term staking
- Protocol's reward distribution mechanism becomes ineffective
Initial State:
// Honest Staker (staking for months)
Staked Amount: 100,000 LP tokens
Total Staked in Protocol: 100,000 LP tokens
// Weekly Reward
50,000 PEAS tokens scheduled for distribution
- Attacker monitors mempool and sees reward distribution transaction:
tokenRewards.depositRewards(PEAS, 50_000e18)
- Front-run: Attacker stakes 1,000,000 LP tokens
New Total Staked = 1,100,000 LP tokens
Attacker's Share = ~91% (1_000_000/1_100_000)
Honest Staker's Share = ~9% (100_000/1_100_000)
- Reward Distribution
50,000 PEAS distributed according to shares:
- Attacker receives: 45,454 PEAS (91%)
- Honest staker receives: 4,546 PEAS (9%)
- Back-run: Attacker unstakes 1,000,000 LP tokens
- Claims 45,454 PEAS
- Honest stakers who staked for months gets only 4,546 PEAS
Here are a few mitigation options:
- Implement minimum staking period lock
- Add vesting period for rewards
- Calculate rewards based on time-weighted position size