Summary
The increase() function in veRAACToken fails to handle the case where the new voting power is less than the current power due to linear decay over time, leading to an underflow when minting tokens.
Vulnerability Details
In veRAACToken::increase(), after calculating the new voting power, the function attempts to mint the difference between the new power and current balance:
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);
}
However, due to the linear decay of voting power implemented in VotingPowerLib::calculateAndUpdatePower(), the new power can be less than the current balance, causing an underflow in the subtraction operation.
The root cause is that the function assumes the new power will always be greater than the initial power, but this is not true due to the time-based decay of voting power.
Impact
Users attempting to increase their lock amount may have their transactions revert due to underflow
This effectively prevents users from increasing their lock amounts after a certain time period
The contract becomes partially unusable as one of its core functions fails
Proof of Concept
Add the following test to the veRAACToken.test.js file:
describe("Increase Lock issue", () => {
it("should revert when increasing lock amount due to underflow", async () => {
const amount = ethers.parseEther("1000");
const additionalAmount = ethers.parseEther("5");
const duration = 365 * 24 * 3600;
const user1 = users[0];
await veRAACToken.connect(user1).lock(amount, duration);
await ethers.provider.send("evm_increaseTime", [30 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine");
expect(veRAACToken.connect(user1).increase(additionalAmount)).to.be.revertedWithPanic(0x11);
});
});
Recommendation
Handle power decrease similar to the extend() function:
- _mint(msg.sender, newPower - balanceOf(msg.sender));
+ uint256 oldPower = balanceOf(msg.sender);
+ if (newPower > oldPower) {
+ _mint(msg.sender, newPower - oldPower);
+ } else if (newPower < oldPower) {
+ _burn(msg.sender, oldPower - newPower);
+ }