DatingDapp

AI First Flight #6
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: high
Valid

The contract doesn't track user funds

Root + Impact

Description

  • When a user funds the contract by calling the likeUser() function and attaching Ether (e.g., 1 ETH), the contract is expected to track these deposited funds to be used by the internal matchRewards() function. This internal function is responsible for calculating and sending the pooled funds to a newly deployed MultiSig contract.

  • Issue: Although the contract defines a userBalances mapping (address => uint256), it completely fails to update this mapping with the msg.value sent by the user during the likeUser() execution.

    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"
    );
    likes[msg.sender][liked] = true;
    emit Liked(msg.sender, liked);
    // Check if mutual like
    if (likes[liked][msg.sender]) {
    matches[msg.sender].push(liked);
    matches[liked].push(msg.sender);
    emit Matched(msg.sender, liked);
    matchRewards(liked, msg.sender);
    }
    }
mapping(address => uint256) public userBalances; // Maps the user funds to the inteded address
function matchRewards(address from, address to) internal {
uint256 matchUserOne = userBalances[from]; //Doesn't update msg.value paid by the user
uint256 matchUserTwo = userBalances[to];//Doesn't update msg.value paid by the user
userBalances[from] = 0;
userBalances[to] = 0;
uint256 totalRewards = matchUserOne + matchUserTwo;
uint256 matchingFees = (totalRewards * FIXEDFEE) / 100;
uint256 rewards = totalRewards - matchingFees;
totalFees += matchingFees;
// Deploy a MultiSig contract for the matched users
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
// Send ETH to the deployed multisig wallet
(bool success, ) = payable(address(multiSigWallet)).call{
value: rewards
}("");
require(success, "Transfer failed");
}

Risk

Likelihood:

  • Because the contract lacks the logic to record the incoming msg.value, userBalances[user] will always evaluate to 0. Consequently, when a match occurs and matchRewards() is triggered, the reward calculation will always be based on a zero balance, resulting in exactly 0 ETH being sent to the deployed MultiSig. This will happen on 100% of all funded matches.

Impact:

  • Impact (High): Because the MultiSig wallet receives 0 ETH, the actual Ether deposited by the users becomes permanently locked inside the LikeRegistry contract. Without a way to properly account for or withdraw these stuck funds, users suffer a complete loss of their deposited Ether.

Proof of Concept

  • The following Foundry test demonstrates the complete failure of the contract's internal accounting. In the test, two users (Alice and Spider) both call likeUser and send 1 Ether each, resulting in a successful match. The assertions prove two critical facts:

  1.     The internal `userBalances` mapping for both users remains exactly `0`, meaning the contract failed to record their deposits.
    
  2. The LikeRegistry contract's actual Ether balance is exactly 2 ether, proving that the contract swallowed the funds and permanently locked them instead of forwarding them to the MultiSig wallet as intended.


modifier mintsTheUser() {
vm.prank(alice);
soulNFT.mintProfile("Alice", 18, "Hello");
vm.prank(spider);
soulNFT.mintProfile("spider", 21, "spider");
_;
}
modifier Alice_and_SpiderLikesEachOther() {
vm.prank(alice);
lregistry.likeUser{value: 1 ether}(spider);
vm.prank(spider);
lregistry.likeUser{value: 1 ether}(alice);
_;
}
function test_TrackUsersBalance_After_Both_user_like_each_other()
public
mintsTheUser
Alice_and_SpiderLikesEachOther
{
//Arrange
uint256 Ether_balance_of_alice = alice.balance;
uint256 Ether_balance_of_spider = spider.balance;
uint256 Ether_balance_of_contract = address(lregistry).balance;
//Act
uint256 funded_balance_of_alice = lregistry.userBalances(alice);
uint256 funded_balance_of_spider = lregistry.userBalances(spider);
//Assert
assertEq(funded_balance_of_alice, 0, "Should be 1 ether");
assertEq(funded_balance_of_spider, 0, "Should be 1 ether");
assertEq(Ether_balance_of_contract, 2 ether, "should be 2 ether");
}

