Reproduced Exploit
Party Protocol — single host can unfairly skip the veto period for a proposal
1. After a proposal passes its vote threshold, hosts get a chance to accept it; once ALL hosts have accepted, the veto delay is skipped and the proposal becomes immediately executable. 2. abdicateHost lets any current host transfer their host status to a
Chain
Other
Category
access-control
Date
Oct 2023
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 29546-h-02-single-host-can-unfairly-skip-veto-period-for-proposal. Standalone Foundry PoC and full write-up: 29546-h-02-single-host-can-unfairly-skip-veto-period-for-proposal_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/access-control/identity-bypass · vuln/governance/veto-bypass · vuln/griefing
Reproduction: a self-contained Foundry PoC that compiles & runs in an isolated project with only
forge-std— no fork, no RPC, noanvil_state. Full trace: output.txt. PoC: test/29546-h-02-single-host-can-unfairly-skip-veto-period-for-proposal_exp.sol.
Key info#
| Impact | HIGH — a single compromised or malicious host can skip a proposal's veto period entirely, without any other host ever agreeing, enabling governance capture |
| Protocol | Party Protocol — collective on-chain governance/asset-ownership platform |
| Vulnerable code | PartyGovernance.abdicateHost — transfers host status to a new address with no proposal-time snapshot |
| Bug class | Identity bypass: a threshold ("all hosts accepted") is counted by address slot, not by distinct, snapshot-verified identity |
| Finding | Code4rena — Party Protocol, 2023-10 · #29546 (H-02) · reporter 3docSec |
| Report | code4rena.com/reports/2023-10-party |
| Source | AuditVault |
| Status | Audit finding — caught in review and confirmed by the sponsor, not exploited on-chain. Reproduced here as a standalone local PoC. |
| Compiler | ^0.8.24 (PoC) |
This is an audit finding, not a historical on-chain incident. The PoC
keeps the vulnerable abdicateHost host-transfer logic verbatim and
reduces proposal voting/acceptance to the minimum needed to demonstrate the
veto-skip, mirroring the finding's own test_maliciousHost PoC shape.
TL;DR#
- After a proposal passes its vote threshold, hosts get a chance to
acceptit; once ALL hosts have accepted, the veto delay is skipped and the proposal becomes immediately executable. abdicateHostlets any current host transfer their host status to a BRAND-NEW address at any time — with no snapshot of who was a host when a given proposal was created.- A single host (Alice)
accepts a proposal once, thenabdicateHosts to a second wallet she also controls, thenaccepts AGAIN from that second wallet. - HARM:
numHostsAcceptedreaches the full host count and the veto period is skipped — even though the OTHER genuinely distinct host (Bob) never accepted at all. A single actor wearing two hats can unilaterally bypass collective host review. (The May 2023 Tornado Cash governance hack used an analogous identity/snapshot bypass to steal ~$1M.)
The vulnerable code#
The host-transfer function (verbatim, PartyGovernance.abdicateHost):
/// @notice Transfer party host status to another.
/// @param newPartyHost The address of the new host.
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);
}
Nothing here (or in accept) checks WHEN the transfer happens relative to
any proposal currently awaiting host review, or whether the "new" host is
truly a distinct participant.
Root cause#
numHostsAccepted == numHosts is meant to mean "every genuinely distinct
host has reviewed and accepted this proposal." But isHost is a live,
mutable mapping with no historical snapshot, and abdicateHost lets any host
move their slot to a new address whenever they like — including mid-proposal.
A single person can therefore occupy two "host slots" in sequence and satisfy
the threshold alone.
Preconditions#
- The attacker must already be one of the party's hosts (a normal, non-privileged-beyond-host-role condition).
- The attacker must control (or be able to acquire) a second address with
enough voting power to remain a plausible host —
abdicateHosthas no further restriction.
Attack walkthrough#
From output.txt:
- Alice and Bob are the two hosts of a party. A proposal passes its normal
vote threshold (status
Passed, veto period active). - Alice
accepts the proposal —numHostsAcceptedbecomes 1 of 2; the proposal correctly staysPassed(Bob has not accepted). - Alice calls
abdicateHost(aliceAlt), transferring her host status to a second wallet she controls. She is no longer a host;aliceAltnow is. aliceAltcallsaccepton the SAME proposal.- HARM:
numHostsAcceptedreaches 2 of 2 and the proposal transitions toReady— veto period skipped — even though Bob, the only genuinely distinct other host, never accepted. A control test confirms that without abdicating, a lone host's accept is correctly insufficient, and the proposal only reachesReadyonce BOTH real hosts accept — isolating the bug to the host-identity bypass viaabdicateHost.
Diagrams#
Remediation#
Snapshot host status per-proposal (similar to how votingPower snapshots
are already used), so accept only increments numHostsAccepted for
callers who were genuinely hosts at the proposal's creation time, and
abdicateHost records new snapshots for both the old and new host rather
than mutating a single live mapping.
How to reproduce#
cd ~/RustroverProjects/audits/evm-hack-registry/29546-h-02-single-host-can-unfairly-skip-veto-period-for-proposal_exp
forge test -vvv
# Fully local — no fork, no RPC, no anvil_state required.
# Expected: all 3 tests PASS:
# test_exploit (Exploit-driven harm)
# test_maliciousHost_standalone (standalone rebuild, mirrors finding's own test_maliciousHost)
# test_control_bothHostsMustAccept (control: no abdication -> both real hosts must accept)
PoC source: test/29546-h-02-single-host-can-unfairly-skip-veto-period-for-proposal_exp.sol
— the verbatim vulnerable abdicateHost, plus the finding's accept/abdicate/
accept-again attack sequence and a control test.
Note: normal proposal voting-power thresholds and cancellation delays are reduced-model simplifications (out of scope here — this PoC drives a proposal directly to
Passed); the reduction preserves the verbatimabdicateHosttransfer and the un-snapshottednumHostsAcceptedcounter, the faithful parts of the bug. This is a non-fund/governance harm — the Playground displays it via the zero-profit-native pattern.
Reference: finding #29546 (H-02) by 3docSec in the Code4rena Party Protocol review (Oct 2023) · curated by AuditVault
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 29546-h-02-single-host-can-unfairly-skip-veto-period-for-proposal_exp (evm-hack-registry mirror).
- AuditVault finding: 29546-h-02-single-host-can-unfairly-skip-veto-period-for-proposal.
Alerts & third-party analyses
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.