Summary
The LikeRegistry::likeUser
function is vulnerable to front-running attacks where an attacker can intercept intended matches by observing pending transactions and submitting their own transaction with a higher gas price, allowing them to manipulate the matching system with minimal ETH commitment.
Vulnerability Details
The likeUser
function in LikeRegistry.sol
only enforces a minimum ETH requirement without any protection against transaction ordering manipulation:
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);
if (likes[liked][msg.sender]) {
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
matchRewards(liked, msg.sender);
}
}
Proof Of Concept
contract LikeRegistryTest is Test {
LikeRegistry registry;
SoulboundProfileNFT nft;
address alice = address(0x1);
address bob = address(0x2);
address attacker = address(0x3);
function setUp() public {
nft = new SoulboundProfileNFT();
registry = new LikeRegistry(address(nft));
vm.startPrank(alice);
nft.mintProfile("Alice", 25, "ipfs://alice");
vm.stopPrank();
vm.startPrank(bob);
nft.mintProfile("Bob", 30, "ipfs://bob");
vm.stopPrank();
vm.startPrank(attacker);
nft.mintProfile("Attacker", 28, "ipfs://attacker");
vm.stopPrank();
}
function testFrontRunningAttack() public {
uint256 initialBalance = address(attacker).balance;
vm.startPrank(bob);
vm.deal(bob, 1 ether);
registry.likeUser{value: 1 ether}(alice);
vm.stopPrank();
vm.startPrank(attacker);
vm.deal(attacker, 1 ether);
registry.likeUser{value: 1 ether}(alice);
vm.stopPrank();
vm.startPrank(alice);
vm.deal(alice, 5 ether);
registry.likeUser{value: 5 ether}(attacker);
address[] memory aliceMatches = registry.getMatches();
assertEq(aliceMatches[0], attacker, "Front-running succeeded");
vm.stopPrank();
vm.startPrank(attacker);
address[] memory attackerMatches = registry.getMatches();
assertEq(attackerMatches[0], alice, "Attacker matched with Alice");
uint256 expectedValue = ((5 ether + 1 ether) * 90) / 100;
assertTrue(expectedValue > 1 ether, "Attacker gained access to more ETH than invested");
vm.stopPrank();
}
}
Impact
High severity because:
Attackers can intercept legitimate matches with minimal ETH commitment
Users lose intended matches to attackers
Platform's matching mechanism can be manipulated
Users may lose significant ETH when matched with attackers
Trust in the platform's matching system is compromised
Tools Used
Recommendations
Implement a commit-reveal scheme:
mapping(address => bytes32) public commitments;
mapping(address => uint256) public commitmentTimes;
function commitLike(bytes32 hashedLike) external {
commitments[msg.sender] = hashedLike;
commitmentTimes[msg.sender] = block.timestamp;
}
function revealLike(address liked, bytes32 salt) external payable {
require(block.timestamp >= commitmentTimes[msg.sender] + 1 hours, "Too early");
require(commitments[msg.sender] == keccak256(abi.encodePacked(liked, salt)), "Invalid reveal");
}
Implement a timelock between likes:
mapping(address => uint256) public lastLikeTime;
function likeUser(address liked) external payable {
require(block.timestamp >= lastLikeTime[msg.sender] + 1 hours, "Like too soon");
lastLikeTime[msg.sender] = block.timestamp;
}