Core Contracts

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

Voting Power Decay Calculation Ignores timeDelta == 0

Summary

The function getCurrentPower() does not apply any decay if timeDelta == 0. This means that if a user calls the function immediately after their last voting power update, the function bypasses decay calculations and returns the full stored voting power (bias).

While this might not be an immediate exploit, it can create unexpected behavior where voting power calculations are inconsistent depending on the exact timestamp of function calls.

Vulnerability Details

The function first calculates timeDelta:

uint256 timeDelta = timestamp - point.timestamp;
  • If timestamp == point.timestamp, then timeDelta = 0.

  • The decay logic only runs when timeDelta > 0

if (timeDelta > 0) {
int128 decay = (point.slope * int128(int256(timeDelta))) / int128(int256(1));
adjustedBias = point.bias - decay;
}
  • Since timeDelta == 0, this condition does not execute, and adjustedBias remains equal to point.bias.

    Users Can Temporarily Maintain Full Voting Power

  • If a user locks tokens and immediately calls getCurrentPower(), they receive full voting power (bias) with no decay applied.

  • This means voting power calculations could be inconsistent across function calls.

PoC

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Voting Power Decay Issue", function () {
let votingContract, owner, user;
beforeEach(async function () {
[owner, user] = await ethers.getSigners();
const VotingPower = await ethers.getContractFactory("VotingPowerTestWrapper");
votingContract = await VotingPower.deploy();
await votingContract.deployed();
});
it("should not apply decay if queried immediately after update", async function () {
const currentTime = (await ethers.provider.getBlock()).timestamp;
const lockDuration = 365 * 24 * 60 * 60; // 1 year
// Update voting power
await votingContract.connect(user).calculateAndUpdatePower(
user.address,
1000,
currentTime + lockDuration
);
// Query immediately (timeDelta == 0)
const powerBeforeDecay = await votingContract.getCurrentPower(user.address, currentTime);
expect(powerBeforeDecay).to.equal(1000); // ✅ No decay applied
// Wait some time and check again
await ethers.provider.send("evm_increaseTime", [10]); // Move forward 10 seconds
await ethers.provider.send("evm_mine");
const powerAfterDecay = await votingContract.getCurrentPower(user.address, (await ethers.provider.getBlock()).timestamp);
expect(powerAfterDecay).to.be.lessThan(1000); // ✅ Decay should now apply
});
});

Impact

  • Users who query getCurrentPower() immediately after an update see full voting power, while others see reduced power.

  • Users can artificially boost their voting power for snapshots by updating and querying instantly.

  • External contracts using getCurrentPower() may receive inaccurate voting power data.

Tools Used

Manual Review, Hardhat

Recommendations

Modify the voting power decay calculation to explicitly account for timeDelta == 0, preventing unintended behavior.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Support

FAQs

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