DatingDapp

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

MultiSigWallet 2-of-2 quorum has no recovery path if one party disappears

Description

  • Normal behavior: executeTransaction() should let two matched users jointly manage the ETH reward pooled into their shared wallet.

  • The issue: quorum is a strict 2-of-2 with no owner-replacement, timeout, or fallback path. MultiSigWallet instances are auto-deployed per matched pair by LikeRegistry.matchRewards(), so the owners are ordinary end users, not cooperating operators.

function executeTransaction(uint256 _txId) external onlyOwners {
Transaction storage txn = transactions[_txId];
// @> strict 2-of-2, no expiry, no owner replacement anywhere in the contract
require(txn.approvedByOwner1 && txn.approvedByOwner2, "Not enough approvals");
...
}

Risk

Likelihood:

  • Reason 1: Matches going unanswered or ghosted is normal user behavior for a dating app, not an edge case.

  • Reason 2: Wallet loss, key loss, or the other party abandoning the app all produce the same unrecoverable outcome.
    Impact:

  • Impact 1: The cooperating user's share of the pooled reward becomes permanently inaccessible.

  • Impact 2: Affects every matched pair where either party stops participating — a common occurrence, not a rare one.

Proof of Concept

test/PoC2.sol. Two tests: one confirms the bug, one confirms the wallet works normally when both owners cooperate.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/MultiSig.sol";
contract MultiSigPoCTest is Test {
MultiSigWallet multisig;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
function setUp() public {
multisig = new MultiSigWallet(alice, bob);
vm.deal(address(multisig), 1 ether); // simulate a delivered match reward
}
function testStuckIfOneOwnerNeverApproves() public {
vm.prank(alice);
multisig.submitTransaction(alice, 1 ether);
vm.prank(alice);
multisig.approveTransaction(0); // bob never approves — ghosted
assertEq(address(multisig).balance, 1 ether);
vm.prank(alice);
vm.expectRevert("Not enough approvals");
multisig.executeTransaction(0); // reverts forever
}
function testExecutesFineWhenBothOwnersApprove() public {
vm.prank(alice);
multisig.submitTransaction(alice, 1 ether);
vm.prank(alice);
multisig.approveTransaction(0);
vm.prank(bob);
multisig.approveTransaction(0); // full quorum reached
vm.prank(alice);
multisig.executeTransaction(0); // succeeds normally
assertEq(address(multisig).balance, 0);
}
}

Both [PASS]. The second test is a control: it shows the wallet works fine when both owners cooperate, isolating the bug to the single-approval case.

Recommended Mitigation

Add a time-boxed escape hatch: if only one owner approved but enough time has passed since submission, that approval alone becomes sufficient.

+ mapping(uint256 => uint256) public submittedAt;
+ uint256 public constant RECOVERY_TIMEOUT = 30 days;
function executeTransaction(uint256 _txId) external onlyOwners {
Transaction storage txn = transactions[_txId];
- require(txn.approvedByOwner1 && txn.approvedByOwner2, "Not enough approvals");
+ bool fullQuorum = txn.approvedByOwner1 && txn.approvedByOwner2;
+ bool timedOutSingleApproval = (txn.approvedByOwner1 || txn.approvedByOwner2)
+ && block.timestamp >= submittedAt[_txId] + RECOVERY_TIMEOUT;
+ require(fullQuorum || timedOutSingleApproval, "Not enough approvals");
...
}

submittedAt[_txId] must be set inside submitTransaction() for this to work.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day 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!