Summary
Underflow in veRAACToken.increase
function will prevent users from increasing their position certain days after initial lock position.
Vulnerability Details
Root Cause Analysis
The vulnerabilty origniates from veRAACToken.increase
function:
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);
}
When a new lock position is created, the user gets granted the following amount of veToken and voting power ref:
veTokenBalance = depositedRAACTokenAmount * duration / MAX_LOCK_DURATION
votingPower = veTokenBalance
And votingPower diminishes as days pass by ref:
votingPower = initialVotingPower - slope * elapsedTime
However, veTokenBalance is not decreased unless user extends duration or withdraws from the position.
With this knowledge, consider the following scenario:
-
User creates a position with 100 RAACToken
-
Half of the lock duration passes by
-
User tries to increase the position with 10 RAACToken
Thus, newPower - balanceOf(msg.sender)
will revert with underflow.
POC
Scenario
Scenario is exactly the same with the aforementioned one.
How to run 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 user = makeAddr("user");
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 {
uint256 duration = veToken.MAX_LOCK_DURATION();
raacToken.mint(user, raacTokenAmount);
vm.startPrank(user);
raacToken.approve(address(veToken), raacTokenAmount);
veToken.lock(raacTokenAmount, duration);
vm.stopPrank();
skip(duration / 2);
uint256 increaseAmount = raacTokenAmount / 10;
raacToken.mint(user, increaseAmount);
vm.startPrank(user);
raacToken.approve(address(veToken), increaseAmount);
vm.expectRevert(stdError.arithmeticError);
veToken.increase(increaseAmount);
vm.stopPrank();
}
}
Impact
Users won't be able to increase their position after certain days.
However, if they provide enough raacToken so that newPower is greater than current balance, they can increase their position.
Tools Used
Manual Review, Foundry
Recommendations
Like in extend
function, if newPower
is smaller than oldPower
, burn veToken:
@@ -267,7 +267,12 @@ contract veRAACToken is ERC20, Ownable, ReentrancyGuard, IveRAACToken {
// Transfer additional tokens and mint veTokens
raacToken.safeTransferFrom(msg.sender, address(this), amount);
- _mint(msg.sender, newPower - balanceOf(msg.sender));
+ uint256 oldPower = balanceOf(msg.sender);
+ if (newPower > oldPower) {
+ _mint(msg.sender, newPower - oldPower);
+ } else if (newPower < oldPower) {
+ _burn(msg.sender, oldPower - newPower);
+ }
emit LockIncreased(msg.sender, amount);
}