Core Contracts

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

Proposer's Vote Omission at Proposal Creation: Oversight Forces a Two-Transaction Process and Increases GAS Costs.

Summary

In the Governance contract, any proposer who meets the required proposal threshold is allowed to submit a proposal using the Governance::propose function. While this function operates correctly, there is a design flaw concerning the proposer's ability to cast their own vote on their proposal.

Although it is acceptable for a proposer to vote on their own proposal, the current implementation forces them to wait until the votingDelay period elapses before they can cast their vote. This approach is inefficient because it requires the proposer to execute a separate transaction to cast their vote, despite already being authorized to vote on their own proposal.

Ideally, the process should be streamlined so that a proposer can include their vote as part of the initial proposal submission, thereby reducing the overall number of transactions and saving on gas costs. This inefficient design not only delays the voting process but also imposes additional gas expenses on proposers, which could be avoided with a more optimized, single-transaction approach.

Vulnerability Details

Governance::propose:

function propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description,
ProposalType proposalType
) external override returns (uint256) {
uint256 proposerVotes = _veToken.getVotingPower(msg.sender);
if (proposerVotes < proposalThreshold) {
revert InsufficientProposerVotes(msg.sender, proposerVotes, proposalThreshold, "Below threshold");
}
if (targets.length == 0 || targets.length != values.length || targets.length != calldatas.length) {
revert InvalidProposalLength(targets.length, values.length, calldatas.length);
}
uint256 proposalId = _proposalCount++;
uint256 startTime = block.timestamp + votingDelay;
uint256 endTime = startTime + votingPeriod;
_proposals[proposalId] = ProposalCore({
id: proposalId,
proposer: msg.sender,
proposalType: proposalType,
startTime: startTime,
endTime: endTime,
executed: false,
canceled: false,
descriptionHash: keccak256(bytes(description)),
targets: targets,
values: values,
calldatas: calldatas
});
// Store the proposal data separately
// @info: storing description of (string type) may cause blockchain storage to reach out of limit overtime
_proposalData[proposalId] = ProposalData(targets, values, calldatas, description);
@> // @info: missing proposer's vote acquisition in their favour
// @danger: proposer has to perform another transaction to give theirselve a vote in their own favour
// consequently, proposer wastes GAS cost
emit ProposalCreated(
proposalId,
msg.sender,
targets,
values,
calldatas,
description,
proposalType,
startTime,
endTime,
proposerVotes
);
return proposalId;
}

In the current implementation of the Governance contract, when a proposer submits a proposal, their vote is not automatically cast as part of the proposal creation process. Instead, the contract requires the proposer to wait until the designated votingDelay period has elapsed before they can cast their vote. This design choice necessitates a second, separate transaction for the proposer to register their vote, even though they are already eligible to vote on their own proposal.

This approach introduces several inefficiencies. First, it delays the commencement of the voting process, potentially affecting the timely evaluation of proposals. More importantly, it forces the proposer to incur additional gas costs by performing an extra transaction solely to record their vote. From a user experience standpoint, it is suboptimal because the proposer's vote could be included in the initial proposal submission, thereby reducing the overall number of transactions and associated fees.

In a more streamlined design, the system would automatically register the proposer's vote at the moment the proposal is created. This single-transaction approach would not only lower the gas expenses for the proposer but also simplify the process by eliminating an unnecessary step. Ultimately, incorporating the vote into the proposal creation process would enhance the efficiency and cost-effectiveness of the governance mechanism, fostering a more user-friendly environment for protocol participation.

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 {TimelockController} from "../src/core/governance/proposals/TimelockController.sol";
import {Governance} from "../src/core/governance/proposals/Governance.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;
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");
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");
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();
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);
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();
}
}
  1. Step 5: Add the following test PoC in the test file, after the setUp -> private helper function.

