code-423n4 / 2024-03-saltyio-mitigation-findings

0 stars 0 forks source link

Proposal can be removed after 30 days without owner's consent #2

Open c4-bot-6 opened 6 months ago

c4-bot-6 commented 6 months ago

Lines of code

https://github.com/othernet-global/salty-io/blob/main/src/dao/DAO.sol#L256

Vulnerability details

Summary & Impact

The mitigation for M-19 was meant to ensure to avoid trapping the proposers indefinitely if their proposal had still not met quorum after 30 days. Hence the function manuallyRemoveBallot() is introduced which can be called by anyone.

The fix incorrectly assumes that the owner will always want their proposal to be cancelled after 30 days. This is not true. If their proposal is quite close to reaching quorum, the owner may want to keep it alive for a few more days. However, a griefer or someone who has voted against the ballot can choose to delete the proposal.

This choice of deleting the ballot ought to remain in the hands of only the proposal owner.

Proof of Concept

Add the following tests inside src/dao/tests/DAO.t.sol and run via COVERAGE="yes" NETWORK="sep" forge test -vv --rpc-url https://rpc.ankr.com/eth_sepolia --mt test_30dayRemoval to see the test pass:

  function test_30dayRemoval() public {
      // ********************* SETUP ********************************
      deal(address(salt), address(DEPLOYER), 6_000_000 ether);
      vm.prank(DEPLOYER);
      staking.stakeSALT(6_000_000 ether);

      // Set up the parameters for the proposal
      uint256 proposalNum = 0; // Assuming an enumeration starting at 0 for parameter proposals
      uint256 ballotID = 1;

      // Alice stakes her SALT to get voting power
      uint256 aliceStakedAmount = 650_000 ether;
      deal(address(salt), address(alice), aliceStakedAmount);
      // ************************************************************

      vm.startPrank(alice);
      staking.stakeSALT(aliceStakedAmount);
      // Propose a parameter ballot
      proposals.proposeParameterBallot(proposalNum, "Increase max pools count");
      // Alice casts a vote, but not enough for quorum
      // minQuorum required = 10% of (6_000_000 + 650_000) ether = 665_000 ether
      proposals.castVote(ballotID, Vote.INCREASE);
      assertEq(proposals.votesCastForBallot(ballotID, Vote.INCREASE), aliceStakedAmount);
      vm.stopPrank();

      skip(30 days);
      assertEq(proposals.ballotForID(ballotID).ballotIsLive, true, "Ballot should have been Live");

      // Bob calls `manuallyRemoveBallot()` to remove the proposal
      vm.prank(bob);
      dao.manuallyRemoveBallot(ballotID);
      assertEq(proposals.ballotForID(ballotID).ballotIsLive, false, "Ballot should have been removed");
  }

Recommended Mitigation Steps

Inside manuallyRemoveBallot(), ensure that msg.sender is the owner of the proposal.

Conclusion

New attack vector created due to the fix.

Assessed type

Access Control

Picodes commented 6 months ago

Downgrading to Low as there is no security issue here for me. If the proposal hasn't reached quorum within the voting period it can be cancelled, which seems fair.

c4-judge commented 6 months ago

Picodes changed the severity to QA (Quality Assurance)

t0x1cC0de commented 6 months ago

Added a comment regarding this in #94