The match wallet is a 2-of-2: ETH leaves only through a transaction that both owners approve (L72). Suppose an owner approves a transaction for a date that is later cancelled. They should be able to withdraw that approval before the transaction executes.
approveTransaction only ever sets approvedByOwner1 or approvedByOwner2 to true (L58, L61), and calling it again reverts AlreadyApproved. No function clears an approval or cancels a pending transaction. The Transaction struct (L14-L20) has no deadline, and executeTransaction checks only !executed and the two flags (L71-L72). Once one owner approves, the other owner alone decides when the transaction runs; in the PoC that is a year later.
The amount is fixed at submission (L45, L75). A stale transaction therefore sends exactly the approved value to the approved recipient, never "the wallet balance". A larger balance can only make an approved transaction that was unfunded at the time executable later.
Likelihood:
It happens when one owner approves a transaction and then changes their mind before the other owner co-signs, and the other owner goes ahead anyway.
The first approver can avoid it by approving last. Under the current code, match wallets receive 0 ETH from LikeRegistry, so only ETH that the owners deposit themselves through receive() is exposed.
Impact:
The first approver cannot withdraw consent. In the PoC, Bob executes a 1 ETH transfer to himself 365 days after Alice approved it, on that year-old approval.
An approved transaction that failed for lack of funds stays pending. It executes once the wallet is funded for another purpose: in the PoC, the approved 1 ETH moves out of a later 5 ETH deposit. The 2-of-2 rule is never broken, and the amount never exceeds what was approved.
The PoC deploys MultiSigWallet(alice, bob) directly. The baseline shows that one approval alone cannot execute (Not enough approvals). The other tests show the following:
No revoke or cancel: four common revoke or cancel selectors all revert, because the contract has no such function and no fallback. Re-approving reverts AlreadyApproved, and the flag stays set.
No expiry: Bob submits a 1 ETH transfer to himself and Alice approves it. 365 days later, Bob alone approves and executes it and receives 1 ETH.
Unfunded transaction: an approved transaction that failed for lack of funds stays pending. 180 days later Alice deposits 5 ETH for another purpose. Bob then executes the transaction: exactly the approved 1 ETH moves and 4 ETH stay in the wallet.
Run from the repository root, with the file saved as test/PoC_DD-12.t.sol:
Result: Suite result: ok. 4 passed; 0 failed; 0 skipped. The logs show bob gained (wei) 1000000000000000000 and wallet balance after (wei) 4000000000000000000.
Let an owner withdraw an approval before execution. Optionally, also add a deadline to Transaction and check it in executeTransaction.
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.