Santa's List

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

`userBalances` never written in `likeUser()` -- all deposited ETH permanently locked

Description

  • The LikeRegistry protocol intends for users to deposit ETH when liking other users, track those deposits in userBalances, and upon mutual match distribute the pooled ETH (minus a 10% fee) to a newly deployed MultiSigWallet.

  • The likeUser() function accepts ETH via msg.value (requiring >= 1 ETH) but never writes to the userBalances mapping. When matchRewards() is triggered on a mutual match, it reads userBalances[from] and userBalances[to] -- both of which are always 0. This means totalRewards = 0, rewards = 0, and the MultiSig receives nothing. Additionally, totalFees is never incremented, so withdrawFees() always reverts. All ETH deposited by users is permanently locked in the contract with no recovery mechanism.

function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
require(!likes[msg.sender][liked], "Already liked");
require(msg.sender != liked, "Cannot like yourself");
require(profileNFT.profileToToken(msg.sender) != 0, "Must have a profile NFT");
require(profileNFT.profileToToken(liked) != 0, "Liked user must have a profile NFT");
// @> msg.value is received but NEVER credited to userBalances[msg.sender]
likes[msg.sender][liked] = true;
emit Liked(msg.sender, liked);
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
matchRewards(liked, msg.sender);
}
}
function matchRewards(address from, address to) internal {
uint256 matchUserOne = userBalances[from]; // @> always 0
uint256 matchUserTwo = userBalances[to]; // @> always 0
userBalances[from] = 0;
userBalances[to] = 0;
uint256 totalRewards = matchUserOne + matchUserTwo; // @> 0 + 0 = 0
uint256 matchingFees = (totalRewards * FIXEDFEE) / 100; // @> 0
uint256 rewards = totalRewards - matchingFees; // @> 0
totalFees += matchingFees; // @> totalFees remains 0
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
(bool success,) = payable(address(multiSigWallet)).call{value: rewards}(""); // @> sends 0 ETH
require(success, "Transfer failed");
}

Risk

Likelihood:

  • This occurs on every single likeUser() call -- the bug is in the core user flow and affects 100% of interactions

  • Every mutual match triggers matchRewards() which reads the always-zero userBalances

Impact:

  • All ETH deposited by users via likeUser() is permanently locked in the LikeRegistry contract

  • The MultiSigWallet deployed on match receives 0 ETH instead of the pooled rewards

  • withdrawFees() always reverts since totalFees is never incremented, so even the protocol owner cannot extract any funds

Proof of Concept

function testUserBalancesNeverWritten() public {
// Setup: mint profiles for alice and bob
vm.prank(alice);
profileNFT.mintProfile("Alice", 25, "img");
vm.prank(bob);
profileNFT.mintProfile("Bob", 26, "img");
// Alice likes Bob with 1 ETH
vm.prank(alice);
likeRegistry.likeUser{value: 1 ether}(bob);
// Bob likes Alice with 1 ETH -- triggers mutual match
vm.prank(bob);
likeRegistry.likeUser{value: 1 ether}(alice);
// 2 ETH is now permanently locked in LikeRegistry
assertEq(address(likeRegistry).balance, 2 ether);
// Owner cannot withdraw fees
vm.prank(owner);
vm.expectRevert("No fees to withdraw");
likeRegistry.withdrawFees();
}

Recommended Mitigation

function likeUser(address liked) external payable {
require(msg.value >= 1 ether, "Must send at least 1 ETH");
require(!likes[msg.sender][liked], "Already liked");
require(msg.sender != liked, "Cannot like yourself");
require(profileNFT.profileToToken(msg.sender) != 0, "Must have a profile NFT");
require(profileNFT.profileToToken(liked) != 0, "Liked user must have a profile NFT");
+ userBalances[msg.sender] += msg.value;
+
likes[msg.sender][liked] = true;
emit Liked(msg.sender, liked);
Updates

Lead Judging Commences

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