Core Contracts

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

Inconsistent Minting Destination and Reward Distribution

Root Cause

The RAACMinter contract mints RAAC tokens in two different ways:

  • In the tick() function, tokens are minted directly to the StabilityPool address while the contract’s internal variable excessTokens is incremented.

  • In the mintRewards() function, tokens are expected to be available in the RAACMinter’s own balance (or minted there if insufficient) so they can be transferred as rewards.

Because tokens minted in tick() never reside in RAACMinter’s balance (they go directly to StabilityPool), the internal accounting via excessTokens becomes inconsistent. Consequently, when mintRewards() is later invoked, the RAACMinter may attempt a transfer from an empty balance or mint additional tokens unnecessarily, leading to reward distribution anomalies.


Attack/Issue Path

  1. Token Minting Mismatch:

    • Tick Process:

      • The tick() function computes an amount to mint and calls:

        raacToken.mint(address(stabilityPool), amountToMint);
      • It then increases the internal excessTokens by amountToMint.

    • Reward Distribution:

      • Later, when the StabilityPool (authorized caller) invokes mintRewards(), the function uses excessTokens to decide whether to mint additional tokens and then calls:

        raacToken.safeTransfer(to, amount);
      • However, since RAACMinter’s own balance wasn’t increased (the minted tokens reside at the StabilityPool), the transfer fails or triggers additional minting.

  2. Exploitation Impact:

    • This inconsistency may lead to double-minting or reward transfers failing because the RAACMinter contract does not hold the tokens it believes it has “available” per its excessTokens variable.


Foundry PoC

Below is a fully working Foundry test that demonstrates the issue:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "openzeppelin-contracts/token/ERC20/ERC20.sol";
import "openzeppelin-contracts/utils/Context.sol";
// Minimal mock RAAC token with mint functionality and dummy tax functions.
contract MockRAACToken is ERC20 {
constructor() ERC20("MockRAAC", "MRAAC") {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
// Dummy implementations to satisfy RAACMinter calls.
function setSwapTaxRate(uint256) external {}
function setBurnTaxRate(uint256) external {}
function setFeeCollector(address) external {}
}
// Dummy StabilityPool; its address is used to satisfy caller checks.
contract MockStabilityPool {}
// Dummy LendingPool with minimal functionality.
contract MockLendingPool {
function getNormalizedDebt() external pure returns (uint256) {
return 1000;
}
}
// Import the RAACMinter contract (assume the correct path).
import "../../../contracts/core/minters/RAACMinter/RAACMinter.sol";
contract RAACMinterTest is Test {
RAACMinter public minter;
MockRAACToken public token;
MockStabilityPool public stabilityPool;
MockLendingPool public lendingPool;
address public owner = address(1);
address public dummyUser = address(2);
function setUp() public {
token = new MockRAACToken();
stabilityPool = new MockStabilityPool();
lendingPool = new MockLendingPool();
// Deploy RAACMinter with initial owner as `owner`
minter = new RAACMinter(address(token), address(stabilityPool), address(lendingPool), owner);
}
function testInconsistentMinting() public {
// Simulate passage of blocks so tick() mints tokens.
uint256 initialBlock = block.number;
vm.roll(initialBlock + 100);
// Call tick() to mint tokens.
minter.tick();
// Verify that tick() increased excessTokens.
uint256 excess = minter.excessTokens();
assertGt(excess, 0);
// Check balances:
// - Tokens minted in tick() go to StabilityPool.
uint256 stabilityPoolBalance = token.balanceOf(address(stabilityPool));
uint256 minterBalance = token.balanceOf(address(minter));
assertEq(stabilityPoolBalance, excess, "StabilityPool balance must equal excess tokens minted");
assertEq(minterBalance, 0, "RAACMinter balance must be zero (tokens minted to StabilityPool)");
// Attempt to claim rewards: StabilityPool calls mintRewards() for dummyUser.
// Request an amount less than excess so that no additional minting is triggered.
uint256 rewardAmount = excess / 2;
vm.prank(address(stabilityPool)); // Ensure msg.sender == stabilityPool.
vm.expectRevert(); // Expect revert due to insufficient balance in RAACMinter.
minter.mintRewards(dummyUser, rewardAmount);
}
}


Fix:
Have both functions mint tokens to the same address—ideally the RAACMinter contract itself—so that the internal excessTokens variable correctly reflects tokens available for distribution. In short, change the destination in tick() from address(stabilityPool) to address(this).

Updates

Lead Judging Commences

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

RAACMinter wrong excessTokens accounting in tick function

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

RAACMinter wrong excessTokens accounting in tick function

Support

FAQs

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

Give us feedback!