function testProposerHasToPerformTwoTxsToProposeAndCast() public {
// first transaction - Proposal creation.
vm.startPrank(ALICE);
uint256 proposalId = governance.propose(
proposalTargets, proposalValues, proposalCalldatas, proposalDescription, proposalProposalType
);
vm.stopPrank();
console.log("Alice's Proposal ID: ", proposalId);
IGovernance.ProposalCore memory proposalCore = governance.getProposal(proposalId);
// second transaction - Vote Casting (before startTime (votingDelay)).
vm.startPrank(ALICE);
vm.expectRevert(
abi.encodeWithSelector(
bytes4(keccak256("VotingNotStarted(uint256,uint256,uint256)")),
proposalId,
proposalCore.startTime,
block.timestamp
)
);
governance.castVote(proposalId, true);
vm.stopPrank();
bool hasVoted = governance.hasVoted(proposalId, ALICE);
console.log("\nBefore time roll...");
console.log("Alice votes? : ", hasVoted);
assertEq(hasVoted, false);
vm.warp(block.timestamp + proposalCore.startTime + 1);
vm.roll(block.number + 1);
// second transaction - Vote Casting (after startTime (votingDelay)).
vm.startPrank(ALICE);
governance.castVote(proposalId, true);
vm.stopPrank();
hasVoted = governance.hasVoted(proposalId, ALICE);
console.log("\nAfter time roll...");
console.log("Alice votes? : ", hasVoted);
assertEq(hasVoted, true);
// So as we can see that alice has to perform two transactions...
// first - Proposal Creation
// Second - Casting Vote for their own Proposal (after startTime)
}
  1. Step 6: To run the test, execute the following commands in your terminal:

forge test --mt testProposerHasToPerformTwoTxsToProposeAndCast -vv
  1. Step 7: Review the output. The expected output should indicate that proposer has to perform two transaction to create proposal and cast vote.

[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/GovernanceTest.t.sol:GovernanceTest
[PASS] testProposerHasToPerformTwoTxsToProposeAndCast() (gas: 590391)
Logs:
Alice's Proposal ID: 0
Before time roll...
Alice votes? : false
After time roll...
Alice votes? : true
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.57ms (812.81µs CPU time)
Ran 1 test suite in 13.77ms (8.57ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

As demonstrated, the test confirms that proposers has to perform two transactions first to create Proposal and second to Cast vote.

Impact

  • Increased Gas Expenditure:
    Proposers are forced to perform an additional transaction to cast their vote after the proposal is created. This extra transaction results in higher gas costs, which can be significant, especially during periods of high network congestion.

  • Delayed Voting Process:
    Since the proposer must wait for the votingDelay period to elapse before casting their vote, it is not bad for proposers to wait to cast their vote for their own proposals as a common sense a proposer shall always vote for their own proposal not against it.

    Delay should be applied on other voters.

  • Reduced User Experience Efficiency:
    The necessity for a separate vote-casting transaction complicates the user experience. Proposers expect a streamlined process where their vote is automatically registered with their proposal, and having to submit a second transaction adds unnecessary complexity.

  • Potential Governance Integrity Issues:
    In situations where rapid voting is critical, the delay and additional transaction might lead to missed voting opportunities or skewed voting results. This can ultimately undermine the fairness and effectiveness of the governance process, affecting the protocol's credibility and stakeholder confidence.

Tools Used

  • Manual Review

  • Foundry

Recommendations

  • Integrate Proposer Vote into Proposal Creation:
    Modify the Governance::propose function so that the proposer's vote is automatically recorded as part of the proposal creation process. This integration would eliminate the need for a subsequent vote-casting transaction, reducing gas costs and improving efficiency.

  • Enhance User Documentation:
    Update the protocol's documentation and user interfaces to reflect the new process. Clear communication will help ensure that proposers understand that their vote will now be automatically included upon proposal submission.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Appeal created

theirrationalone Submitter
4 months ago
inallhonesty Lead Judge
4 months ago
inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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