Skip to content

sh4dex/NFT-Collection

Repository files navigation

🖼️ NFT Collection

Version: V.0.1.1 Solidity: 0.8.34 Framework: Foundry

A trustless NFT marketplace built on Solidity. Any ERC-721 token can be listed, bought, or delisted — the contract holds no funds or NFTs at any point. Payment flows directly from buyer to seller and the NFT transfers atomically in the same transaction.

NFTCollection Architecture

The diagram shows the external actor (User), the three public entry points (listNft, buyNft, cancelListing), how the listings mapping is affected, and which events are emitted.

⚙️ How It Works

User ──► listNft()       ── listings[nft][id] = Listing ──► NftListed(seller, nft, id, price)
User ──► buyNft()        ── delete listing ──► safeTransferFrom ──► ETH to seller ──► NftSold(...)
User ──► cancelListing() ── delete listing ──────────────────────────────────────────► CanceledListing(nft, id)
  1. List — The seller calls listNft(nftAddress, tokenId, price). The contract verifies the caller owns the token and that the price is non-zero, then stores the listing. The seller must also call approve(marketplace, tokenId) on the NFT contract so the marketplace can transfer the token when a buyer arrives.

  2. Buy — The buyer calls buyNft(nftAddress, tokenId) sending exactly the listed price in ETH. The contract deletes the listing first (Checks-Effects-Interactions), transfers the NFT from seller to buyer via safeTransferFrom, then forwards the ETH to the seller.

  3. Cancel — The original seller calls cancelListing(nftAddress, tokenId) to remove their listing at any time. No other address can cancel someone else's listing.

🚀 Deployment

Prerequisites

# Install Foundry
curl -L https://foundry.paradigm.xyz | bash && foundryup

Environment

Create a .env file in the project root:

PRIVATE_KEY=0x...   # deployer private key

The deploy script reads PRIVATE_KEY from the environment via vm.envUint. Without it the script will revert before broadcasting.

Deploy

# Build
forge build

# Deploy to a local node
anvil
source .env
forge script script/DeployNFTCollection.s.sol --broadcast --rpc-url http://127.0.0.1:8545

# Deploy to testnet / mainnet
source .env
forge script script/DeployNFTCollection.s.sol --broadcast --rpc-url <RPC_URL>

Interact

# Approve the marketplace to transfer your NFT (run on the NFT contract)
cast send <NFT_ADDRESS> "approve(address,uint256)" <MARKETPLACE_ADDRESS> <TOKEN_ID> \
  --rpc-url <RPC_URL> --private-key $PRIVATE_KEY

# List an NFT for 1 ETH
cast send <MARKETPLACE_ADDRESS> "listNft(address,uint256,uint256)" \
  <NFT_ADDRESS> <TOKEN_ID> 1000000000000000000 \
  --rpc-url <RPC_URL> --private-key $PRIVATE_KEY

# Buy a listed NFT (send exact price)
cast send <MARKETPLACE_ADDRESS> "buyNft(address,uint256)" \
  <NFT_ADDRESS> <TOKEN_ID> --value 1000000000000000000 \
  --rpc-url <RPC_URL> --private-key $PRIVATE_KEY

# Cancel a listing
cast send <MARKETPLACE_ADDRESS> "cancelListing(address,uint256)" \
  <NFT_ADDRESS> <TOKEN_ID> \
  --rpc-url <RPC_URL> --private-key $PRIVATE_KEY

# Read a listing
cast call <MARKETPLACE_ADDRESS> "listings(address,uint256)" <NFT_ADDRESS> <TOKEN_ID> \
  --rpc-url <RPC_URL>

🧪 Testing

forge test -vvv

The test suite uses a MockNFT (a minimal ERC-721 with a public mint) as a stand-in for any real collection. Four actors are used: deployer, seller, buyer, and randomUser.

# Test Covers
1 testMintNft MockNFT ownership after mint
2 testShouldRevertNftOwnerListingFails Non-owner cannot list
3 testShouldRevertNftByOtherUserListingFails Owning a different token doesn't grant rights
4 testShouldRevertNotValidPriceListing Zero-price listing reverts
5 testShouldRevertNotValidTokenId Non-existent token ID reverts
6 testListingWorksProperly Listing creates correct storage entry
7 testShouldRevertIfNotOwner Non-seller cannot cancel
8 testCancelListingWorksProperly Cancellation removes listing from storage
9 testShouldRevertBuyNftIfNotListed Buying unlisted token reverts
10 testShouldRevertIfBuyPriceIsDifferentFormListed Wrong ETH amount reverts
11 testBuyShouldDeleteListingProperly Purchase removes listing from storage
12 testBuyShouldTransferNftProperly NFT ownership transfers to buyer
13 testBalanceShouldBeTransferProperly ETH debited from buyer, credited to seller

Coverage

forge coverage
File % Lines % Statements % Branches % Funcs
src/NFTCollection.sol 100% 100% 91.67% 100%
test/NFTCollection.t.sol 100% 100% 100% 100%
script/DeployNFTCollection.s.sol 0% 0% 100% 0%
Total 79.17% 76.19% 91.67% 80%

The one uncovered branch in NFTCollection.sol (91.67%) is the require(success, "transfer failed") failure path inside buyNft — triggered only if the seller's address reverts on receiving ETH, which requires a dedicated malicious-receiver contract.

🔒 Security Notes

OpenZeppelin ReentrancyGuard

All three state-changing functions (listNft, buyNft, cancelListing) are guarded by OpenZeppelin's nonReentrant modifier. This prevents any re-entrant call from a malicious NFT contract or seller address from executing a second state-changing function while the first is still in progress.

Checks-Effects-Interactions in buyNft

buyNft deletes the listing from storage before making any external calls (token transfer and ETH payment). This means that even if a malicious seller receive() function attempts to call back into buyNft, the listing no longer exists and the call reverts — providing a second layer of protection on top of nonReentrant.

OpenZeppelin Ownable

The contract deploys with the deployer set as owner via Ownable(msg.sender). Ownership is reserved for future administrative functions (e.g. fee collection). No owner-only logic is active in this version.

About

NFT-Collection Project made for ERC 721 tokens collection and storage

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors