Summary
Whenever we increase the amount of locked RAAC tokens in the veRAACToken contract by calling the increase function we get an incorrect higher calculation of the voting power due to incorrectly adding userLock.amount twice.
function increase(uint256 amount) external nonReentrant whenNotPaused {
console.log("User voting power before increase: %s", _votingState.getCurrentPower(msg.sender, block.timestamp));
console.log("User lock amount before increase: %s", _lockState.locks[msg.sender].amount);
_lockState.increaseLock(msg.sender, amount);
_updateBoostState(msg.sender, locks[msg.sender].amount);
console.log("=======================================================");
console.log("User voting power after increase: %s", _votingState.getCurrentPower(msg.sender, block.timestamp));
console.log("User lock amount after increase: %s", _lockState.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
);
console.log("=======================================================");
console.log("User voting power after calculate and update: %s", _votingState.getCurrentPower(msg.sender, block.timestamp));
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);
}
Vulnerability Details
Due to an incorrect double addition of the amount of RAAC tokens that the user is depositing the calculated voting power for the veRAAC token holder is higher than it is supposed to. The issue happens when we call _votingState.calculateAndUpdatePowerand pass the userLock.amount + amount, which is incorrect as the userLock.amount was already incremented in the _lockState.increaseLock call and holds the new amount the user has locked.
This can be exploited to get higher voting power and in the same time invest less than requiered RAAC tokens to do so.
Impact
Unfair gain of voting power whenever RAAC token amount locked is increased for a user. This in turn can allow user to vote with a bigger weight when calling the castVote function in the Governance contract and make a propolsals pass.
Tools Used
PoC
This test should be put in the veRAACToken.test.js file in the describe("Lock Mechanism") test suite group.
it("PoC user voting power is incorrectly calculated when lock is increased", async () => {
const initialAmount = ethers.parseEther("1000");
const additionalAmount = ethers.parseEther("500");
const duration = 365 * 24 * 3600;
await veRAACToken.connect(users[0]).lock(initialAmount, duration);
console.log("User voting power before call to lock increase: ", await veRAACToken.getVotingPower(users[0].address));
console.log("=====================================================")
await expect(veRAACToken.connect(users[0]).increase(additionalAmount))
.to.emit(veRAACToken, "LockIncreased")
.withArgs(users[0].address, additionalAmount);
const userVotingPower = await veRAACToken.getVotingPower(users[0].address);
expect(userVotingPower).to.equal(ethers.parseEther("500"));
});
Console log output:
User voting power before call to lock increase: 250000000000000000000n
=====================================================
User voting power before lock increase: 250000000000000000000
User lock amount before increase: 1000000000000000000000
=======================================================
User voting power after lock increase but before recalculation: 250000000000000000000
User lock amount after increase: 1500000000000000000000
=======================================================
User voting power after calculate and update: 500000000000000000000
Recommendations
Removed the unesecarry double addition of the lock amount when calculating the voting power:
function increase(uint256 amount) external nonReentrant whenNotPaused {
console.log("User voting power before increase: %s", _votingState.getCurrentPower(msg.sender, block.timestamp));
console.log("User lock amount before increase: %s", _lockState.locks[msg.sender].amount);
// Increase lock using LockManager
_lockState.increaseLock(msg.sender, amount);
_updateBoostState(msg.sender, locks[msg.sender].amount);
console.log("
console.log("User voting power after increase: %s", _votingState.getCurrentPower(msg.sender, block.timestamp));
console.log("User lock amount after increase: %s", _lockState.locks[msg.sender].amount);
// Update voting power
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
(int128 newBias, int128 newSlope) = _votingState.calculateAndUpdatePower(
msg.sender,
//@Audit the amount was already increased when we called increaseLock se we are doing a doulbe increase here
- userLock.amount + amount,
+ userLock.amount,
userLock.end
);
console.log("
console.log("User voting power after calculate and update: %s", _votingState.getCurrentPower(msg.sender, block.timestamp));
// Update checkpoints
uint256 newPower = uint256(uint128(newBias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
// Transfer additional tokens and mint veTokens
raacToken.safeTransferFrom(msg.sender, address(this), amount);
_mint(msg.sender, newPower - balanceOf(msg.sender));
emit LockIncreased(msg.sender, amount);
}