MultiSigWallet.executeTransaction is designed to send ETH to a target address only after both owners have approved the transaction, marking it as executed to prevent replay.
While txn.executed = true is correctly set before the external .call, the TransactionExecuted event is emitted after the call returns. A malicious recipient contract can re-enter wallet functions during the call, causing the event to fire at an unexpected point in the call stack.
Likelihood:
Occurs whenever the recipient address (txn.to) is a smart contract with a receive or fallback that calls back into the MultiSig.
Impact:
Event logs are emitted in incorrect order, breaking off-chain monitoring, subgraph indexing, and audit tooling that relies on event ordering to reconstruct on-chain state.
Although the state guard (txn.executed = true) prevents double-spending, corrupted event sequencing misleads integrations and users.
This test demonstrates that a TransactionCreated event (emitted from inside the reentrant receive()) appears in the log array before the TransactionExecuted event for the original transaction — even though logically the execution should be logged first:
Setup — A ReentrantRecipient contract is deployed with a reference to the MultiSigWallet. The wallet is funded with 5 ETH. A transaction targeting ReentrantRecipient for 1 ETH is submitted and approved by both owners.
Execution starts — executeTransaction(0) is called. txn.executed is set to true. Then .call{value: 1 ether} is made to ReentrantRecipient.
Reentrancy inside receive() — ReentrantRecipient.receive() calls wallet.submitTransaction(address(this), 1). This succeeds (it doesn't re-enter executeTransaction), emitting TransactionCreated(txId=1) from inside the external call of executeTransaction(0).
Call returns — Control returns to executeTransaction, which then emits TransactionExecuted(txId=0).
Corrupted log order — The recorded logs show TransactionCreated(1) before TransactionExecuted(0). Any indexer replaying these events in order will process a new transaction creation before the originating execution is finalized.
To run: forge test --match-test test_eventEmittedAfterExternalCall -vvvv
Emit the event before the external call:
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.