DatingDapp

AI First Flight #6
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

unbounded matches array can break match retrieval

Root + Impact

Description

The protocol stores every match for each user in a dynamic array. getMatches() returns the entire array for msg.sender in one call.

The array has no maximum size, no pagination, and no removal path when profiles are burned or blocked. A user with many matches can eventually have a match list too large to reliably return through eth_call, and any on-chain integration that calls getMatches() can run out of gas because the full storage array must be copied into memory and returned.

mapping(address => address[]) public matches;
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]) {
// @> These arrays grow without bound
matches[msg.sender].push(liked);
matches[liked].push(msg.sender);
emit Matched(msg.sender, liked);
matchRewards(liked, msg.sender);
}
}
function getMatches() external view returns (address[] memory) {
// @> Returns the full unbounded array in one call
return matches[msg.sender];
}

Risk

Likelihood:

  • This occurs naturally for active users who accumulate many matches over time.

  • This can be accelerated by sybil accounts that create profiles and mutually like a target account.

Impact:

  • Users with large match lists may be unable to retrieve their full matches through the frontend or RPC providers.

  • On-chain consumers of getMatches() can revert from excessive gas or memory usage.

Proof of Concept

function testGetMatchesCostGrowsWithArrayLength() public {
SoulboundProfileNFT nft = new SoulboundProfileNFT();
LikeRegistry registry = new LikeRegistry(address(nft));
address target = address(0xA11CE);
vm.deal(target, 1000 ether);
vm.prank(target);
nft.mintProfile("Target", 25, "ipfs://target");
for (uint160 i = 1; i <= 200; i++) {
address user = address(i);
vm.deal(user, 2 ether);
vm.prank(user);
nft.mintProfile("User", 25, "ipfs://user");
vm.prank(target);
registry.likeUser{value: 1 ether}(user);
vm.prank(user);
registry.likeUser{value: 1 ether}(target);
}
uint256 gasBefore = gasleft();
vm.prank(target);
address[] memory targetMatches = registry.getMatches();
uint256 gasUsed = gasBefore - gasleft();
assertEq(targetMatches.length, 200);
assertGt(gasUsed, 0);
}

The same pattern continues as the array grows. Because the contract only exposes a full-array getter, clients cannot request a bounded slice once the match list becomes large.

Recommended Mitigation

Expose bounded pagination and a match count instead of requiring callers to retrieve the entire array.

-function getMatches() external view returns (address[] memory) {
- return matches[msg.sender];
-}
+function getMatchCount(address user) external view returns (uint256) {
+ return matches[user].length;
+}
+
+function getMatchAt(address user, uint256 index) external view returns (address) {
+ return matches[user][index];
+}
+
+function getMatchesPaginated(address user, uint256 offset, uint256 limit)
+ external
+ view
+ returns (address[] memory page)
+{
+ uint256 length = matches[user].length;
+ if (offset >= length) return new address[](0);
+
+ uint256 end = offset + limit;
+ if (end > length) end = length;
+
+ page = new address[](end - offset);
+ for (uint256 i = offset; i < end; i++) {
+ page[i - offset] = matches[user][i];
+ }
+}
Updates

Lead Judging Commences

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