DatingDapp

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

Owner Cant Withdraw TotalFees

Root + Impact

Description

  • In the Dating DApp (Web3 Audit) protocol, the owner is permanently locked out of withdrawing accumulated protocol fees. The withdrawFees() function correctly restricts access using the onlyOwner modifier and requires that totalFees > 0. However, because of the previously identified broken accounting in the likeUser() function, userBalances always evaluate to zero. When matchRewards() executes, it calculates the matchingFees as a percentage of 0. Consequently, totalFees always increases by 0, leaving the state variable permanently empty.

    function likeUser(address liked) external payable {
    require(msg.value >= 1 ether, "Must send at least 1 ETH"); //Checks but doesn't update the state
    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);
    }
    }
    function matchRewards(address from, address to) internal {
    uint256 matchUserOne = userBalances[from];
    uint256 matchUserTwo = userBalances[to];
    userBalances[from] = 0;
    userBalances[to] = 0;
    uint256 totalRewards = matchUserOne + matchUserTwo;
    uint256 matchingFees = (totalRewards * FIXEDFEE) / 100;
    uint256 rewards = totalRewards - matchingFees;
    totalFees += matchingFees; //Doesnt update the state which cause the revert
    // 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");
    }
function withdrawFees() external onlyOwner {
require(totalFees > 0, "No fees to withdraw");
uint256 totalFeesToWithdraw = totalFees;
totalFees = 0;
(bool success, ) = payable(owner()).call{value: totalFeesToWithdraw}(
""
);
require(success, "Transfer failed");
}

Risk

Likelihood:

  • Because the root cause lies in a guaranteed mathematical failure during the matching process, totalFees will never increment under any circumstances. Therefore, every single attempt by the owner to call withdrawFees() will fail.


Impact:

  • The protocol suffers a 100% loss of its intended revenue. The Ether that should have been collected as fees remains permanently trapped inside the LikeRegistry contract, alongside the users' stuck funds, with no alternative administrative function available to sweep or recover it.

Proof of Concept

The accompanying Foundry test demonstrates the downstream failure of the fee accounting system. The test simulates a successful match where Alice and Spider each deposit 1 Ether (totaling 2 Ether held by the contract). When the protocol owner attempts to call withdrawFees(), the transaction reverts with the specific error "No fees to withdraw". The final assertions definitively prove that the contract balance remains exactly at 2 Ether and the owner successfully retrieves nothing.

function test__Owner_cant_Withdraw_Fees()
public
mintsTheUser
Alice_and_SpiderLikesEachOther
{
//Arrange
uint256 balance_of_contract_before_ownerWithdraws = address(lregistry)
.balance;
console.log(
"balance before owner withdraws: ",
balance_of_contract_before_ownerWithdraws
);
//Act
vm.startPrank(DEFAULT_FOUNDRY);
vm.expectRevert();
lregistry.withdrawFees();
uint256 balance_Of_contract_after_ownerWithdraws = address(lregistry)
.balance;
console.log(
"Ether_balance_of_contract_after_ownerWithdraws",
balance_Of_contract_after_ownerWithdraws
);
//Assert
assertEq(
balance_Of_contract_after_ownerWithdraws,
2 ether,
"should be 0 ether"
);
}
[PASS] test__Owner_cant_Withdraw_Fees() (gas: 1416537)
Logs:
balance before owner withdraws: 2000000000000000000
Ether_balance_of_contract_after_ownerWithdraws 2000000000000000000
Traces:
[1416537] TestVulnDapp::test__Owner_cant_Withdraw_Fees()
├─ [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]
├─ [0] console::log("balance before owner withdraws: ", 2000000000000000000 [2e18]) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::startPrank(DefaultSender: [0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38])
│ └─ ← [Return]
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [2898] LikeRegistry::withdrawFees()
│ └─ ← [Revert] No fees to withdraw
├─ [0] console::log("Ether_balance_of_contract_after_ownerWithdraws", 2000000000000000000 [2e18]) [staticcall]
│ └─ ← [Stop]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.37ms (2.03ms CPU time)

Recommended Mitigation

  • Because this is a cascading failure, resolving this issue requires fixing the root cause upstream rather than changing the withdrawFees() function itself.

function likeUser(address targetAddress) external payable {
// Increment the user's balance to fix the accounting chain
userBalances[msg.sender] += msg.value;
// ... existing 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!