Summary
In the veRAACToken::increase function, the calculation of the new voting power of the user uses an incorrect amount of tokens that lead to users gaining more voting power than they should.
Vunerability Details
The veRAACToken::increase function is used to increases the amount of locked RAAC tokens whithout changing the unlock time.
File: contracts/core/tokens/veRAACToken.sol#L251-L273
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);
}
It invokes the LockManager::increaseLock function to add the given amount of RAAC tokens to the existing lock of the user (the caller of the function). Then it calculates the new voting power of the user and mint the difference (newPower - oldPower) to the user.
The issue here is that the amount used to calculates the new voting power is userLock.amount + amount where userLock.amount is supposed to be the old amount of locked RAAC tokens that the user wants to increase. However, we already added amount to the old amount of locked tokens when increasing the lock using the LockManager::increaseLock function.
File: contracts/libraries/governance/LockManager.sol#L152-L170
function increaseLock(
LockState storage state,
address user,
uint256 additionalAmount
) internal {
Lock storage lock = state.locks[user];
if (!lock.exists) revert LockNotFound();
if (lock.end <= block.timestamp) revert LockExpired();
if (lock.amount + additionalAmount > state.maxLockAmount) revert AmountExceedsLimit();
@-> lock.amount += additionalAmount;
state.totalLocked += additionalAmount;
emit LockIncreased(user, additionalAmount);
}
This means that userLock.amount + amount is actually userLock.amount + amount + amount.
Impact
This increases the voting power by more than it should.
Proof Of Concept
Place the following test in test/unit/core/tokens/veRAACToken.test.js.
it.only("should increase voting power by more than it should", async () => {
const initialAmount = ethers.parseEther("1000");
const additionalAmount = ethers.parseEther("500");
const duration = 365 * 24 * 3600;
await veRAACToken.connect(users[0]).lock(initialAmount, duration);
const expectedVotingPower = ((initialAmount + additionalAmount) * BigInt(duration)) / BigInt(MAX_LOCK_DURATION);
await expect(veRAACToken.connect(users[0]).increase(additionalAmount))
.to.emit(veRAACToken, "LockIncreased")
.withArgs(users[0].address, additionalAmount);
const position = await veRAACToken.getLockPosition(users[0].address);
expect(position.amount).to.equal(initialAmount + additionalAmount);
const votingPower = await veRAACToken.balanceOf(users[0]);
console.log({expectedVotingPower, votingPower});
expect(votingPower).to.gt(expectedVotingPower);
});
Run the npx hardhat test test/unit/core/tokens/veRAACToken.test.js command in the terminal.
Tools Used
Manual review.
Recommendations
In veRAACToken.sol#L260 make the following change.
- userLock.amount + amount,
+ userLock.amount,