Summary
The function veRAACToken::increase() allows an user to add more tokens to an existing lock without changing the unlock time. However, the function is incorrectly calculates user's new voting power, which results higher voting power than expected
Vulnerability Details
The function veRAACToken::increase() is incorrect that it adds twice amount to user's lock state for calculating voting power. As shown below, _lockState.increaseLock(msg.sender, amount) handles adding the amount into user lock state. But when calling _votingState.calculateAndUpdatePower(), the function still uses userLock.amount + amount, which means that amount is counted twice.
As a result, users voting power is calculated inaccurately.
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);
}
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);
}
PoC
Add the test to test/unit/core/tokens/veRAACToken.test.js
describe("Lock Mechanism", () => {
...
it.only("increase lock amount will increase wrong power", async () => {
const amount1 = ethers.parseEther("1000");
const amount2 = ethers.parseEther("500");
const amount3 = ethers.parseEther("1500");
const duration = 365 * 24 * 3600;
await veRAACToken.connect(users[0]).lock(amount1, duration);
await veRAACToken.connect(users[1]).lock(amount2, duration);
await veRAACToken.connect(users[1]).increase(amount2)
await veRAACToken.connect(users[2]).lock(amount3, duration);
let user0Power = await veRAACToken.getVotingPower(users[0].address)
let user1Power = await veRAACToken.getVotingPower(users[1].address)
let user2Power = await veRAACToken.getVotingPower(users[2].address)
console.log(`user0 locks 1000 tokens, receives ${user0Power} voting power`)
console.log(`user1 locks (500 + 500) tokens, receives ${user1Power} voting power`)
console.log(`user2 locks 1500 tokens, receives ${user2Power} voting power`)
});
Run the test and it show
veRAACToken
Lock Mechanism
user0 locks 1000 tokens, receives 250000000000000000000 voting power
user1 locks (500 + 500) tokens, receives 375000000000000000000 voting power
user2 locks 1500 tokens, receives 375000000000000000000 voting power
✔ increase lock amount will increase wrong power
1 passing (2s)
It means that user1 locks total 1000 tokens but voting power is not equal to user0 but it equals to user2
Impact
Tools Used
Manual
Recommendations
(int128 newBias, int128 newSlope) = _votingState.calculateAndUpdatePower(
msg.sender,
- userLock.amount + amount,
+ userLock.amount,
userLock.end
);