Trick or Treat

First Flight #27
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: high
Invalid

Security Report: Double-Selling Vulnerability in SpookySwap Contract

The SpookySwap contract allows users to purchase NFTs with variable pricing scenarios through the trickOrTreat function. However, due to insufficient access control in the resolveTrick function, the contract is vulnerable to a Double-Selling Vulnerability. This flaw allows multiple buyers to pay for the same pending NFT, which could lead to financial losses for buyers and revenue discrepancies for the contract.

Vulnerability Details

The double-selling vulnerability occurs in the resolveTrick function, where NFTs in a pending state (due to incomplete payment under a double-price scenario) can be completed by any buyer rather than only the original buyer. The sequence of events is as follows:

  1. If a buyer initiates a purchase in the double-price scenario and sends less than the required ETH, the NFT is minted to the contract’s address, marking it as pending until the buyer completes payment via resolveTrick.

  2. The resolveTrick function does not restrict access to only the original buyer, meaning other users can attempt to complete the transaction by submitting additional payments, even if they were not the initial buyer.

  3. This lack of restriction allows multiple buyers to each pay for the same NFT, although only one NFT is available to be transferred.

Impact

This vulnerability creates significant risks:

Financial Loss for Buyers: Multiple buyers could each pay for the same NFT, expecting to receive it. However, only one NFT exists, resulting in financial losses for those who do not receive the NFT.
Revenue and Inventory Discrepancies**: The contract may collect excess funds while transferring only one NFT, leading to revenue discrepancies and potential disputes.
Reputation Damage: The vulnerability may reduce buyer confidence, as users could perceive the contract as unreliable or prone to exploitation.

Tools Used

Tests

Recommendations

Restrict resolveTrick Access to the Original Buyer:

Modify resolveTrick to enforce that only the original buyer can complete the transaction by checking that msg.sender matches the address stored in pendingNFTs[tokenId]. This will prevent unauthorized buyers from attempting to complete another buyer’s pending purchase.

Example:

`function resolveTrick(uint256 tokenId) public payable nonReentrant { require(pendingNFTs[tokenId] == msg.sender, "Only the original buyer can complete this purchase"); // Rest of the resolveTrick function logic... }`

Implement Expiration for Pending NFTs:

Introduce an expiration period for pending NFTs to ensure they do not remain indefinitely locked if the original buyer does not complete the transaction. If the buyer does not complete the payment within the set time, the NFT should expire, allowing the contract owner to reclaim or relist it.

Example:

mapping(uint256 => uint256) public pendingNFTExpiration; uint256 public expirationPeriod = 1 days; // Set expiration period function trickOrTreat(string memory _treatName) public payable nonReentrant { ... if (msg.value < requiredCost) { uint256 tokenId = nextTokenId; _mint(address(this), tokenId); _setTokenURI(tokenId, treat.metadataURI); // Set expiration for pending NFT pendingNFTExpiration[tokenId] = block.timestamp + expirationPeriod; ... } } function resolveTrick(uint256 tokenId) public payable nonReentrant { require(block.timestamp < pendingNFTExpiration[tokenId], "Pending purchase expired"); // Complete the purchase... }

Add Events for Transparency:

Emit events for the creation and completion of pending mints, including buyer address and token ID, to improve transaction transparency. This can help buyers track their transaction status and prevent misunderstandings.

Example:

event PendingMintCreated(address indexed buyer, uint256 tokenId, string treatName, uint256 amountPaid); event PendingMintCompleted(address indexed buyer, uint256 tokenId); function trickOrTreat(string memory _treatName) public payable nonReentrant { ... if (msg.value < requiredCost) { ... emit PendingMintCreated(msg.sender, tokenId, _treatName, msg.value); } ... } function resolveTrick(uint256 tokenId) public payable nonReentrant { ... emit PendingMintCompleted(msg.sender, tokenId); ... }

These mitigations will prevent unauthorized buyers from completing a pending purchase, limit the time pending NFTs remain in limbo, and improve transparency, strengthening the SpookySwap contract’s reliability and user trust.

Updates

Appeal created

bube Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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