Trick or Treat

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

Reentrancy Attack in trickOrTreat Allows Unlimited NFT Minting

Summary

The trickOrTreat function is vulnerable to a reentrancy attack that allows an attacker to mint an unlimited number of NFTs without paying the required cost per mint. By exploiting reentrancy, an attacker can repeatedly call trickOrTreat within a single transaction, minting multiple NFTs from a single function call. This exploit disrupts token scarcity and severely impacts protocol revenue.

Vulnerability Details

The trickOrTreat function’s reentrancy vulnerability allows attackers to mint multiple instances of the same treat (NFT) repeatedly within a single transaction. By bypassing the intended non-reentrant behavior, attackers can create numerous identical NFTs with the same treat name, cost, and metadata URI. This degrades the NFT’s uniqueness and scarcity, which are essential characteristics of any valuable NFT collection.

Here is what my attack contract looks like:

contract MaliciousReentrant {
SpookySwap public spookySwap;
constructor(address _spookySwap) {
spookySwap = SpookySwap(_spookySwap);
}
function attack(string memory treatName) public payable {
spookySwap.trickOrTreat{value: msg.value}(treatName); // Initial call
// Force re-entrancy by calling trickOrTreat again within the same function
spookySwap.trickOrTreat{value: msg.value}(treatName);
}
}

Proof of Code

We set up a test using a malicious contract, MaliciousReentrant, to demonstrate how an attacker can exploit this vulnerability. Here’s the test result, showing that the attacker owns multiple identical NFTs after initiating reentrant calls:

Attack test:

function testReentrancyAttackWithTokenDetails() public {
// Deploy the attacker contract and provide funds
MaliciousReentrant attacker = new MaliciousReentrant(address(spookySwap));
vm.deal(address(attacker), 3 ether);
// Log attacker balance and attempt reentrancy
uint256 attackerBalanceBefore = address(attacker).balance;
console.log("Attacker balance before attack:", attackerBalanceBefore);
// Execute attack with 1 ETH
vm.startPrank(address(attacker));
attacker.attack{value: 1 ether}("CandyCorn");
vm.stopPrank();
// Confirm attacker holds multiple tokens after reentrancy
uint256 attackerBalanceAfter = address(attacker).balance;
console.log("Attacker balance after attack:", attackerBalanceAfter);
console.log("Total tokens owned by attacker after reentrancy:", spookySwap.balanceOf(address(attacker)));
// Assertions
assertEq(spookySwap.balanceOf(address(attacker)), 2, "Reentrancy attack allowed minting duplicate NFTs");
}

Logs:

Total tokens owned by attacker after reentrancy: 2
Treat name: CandyCorn
Treat cost after attack: 1 ETH
Treat metadata URI: ipfs://Qm...CandyCorn

Impact

The reentrancy vulnerability in the trickOrTreat function allows attackers to mint multiple identical NFTs repeatedly, bypassing the protocol's intended uniqueness and scarcity of each NFT. The key impacts include:

  1. NFT Uniqueness and Scarcity Compromise: Attackers can mint multiple identical NFTs, undermining the uniqueness and collectible nature of each treat. This devalues the NFT collection and can damage the reputation and trust of legitimate users who expect exclusive ownership.

  2. Protocol Integrity Risks: The unchecked reentrancy flaw allows attackers to call trickOrTreat recursively within a single transaction. Although each mint incurs a cost, the ability to bypass scarcity controls poses a substantial risk to the protocol's intended operation and economics.

Tools Used

Foundry Testing Framework: This framework allowed the creation of a test simulating the reentrancy attack, helping confirm the multiple minting exploit. As well as for console logging.

Recommendations

To prevent reentrancy-based duplication of treats, add a restriction to the mintTreat function that enforces a unique treat name. Implement a mintedTreats mapping to track each treat’s name and verify if it has already been minted. This approach ensures that duplicate treats cannot be minted in a single transaction or due to reentrancy.

mapping(string => bool) private mintedTreats; // Tracks if a treat name has already been minted
function mintTreat(address recipient, Treat memory treat) internal {
require(!mintedTreats[treat.name], "Treat with this name has already been minted.");
mintedTreats[treat.name] = true; // Prevent further minting of this treat name
uint256 tokenId = nextTokenId;
_mint(recipient, tokenId);
_setTokenURI(tokenId, treat.metadataURI);
nextTokenId += 1;
emit Swapped(recipient, treat.name, tokenId);
}

This solution provides robust protection against reentrancy issues by enforcing treat uniqueness through treat names, maintaining protocol integrity and scarcity.

Updates

Appeal created

bube Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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