withdraw(target) is the protocol's only ETH exit. The owner calls it to send every pass-sale proceed to a target of their choice. The README says the owner "collects the festival proceeds", and the interface NatSpec says it "sends to a target inputed by the owner".
withdraw uses payable(target).transfer(...), which forwards only 2300 gas. Two kinds of contract treasury therefore cannot receive:
A Safe-style proxy. Its fallback does a cold SLOAD (2100 gas) and a cold DELEGATECALL (2600 gas) in an ordinary transaction without an access list, which exceeds the stipend, so the whole withdraw reverts.
Any treasury whose receive() writes storage. SSTORE needs more than 2300 gas remaining (EIP-2200), so this fails whether the contract is warm or cold.
Likelihood:
It happens when the owner names a Safe multisig or another smart-contract treasury as the target, which is a common custody setup for proceeds, and sends an ordinary transaction without an EIP-2930 access list.
Only the trusted owner calls withdraw, and they can retry immediately with an EOA or a gas-light target, so likelihood is Low.
Impact:
Withdrawal to the intended treasury reverts and the ETH stays in FestivalPass. The owner has to route proceeds through an EOA or an intermediary, which adds an operational and trust hop.
Treasuries with storage-writing receive() logic, such as deposit accounting, can never be a withdraw target. No funds are lost.
Two replica contracts model the Safe treasury; no fork is used. SafeProxyLike reproduces SafeProxy: the singleton sits in slot 0, and the fallback does sload(0) and a delegatecall with all gas. SafeSingletonLike reproduces Safe's receive(), which emits SafeReceived. Both are deployed in setUp, so they are cold inside the test.
test_baseline_withdrawToEOA_succeeds moves 2 ETH to an EOA.
test_withdrawToSafeProxy_reverts shows the withdraw reverting with the 2 ETH still in FestivalPass. A control call{value: 1 ether} to the same proxy succeeds, and a retry to an EOA recovers the funds.
test_prewarmedSafe_withdrawSucceeds_showsColdAccessIsTheCause shows that once the proxy's slot 0 and the singleton address are warmed, the identical withdraw succeeds. This confirms that EIP-2929 cold-access costs are the cause.
test_withdrawToAccountingTreasury_alwaysReverts shows that a warm treasury whose receive() writes storage still reverts.
Save as test/PoC_BF-07.t.sol and run from the repository root:
FOUNDRY_FFI=false forge test --match-path test/PoC_BF-07.t.sol -vv
Observed output (excerpt):
Forward all gas with a low-level call and check the result. The function is onlyOwner and changes no state after the send. The fix also emits the FundsWithdrawn event that the interface declares but the contract never emits.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.