Core Contracts

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

Missing Validation in _setLastUpdateBlock

Summary

The _setLastUpdateBlock internal function in the RAACMinter contract is responsible for updating the lastUpdateBlock variable, which tracks the block number of the last token minting event triggered by the tick function. While the function includes a check to ensure newLastUpdateBlock does not exceed the current block number (block.number), it lacks a validation to prevent setting newLastUpdateBlock to a value less than the existing lastUpdateBlock. This omission allows an authorized caller (e.g., someone with the PAUSER_ROLE) to "rewind" lastUpdateBlock, potentially leading to over-minting of RAAC tokens when tick is subsequently called.

function _setLastUpdateBlock(uint256 newLastUpdateBlock) internal {
if (newLastUpdateBlock > block.number) revert InvalidBlockNumber();
lastUpdateBlock = newLastUpdateBlock == 0 ? block.number : newLastUpdateBlock;
emit LastUpdateBlockSet(lastUpdateBlock);
}
  • The function is called by a pause, unpause, and emergency shutdown when updateLastBlock is true.

  • The tick function calculates the number of blocks since the last update as blocksSinceLastUpdate = currentBlock - lastUpdateBlock and mints tokens proportional to this difference (emissionRate * blocksSinceLastUpdate).

The tick function calculates the number of blocks since the last update as blocksSinceLastUpdate = currentBlock - lastUpdateBlock and mints tokens proportional to this difference (emissionRate * blocksSinceLastUpdate).

Impact

: By setting lastUpdateBlock to a lower value than its current state, the difference blocksSinceLastUpdate increases artificially when tick is called. This results in minting more RAAC tokens than intended, inflating the token supply beyond the designed emission schedule.

Root cause

The absence of a check ensuring newLastUpdateBlock >= lastUpdateBlock allows rewinding of lastUpdateBlock.

Proof of concept

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("RAACMinter - Rewind Protection", function () {
let raacMinter, owner, dummyStabilityPool, dummyLendingPool, dummyRAACToken;
beforeEach(async function () {
[owner] = await ethers.getSigners();
const DummyFactory = await ethers.getContractFactory("DummyContract");
dummyStabilityPool = await DummyFactory.deploy();
await dummyStabilityPool.deployed();
dummyLendingPool = await DummyFactory.deploy();
await dummyLendingPool.deployed();
dummyRAACToken = await DummyFactory.deploy();
await dummyRAACToken.deployed();
const RAACMinterFactory = await ethers.getContractFactory("RAACMinter");
raacMinter = await RAACMinterFactory.deploy(
dummyRAACToken.address,
dummyStabilityPool.address,
dummyLendingPool.address,
owner.address
);
await raacMinter.deployed();
});
it("should revert when trying to rewind lastUpdateBlock", async function () {
const currentLastUpdate = await raacMinter.lastUpdateBlock();
// Attempt to set to a lower block number
await expect(
raacMinter.pause(true, currentLastUpdate.sub(100))
).to.be.revertedWithCustomError(raacMinter, "CannotRewindLastUpdateBlock");
});
it("should allow update when setting newLastUpdateBlock equal or higher", async function () {
const currentLastUpdate = await raacMinter.lastUpdateBlock();
// Passing 0 uses the current block, which is >= currentLastUpdate.
await expect(raacMinter.pause(true, 0)).to.emit(raacMinter, "LastUpdateBlockSet");
const newLastUpdate = await raacMinter.lastUpdateBlock();
expect(newLastUpdate).to.be.gte(currentLastUpdate);
});
it("should mint tokens correctly after a valid update", async function () {
// Unpause without update to continue normal operations
await raacMinter.unpause(false, 0);
const prevLastUpdate = await raacMinter.lastUpdateBlock();
// Mine extra blocks
const blocksToMine = 10;
for (let i = 0; i < blocksToMine; i++) {
await ethers.provider.send("evm_mine", []);
}
// Call tick to trigger minting; dummy logic will update lastUpdateBlock accordingly.
await raacMinter.tick();
const newLastUpdate = await raacMinter.lastUpdateBlock();
expect(newLastUpdate.sub(prevLastUpdate)).to.equal(blocksToMine);
});
});

Example scenario

  1. Current state: lastUpdateBlock = 1000, block.number = 1500, emissionRate = 1e18 RAAC per block.

  2. pause(true, 500) is called by a PAUSER_ROLE holder, setting lastUpdateBlock = 500.

  3. Later, at block.number = 1600, tick is called:

  • blocksSinceLastUpdate = 1600 - 500 = 1100.

  • Minted tokens= 1e18 * 1100 = 1100e18 Raac.

  1. Without rewinding, if lastUpdateBlock remained 1000:

  • blocksSinceLastUpdate = 1600 - 1000 = 600.

  • Minted tokens = 1e18 * 600 = 600e18 RAAC.

An extra 500e18 RAAC is minted due to the rewind, exceeding the intended emission.

Recommendations

function _setLastUpdateBlock(uint256 newLastUpdateBlock) internal {
if (newLastUpdateBlock > block.number) revert InvalidBlockNumber();
if (newLastUpdateBlock < lastUpdateBlock) revert CannotRewindLastUpdateBlock();
lastUpdateBlock = newLastUpdateBlock == 0 ? block.number : newLastUpdateBlock;
emit LastUpdateBlockSet(lastUpdateBlock);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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