Santa's List

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

Aggregated `userBalances` drain on first match -- deposits for other likes stolen

Description

  • The protocol tracks ETH deposits in a single userBalances[user] mapping per address. When a user likes multiple people, all deposits accumulate into this single balance.

  • When any one match triggers, matchRewards() reads and drains the user's entire userBalances to that single MultiSigWallet, leaving zero for subsequent matches. This means deposits intended for other potential matches are redirected to whichever match fires first.

function matchRewards(address from, address to) internal {
uint256 matchUserOne = userBalances[from]; // @> reads ENTIRE balance, not per-pair
uint256 matchUserTwo = userBalances[to]; // @> reads ENTIRE balance
userBalances[from] = 0; // @> drains ALL, including deposits for other likes
userBalances[to] = 0;
uint256 totalRewards = matchUserOne + matchUserTwo;
// ...
}

Risk

Likelihood:

  • This occurs whenever a user likes multiple people -- a natural and expected usage pattern in a dating app

  • The first mutual match drains all accumulated deposits regardless of how many other pending likes exist

Impact:

  • Deposits meant for one match are sent to a different match's MultiSig -- the wrong pair receives the excess ETH

  • Subsequent matches receive 0 rewards because the balance was already fully drained

  • Users lose ETH intended for other matches with no recourse

Proof of Concept

function testAggregatedBalanceDrain() public {
// Setup profiles
vm.prank(alice);
profileNFT.mintProfile("Alice", 25, "img");
vm.prank(bob);
profileNFT.mintProfile("Bob", 26, "img");
vm.prank(carol);
profileNFT.mintProfile("Carol", 27, "img");
// Alice likes Bob and Carol (1 ETH each)
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(carol);
// userBalances[alice] = 2 ETH
// Bob likes Alice -- match triggers, drains ALL of Alice's balance
vm.prank(bob);
likeRegistry.likeUser{value: 1 ether}(alice);
// Alice-Bob MultiSig gets (2 + 1) * 0.9 = 2.7 ETH instead of 1.8 ETH
// Carol likes Alice -- match triggers but Alice's balance is 0
vm.prank(carol);
likeRegistry.likeUser{value: 1 ether}(carol);
// Alice-Carol MultiSig gets only Carol's 0.9 ETH, Alice's deposit was stolen
}

Recommended Mitigation

- mapping(address => uint256) public userBalances;
+ mapping(address => mapping(address => uint256)) public pairDeposits;
function likeUser(address liked) external payable {
// ...
- userBalances[msg.sender] += msg.value;
+ pairDeposits[msg.sender][liked] += msg.value;
// ...
}
function matchRewards(address from, address to) internal {
- uint256 matchUserOne = userBalances[from];
- uint256 matchUserTwo = userBalances[to];
- userBalances[from] = 0;
- userBalances[to] = 0;
+ uint256 matchUserOne = pairDeposits[from][to];
+ uint256 matchUserTwo = pairDeposits[to][from];
+ pairDeposits[from][to] = 0;
+ pairDeposits[to][from] = 0;
// ...
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 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!