[PASS] test_TrackUsersBalance_After_Both_user_like_each_other() (gas: 1417375)
Logs:
funded_balance_of_alice 0
funded_balance_of_spider 0
Ether_balance_of_contract 2000000000000000000
Traces:
[1417375] TestVulnDapp::test_TrackUsersBalance_After_Both_user_like_each_other()
├─ [0] VM::prank(Alice: [0xBf0b5A4099F0bf6c8bC4252eBeC548Bae95602Ea])
│ └─ ← [Return]
├─ [165657] SoulboundProfileNFT::mintProfile("Alice", 18, "Hello")
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: Alice: [0xBf0b5A4099F0bf6c8bC4252eBeC548Bae95602Ea], tokenId: 1)
│ ├─ emit ProfileMinted(user: Alice: [0xBf0b5A4099F0bf6c8bC4252eBeC548Bae95602Ea], tokenId: 1, name: "Alice", age: 18, profileImage: "Hello")
│ └─ ← [Stop]
├─ [0] VM::prank(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63])
│ └─ ← [Return]
├─ [143757] SoulboundProfileNFT::mintProfile("spider", 21, "spider")
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63], tokenId: 2)
│ ├─ emit ProfileMinted(user: spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63], tokenId: 2, name: "spider", age: 21, profileImage: "spider")
│ └─ ← [Stop]
├─ [0] VM::prank(Alice: [0xBf0b5A4099F0bf6c8bC4252eBeC548Bae95602Ea])
│ └─ ← [Return]
├─ [32614] LikeRegistry::likeUser{value: 1000000000000000000}(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63])
│ ├─ [913] SoulboundProfileNFT::profileToToken(Alice: [0xBf0b5A4099F0bf6c8bC4252eBeC548Bae95602Ea]) [staticcall]
│ │ └─ ← [Return] 1
│ ├─ [913] SoulboundProfileNFT::profileToToken(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63]) [staticcall]
│ │ └─ ← [Return] 2
│ ├─ emit Liked(liker: Alice: [0xBf0b5A4099F0bf6c8bC4252eBeC548Bae95602Ea], liked: spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63])
│ └─ ← [Stop]
├─ [0] VM::prank(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63])
│ └─ ← [Return]
├─ [1029658] LikeRegistry::likeUser{value: 1000000000000000000}(Alice: [0xBf0b5A4099F0bf6c8bC4252eBeC548Bae95602Ea])
│ ├─ [913] SoulboundProfileNFT::profileToToken(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63]) [staticcall]
│ │ └─ ← [Return] 2
│ ├─ [913] SoulboundProfileNFT::profileToToken(Alice: [0xBf0b5A4099F0bf6c8bC4252eBeC548Bae95602Ea]) [staticcall]
│ │ └─ ← [Return] 1
│ ├─ emit Liked(liker: spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63], liked: Alice: [0xBf0b5A4099F0bf6c8bC4252eBeC548Bae95602Ea])
│ ├─ emit Matched(user1: spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63], user2: Alice: [0xBf0b5A4099F0bf6c8bC4252eBeC548Bae95602Ea])
│ ├─ [870275] → new MultiSigWallet@0xd04404bcf6d969FC0Ec22021b4736510CAcec492
│ │ └─ ← [Return] 4122 bytes of code
│ ├─ [55] MultiSigWallet::receive()
│ │ └─ ← [Stop]
│ └─ ← [Stop]
├─ [847] LikeRegistry::userBalances(Alice: [0xBf0b5A4099F0bf6c8bC4252eBeC548Bae95602Ea]) [staticcall]
│ └─ ← [Return] 0
├─ [847] LikeRegistry::userBalances(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("funded_balance_of_alice", 0) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("funded_balance_of_spider", 0) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("Ether_balance_of_contract", 2000000000000000000 [2e18]) [staticcall]
│ └─ ← [Stop]
└─ ← [Stop]

Recommended Mitigation

  • Resolve this issue, the contract must correctly account for the Ether deposited by users when they initiate a like.

Update the likeUser function to increment the caller's internal balance by the msg.value they sent. This update must happen before any matching logic or external calls are executed.

function likeUser(address targetAddress) external payable {
// 1. Add this line to properly track the user's deposited funds
userBalances[msg.sender] += msg.value;
// ... existing like and matching logic ...
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-01] After the user calls the `likeUser` function, the userBalance does not increase by the corresponding value.

## Description User A calls `likeUser` and sends `value > 1` ETH. According to the design of DatingDapp, the amount for user A should be accumulated by `userBalances`. Otherwise, in the subsequent calculations, the balance for each user will be 0. ## Vulnerability Details When User A calls `likeUser`, the accumulation of `userBalances` is not performed. ```solidity 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"); likes[msg.sender][liked] = true; emit Liked(msg.sender, liked); // Check if mutual like if (likes[liked][msg.sender]) { matches[msg.sender].push(liked); matches[liked].push(msg.sender); emit Matched(msg.sender, liked); matchRewards(liked, msg.sender); } } ``` This will result in `totalRewards` always being 0, affecting all subsequent calculations: ```solidity uint256 totalRewards = matchUserOne + matchUserTwo; uint256 matchingFees = (totalRewards * FIXEDFEE ) / 100; uint256 rewards = totalRewards - matchingFees; totalFees += matchingFees; ``` ## POC ```solidity function testUserBalanceshouldIncreaseAfterLike() public { vm.prank(user1); likeRegistry.likeUser{value: 20 ether}(user2); assertEq(likeRegistry.userBalances(user1), 20 ether, "User1 balance should be 20 ether"); } ``` Then we will get an error: ```shell [FAIL: User1 balance should be 20 ether: 0 != 20000000000000000000] ``` ## Impact - Users will be unable to receive rewards. - The contract owner will also be unable to withdraw ETH from the contract. ## Recommendations Add processing for `userBalances` in the `likeUser` function: ```diff 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"); likes[msg.sender][liked] = true; + userBalances[msg.sender] += msg.value; emit Liked(msg.sender, liked); [...] } ```

Support

FAQs

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

Give us feedback!