Summary
After a proposal ends the check inaccurately checks if the total vote casted is below some percentage of The total votes.
An attacker/ a normal user can decide to deposit, the attacker can deposit as low as 1 wei depending on the number of users that voted. While a normal users action also will increament the total amount needed and cause an unintended reversion.
Vulnerability Details
This bug exist because the Execution function depends on the total supply or total number of votes in the entire veRAAC token contract.
* @notice Executes a successful proposal through the timelock
* @dev Two-step execution process:
* 1. Queue - Schedule proposal in timelock when vote succeeds
* 2. Execute - Execute proposal after timelock delay
* @param proposalId The ID of the proposal to execute
*/
function execute(uint256 proposalId) external override nonReentrant {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.executed) revert ProposalAlreadyExecuted(proposalId, block.timestamp);
@audit>>>>> ProposalState currentState = state(proposalId);
if (currentState == ProposalState.Succeeded) {
_queueProposal(proposalId);
} else if (currentState == ProposalState.Queued) {
_executeProposal(proposalId);
} else {
revert InvalidProposalState(
proposalId,
currentState,
currentState == ProposalState.Active ? ProposalState.Succeeded : ProposalState.Queued,
"Invalid state for execution"
);
}
}
Locking RAAC tokens mints voting powers in the veRAAC contract.
there is no minimum to LOCK hence an attack is possible with as low as 1 wei if participation is low
Also an honest user can deposit and his deposit will also affect a proposal execution
IN THE VERAAC contract,
* @notice Creates a new lock position for RAAC tokens
* @dev Locks RAAC tokens for a specified duration and mints veRAAC tokens representing voting power
* @param amount The amount of RAAC tokens to lock
* @param duration The duration to lock tokens for, in seconds
*/
function lock(uint256 amount, uint256 duration) external nonReentrant whenNotPaused {
if (amount == 0) revert InvalidAmount();
if (amount > MAX_LOCK_AMOUNT) revert AmountExceedsLimit();
if (totalSupply() + amount > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
if (duration < MIN_LOCK_DURATION || duration > MAX_LOCK_DURATION)
revert InvalidLockDuration();
raacToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 unlockTime = block.timestamp + duration;
_lockState.createLock(msg.sender, amount, duration);
_updateBoostState(msg.sender, amount);
(int128 bias, int128 slope) = _votingState.calculateAndUpdatePower(
msg.sender,
amount,
unlockTime
);
uint256 newPower = uint256(uint128(bias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
@audit>>> _mint(msg.sender, newPower);
emit LockCreated(msg.sender, amount, unlockTime);
}
Total supply will change hence total vote
* @notice Gets the total voting power of all veRAAC tokens
* @dev Returns the total supply of veRAAC tokens
* @return The total voting power across all holders
*/
function getTotalVotingPower() external view override returns (uint256) {
return totalSupply();
}
This will affect the calculation
*/
function state(uint256 proposalId) public view override returns (ProposalState) {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.startTime == 0) revert ProposalDoesNotExist(proposalId);
if (proposal.canceled) return ProposalState.Canceled;
if (proposal.executed) return ProposalState.Executed;
if (block.timestamp < proposal.startTime) return ProposalState.Pending;
if (block.timestamp < proposal.endTime) return ProposalState.Active; <= note
ProposalVote storage proposalVote = _proposalVotes[proposalId];
@audit>>> uint256 currentQuorum = proposalVote.forVotes + proposalVote.againstVotes;
@audit>>> uint256 requiredQuorum = quorum();
@audit>>> if (currentQuorum < requiredQuorum || proposalVote.forVotes <= proposalVote.againstVotes) {
return ProposalState.Defeated;
}
Since the quorum is dynamic,
increase in locked funds will affect the amount returned, this increasing due to an attcker/honest user will dos proposals
* @notice Gets the current quorum requirement
* @dev Calculates required quorum based on total voting power
* Uses quorumNumerator/QUORUM_DENOMINATOR ratio
* @return Current quorum threshold in voting power units
*/
function quorum() public view override returns (uint256) {
@audit>> return (_veToken.getTotalVotingPower() * quorumNumerator) / QUORUM_DENOMINATOR;
}
Impact
Inability to execute proposals because of an increase in the Quorum amount
Tools Used
Manual Review
Recommendations
It is safer to cache and save the total vote power at the point of proposing or set a fixed threshold instead of using a dynamic one.