DatingDapp

AI First Flight #6
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

MultiSigWallet has no cancellation or timeout mechanism, permanently locking matched users' ETH if either owner refuses to cooperate

Root + Impact

Description

  • matchRewards() deploys a fresh MultiSigWallet for each match and sends the reward ETH to it. The wallet requires both matched users (owner1, owner2) to approve every transaction before execution.

  • MultiSigWallet provides submitTransaction, approveTransaction, and executeTransaction but no way to cancel a submitted transaction and no timeout after which a unilateral withdrawal is possible. If one matched user loses their key, becomes unresponsive, or simply refuses to approve, all ETH in the wallet is permanently locked with no recovery path.

// src/MultiSig.sol — no cancel, no timeout, no escape hatch
function submitTransaction(address _to, uint256 _value) external onlyOwners { ... }
function approveTransaction(uint256 _txId) external onlyOwners { ... }
function executeTransaction(uint256 _txId) external onlyOwners {
// @> requires BOTH approvals — unilateral exit is impossible
require(txn.approvedByOwner1 && txn.approvedByOwner2, "Not enough approvals");
...
}
// @> no cancelTransaction(), no withdrawAfterTimeout(), no emergency drain

Risk

Likelihood:

  • Every matched pair's ETH lands in a two-of-two MultiSig deployed with no escape hatch. Any disagreement, key loss, or one party going offline makes the funds unrecoverable.

  • The protocol currently deposits 0 ETH to the MultiSig due to H-01 (userBalances never credited), but fixing H-01 without adding a cancellation mechanism would make this locking scenario directly exploitable.

Impact:

  • Matched users' reward ETH is permanently locked whenever either owner cannot or will not cooperate. There is no owner, governance address, or timeout mechanism that can unblock the funds.

  • One malicious user can hold their match's ETH hostage indefinitely by simply never approving any transaction.

Proof of Concept

The sequence below shows the lock-up scenario:

function testMatchedFundsLockedIfOwnerRefuses() public {
// Assume H-01 is fixed so userBalances are credited
// Alice and Bob match — matchRewards deploys a MultiSig and sends 1.8 ETH
MultiSigWallet wallet = MultiSigWallet(payable(deployedWalletAddress));
// Alice submits a split transaction
vm.prank(alice);
wallet.submitTransaction(alice, 0.9 ether); // txId = 0
// Alice approves
vm.prank(alice);
wallet.approveTransaction(0);
// Bob never calls approveTransaction — funds are permanently locked
// No cancelTransaction exists, no timeout, no owner override
vm.prank(alice);
vm.expectRevert("Not enough approvals");
wallet.executeTransaction(0);
// ETH is permanently stuck — no recovery mechanism
assertEq(address(wallet).balance, 1.8 ether);
}

Alice's ETH remains trapped in the MultiSig because Bob refuses to approve, and the contract offers no alternative exit.

Recommended Mitigation

Add a cancellation function callable by either owner, and a timeout-based unilateral withdrawal for transactions that remain unexecuted past a deadline:

+ uint256 public constant TIMEOUT = 7 days;
+ mapping(uint256 => uint256) public txTimestamp;
function submitTransaction(address _to, uint256 _value) external onlyOwners {
...
+ txTimestamp[txId] = block.timestamp;
}
+ function cancelTransaction(uint256 _txId) external onlyOwners {
+ Transaction storage txn = transactions[_txId];
+ require(!txn.executed, "Already executed");
+ txn.executed = true; // mark to prevent future execution
+ }
+ function withdrawAfterTimeout(uint256 _txId, address _to) external onlyOwners {
+ require(block.timestamp >= txTimestamp[_txId] + TIMEOUT, "Timeout not reached");
+ Transaction storage txn = transactions[_txId];
+ require(!txn.executed, "Already executed");
+ txn.executed = true;
+ (bool success,) = payable(_to).call{value: txn.value}("");
+ require(success, "Transfer failed");
+ }
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 4 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!