How to Make an On-chain Giveaway to NFT Holders
June 30th, 2022

You created your NFT project, but now you want to promote it. Instead of airdropping the NFT spamming and pissing off a lot of people, just let them being incentivized to get your NFT. Run a giveaway. You can do it using Twitter or any other social network, but maybe you want to let the code to pick and reward the winners, since we believe the code is law. Let’s create an smart contract that handle the giveaway.

Suppose your NFT have some traits and rarity. You can use those trait to pick the NFT holder winner. On this article I will be using as example the generic NFT Pet Number ( https://petnumber.xyz ) but the same principle can be used for any other NFT project. For more details about Pet Number you can read this article: https://medium.com/p/218067356f3e.

The NFT traits

Our NFT (Pet Number) has 4 traits that are the number of Zeros, Ones, Twos and Threes. The Pet is a 32 bytes number. A Zero traits occurs when all the bits in a byte are 0, a One traits is present when the bit 0 is 1 and the rest are 0, and so on with Two and Three. The rarer case is the Zero trait.

Pet Number 20 with a Zero on byte 23.
Pet Number 20 with a Zero on byte 23.

The winners NFTs will be those having at least a Zero trait.

The Painter

I wanted to start my giveaway on Easter, so I changed the default Painter of Pet Number to the EasterPainter contract, since my NFT is customizable. That means that only the new minted Pet Numbers will be represented as an Easter Egg. More on the customizable NFTs on this article: https://medium.com/p/bad255a6374f.

// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./Painter.sol";

contract EasterPainter is Ownable, Painter {
  using Strings for uint256;
  

constructor() 
  
 {
    
  }
function background(uint256 number) internal pure virtual returns (string memory){
    uint256 colorB = (number)&0xFF;
    uint256 colorG = (number>>8)&0xFF;
    uint256 colorR = (number>>(8*2))&0xFF;
     return string(abi.encodePacked(
       '<g id="egg_layer_line" clip-path="url(#egg)" filter="url(#specfilter)">',
     '<rect width="500" height="500" fill="RGB(',
     colorR.toString(),',',
     colorG.toString(),',',
     colorB.toString(),')"/>'
     ));
  }

  function selectLayer(uint256 number, uint256 position) internal pure virtual returns (string memory){
    string[7] memory layers;
    uint8 decoration = uint8(number%7);
    uint256 colorB = (number>>8)&0xFF;
    uint256 colorG = (number>>(8*2))&0xFF;
    uint256 colorR = (number>>(8*3))&0xFF;
    uint256 colorB2 = (number>>8*4)&0xFF;
    uint256 colorG2 = (number>>(8*5))&0xFF;
    uint256 colorR2 = (number>>(8*6))&0xFF;
    string memory color = string(abi.encodePacked('"RGB(',
     colorR.toString(),',',
     colorG.toString(),',',
     colorB.toString(),')"'));
    string memory color2 = string(abi.encodePacked('"RGB(',
     colorR2.toString(),',',
     colorG2.toString(),',',
     colorB2.toString(),')"'));
     position = position*100;
    string memory strpos = position.toString();
    layers[0] = string(abi.encodePacked('<path transform="translate(0,',strpos,')" stroke=',color2,' stroke-width="60" id="north" d="m 0 0 h 500"/>'));
    layers[1] = string(abi.encodePacked('<path transform="translate(0,',strpos,')" stroke=',color,' stroke-width="10" id="north_z" fill="transparent" d="m 0 30 l 45, -60 l 45, 60 l 45, -60 l 45, 60 l 45, -60 l 45, 60 l 45, -60 l 45, 60 l 45, -60 l 45, 60 "/>'));
    layers[2] = string(abi.encodePacked('<path transform="translate(0,',strpos,')" stroke=',color,' stroke-width="10" id="north_s" fill="transparent" d="m 0 0 c 30 -70, 55 -70, 85 0 s 55 -70, 85 0s 55 -70, 85 0s 55 -70, 85 0s 55 -70, 85 0 s 55 -70, 85 0"/>'));
    layers[3] = string(abi.encodePacked('<g  transform="translate(0,',strpos,')" fill=',color,' ><circle cx="50" cy="20" r="10"/><circle cx="100" cy="-20" r="10"/><circle cx="140" cy="20" r="10"/>  <circle cx="180" cy="-20" r="10"/>  <circle cx="220" cy="20" r="10"/>  <circle cx="260" cy="-20" r="10"/>  <circle cx="300" cy="20" r="10"/>  <circle cx="340" cy="-20" r="10"/>  <circle cx="380" cy="20" r="10"/><circle cx="420" cy="-20" r="10"/></g>'));
    layers[4] = string(abi.encodePacked(
        layers[0],
        layers[1]
      ));
    layers[5] = string(abi.encodePacked(
        layers[0],
        layers[2]
      ));
    layers[6] = string(abi.encodePacked(
        layers[0],
        layers[3]
      ));
     return layers[decoration];
  }

  function clipPath()internal pure virtual returns(string memory){
      return string(abi.encodePacked(
        '<clipPath id="egg" transform="translate(250,250)">',
        '<path transform="scale(1.5, 1.5)" d="M 0 -125 c -42.601 0, -100 91.911, -100 151.51 s 49.218 100, 100 100 M 0 -125 c 42.601 0, 100 91.911, 100 151.51 s -49.218 100, -100 100 z"/>',
        '</clipPath>'
          ));
  }

  function egg(uint256 number) internal pure virtual returns (string memory){
      uint256 north = (number>>24)&0xFFFFFFFFFFFFFF;
      uint256 tropic = (number>>(56))&0xFFFFFFFFFFFFFF;
      uint256 equator = (number>>(56*2))&0xFFFFFFFFFFFFFF;
      uint256 south = (number>>(56*3))&0xFFFFFFFFFFFFFF;
     return string(abi.encodePacked(
      clipPath(),
      background(number),
      selectLayer(north,1),
      selectLayer(tropic,2),
      selectLayer(equator,3),
      selectLayer(south,4),
     '</g>'
     ));
  }

    function filters()internal pure virtual returns(string memory){
      return string(abi.encodePacked(
        '<filter id="shadowfilter">',
	      '<feGaussianBlur result="blurOut" in="SourceGraphic" stdDeviation="10" />',
        '</filter>',
        '<filter id = "specfilter">',
        '<feSpecularLighting result="specOut" specularExponent="20" lighting-color="#bbbbbb">',
        '<fePointLight x="250" y="0" z="150"/>',
        '</feSpecularLighting>',
        '<feComposite in="SourceGraphic" in2="specOut" operator="arithmetic" k1="0" k2="1" k3="1" k4="0"/>',
        '</filter>'
          ));
  }
      function shadow()internal pure virtual returns(string memory){
      return string(abi.encodePacked(
        '<g filter="url(#shadowfilter)" transform="translate(250,250)">',
        '<ellipse fill="hsla(0, 0%, 0%, 60%)" cx="0" cy="190" rx="150" ry="25" />',
        '<path transform="scale(1.5, 1.5)" d="M 0 -125" fill="transparent"/>',
        '</g>'
          ));
  }

    function paint(uint256 number)external pure override returns(string memory){
      
      // uint256 number = getPetNumber(_tokenId);
      uint256 bg1 = (number&65535)%361;
      uint256 bg2 = ((number>>16)&65535)%361;
      number = number>>32;
      
      return string(abi.encodePacked(
        '<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" >',
         '<defs><linearGradient id="nG" gradientTransform="rotate(45)"><stop offset="5%" stop-color="hsl(',bg1.toString(),',50%, 25%)"/>',
         '<stop offset="95%" stop-color="hsl(',bg2.toString(),',50%, 25%)"/></linearGradient></defs>',
        
        '<rect width="500" height="500" fill="url(\'#nG\')"/>',
        filters(),
        shadow(),
            egg(number),
        '</svg>'
          ));
  }
     
}

EasterPainter contract.

The eggs under the EasterPainter contract looks like this:

Pet Number 283, represented by the EasterPainter contract.
Pet Number 283, represented by the EasterPainter contract.

The smart contract

The way the NFT Pet Number looks doesn’t matter from the giveaway smart contract perspective, I just wanted to put the participants in context and justify the name of the giveaway contract: EasterContract.

We want to start creating a public variable of type PetNumber called pet, line 7, because we want to use that to handle the NFT inside this contract. We want to make sure that pass that NFT contract address on the constructor, line 9.

// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

contract EasterContract {

    PetNumber public pet; 
   
    constructor(address _petAddress){
        pet = PetNumber(payable(_petAddress));
    }

}

EasterContract getting the NFT address.

If you compile this right now is going to fail, because PetNumber contract is not on the scope of the EasterContract. Let’s create an empty PetNumber interface for now:

// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

interface PetNumber{

}

contract EasterContract {

   PetNumber public pet; 
   
   constructor(address _petAddress){
        pet = PetNumber(payable(_petAddress));
    }

}

Now that should compile just fine.

We need now to check if the NFT have the Zero trait. The method is called potentialReward, line 43 to 58.

// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

import "@openzeppelin/contracts/access/Ownable.sol";
interface PetNumber{
    function balanceOf(
        address owner
        )
        external
        view
        returns (
        uint256 balance
        );
    function tokenOfOwnerByIndex(
        address owner, uint256 index
        ) 
        external 
        view 
        returns (
            uint256 tokenId
        );
    function getPetTraits(
        uint256 _tokenId
        )
        external 
        view 
        returns(
            uint8 zeros, uint8 ones, 
            uint8 twos, uint8 threes
        );
}

contract EasterContract is Ownable {

    PetNumber public pet; 
    
    mapping(uint256 => bool) rewardedNFTs;
    constructor(address _petAddress){
        pet = PetNumber(payable(_petAddress));
    }

    function potentialReward() external view returns(bool) {
        for (uint256 i=0; i<pet.balanceOf(msg.sender);i++){
            uint256 ownerTokenId = pet.tokenOfOwnerByIndex(msg.sender, i);

            uint256 zeros=0;
            uint256 ones=0;
            uint256 twos=0;
            uint256 threes=0;
            (zeros, ones, twos, threes) = pet.getPetTraits(ownerTokenId);
            if (zeros>0){
                return true;
            }
        }
        return false;
        
    }
    
}

First Version of the potentialReward method.

The method needs to check every Pet hold by the client (sender), look at the for loop line 44. On the line 45 we are taking the token ID and then getting the traits Zeros, Ones, Twos and Threes for that Pet, lines 47–51. If the amount of Zeros is above 0 then return true, lines 52–54, else continue looping for each Pet Number. After the loop if still in the method, return false.

Of course you need to fill up the PetNumber interface with the signature of balanceOf, tokenOfOwnerByIndex and getPetTraits.

That is a good start, but, imagine that you have more than one prize, let’s said you are giving away 15 ETH (or MATIC, on Polygon) to be giveaway in amounts of 5 each time. On the current state the first winner can collect the rest of the claim. We need to record who already won.

// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

interface PetNumber{
    function balanceOf(
        address owner
        )
        external
        view
        returns (
        uint256 balance
        );
    function tokenOfOwnerByIndex(
        address owner, uint256 index
        ) 
        external 
        view 
        returns (
            uint256 tokenId
        );
    function getPetTraits(
        uint256 _tokenId
        )
        external 
        view 
        returns(
            uint8 zeros, uint8 ones, 
            uint8 twos, uint8 threes
        );
}

contract EasterContract {

    PetNumber public pet; 
    mapping(uint256 => bool) public rewardedNFTs;
    constructor(address _petAddress){
        pet = PetNumber(payable(_petAddress));
    }

    function potentialReward() external view returns(bool) {
        for (uint256 i=0; i<pet.balanceOf(msg.sender);i++){
            uint256 ownerTokenId = pet.tokenOfOwnerByIndex(msg.sender, i);
            if (!rewardedNFTs[ownerTokenId]){
                uint256 zeros=0;
                uint256 ones=0;
                uint256 twos=0;
                uint256 threes=0;
                (zeros, ones, twos, threes) = pet.getPetTraits(ownerTokenId);
                if (zeros>0){

                    return true;
                }
            }
        }
        return false;
        
    }

}

The line 36 was added to store on the blockchain the rewarded NFTs, token ID mapped to a boolean. We can check now using the if section on line 44 if the token ID was already rewarded you want to skip that token.

Next we need to actually reward the holder, the method is called getReward (but maybe claimReward is a much better name).

// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

import "@openzeppelin/contracts/access/Ownable.sol";
interface PetNumber{
    function balanceOf(
        address owner
        )
        external
        view
        returns (
        uint256 balance
        );
    function tokenOfOwnerByIndex(
        address owner, uint256 index
        ) 
        external 
        view 
        returns (
            uint256 tokenId
        );
    function getPetTraits(
        uint256 _tokenId
        )
        external 
        view 
        returns(
            uint8 zeros, uint8 ones, 
            uint8 twos, uint8 threes
        );
}

contract EasterContract is Ownable {
    event Rewarded(address indexed _to, uint256 _amount);
    PetNumber public pet; 

    mapping(uint256 => bool) public rewardedNFTs;
    constructor(address _petAddress){
        pet = PetNumber(payable(_petAddress));
    }

    function potentialReward() external view returns(bool) {
        for (uint256 i=0; i<pet.balanceOf(msg.sender);i++){
            uint256 ownerTokenId = pet.tokenOfOwnerByIndex(msg.sender, i);
            if (!rewardedNFTs[ownerTokenId]){
                uint256 zeros=0;
                uint256 ones=0;
                uint256 twos=0;
                uint256 threes=0;
                (zeros, ones, twos, threes) = pet.getPetTraits(ownerTokenId);
                if (zeros>0){

                    return true;
                }
            }
        }
        return false;
        
    }

    function getReward() external returns(bool){
        uint256 thisBalance = address(this).balance;
        require(thisBalance>0,"Balance is 0");
        for (uint256 i=0; i<pet.balanceOf(msg.sender);i++){
            uint256 ownerTokenId = pet.tokenOfOwnerByIndex(msg.sender, i);
            if (!rewardedNFTs[ownerTokenId]){
                uint256 zeros=0;
                uint256 ones=0;
                uint256 twos=0;
                uint256 threes=0;
                (zeros, ones, twos, threes) = pet.getPetTraits(ownerTokenId);
                if (zeros>0){

                    uint256 extractValue = thisBalance < 5 ether ? thisBalance : 5 ether;
                    (bool os, ) = payable(msg.sender).call{value: extractValue}("");
                    require(os);
                    rewardedNFTs[ownerTokenId] = true;
                    emit Rewarded(msg.sender, extractValue);
                    return true;
                }
            }
        }
        return false;
    }

}

The addition of getReward method.

Lines 63 and 64 checks for enough funds on the current contract, this, using the balance property.

From the lines 65 to 73 is a copy of the potentialReward method but this time we need to actually pay to the holder of the NFT, on the lines 75–77.

Line 78 add the token ID to the rewardedNFTs to keep the traking of the rewarded NFTs.

Line 79 log a Rewarded event with the winner address and amount rewarded.

Few more improvements

To get a practical contract we need to deposit the tokens to giveaway using the especial method receive, of course we are adding a withdraw method too.

// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

import "@openzeppelin/contracts/access/Ownable.sol";
//.
//.
//.

contract EasterContract is Ownable {

    event Received(address indexed _from, uint256 _amount);
    //.
    //.
    //.
    receive() external payable virtual {
        emit Received(msg.sender, msg.value);
    }

    function withdraw() external virtual onlyOwner {

    // Do not remove this otherwise you will not be able to withdraw the funds.
    // =============================================================================
    (bool os, ) = payable(owner()).call{value: address(this).balance}("");
    require(os);
    // =============================================================================
    }

    //.
    //.
    //.
}

receive and withdraw methods.

Note the onlyOwner modifier on the withdraw method, line 20, and the import of the Open Zeppeling Ownable, line 5, also the inheritance at line 10.

The contract is now usable, but what happens if you want to use the same EasterContract on the next year? Well the holders of Pet Numbers with Zeros traits minted out of the giveaway are going to be able to claim the rewards. We need to prevent that holders out of the giveaway period can claim the prizes. That is possible setting a minimum token ID, since they are incremental.

The finished contract looks like this:

// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

import "@openzeppelin/contracts/access/Ownable.sol";
interface PetNumber{
    function balanceOf(
        address owner
        )
        external
        view
        returns (
        uint256 balance
        );
    function tokenOfOwnerByIndex(
        address owner, uint256 index
        ) 
        external 
        view 
        returns (
            uint256 tokenId
        );
    function getPetTraits(
        uint256 _tokenId
        )
        external 
        view 
        returns(
            uint8 zeros, uint8 ones, 
            uint8 twos, uint8 threes
        );
}

contract EasterContract is Ownable {

    event Received(address indexed _from, uint256 _amount);
    event Rewarded(address indexed _to, uint256 _amount);
    PetNumber public pet; 
    uint256 public lastTokenId=0;
    mapping(uint256 => bool) public rewardedNFTs;
    constructor(address _petAddress){
        pet = PetNumber(payable(_petAddress));
    }

    receive() external payable virtual {
        emit Received(msg.sender, msg.value);
    }

    function withdraw() external virtual onlyOwner {

    // Do not remove this otherwise you will not be able to withdraw the funds.
    // =============================================================================
    (bool os, ) = payable(owner()).call{value: address(this).balance}("");
    require(os);
    // =============================================================================
    }

    function setLastTokenId(uint256 _lastTokenId) external onlyOwner {
        lastTokenId = _lastTokenId;
    }

    function potentialReward() external view returns(bool) {
        uint256 thisBalance = address(this).balance;
        if (thisBalance<=0) return false;
        for (uint256 i=0; i<pet.balanceOf(msg.sender);i++){
            uint256 ownerTokenId = pet.tokenOfOwnerByIndex(msg.sender, i);
            if (ownerTokenId>lastTokenId && !rewardedNFTs[ownerTokenId]){
                uint256 zeros=0;
                uint256 ones=0;
                uint256 twos=0;
                uint256 threes=0;
                (zeros, ones, twos, threes) = pet.getPetTraits(ownerTokenId);
                if (zeros>0){

                    return true;
                }
            }
        }
        return false;
        
    }

    function getReward() external returns(bool){
        uint256 thisBalance = address(this).balance;
        require(thisBalance>0,"Balance is 0");
        for (uint256 i=0; i<pet.balanceOf(msg.sender);i++){
            uint256 ownerTokenId = pet.tokenOfOwnerByIndex(msg.sender, i);
            if (ownerTokenId>lastTokenId && !rewardedNFTs[ownerTokenId]){
                uint256 zeros=0;
                uint256 ones=0;
                uint256 twos=0;
                uint256 threes=0;
                (zeros, ones, twos, threes) = pet.getPetTraits(ownerTokenId);
                if (zeros>0){

                    uint256 extractValue = thisBalance < 5 ether ? thisBalance : 5 ether;
                    (bool os, ) = payable(msg.sender).call{value: extractValue}("");
                    require(os);
                    rewardedNFTs[ownerTokenId] = true;
                    emit Rewarded(msg.sender, extractValue);
                    return true;
                }
            }
        }
        return false;
    }

}

Whole contract, EasterContract.

Note the lines 58 to 60 are a method to be only used by the contract owner to modify the last token ID authorized to claim the reward. There are also necessary changes on the lines 67 and 88.

Conclusion

We created a simple contract to do a giveaway ruled by the code, on holders of a generic NFT, Pet Number on this example, but this know-how can be applied to any other NFT project. Just need to change the interface and the potentialReward and getReward methods slightly.

I recommend to write a web app to host your giveaway, or you can modify the same minting app of the NFT to do it.

You can find a version of this contract deployed on Polygon here: https://polygonscan.com/address/0xbcc98fa20ae3eaca3b2823bd0c7213eaa8dc62b5#code.

I hope you have found valuable this content. Thank you!

Subscribe to 0x0ff0…0732
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from 0x0ff0…0732

Skeleton

Skeleton

Skeleton