Core Contracts

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

Premature division in reward calculation leads to reduced payout precision

Description

The GaugeController::_calculateReward function performs division operations before final multiplication, causing precision loss that results in underpaid rewards. The current implementation calculates gauge and type shares using intermediate divisions (WEIGHT_PRECISION denominator), truncating fractional values before the final reward calculation. This approach magnifies rounding errors when multiple divisions occur sequentially.

Proof of Concept

  • Configure test environment with:

    • Period emission: 123,456,789 tokens

    • Gauge weight: 1,234

    • Total weight: 5,555

    • Type weight: 6,666

    • MAX_TYPE_WEIGHT: 10,000

  • Original calculation:

// GaugeController.sol
uint256 gaugeShare = (1234 * 10000) / 5555 = 2221 (rounded down)
uint256 typeShare = (6666 * 10000) / 10000 = 6666
reward = (123456789 * 2221 * 6666) / (10000 * 10000) = ~18,278,007
  • Improved calculation:

// Proposed fix
reward = (123456789 * 1234 * 6666) / (5555 * 10000) = ~18,281,481

In this example, 18,281,481 - 18,278,007 = ~3474 tokens lost as a result.

Add to test file GaugeController.test.js:

it("demonstrates reward calculation precision loss", async () => {
// Deploy new gauge to avoid conflicts with existing setup
const NewRWAGauge = await ethers.getContractFactory("RWAGauge");
const newRwaGauge = await NewRWAGauge.deploy(
await rewardToken.getAddress(),
await veRAACToken.getAddress(),
await gaugeController.getAddress()
);
await newRwaGauge.waitForDeployment();
// Add gauge with specific weight
await gaugeController.connect(gaugeAdmin).addGauge(
await newRwaGauge.getAddress(),
0, // RWA type
1234n // Initial weight (BigInt)
);
// Add dummy gauge to create total weight of 5555
const DummyGauge = await ethers.getContractFactory("RWAGauge");
const dummyGauge = await DummyGauge.deploy(
await rewardToken.getAddress(),
await veRAACToken.getAddress(),
await gaugeController.getAddress()
);
await dummyGauge.waitForDeployment();
await gaugeController.connect(gaugeAdmin).addGauge(
await dummyGauge.getAddress(),
0, // RWA type
4321n // 5555 - 1234 = 4321
);
// Grant controller role to owner for emission setting
await newRwaGauge.grantRole(
await newRwaGauge.CONTROLLER_ROLE(),
owner.address
);
// Set fixed emission directly through gauge
const TEST_EMISSION = ethers.parseEther("123456789"); // 123,456,789 tokens
await newRwaGauge.connect(owner).setMonthlyEmission(TEST_EMISSION);
// Set type weight in controller
await gaugeController.connect(gaugeAdmin).setTypeWeight(0, 6666n);
// Get required parameters
const totalWeight = await gaugeController.getTotalWeight();
const gaugeWeight = 1234n;
const typeWeight = 6666n;
const WEIGHT_PRECISION = 10000n;
const MAX_TYPE_WEIGHT = 10000n;
// Simulate original contract calculation with precision loss
const gaugeShare = (gaugeWeight * WEIGHT_PRECISION) / totalWeight;
const typeShare = (typeWeight * WEIGHT_PRECISION) / MAX_TYPE_WEIGHT;
const originalCalc =
(TEST_EMISSION * gaugeShare * typeShare) /
(WEIGHT_PRECISION * WEIGHT_PRECISION);
// Calculate improved version without early division
const improvedCalc =
(TEST_EMISSION * gaugeWeight * typeWeight) /
(totalWeight * MAX_TYPE_WEIGHT);
console.log("originalCalc: ", originalCalc);
console.log("improvedCalc: ", improvedCalc);
console.log(
`Precision loss difference: ${ethers.formatEther(
improvedCalc - originalCalc
)}`
);
// Verify calculations match expected behavior
expect(originalCalc).to.be.lt(improvedCalc); // Original has less due to precision loss
// Convert BigInt to Number with 2 decimal places precision
const actualDiff = Number(
ethers.formatUnits(improvedCalc - originalCalc, 18)
);
const expectedDiff = 3474; // Expected difference in whole tokens
// Allow 0.1% tolerance (≈3.474 tokens)
expect(actualDiff).to.be.closeTo(expectedDiff, expectedDiff * 0.001);
});

Impact

High severity. Direct financial impact to users through systematically underpaid rewards. Protocol loses accuracy in incentive distribution, potentially undermining gauge participation. Difference of 3,474 tokens in example scales linearly with larger emissions.

Recommendation

  • Formula restructuring

- return (periodEmission * gaugeShare * typeShare) / (WEIGHT_PRECISION * WEIGHT_PRECISION);
+ return (periodEmission * g.weight * typeWeights[g.gaugeType]) / (totalWeight * MAX_TYPE_WEIGHT);
Updates

Lead Judging Commences

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

GaugeController::_calculateReward performs divisions before multiplications causing precision loss and systematic underpayment of rewards

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

GaugeController::_calculateReward performs divisions before multiplications causing precision loss and systematic underpayment of rewards

Appeal created

anonymousjoe Auditor
7 months ago
inallhonesty Lead Judge
7 months ago
anonymousjoe Auditor
7 months ago
inallhonesty Lead Judge
6 months ago
inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

GaugeController::_calculateReward performs divisions before multiplications causing precision loss and systematic underpayment of rewards

Support

FAQs

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

Give us feedback!