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: 5/65
Findings: 2
Award: $1,261.79
🌟 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
https://github.com/code-423n4/2023-10-party/blob/main/contracts/party/PartyGovernance.sol#L553 https://github.com/code-423n4/2023-10-party/blob/main/contracts/party/PartyGovernance.sol#L595 https://github.com/code-423n4/2023-10-party/blob/main/contracts/party/PartyGovernance.sol#L457
In the current implementation, hosts in the crowdfunding system possess significant power. This might lead to potential misuse especially hosts can also be a member of the party.
Based on Docs:
Hosts are trusted addresses with the ability to unilaterally veto proposals and configure Rage Quit in the Party that is created after the crowdfund is won. While the crowdfund is active, the Host can finalize the crowdfund early if it has reached its minimum crowdfunding goal.
While I understand the Host is a trusted role and should not act maliciously. However, the current implementation (various functions combined) gives hosts too much power considering they can also be a member of the party.
Consider the following scenario:
A host, who is also a party member, initiates a proposal using the propose()
function. The implementation promptly triggers accept()
, marking the host as having voted (info.hasVoted[msg.sender] = true
). This, in turn, bars the host from casting further votes on this proposal. Additionally, values.votes
increases by the host's votingPower
, and values.numHostsAccepted
also increments.
info.hasVoted[msg.sender] = true; // Increase the total votes that have been cast on this proposal. uint96 votingPower = getVotingPowerAt(msg.sender, values.proposedTime - 1, snapIndex); values.votes += votingPower; if (isHost[msg.sender]) { ++values.numHostsAccepted; } info.values = values;
The problem arises with the abdicateHost()
function. It allows the host to transfer their host status to another address without requiring party approval. Consequently, a host can create multiple accounts and continually accept()
proposals that may personally benefit them.
function abdicateHost(address newPartyHost) external { _assertHost(); // 0 is a special case burn address. if (newPartyHost != address(0)) { // Cannot transfer host status to an existing host. if (isHost[newPartyHost]) { revert InvalidNewHostError(); } isHost[newPartyHost] = true; } else { // Burned the host status --numHosts; } isHost[msg.sender] = false; emit HostStatusTransferred(msg.sender, newPartyHost); }
This situation leads to a skewed system, as the platform erroneously recognizes that all hosts have accepted the proposal. As a result, the execution delay is bypassed. When a proposal is execute()
, other hosts do not the authority to veto()
because the ProposalStatus
is either inProgress
or Complete
.
function _hostsAccepted( uint8 snapshotNumHosts, uint8 numHostsAccepted ) private pure returns (bool) { return snapshotNumHosts > 0 && snapshotNumHosts == numHostsAccepted; }
Manual Review
There are 2 ways to prevent this issue:
Context
#0 - ydspa
2023-11-12T07:38:20Z
host is trusted role
Invalid: OOS
#1 - c4-pre-sort
2023-11-12T07:38:25Z
ydspa marked the issue as insufficient quality report
#2 - c4-judge
2023-11-19T17:52:37Z
gzeon-c4 marked the issue as unsatisfactory: Invalid
#3 - Henrychang26
2023-11-21T21:12:41Z
Thanks for judging @gzeon-c4, I believe this submission should be a valid dupe of #538. It talks about the core issue where a single host, can manipulate the governance by passing around the host status and accept proposals.
#4 - c4-judge
2023-11-23T08:00:39Z
gzeon-c4 marked the issue as duplicate of #233
#5 - c4-judge
2023-11-23T08:00:45Z
gzeon-c4 marked the issue as partial-25
#6 - gzeon-c4
2023-11-23T08:02:12Z
Only partial credits due to missing the critical snapshot root cause.
#7 - Henrychang26
2023-11-23T22:51:31Z
@gzeon-c4 I believe this submission deserves full credit. There are a number of duplicates that did not mention "snapshot root cause" and received full credit: #315 #323 #331 #370 #396 #444 #488
I would also like to add snapshot isn't the root cause here. Whether snapshotNumHosts
is 5 or 100, attacker can repeatedly abdicateHost()
and manipulate governance.
#8 - c4-judge
2023-11-26T16:47:52Z
gzeon-c4 marked the issue as full credit
#9 - c4-judge
2023-11-26T16:47:58Z
gzeon-c4 changed the severity to 3 (High Risk)
#10 - c4-judge
2023-11-26T16:48:03Z
gzeon-c4 marked the issue as satisfactory
1061.8613 USDC - $1,061.86
https://github.com/code-423n4/2023-10-party/blob/main/contracts/party/PartyGovernance.sol#L706 https://github.com/code-423n4/2023-10-party/blob/main/contracts/party/PartyGovernance.sol#L860 https://github.com/code-423n4/2023-10-party/blob/main/contracts/proposals/ProposalExecutionEngine.sol#L146
executeProposal()
on ProposalExecutionEngine.sol
is not marked as payable. Any proposal that requires using/sending native token will brick.
Once a proposal successfully passes governance, it becomes eligible for execution by an active member through the execute()
function.
bool completed = _executeProposal( proposalId, proposal, preciousTokens, preciousTokenIds, _getProposalFlags(values), progressData, extraData ); if (!completed) { // Proposal did not complete. proposalState.values.completedTime = 0; }
This triggers the _executeProposal()
function after passing essential checks. _executeProposal()
will delegateCall
ProposalExecutionEngine.sol
. However, _executeProposal()
encounters a problem when dealing with proposals that involve native tokens since the executeProposal()
function in ProposalExecutionEngine.sol
lacks the payable
modifier. Consequently, executing proposals requiring native tokens results in a failure, causing the proposal to remain in the "inProgress" state and eventually leading to its cancel()
by the party.
function executeProposal( ExecuteProposalParams memory params ) external onlyDelegateCall returns (bytes memory nextProgressData) {
Manual Review
function executeProposal( ExecuteProposalParams memory params - ) external onlyDelegateCall returns (bytes memory nextProgressData) { + ) external payable onlyDelegateCall returns (bytes memory nextProgressData) { ...
Payable
#0 - c4-pre-sort
2023-11-12T08:24:39Z
ydspa marked the issue as duplicate of #475
#1 - c4-pre-sort
2023-11-12T08:24:43Z
ydspa marked the issue as sufficient quality report
#2 - c4-judge
2023-11-19T14:45:46Z
gzeon-c4 marked the issue as satisfactory