Summary
The function veRAACToken::increase() increases an user's lock position, in which position's voting power is computed and updated accordingly. However, the computed voting power is inaccurately calculated according to the current formula
Vulnerability Details
veRAACToken::increase() calls VotingPowerLib::calculateAndUpdatePower() to calculate and update user lock position.
The function calculateAndUpdatePower() computes the voting power based on amount and the lock duration, which does not consider user's current voting power. This causes the result of voting power calculation bias at a random time can definitely less than the current voting power, resulting the function veRAACToken::increase() to fail at the line _mint(msg.sender, newPower - balanceOf(msg.sender)) because of arithmetic underflow
function calculateAndUpdatePower(
VotingPowerState storage state,
address user,
uint256 amount,
uint256 unlockTime
) internal returns (int128 bias, int128 slope) {
if (amount == 0 || unlockTime <= block.timestamp) revert InvalidPowerParameters();
uint256 MAX_LOCK_DURATION = 1460 days;
@> uint256 duration = unlockTime - block.timestamp;
@> uint256 initialPower = (amount * duration) / MAX_LOCK_DURATION;
@> bias = int128(int256(initialPower));
slope = int128(int256(initialPower / duration));
uint256 oldPower = getCurrentPower(state, user, block.timestamp);
state.points[user] = RAACVoting.Point({
@> bias: bias,
slope: slope,
timestamp: block.timestamp
});
_updateSlopeChanges(state, unlockTime, 0, slope);
emit VotingPowerUpdated(user, oldPower, uint256(uint128(bias)));
@> return (bias, slope);
}
function increase(uint256 amount) external nonReentrant whenNotPaused {
_lockState.increaseLock(msg.sender, amount);
_updateBoostState(msg.sender, locks[msg.sender].amount);
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
@> (int128 newBias, int128 newSlope) = _votingState.calculateAndUpdatePower(
msg.sender,
userLock.amount + amount,
userLock.end
);
@> uint256 newPower = uint256(uint128(newBias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
raacToken.safeTransferFrom(msg.sender, address(this), amount);
@> _mint(msg.sender, newPower - balanceOf(msg.sender));
emit LockIncreased(msg.sender, amount);
}
PoC
Add the test to test/unit/core/tokens/veRAACToken.test.js
describe("Lock Mechanism", () => {
...
it.only("increase lock updates wrong voting power", async function(){
const amount = ethers.parseEther("1000");
const year = 365n * 24n * 3600n
const duration = 3n * year;
await veRAACToken.connect(users[0]).lock(amount, duration);
let newBlockTime = BigInt(await time.increase(year * 2n));
const lock = await veRAACToken.getLockPosition(users[0].address)
const currentVotingPower = lock.power;
const MAX_LOCK_DURATION = 126144000n
const increaseAmount = amount / 2n;
const newVotingPower = (amount + increaseAmount) * (lock.end - newBlockTime) / MAX_LOCK_DURATION
console.log(currentVotingPower)
console.log(newVotingPower)
await veRAACToken.connect(users[0]).increase(amount / 2n);
})
Run the test and console shows:
veRAACToken
Lock Mechanism
750000000000000000000n
375000000000000000000n
1) increase lock updates wrong voting power
0 passing (2s)
1 failing
1) veRAACToken
Lock Mechanism
increase lock updates wrong voting power:
Error: VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation overflowed outside of an unchecked block)
at veRAACToken.increase (contracts/core/tokens/veRAACToken.sol:291)
Impact
Tools Used
Manual
Recommendations
Take into account the current voting power of a position to calculate the new voting power