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;
lastUpdateBlock = currentBlock;
raacToken.mint(address(stabilityPool), amountToMint);
emit RAACMinted(amountToMint);
}
2 In the mintRewards() function:
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.
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;
}
return abi.decode(returnData, (uint256));
}
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);
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:
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:
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);
}
+ raacToken.safeTransferFrom(address(stabilityPool), to, amount);
emit RAACMinted(amount);
}