Core Contracts

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

Token Accounting Mismatch Between tick() and mintRewards() in RAACMinter

Summary

The RAACMinter contract implements a dynamic emissions schedule for minting and distributing RAAC tokens. However, a critical flaw exists in its reward distribution mechanism: there is a mismatch between where excess tokens are minted and where they are expected to reside. Specifically, the tick() function mints tokens to the StabilityPool while increasing the excessTokens counter in RAACMinter. Later, the mintRewards() function attempts to transfer tokens from RAACMinter's balance based on the excessTokens counter. This misalignment leads to failed transfers (insufficient balance) and interrupts the proper distribution of rewards.

Vulnerability Details

The flaw arises from the following logic:

1. In the tick() function:

  • After calculating the amount to mint (amountToMint), the contract increases excessTokens:

    This means that while excessTokens is increased, the newly minted tokens are sent directly to the StabilityPool, not RAACMinter.

    if (amountToMint > 0) {
    excessTokens += amountToMint; // Increase tracking variable
    lastUpdateBlock = currentBlock;
    raacToken.mint(address(stabilityPool), amountToMint); //@audit Mint tokens to StabilityPool
    emit RAACMinted(amountToMint);
    }

2 In the mintRewards() function:

  • The function is called by StabilityPool to distribute rewards:

    function mintRewards(address to, uint256 amount) external nonReentrant whenNotPaused {
    if (msg.sender != address(stabilityPool)) revert OnlyStabilityPool();
    uint256 toMint = excessTokens >= amount ? 0 : amount - excessTokens;
    excessTokens = excessTokens >= amount ? excessTokens - amount : 0;
    if (toMint > 0) {
    raacToken.mint(address(this), toMint); // Mints tokens to RAACMinter
    }
    raacToken.safeTransfer(to, amount); //@audit Tries to transfer tokens from RAACMinter’s balance
    emit RAACMinted(amount);
    }

Here, the function calculates toMint based on the excessTokens counter and then expects that any shortage in RAACMinter’s balance will be remedied by minting additional tokens. However, because tick() mints tokens directly to StabilityPool, RAACMinter’s balance is not increased accordingly. This will cause the subsequent safeTransfer() call to revert due to insufficient token balance.

PoC

Setup MockStabilityPool, I just added two functions to the existing MockStabilityPool contract because it is only the stablilityPool that can call the mintRewards functions in the RAACMinter contract.

I added:

  • setRAACMinter : to set the RAACMinter contract address, so as to be able call the mintRewards function.

  • mintRewards: to call the mintRewards function in the RAACMinters contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "../primitives/MockContractWithConfig.sol";
import "../../../core/tokens/RAACToken.sol";
import "../../../core/minters/RAACMinter/RAACMinter.sol";
contract MockStabilityPool is MockContractWithConfig {
RAACToken public raacToken;
RAACMinter public raacMinter;
constructor(address _raacToken) {
raacToken = RAACToken(_raacToken);
}
function getTotalDeposits() external view returns (uint256) {
bytes4 functionSig = this.getTotalDeposits.selector;
bytes memory returnData = mockConfig[functionSig];
if (returnData.length == 0) {
return 0; // Default return value if not mocked
}
return abi.decode(returnData, (uint256));
}
// Mock the getTotalDeposits function
function mockGetTotalDeposits(uint256 returnValue) external {
bytes4 functionSig = this.getTotalDeposits.selector;
mockConfig[functionSig] = abi.encode(returnValue);
}
function transfer(address recipient, uint256 amount) public returns (bool) {
require(raacToken.transfer(recipient, amount), "Transfer failed");
return true;
}
function setRAACMinter(address _raacMinter) public {
raacMinter = RAACMinter(_raacMinter);
}
function mintRewards(address recipient, uint256 amount) public returns (bool) {
raacMinter.mintRewards(recipient, amount);
return true;
}
}

the test its

