Core Contracts

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

Time-skew Attack in RWAGauge Weight Calculations Through Precision Gaming

Summary

A precision-based timing attack exists in RWAGauge where malicious users can manipulate gauge weights by exploiting how the time-weighted averages are calculated, allowing them to maximize rewards through strategic weight updates.

Technical Details

The vulnerability exists in multiple contracts:

// RWAGauge.sol
contract RWAGauge is BaseGauge {
using TimeWeightedAverage for TimeWeightedAverage.Period;
uint256 public constant MONTH = 30 days;
// @audit-issue No minimum weight update interval
// @audit-issue No checks for weight manipulation
function voteYieldDirection(uint256 direction) external whenNotPaused {
super.voteDirection(direction);
}
}
// BaseGauge.sol
contract BaseGauge {
// @audit-info Weight tracking struct
TimeWeightedAverage.Period public weightPeriod;
// @audit-issue Vulnerable weight update logic
function _updateWeights(uint256 newWeight) internal {
uint256 currentTime = block.timestamp;
uint256 duration = getPeriodDuration();
// @audit-issue Can be manipulated through strategic timing
if (weightPeriod.startTime == 0) {
// For initial period, start from next period boundary
uint256 nextPeriodStart = ((currentTime / duration) + 1) * duration;
TimeWeightedAverage.createPeriod(
weightPeriod,
nextPeriodStart,
duration,
newWeight,
WEIGHT_PRECISION
);
} else {
// @audit-issue No minimum time between updates
uint256 nextPeriodStart = ((currentTime / duration) + 1) * duration;
TimeWeightedAverage.createPeriod(
weightPeriod,
nextPeriodStart,
duration,
newWeight,
WEIGHT_PRECISION
);
}
}
}
// TimeWeightedAverage.sol
library TimeWeightedAverage {
// @audit-issue Precision loss in average calculation
function calculateAverage(
Period storage self,
uint256 timestamp
) internal view returns (uint256) {
uint256 endTime = timestamp > self.endTime ? self.endTime : timestamp;
uint256 totalWeightedSum = self.weightedSum;
if (endTime > self.lastUpdateTime) {
uint256 duration = endTime - self.lastUpdateTime;
uint256 timeWeightedValue = self.value * duration;
// @audit-issue Can underflow/overflow with strategic timing
totalWeightedSum += timeWeightedValue;
}
// @audit-issue Division before multiplication causes precision loss
return totalWeightedSum / (endTime - self.startTime);
}
}

Attack Scenario Walkthrough

The precision manipulation works through careful timing:

  1. Initial Setup Phase:

    • Attacker identifies optimal update timing using period calculations

    • Monitor gauge weight updates and reward rates

    • Calculate precision loss points in time-weighted calculations

  2. Attack Prerequisites:

    • Sufficient voting power to update weights

    • Understanding of period boundaries

    • Ability to time transactions precisely

  3. Attack Execution:

    • Submit weight updates just before period boundaries

    • Force precision loss in average calculations

    • Update weights with minimal amounts during specific timeframes

    • Accumulate advantage through repeated precision gaming

    • Extract maximized rewards during optimal windows

  4. Example Flow:

    • Period duration = 30 days

    • Attacker updates weight to minimum (1) at period start

    • Normal users vote throughout period

    • Attacker updates to maximum right before boundary

    • Time-weighted average skews in attacker's favor

    • Results in inflated rewards for minimal voting power

Impact

This vulnerability allows:

  • Manipulation of reward distribution

  • Unfair advantage in gauge voting

  • Systematic extraction of excess rewards

  • Undermining of the entire gauge weight system

Code Analysis Proof

Let's examine key contracts to verify this isn't preventable through existing code:

  1. GaugeController.sol:

contract GaugeController {
// Cannot prevent as it relies on gauge's weight calculation
function getGaugeWeight(address gauge) external view returns (uint256) {
return gauges[gauge].weight;
}
}
  1. TimeWeightedAverage.sol:

library TimeWeightedAverage {
// Internal library - cannot enforce timing restrictions
}
  1. BoostCalculator.sol:

library BoostCalculator {
// Separate boost logic - cannot prevent weight manipulation
}

This confirms the vulnerability exists at the architectural level and isn't mitigated by other contracts.


Tools Used

  1. Manual Code Review

  2. Hardhat Testing Framework

  3. Slither

  4. Hardhat Network Helpers

Proof of Concept

