Summary
https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/proposals/Governance.sol#L127C5-L169C1
Proposal Threshold Verification Inconsistency
Vulnerability Details
https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/proposals/Governance.sol#L127C5-L169C1
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"
);
}
uint256 proposalId = _proposalCount++;
_proposals[proposalId] = ProposalCore({
id: proposalId,
proposer: msg.sender,
});
}
function cancel(uint256 proposalId) external override {
ProposalCore storage proposal = _proposals[proposalId];
ProposalState currentState = state(proposalId);
if (currentState == ProposalState.Executed) {
revert InvalidProposalState(proposalId, currentState, ProposalState.Active, "Cannot cancel executed proposal");
}
if (msg.sender != proposal.proposer &&
_veToken.getVotingPower(proposal.proposer) >= proposalThreshold) {
revert InsufficientProposerVotes(
proposal.proposer,
_veToken.getVotingPower(proposal.proposer),
proposalThreshold,
"Proposer lost required voting power"
);
}
proposal.canceled = true;
emit ProposalCanceled(proposalId, msg.sender, "Proposal canceled by proposer");
}
The Inconsistencies can be seen below:
uint256 proposerVotes = _veToken.getVotingPower(msg.sender);
if (proposerVotes < proposalThreshold) { ... }
if (_veToken.getVotingPower(proposal.proposer) >= proposalThreshold) { ... }
if (proposerVotes < proposalThreshold) { revert; }
if (_veToken.getVotingPower(proposal.proposer) >= proposalThreshold) { revert; }
struct ProposalCore {
uint256 id;
address proposer;
}
T1: proposerVotes = 110k (propose passes)
T2: proposerVotes = 95k (below threshold but can't cancel)
T3: proposerVotes = 105k (above threshold again)
1. Attacker borrows 100k veRAAC
2. Creates proposal (passes threshold check)
3. Returns borrowed tokens
4. Proposal can't be canceled because inverse check
5. Proposal remains active with insufficient stake
Impact
Proposal Threshold Verification Inconsistency
Tools Used
Foundry
Recommendations
function cancel(uint256 proposalId) external override {
ProposalCore storage proposal = _proposals[proposalId];
uint256 currentVotes = _veToken.getVotingPower(proposal.proposer);
if (msg.sender != proposal.proposer &&
currentVotes < proposal.thresholdAtCreation) {
proposal.canceled = true;
emit ProposalCanceled(proposalId, msg.sender, "Insufficient proposer votes");
}
}
modifier maintainsThreshold(uint256 proposalId) {
_;
ProposalCore storage proposal = _proposals[proposalId];
require(
_veToken.getVotingPower(proposal.proposer) >= proposal.thresholdAtCreation,
"Must maintain threshold"
);
}