DatingDapp

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

matchRewards is called with inverted arguments, assigning wrong ownership in every deployed MultiSig


Root + Impact

Description

  • matchRewards(address from, address to) is intended to reward both matched users by deploying MultiSigWallet(from, to) where from represents the initiating liker (owner1) and to represents the liked party (owner2).

  • The call site in likeUser passes liked as from and msg.sender (the liker) as to, permanently inverting the ownership roles. The account that pressed "like" to trigger the match becomes owner2 of the shared wallet instead of owner1.

if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
@> matchRewards(liked, msg.sender); // liked=from → owner1, msg.sender=to → owner2 (inverted)
}
function matchRewards(address from, address to) internal {
uint256 matchUserOne = userBalances[from]; // from = liked party
uint256 matchUserTwo = userBalances[to]; // to = liker
...
@> MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
// owner1 = liked party ← should be liker
// owner2 = liker ← should be liked party
}

Risk

Likelihood:

  • Every successful match triggers matchRewards — 100% of deployed MultiSig wallets have inverted ownership roles.

Impact:

  • Off-chain tooling, front-end displays, and any protocol logic keyed to owner1 being the liker will behave incorrectly for every match.

  • Once H-2 is fixed and real ETH flows into wallets, affected users interacting with the wrong role could face UX failures or unexpected permission assignments.

Proof of Concept

This test demonstrates the ownership inversion by reading owner1 and owner2 directly from the deployed MultiSigWallet after a match:

Setup — Bob likes Alice first (Bob is the first liker). Alice is funded and likes Bob second, which triggers the match.

likeUser fires — Inside likeUser, msg.sender = alice and liked = bob. The call is matchRewards(liked=bob, msg.sender=alice), so from = bob and to = alice.

MultiSig deployed — new MultiSigWallet(bob, alice) is called, making owner1 = bob (the previously-liked party) and owner2 = alice (the one who triggered the match).

Expected versus actual — Alice initiated the match-triggering call, so she should logically be owner1. Instead she is owner2. The roles are swapped for every matched pair in the protocol.

To run: forge test --match-test test_ownershipInvertedInMultiSig -vvvv

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/SoulboundProfileNFT.sol";
import "../src/LikeRegistry.sol";
import "../src/MultiSig.sol";
contract H3_ArgumentInversionTest is Test {
SoulboundProfileNFT profileNFT;
LikeRegistry likeRegistry;
address alice = makeAddr("alice"); // will like bob second — triggers match
address bob = makeAddr("bob"); // likes alice first
// We capture the MultiSig address from the factory pattern by deploying it ourselves
// to predict the address via CREATE, or alternatively intercept events.
// For clarity, we deploy a test-harness version of matchRewards logic.
function setUp() public {
profileNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(profileNFT));
vm.deal(alice, 10 ether);
vm.deal(bob, 10 ether);
vm.prank(alice);
profileNFT.mintProfile("Alice", 25, "ipfs://alice");
vm.prank(bob);
profileNFT.mintProfile("Bob", 26, "ipfs://bob");
// Bob likes Alice first
vm.prank(bob);
likeRegistry.likeUser{value: 1 ether}(alice);
}
function test_ownershipInvertedInMultiSig() public {
// Alice likes Bob — triggers match, matchRewards(liked=bob, msg.sender=alice) fires
// MultiSigWallet(bob, alice) is deployed → owner1=bob, owner2=alice
// Expected: owner1=alice (the match-triggering liker), owner2=bob
// Predict deployed MultiSig address using CREATE (nonce-based)
address expectedMultiSig = computeCreateAddress(address(likeRegistry), vm.getNonce(address(likeRegistry)));
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
MultiSigWallet wallet = MultiSigWallet(payable(expectedMultiSig));
address owner1 = wallet.owner1();
address owner2 = wallet.owner2();
// owner1 is bob (the liked party), but alice triggered the match
assertEq(owner1, bob, "owner1 is the liked party — inverted");
assertEq(owner2, alice, "owner2 is the liker — inverted");
console.log("owner1 (should be alice/liker):", owner1); // prints bob
console.log("owner2 (should be bob/liked):", owner2); // prints alice
}
}

Recommended Mitigation

Swap the argument order to match the intended semantics:

-matchRewards(liked, msg.sender);
+matchRewards(msg.sender, liked); // liker = from (owner1), liked = to (owner2)
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!