Core Contracts

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

Reward (RAAC) tokens will be stucked in the StabilityPool contract

Summary

Reward tokens will be permanently stucked in the StabilityPool contract

Vulnerability Details

RAAC tokens are minted into the StabilityPool whenever deposit() or withdraw() is called. In the deposit() function, after executing its logic, it calls  _mintRAACRewards() which is defined as :

function _mintRAACRewards() internal {
   if ( address(raacMinter) != address(0)) {
   @>>  raacMinter.tick();  
  }  
 }

The raacMinter.tick() function shown at the part marked @>> mints new RAAC tokens by calling the RAAC token’s mint function. As a result, the StabilityPool ends up holding a balance of RAAC tokens that are meant as rewards.

When a user later calls withdraw() , the StabilityPool attempts to calculate and distribute RAAC rewards. The withdraw() function contains the following snippet:

function withdraw(uint256 deCRVUSDAmount) external nonReentrant whenNotPaused validAmount(deCRVUSDAmount) {
_update();
if (deToken.balanceOf(msg.sender) < deCRVUSDAmount) revert InsufficientBalance();
uint256 rcrvUSDAmount = calculateRcrvUSDAmount(deCRVUSDAmount);
@>>1 uint256 raacRewards = calculateRaacRewards(msg.sender);
if (userDeposits[msg.sender] < rcrvUSDAmount) revert InsufficientBalance();
userDeposits[msg.sender] -= rcrvUSDAmount;
if (userDeposits[msg.sender] == 0) {
delete userDeposits[msg.sender];
}
deToken.burn(msg.sender, deCRVUSDAmount);
rToken.safeTransfer(msg.sender, rcrvUSDAmount);
@>>2 if (raacRewards > 0) {
raacToken.safeTransfer(msg.sender, raacRewards);
}
}

Reward calculation is performed in calculateRaacRewards() showed at the part marked @>>1 in the function above, which is implemented as follows:

function calculateRaacRewards(address user) public view returns (uint256) {
uint256 userDeposit = userDeposits[user];
uint256 totalDeposits = deToken.totalSupply();
uint256 totalRewards = raacToken.balanceOf(address(this));
//@audit-info : if total deposit is less than 1e6 return 0
if (totalDeposits < 1e6) return 0;
return (totalRewards * userDeposit) / totalDeposits;
}

There is a hardcoded threshold here: if totalDeposits is less than 1e6, the function will immediately return 0

Since the withdraw() function checks if(raacRewards > 0) before transferring tokens, the minted RAAC tokens will remain in the StabilityPool contract permanently.

Thus, because the rewards calculation returns 0, no RAAC tokens are ever transferred out to users, causing the tokens to be permanently locked in the StabilityPool contract.

Proof Of Concept

See how to intigrate foundry to hardhat project
.Create a new file POC.t.sol in project /test/ folder . Paste the poc and run forge test --mt test_POC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {console} from "forge-std/console.sol";
import {LendingPool} from "../contracts/core/pools/LendingPool/LendingPool.sol";
import {StabilityPool} from "../contracts/core/pools/StabilityPool/StabilityPool.sol";
import {RAACNFT} from "../contracts/core/tokens/RAACNFT.sol";
import {RAACHousePrices} from "contracts/core/primitives/RAACHousePrices.sol";
import {RToken} from "../contracts/core/tokens/RToken.sol";
import {DebtToken} from "../contracts/core/tokens/DebtToken.sol";
import {crvUSDToken} from "contracts/mocks/core/tokens/crvUSDToken.sol";
import {ILendingPool} from "contracts/interfaces/core/pools/LendingPool/ILendingPool.sol";
import "../contracts/core/tokens/DEToken.sol";
import "../contracts/core/tokens/RAACToken.sol";
import {RAACMinter} from "../contracts/core/minters/RAACMinter/RAACMinter.sol";
contract Audit_Test is Test {
LendingPool public lendingPool;
StabilityPool public stabilityPool;
RAACNFT public raacNFT;
RAACHousePrices public priceOracle;
RToken public rToken;
DebtToken public debtToken;
crvUSDToken public crvusd;
DEToken public deToken;
RAACToken public raacToken;
RAACMinter public raacMinter;
address owner = makeAddr("owner");
uint256 constant INITIAL_PRIME_RATE = 1e27; // 1 RAY
function setUp() public {
vm.startPrank(owner);
// Base tokens
crvusd = new crvUSDToken(owner);
crvusd.setMinter(owner);
// Price oracle
priceOracle = new RAACHousePrices(owner);
priceOracle.setOracle(owner);
// NFT
raacNFT = new RAACNFT(address(crvusd), address(priceOracle), owner);
// Pool tokens
rToken = new RToken("RToken", "RT", owner, address(crvusd));
debtToken = new DebtToken("DebtToken", "DT", owner);
deToken = new DEToken("DEToken", "DET", owner, address(rToken));
// Deploy LendingPool
uint256 initialPrimeRate = 0.1e27;
lendingPool = new LendingPool(
address(crvusd),
address(rToken),
address(debtToken),
address(raacNFT),
address(priceOracle),
initialPrimeRate
);
// Deploy RAAC token with correct tax rates
raacToken = new RAACToken(owner, 1000, 1000);
// Deploy StabilityPool first
stabilityPool = new StabilityPool(owner);
// Deploy RAACMinter last
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
owner
);
// Initialize StabilityPool
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(crvusd),
address(lendingPool)
);
// Setup cross-contract references
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
deToken.transferOwnership(address(stabilityPool));
lendingPool.setStabilityPool(address(stabilityPool));
raacToken.setMinter(address(raacMinter));
raacToken.manageWhitelist(address(stabilityPool), true);
vm.stopPrank();
}
function test_POC() public {
uint256 user1Amount = 13187;
uint256 user2Amount = 11594;
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
// Mint and approve for users
vm.startPrank(owner);
crvusd.mint(user1, user1Amount);
crvusd.mint(user2, user2Amount);
vm.stopPrank();
// User1 deposits
vm.startPrank(user1);
crvusd.approve(address(lendingPool), user1Amount);
lendingPool.deposit(user1Amount);
rToken.approve(address(stabilityPool), user1Amount);
stabilityPool.deposit(user1Amount);
vm.stopPrank();
// User2 deposits
vm.startPrank(user2);
crvusd.approve(address(lendingPool), user2Amount);
lendingPool.deposit(user2Amount);
rToken.approve(address(stabilityPool), user2Amount);
stabilityPool.deposit(user2Amount);
vm.stopPrank();
// Wait 7 days
vm.warp(block.timestamp + 7 days);
vm.roll(block.number + 1);
// Update emission rate and accumulate rewards
raacMinter.updateEmissionRate();
vm.warp(block.timestamp + 1 days);
vm.roll(block.number + 1);
raacMinter.tick();
vm.prank(user1);
stabilityPool.withdraw(user1Amount);
vm.prank(user2);
stabilityPool.withdraw(user2Amount);
assertEq(rToken.balanceOf(user1), user1Amount);
assertEq(rToken.balanceOf(user2), user2Amount);
assertEq(raacToken.balanceOf(user1), 0);
assertEq(raacToken.balanceOf(user2), 0);
assertEq(raacToken.balanceOf(address(stabilityPool)), 306249999999999996);
}
}

Impact

Permanently stuck reward tokens in the contract

Tools Used

Manual Review

Recommendations

Implement a function to withdraw excess tokens from the contract if no rewards are being distributed to the user when deposit is < 1e6

Updates

Lead Judging Commences

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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