Core Contracts

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

Treasury Over-Allocation vulnerability. The allocateFunds function takes a recipient and an amount but doesn't specify a token.

Summary:

The allocateFunds function in Treasury.sol allows allocators to assign arbitrary amounts to recipients without checking if the treasury holds sufficient funds. This creates a discrepancy between allocated amounts and actual balances, leading to invalid accounting.

function allocateFunds(
address recipient,
uint256 amount
) external override onlyRole(ALLOCATOR_ROLE) {
if (recipient == address(0)) revert InvalidRecipient();
if (amount == 0) revert InvalidAmount();
_allocations[msg.sender][recipient] = amount;
emit FundsAllocated(recipient, amount);
}

Vulnerability Details:

The allocateFunds function takes a recipient and an amount but doesn't specify a token. The contract tracks a totalValue and balances per token, but allocations aren't tied to any specific token. It only tracks a generic amount and increments _totalValue, which aggregates all token balances. So the problem here is that when an allocator calls allocateFunds, they can allocate any amount without considering which tokens are available.

For example,

  • Treasury holds 100 RAAC and 200 USDC (total value = 300).

  • An allocator can "allocate" 300 RAAC (even though only 100 RAAC exists).

  • The allocation succeeds because _totalValue (300) ≥ allocated amount (300), but the actual RAAC balance is insufficient.

The contract allows this because it only checks the totalValue across all tokens, not individual token balances. This means allocations are based on the total value, not actual token availability.

POC

// Test scenario:
// 1. Treasury holds 100 RAAC tokens.
// 2. Allocator assigns 150 RAAC to a recipient.
// 3. Withdrawal fails due to insufficient balance.
// Setup
address admin = address(0x123);
Treasury treasury = new Treasury(admin);
// Deposit 100 RAAC tokens
address RAAC = address(0x456);
uint256 depositAmount = 100e18;
deal(RAAC, address(this), depositAmount); // Give test contract tokens
IERC20(RAAC).approve(address(treasury), depositAmount);
treasury.deposit(RAAC, depositAmount); // Treasury now has 100 RAAC
// Allocate 150 RAAC (exceeds actual balance)
address recipient = address(0x789);
vm.prank(admin); // Admin has ALLOCATOR_ROLE
treasury.allocateFunds(recipient, 150e18);
// Verify allocation
assertEq(treasury.getAllocation(admin, recipient), 150e18);
// Attempt withdrawal (fails)
vm.prank(admin); // Admin has MANAGER_ROLE
vm.expectRevert("InsufficientBalance");
treasury.withdraw(RAAC, 150e18, recipient); // Fails: Only 100 RAAC exists
[Test Steps] │ [Result] │ [Expected?]
───────────────────────┼────────────────┼─────────────
Deposit 100 RAAC │ Success ✅ │ Yes
Allocate 150 RAAC │ Success ✅ │ No (Bug!)
Verify Allocation │ Passes ✅ │ No (Bug!)
Withdraw 150 RAAC │ Reverts ❌ │ Yes (But only due to insufficient balance)
  • Allocation Succeeds Incorrectly: The test exposes the critical flaw where allocations are allowed to exceed actual token balances.

  • Withdrawal Fails Correctly: The withdrawal properly reverts due to insufficient funds, but this is only because the treasury lacks a validation mechanism during allocation.

  • Impact Demonstrated: The test proves that the treasury’s accounting is broken, as it permits allocations that cannot be fulfilled.

Impact:

If off-chain systems or users see an allocation, they might assume the Treasury has the specific tokens to back it up. But when they try to withdraw, the transaction fails because the actual token balance is insufficient. This can lead to confusion, failed transactions, and loss of trust in the protocol.

Tools Used: Manual Review and Foundry

Recommendations:

  1. Modify allocateFunds to track allocations per token and validate balances:

// Updated function signature
function allocateFunds(
address token, // Specify token being allocated
address recipient,
uint256 amount
) external onlyRole(ALLOCATOR_ROLE) {
require(token != address(0), "Invalid token");
require(recipient != address(0), "Invalid recipient");
require(amount > 0, "Invalid amount");
require(_balances[token] >= amount, "Insufficient balance"); // Critical check
_allocations[token][msg.sender][recipient] = amount;
emit FundsAllocated(token, recipient, amount);
}

2.Adjust mappings to track allocations per token:

// Before
mapping(address => mapping(address => uint256)) private _allocations;
// After
mapping(address => mapping(address => mapping(address => uint256))) private _allocations;
// [token][allocator][recipient] => amount


3. Ensure withdrawals reference token-specific allocations:

function withdraw(
address token,
uint256 amount,
address recipient
) external override nonReentrant onlyRole(MANAGER_ROLE) {
require(_allocations[token][msg.sender][recipient] >= amount, "Over-allocation");
}

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

Treasury::allocateFunds doesn't say what token you are actually allocating, doesn't check balances, or existing allocations to other recipients

Support

FAQs

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