NFT Dealers

First Flight #58
Beginner FriendlyFoundry
100 EXP
Submission Details
Impact: high
Likelihood: high

Listing ID Mismatch

Author Revealed upon completion

Logic problem in the list function

Description

  • In the 'list' function,the protocol implements the 'listingsCounter' variable but stores the data in the mapping 's_listings' using '_tokenId' as the key

// Root cause in the codebase with @> marks to highlight the relevant section
function list(uint256 _tokenId, uint32 _price) external onlyWhitelisted {
require(_price >= MIN_PRICE, "Price must be at least 1 USDC");
require(ownerOf(_tokenId) == msg.sender, "Not owner of NFT");
require(s_listings[_tokenId].isActive == false, "NFT is already listed");
require(_price > 0, "Price must be greater than 0");
@> listingsCounter++;
activeListingsCounter++;
@> s_listings[_tokenId] =
Listing({seller: msg.sender, price: _price, nft: address(this), tokenId: _tokenId, isActive: true});
emit NFT_Dealers_Listed(msg.sender, listingsCounter);
}

Risk

Likelihood:

The issue happens when the user tries to buy or cancel a listing

Impact:

  • The buy and cancelListing functions expect a _listingId. If a user tries to buy Listing 1, the contract looks for s_listings[1]. Unless the NFT being sold actually has tokenId = 1, the lookup will return an empty, inactive listing, making it impossible to buy or cancel.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.34;
import "forge-std/Test.sol";
import "../src/NFTDealers.sol";
import "../src/MockUSDC.sol";
contract NFTDealersTest is Test {
NFTDealers public dealers;
MockUSDC public usdc;
address public owner = address(0x1);
address public alice = address(0x2);
address public bob = address(0x3);
function setUp() public {
usdc = new MockUSDC();
dealers = new NFTDealers(
owner,
address(usdc),
"Dealers",
"NFTD",
"ipfs://",
20e6
);
vm.startPrank(owner);
dealers.revealCollection();
dealers.whitelistWallet(alice);
vm.stopPrank();
// Setup Alice and Bob with funds
usdc.mint(alice, 1000e6);
usdc.mint(bob, 1000e6);
vm.prank(alice);
usdc.approve(address(dealers), type(uint256).max);
vm.prank(bob);
usdc.approve(address(dealers), type(uint256).max);
}
function test_PoC_ListingMismatch() public {
vm.startPrank(alice);
// Alice mints two NFTs (Token #1 and Token #2)
dealers.mintNft();
dealers.mintNft();
// Alice lists ONLY Token #2.
// This is the FIRST listing (listingsCounter = 1).
// The contract saves data in s_listings[2] (using tokenId)
// but emits event saying listingId is 1.
dealers.list(2, 100e6);
vm.stopPrank();
vm.startPrank(bob);
// Bob sees the event for "Listing #1" and tries to buy it.
// This REVERTS because s_listings[1] is empty/inactive.
vm.expectRevert();
dealers.buy(1);
vm.stopPrank();
// Proof: The listing is "hidden" at index 2 (the tokenId) instead of 1.
(,,,uint256 tid, bool active) = dealers.s_listings(2);
assertEq(tid, 2);
assertTrue(active, "Listing is at wrong index!");
}
}

Recommended Mitigation

Consistently use listingsCounter as the primary key for the s_listings mapping.

Support

FAQs

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

Give us feedback!