it.only("should call the tick and mint rewards", async function () {
const initialSupply = await raacToken.totalSupply();
await stabilityPool.setRAACMinter(await raacMinter.getAddress());
await raacMinter.tick();
const newSupply = await raacToken.totalSupply();
console.log(
"owner raactokens balance",
await raacToken.balanceOf(owner.address)
);
console.log(
"user raactokens balance",
await raacToken.balanceOf(user1.address)
);
console.log(
"minter balance",
await raacToken.balanceOf(await raacMinter.getAddress())
);
console.log(
"stability pool balance",
await raacToken.balanceOf(stabilityPool)
);
console.log(
"excess token in the contrract before",
await raacMinter.excessTokens()
);
await stabilityPool.mintRewards(user1.address, 200); //call the mintRewards via the stabilityPool
const excesstokens = await raacMinter.excessTokens();
console.log("execesstokens", excesstokens);
console.log(
"owner raactokens balance",
await raacToken.balanceOf(owner.address)
);
console.log(
"user raactokens balance",
await raacToken.balanceOf(user1.address)
);
});

output of the test

it revert with the error:

'ERC20InsufficientBalance("0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", 0, 2)' Beacuse we are trying to send tokens from RAACMinter contract instead of the stabilityPool that we minted to in the tick function.

RAACMinter
owner raactokens balance 0n
user raactokens balance 0n
minter balance 0n
stability pool balance 395833333333333332n
excess token in the contrract before 395833333333333332n
1) should call the tick and mint rewards
1 failing
1) RAACMinter
should call the tick and mint rewards:
Error: VM Exception while processing transaction: reverted with custom error 'ERC20InsufficientBalance("0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", 0, 2)'
at RAACToken.transferOwnership (@openzeppelin/contracts/access/Ownable.sol:86)
at RAACToken._update (contracts/core/tokens/RAACToken.sol:201)
at RAACToken._transfer (@openzeppelin/contracts/token/ERC20/ERC20.sol:178)
at RAACToken.transfer (@openzeppelin/contracts/token/ERC20/ERC20.sol:111)
at RAACMinter.functionCallWithValue (@openzeppelin/contracts/utils/Address.sol:87)
at RAACMinter.verifyCallResultFromTarget (@openzeppelin/contracts/utils/Address.sol:120)
at RAACMinter.functionCallWithValue (@openzeppelin/contracts/utils/Address.sol:88)
at RAACMinter.functionCall (@openzeppelin/contracts/utils/Address.sol:71)
at RAACMinter._callOptionalReturn (@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol:96)
at RAACMinter.safeTransfer (@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol:37)
at RAACMinter.mintRewards (contracts/core/minters/RAACMinter/RAACMinter.sol:191)
at MockStabilityPool.mintRewards (contracts/mocks/core/pools/MockStabilityPool.sol:38)
at EdrProviderWrapper.request (node_modules/hardhat/src/internal/hardhat-network/provider/provider.ts:444:41)
at HardhatEthersSigner.sendTransaction (node_modules/@nomicfoundation/hardhat-ethers/src/signers.ts:125:18)
at send (node_modules/ethers/src.ts/contract/contract.ts:313:20)
at Proxy.mintRewards (node_modules/ethers/src.ts/contract/contract.ts:352:16)
at Context.<anonymous> (file:///audit/2025-02-raac/test/unit/core/minters/RAACMinter.test.js:89:5)

Impact

  • Reward Distribution Breakdown: MintRewards() will revert when it attempts to transfer tokens not held by RAACMinter, preventing users from receiving their intended rewards.

  • Protocol Incentive Failure: With rewards failing to distribute properly, the entire emissions mechanism is compromised, which can impact user participation and overall protocol trust.

  • It will affect the token’s economics and the incentives that drive the protocol.

Tools Used

Manual Code Review

Recommendations

Align Minting Destination with Accounting:

  • modify mintRewards() to transfer tokens directly from StabilityPool's balance rather than expecting them in RAACMinter.

function mintRewards(address to, uint256 amount) external nonReentrant whenNotPaused {
if (msg.sender != address(stabilityPool)) revert OnlyStabilityPool();
uint256 toMint = excessTokens >= amount ? 0 : amount - excessTokens;
excessTokens = excessTokens >= amount ? excessTokens - amount : 0;
if (toMint > 0) {
raacToken.mint(address(this), toMint);
}
// Instead of raacToken.safeTransfer(to, amount);
// Use safeTransferFrom to pull tokens from the StabilityPool
+ raacToken.safeTransferFrom(address(stabilityPool), to, amount);
emit RAACMinted(amount);
}

Updates

Lead Judging Commences

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

RAACMinter wrong excessTokens accounting in tick function

inallhonesty Lead Judge about 1 month 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.