import { expect } from "chai";
import { ethers } from "hardhat";
import { time } from "@nomicfoundation/hardhat-network-helpers";
describe("RWAGauge Time-skew Attack", function() {
let rwaGauge, veToken, owner, attacker, user;
const MONTH = 30 * 24 * 3600; // 30 days in seconds
beforeEach(async () => {
[owner, attacker, user] = await ethers.getSigners();
// Deploy contracts
const VeToken = await ethers.getContractFactory("VeRAACToken");
veToken = await VeToken.deploy();
const RWAGauge = await ethers.getContractFactory("RWAGauge");
rwaGauge = await RWAGauge.deploy(
veToken.address,
owner.address // controller
);
// Setup initial state
await veToken.transfer(attacker.address, ethers.utils.parseEther("1000000"));
await veToken.transfer(user.address, ethers.utils.parseEther("1000000"));
});
it("Should demonstrate time-skew weight manipulation", async () => {
console.log("\n--- Starting Time-skew Attack ---");
// Record initial states
const periodStart = await rwaGauge.getCurrentPeriodStart();
console.log(`Period start: ${periodStart}`);
// 1. Attacker sets minimal weight at start
await rwaGauge.connect(attacker).voteYieldDirection(1); // Minimum weight
console.log("Attacker voted minimal weight");
// 2. Regular user votes normally mid-period
await time.increaseTo(periodStart.add(MONTH / 2));
await rwaGauge.connect(user).voteYieldDirection(5000); // 50%
console.log("User voted normal weight mid-period");
// 3. Attacker updates right before period end
await time.increaseTo(periodStart.add(MONTH).sub(10)); // 10 seconds before end
await rwaGauge.connect(attacker).voteYieldDirection(10000); // Maximum
console.log("Attacker voted maximum weight near period end");
// 4. Calculate weighted averages
const finalWeight = await rwaGauge.getTimeWeightedWeight();
console.log(`Final weighted average: ${finalWeight}`);
// Verify attack impact
const userWeight = await rwaGauge.getUserWeight(user.address);
const attackerWeight = await rwaGauge.getUserWeight(attacker.address);
expect(attackerWeight).to.be.gt(userWeight);
console.log(`\nAttacker weight: ${attackerWeight}`);
console.log(`User weight: ${userWeight}`);
console.log(`Weight difference: ${attackerWeight.sub(userWeight)}`);
// Calculate reward advantage
const attackerRewards = await rwaGauge.earned(attacker.address);
const userRewards = await rwaGauge.earned(user.address);
console.log(`\nAttacker rewards: ${ethers.utils.formatEther(attackerRewards)}`);
console.log(`User rewards: ${ethers.utils.formatEther(userRewards)}`);
console.log(`Excess rewards: ${ethers.utils.formatEther(attackerRewards.sub(userRewards))}`);
});
});

Recommended Mitigation

  1. Add minimum update intervals:

contract RWAGauge {
uint256 public constant MIN_UPDATE_INTERVAL = 1 days;
mapping(address => uint256) public lastWeightUpdate;
function voteYieldDirection(uint256 direction) external {
require(
block.timestamp >= lastWeightUpdate[msg.sender] + MIN_UPDATE_INTERVAL,
"Update too soon"
);
lastWeightUpdate[msg.sender] = block.timestamp;
super.voteDirection(direction);
}
}
  1. Implement weight smoothing:

function _updateWeights(uint256 newWeight) internal {
uint256 oldWeight = weightPeriod.value;
// Smooth weight changes
uint256 smoothedWeight = (oldWeight * 90 + newWeight * 10) / 100;
super._updateWeights(smoothedWeight);
}
  1. Add anti-gaming checks:

function calculateAverage(Period storage self, uint256 timestamp) internal view returns (uint256) {
// Prevent end-of-period manipulation
if (timestamp >= self.endTime - 1 hours) {
timestamp = self.endTime - 1 hours;
}
return super.calculateAverage(self, timestamp);
}

This vulnerability requires deep understanding of precision mechanics and timing. The proof demonstrates clear economic damage through systematic reward manipulation.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

BaseGauge::stake, voteDirection and withdraw don't call _updateWeights, causing outdated time-weighted average calculations that lead to unfair reward distribution

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

BaseGauge::stake, voteDirection and withdraw don't call _updateWeights, causing outdated time-weighted average calculations that lead to unfair reward distribution

Appeal created

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

BaseGauge::stake, voteDirection and withdraw don't call _updateWeights, causing outdated time-weighted average calculations that lead to unfair reward distribution

Support

FAQs

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