Open c4-bot-6 opened 7 months ago
Picodes marked the issue as primary issue
othernet-global (sponsor) confirmed
There is now a default 30 day period after which ballots can be removed by any user.
https://github.com/othernet-global/salty-io/commit/758349850a994c305a0ab9a151d00e738a5a45a0
Picodes marked the issue as satisfactory
Picodes marked the issue as selected for report
Lines of code
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/Proposals.sol#L396-L397 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/DAO.sol#L281-L281
Vulnerability details
Impact
A staker is restricted to sponsoring a single proposal at any given time. This proposal remains active until a predetermined duration has elapsed, and it has received the sufficient number of votes to reach a quorum, after which it can be finalized. However, if a staker sponsors a proposal that fails to attract the necessary quorum of votes, it remains indefinitely in an active state. This situation effectively locks the staker in a position where they cannot propose any new initiatives, as they are stuck with an unresolved proposal.
There is no mechanism for sponsors to withdraw or cancel their proposals. So when a proposal is unable to achieve quorum, the sponsor is left in a predicament where they are unable to further participate in the governance through the initiation of new proposals.
Furthermore, there is a noticeable lack of motivation for stakers to vote against proposals that are not on track to meet the quorum. As these proposals cannot pass without achieving the required quorum, resulting in a situation where voting against such proposals does not offer any tangible benefit.
Proof of Concept
Steps
A staker can have only one active proposal
Within
Proposals
stakers are identified asusers
and when creating a proposal there is a check Proposals::_possiblyCreateProposal()With the state being pushed later in Proposals::_possiblyCreateProposal()
A proposal can only be finalized after sufficient time has passed
The check to ensure the minimum time has passed in Proposals::canFinalizeBallot()
A proposal can only be finalized after quorum is reached
The check to ensure quorum is reached in Proposals::canFinalizeBallot()
Test Case
User sponsors a proposal that fails to reach quorum cannot be finalized and with no other way to close will have an active proposal, preventing further proposals. Add the test case to [Proposals.t.sol](https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/dao/tests/Proposals.t.sol#L1717) ```solidity function test_quorum_needed_to_finalize_ballot() external { uint256 ballotID = 1; uint256 stakeAmount = 250000 ether; // Fund Alice, Bob and Charlie with equal SALT vm.startPrank( DEPLOYER ); salt.transfer( alice, stakeAmount ); salt.transfer( bob, stakeAmount ); salt.transfer( charlie, stakeAmount ); vm.stopPrank(); // Alice, Bob and Charlie all stake their equal amounts of SALT _stakeSalt(alice, stakeAmount); _stakeSalt(bob, stakeAmount); _stakeSalt(charlie, stakeAmount); // Alice proposes a ballot vm.prank(alice); proposals.proposeCountryInclusion("US", "proposed ballot"); // Alice votes YES vm.prank(alice); proposals.castVote( ballotID, Vote.YES ); // Now, we allow some time to pass in order to finalize the ballot vm.warp(block.timestamp + daoConfig.ballotMinimumDuration()); // The ballot cannot be finalized as quorum is not reached assertFalse(proposals.canFinalizeBallot(ballotID), "Ballot cannot be finalized"); assertGt(proposals.requiredQuorumForBallotType(BallotType.INCLUDE_COUNTRY),stakeAmount, "Quorum exceeds stakeAmount" ); assertTrue( proposals.userHasActiveProposal(alice), "Alice has an active proposal"); } function _stakeSalt(address wallet, uint256 amount ) private { vm.startPrank( wallet ); salt.approve(address(staking), amount); staking.stakeSALT(amount); vm.stopPrank(); } ```Tools Used
Manual review, Foundry test
Recommended Mitigation Steps
Allow the proposals to be closed (equivalent to finalized as
NO
orNO_CHANGE
), which would allow the sponsor to afterward make a different proposal.(This feature would also generally allow removing dead proposals)
Add a time field to
Ballot
in IProposalsPopulate the
ballotCloseTime
in Proposal::_possiblyCreateProposal , using a constant in this example, it could always another DAO configuration option.Add a function to return whether a proposal can be closed to Proposal
Add a function to close a ballot without any side effect to DAO
Assessed type
Governance