Core Contracts

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

StabilityPool's `depositRAACFromPool` Will Always Revert Due to RAAC Token's Fee-on-Transfer Mechanism

Summary

The depositRAACFromPool function in StabilityPool fails to account for RAAC token's fee-on-transfer mechanism, causing all deposits to revert due to a strict balance check.

Vulnerability Details

The depositRAACFromPool function implements a balance check to verify the exact amount of RAAC tokens received. However, RAAC token implements both swap and burn taxes through its _update function which is called on every transfer;
The RAAC token applies: A swap tax (default 1%), A burn tax (default 0.5%) thus Total tax of 1.5% on transfers. In RAACToken contract;

function _update(address from, address to, uint256 amount) internal virtual override {
// ...
uint256 totalTax = amount.percentMul(baseTax);
uint256 burnAmount = totalTax * burnTaxRate / baseTax;
super._update(from, feeCollector, totalTax - burnAmount);
super._update(from, address(0), burnAmount);
super._update(from, to, amount - totalTax);
}

When depositRAACFromPool transfers RAAC tokens:

uint256 preBalance = raacToken.balanceOf(address(this));
raacToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 postBalance = raacToken.balanceOf(address(this));
// @audit will always revert
if (postBalance != preBalance + amount) revert InvalidTransfer();

Due to the tax mechanism, the actual received amount will always be less than the transferred amount:

  • For a transfer of 100 RAAC:

  • 1.5 RAAC gets deducted (1% swap + 0.5% burn)

  • Only 98.5 RAAC arrives at the destination

Therefore, postBalance will always be less than preBalance + amount, causing the check to revert.

PoC

// 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 {RAACToken} from "contracts/core/tokens/RAACToken.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 {
constructor(string memory name) ERC20(name, name) {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract RAACTokenFoTTest is Test {
address private liquidity_pool = makeAddr("liquidity_pool");
address private bob = makeAddr("bob");
StabilityPool private stabilityPool;
StabilityPool private implementation;
ERC1967Proxy private proxy;
RAACToken private raacToken;
address private mockRToken = address(new MockERC20("RToken"));
address private mockDeToken = address(new MockERC20("DeToken"));
address private mockCrvUSDToken = address(new MockERC20("crvToken"));
address private mockRaacMinter = makeAddr("raac_minter");
address private mockLendingPool = makeAddr("lending_pool");
function setUp() public {
// Deploy RAAC token with default tax rates
// 1% swap tax + 0.5% burn tax
raacToken = new RAACToken(
address(this), // owner
100, // 1% swap tax
50 // 0.5% burn tax
);
// Deploy StabilityPool implementation and proxy
implementation = new StabilityPool(address(this));
// for all other addresses except raacToken, I have used mocks (for simplicity) since they are not relevant to this test.
bytes memory initData = abi.encodeWithSelector(
StabilityPool.initialize.selector,
mockRToken,
mockDeToken,
address(raacToken),
mockRaacMinter,
mockCrvUSDToken,
mockLendingPool
);
proxy = new ERC1967Proxy(address(implementation), initData);
stabilityPool = StabilityPool(address(proxy));
// Set up liquidity pool (liquidity_pool)
stabilityPool.setLiquidityPool(liquidity_pool);
raacToken.setMinter(mockRaacMinter);
// Mint RAAC tokens to liquidity_pool (liquidity pool)
vm.prank(mockRaacMinter);
raacToken.mint(liquidity_pool, 1000 ether);
// Set fee collector for RAAC token
raacToken.setFeeCollector(bob);
}
function testDepositRAACFromPoolReverts() public {
vm.startPrank(liquidity_pool);
// Approve StabilityPool to spend liquidity_pool's RAAC
raacToken.approve(address(stabilityPool), raacToken.balanceOf(liquidity_pool));
// Try to deposit RAAC - this will revert
vm.expectRevert(abi.encodeWithSignature("InvalidTransfer()"));
stabilityPool.depositRAACFromPool(500 ether);
vm.stopPrank();
}
}

Impact

The depositRAACFromPool function is completely unusable (DoS), breaking a core functionality of the protocol where the LiquidityPool should be able to provide RAAC rewards to the StabilityPool.

Tools Used

Manual Review

Recommendations

Modify the balance check to account for the balance differences rather than just the sent amount:

function depositRAACFromPool(uint256 amount) external onlyLiquidityPool validAmount(amount) {
uint256 preBalance = raacToken.balanceOf(address(this));
raacToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 actualReceived = raacToken.balanceOf(address(this)) - preBalance;
if (actualReceived == 0) revert InvalidTransfer();
emit RAACDepositedFromPool(msg.sender, actualReceived);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
inallhonesty Lead Judge 4 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.