Automate Dynamic NFT using OpenZeppelin Defender and Celo Composer

Automate Dynamic NFT using OpenZeppelin Defender and Celo Composer https://celo.academy/uploads/default/optimized/2X/5/52c6f35f5003eb495c9f86562c155a0942efbd84_2_1024x576.png
none 0.0 0

Introduction

Using Oracle services like Chainlink seems to be the go-to platform when one is faced with the task of doing some automation and running CRON jobs on one’s dApp. The challenge comes when your preferred blockchain in not supported by Chainlink or some of the alternative oracle platforms, hence looking at how to get going with OpenZepplin Defender.

OpenZepplin Defender handles automation and much more.

To illustrate, we are building a product marketplace where customers can earn NFTs as a form of incentive. The more a customer patronizes the business, the more points they will earn. The points would be used for appropriate NFT upgrade.

Prerequisites

To successfully follow along in this tutorial, you need basic knowledge of the following:
HTML, CSS, and React
Blockchain, Solidity, and Hardhat

Requirements​

Project setup with Celo Composer

We are going to start by running the below command to scaffold our frontend (React) and backend (Hardhat framework).

npx @celo/celo-composer@latest create

For context, Celo Composer is a tool that makes it easy to build and deploy decentralized applications on the Celo blockchain. It provides a number of features that make it a powerful tool for developers, including:

  • A set of templates for React, React Native, Flutter, and Angular that make it easy to get started with building a dApp on Celo.
  • A CLI tool that helps you manage your dApp’s development environment, including setting up a local blockchain node, installing dependencies, and running tests.
  • A library of Celo specific functionality that makes it easy to interact with the Celo blockchain, such as sending transactions, querying for data, and signing messages.
  • A community of developers who are using Celo Composer to build dApps.

Celo Composer is a valuable tool for developers who want to build dApps on the Celo blockchain. It makes it easy to create, modify, test, and deploy dApp code, and it provides a number of features that make it a powerful tool for developers.

Setting up the Smart Contract

To get started, we need to locate the contracts directory inside hardhat folder. Create a solidity file and name it AutoNFT.sol, Feel free to name it whatever that suites your case and paste this code below.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

contract AutoNFT is ERC721, ERC721Enumerable, ERC721URIStorage {

    address[] private customerAddresses;

    struct Product {
      string name;
      string imagePath;
      uint256 price;
    }

    Product[] public products;

    mapping(address => uint256) public purchaseCounts;

    mapping(address => bool) public userMinted;


    constructor() ERC721("Loyalty", "LTY") {
      products.push(Product("Gucci Bag", "gucci.jpg", 30000000000000000));
      products.push(Product("Zara Bag", "zara.jpg", 40000000000000000));
      products.push(Product("Nike Shoe", "nike.jpg", 50000000000000000));
    }

    function _baseURI() internal pure override returns (string memory) {
      return "https://ipfs.io/ipfs/QmdfZ1zpmKEdS3QjbYzLULhm1H1KAkDe8C5NPh9ZXu8r61/";
    }

    // The following functions are overrides required by Solidity.

    function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
      super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) {
      return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) {
      return super.supportsInterface(interfaceId);
    }

    function _beforeTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize) internal override(ERC721, ERC721Enumerable) {
      super._beforeTokenTransfer(from, to, tokenId, batchSize);
    }

    function buyProduct(uint256 _productIndex) public payable {

      Product memory product = products[_productIndex];

      require(product.price == msg.value, "Incorrect product amount");

      if (!userMinted[msg.sender]) {

        uint256 tokenId = 0;
        string memory uri = "nft1.json";
        _safeMint(msg.sender, tokenId);
        _setTokenURI(tokenId, uri);
        userMinted[msg.sender] = true;
      }


      purchaseCounts[msg.sender] ++;

      addCustomerAddress(msg.sender);
    }

    function getProducts() public view returns(Product[] memory) {
      return products;
    }

    function getTokenId() public view returns (uint256) {

      require(userMinted[msg.sender], "User has not NFT");

      // User will always have 1 token.
      uint256 tokenIndex = 0;

      return tokenOfOwnerByIndex(msg.sender, tokenIndex);

    }

    function upgradeCustomers(address _nftAddress) public {
      for (uint256 i = 0; i < customerAddresses.length; i++) {
        address customer = customerAddresses[i];

        uint256 point = purchaseCounts[customer];

        if (userMinted[customer]) {

        uint256 tokenId = ERC721Enumerable(_nftAddress).tokenOfOwnerByIndex(customer, 0);

        // Check if user can be upgraded.

        if (point < 2) {
            string memory uri = "nft1.json";
            _setTokenURI(tokenId, uri);
        } else if (point < 3) {
            string memory uri = "nft2.json";
            _setTokenURI(tokenId, uri);
        } else {
            string memory uri = "nft3.json";
            _setTokenURI(tokenId, uri);
        }

        } else {

          uint256 tokenId = 0;
          string memory uri = "nft1.json";
          _safeMint(customer, tokenId);
          _setTokenURI(tokenId, uri);
          userMinted[customer] = true;
        }
      }
    }

    function addCustomerAddress(address _address) private {
      bool exists = false;

      for (uint i = 0; i < customerAddresses.length; i++) {
        if (customerAddresses[i] == _address) {
          exists = true;
          break;
        }
      }

      if (!exists) {
        customerAddresses.push(_address);
      }
    }

}

We imported the required dependencies. We used the OpenZeppelin library, which provides secure and audited implementations of ERC721 tokens. Additionally, we imported the ERC721, ERC721Enumerable, and ERC721URIStorage contracts from the OpenZeppelin library. These contracts provide the necessary functionality for our loyalty NFT system, such as minting, transferring, and querying token information.

Defining the NFT Structure

In our AutoNFT contract, we define a struct called Product to represent the products available for purchase in the loyalty program. Each product has a name, an image path, and a price. We store an array of products to keep track of the available options.

getProducts(): This function returns a list of all the products that are available for purchase. Each Product object has the following properties:

  • name: The name of the product
  • imagePath: The path to the image of the product
  • price: The price of the product

getTokenId(): This function returns the ID of the NFT that a customer has earned. The function takes one parameter, which is the address of the customer. The function then checks to see if the customer has any NFTs. If the customer does have any NFTs, the function returns the ID of the first and only NFT that the customer owns.

The buyProduct function allows customers to purchase a product by providing the product index and the required payment. The function verifies that the payment amount matches the price of the selected product. If the customer has not already minted an NFT, a new NFT is minted and assigned to their address. The purchase count for the customer is incremented, and their address is added to the customerAddresses array.

The upgradeCustomers function upgrades the loyalty NFTs of customers based on their purchase history. The function iterates over the customerAddresses array and checks the purchase count of each customer. If the customer has a minted NFT, the function retrieves the token ID and updates the token’s URI with the customer’s purchase count. If the customer does not have a minted NFT, a new NFT is minted for them.

In more detail, the upgradeCustomers function works as follows:

  1. The function iterates over the customerAddresses array.
  2. For each customer, the function checks the customer’s purchase count.
  3. If the customer has a minted NFT, the function retrieves the token ID.
  4. The function updates the token’s URI with the customer’s purchase count.
  5. If the customer does not have a minted NFT, a new NFT is minted for them.

The upgradeCustomers function is a critical part of the loyalty program. It ensures that customers are rewarded for their loyalty by upgrading their NFTs as they make more purchases. This is the function we will be calling in the Open Zeppelin Defender Autotask module.

The addCustomerAddress function is a helper function used to manage the customerAddresses array. It checks if an address already exists in the array and adds it if it doesn’t.

OpenZeppelin Defender

Defender by OpenZeppelin is a widely used tool for automating secure processes in smart contracts. It is compatible with Layer 1 blockchains, Layer 2 blockchains, and sidechains, offering various features for transparent smart contract management.

These features include access control, upgrade functionality, and pausing capabilities.

The Admin module acts as an intermediary interface, allowing secure control of smart contract projects through multi-signature contracts. Defender also provides the option to create Relayers, which securely handle private API keys and enforce policies like gas price caps using Relay. Autotask enables the scheduling and execution of JavaScript code scripts on smart contracts at regular intervals, while Sentinel monitors smart contracts for transactions and sends notifications based on predefined conditions. The Logging service in Defender keeps a centralized record of all actions performed manually or automatically within your Defender account.

Relay

To utilize the OpenZeppelin Defender Relay dashboard for creating a Relayer, begin by accessing the dashboard and proceed with the following steps.

Firstly, pick a suitable name for your Relayer and specify the network on which you intend to deploy it. We are going to select Celo Alfajores testnet. Once these initial selections are made, you can proceed to create your Relayer. By doing so, you gain the capability to securely and effectively relay data between your smart contracts and external data sources, enhancing the overall efficiency of your operations.

Once you have successfully created a Relayer, you will be provided with a Celo address. Initially, this address will have a balance of zero and will be in the Running state.

To fund your account, copy the generated Celo address and proceed to the Celo Faucet. At the Celo Faucet, you can request funds to be transferred to your Relayer’s Celo address. This will ensure that your account has the necessary balance to perform transactions and fulfill its relay functionality.

When a smart contract requires data from an external source, it communicates this need by sending a request to the OpenZeppelin Relayer. The Relayer acts as an intermediary, forwarding the request to the designated data source. Once the data source provides a response, the Relayer relays it back to the smart contract. It’s important to note that these transactions incur gas fees, which serve as the payment unit for executing transactions on the network.

The cost of gas for a transaction is determined by the computational resources it consumes and the level of congestion on the network. When a smart contract sends a request to the OpenZeppelin Relayer, it is essential to include an adequate amount of gas to cover the expenses associated with the relay transaction. This ensures that the transaction can be processed smoothly without any issues related to insufficient gas supply.

To guarantee sufficient gas for relaying requests and responses between smart contracts and external data sources, it is crucial to fund the OpenZeppelin Relayer account. This funding ensures that transaction fees associated with the relay operations are adequately covered. By doing so, the relay transactions can be executed smoothly and efficiently, enabling prompt and dependable delivery of the required data to the smart contracts.

Autotask

To establish a connection between your recently created Relayer and an Autotask, begin by creating a new Autotask within the dashboard. During the configuration process, specify the Relayer as the designated communication channel for the Autotask. This setup allows your Autotask to securely and optimally interact with your smart contracts and external data sources through the Relayer. By utilizing the Relayer as a communication bridge, you enhance the security and efficiency of data transmission between your Autotask and relevant components.

const { DefenderRelaySigner, DefenderRelayProvider } = require('defender-relay-client/lib/ethers');
const { ethers } = require("ethers");
const ABI = [
{
      "inputs": [
        {
          "internalType": "address",
          "name": "_nftAddress",
          "type": "address"
        }
      ],
      "name": "upgradeCustomers",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    }
  ];

const ADDRESS = '0x168b4369717EA0405D6461570624a0364A17B8CA'

async function main(signer) {
  const contract = new ethers.Contract(ADDRESS, ABI, signer);
  await contract.upgradeCustomers(ADDRESS);
}

exports.handler = async function(params) {
  const provider = new DefenderRelayProvider(params);
  const signer = new DefenderRelaySigner(params, provider, { speed: 'fast' })
  console.log(`Using relayer ${await signer.getAddress()}`);
  await main(signer);
}

For our automation process, we leverage the OpenZeppelin Defender Relayer client library along with the ethers.js library. The script incorporates the following steps: Firstly, it creates a new instance of DefenderRelayProvider and DefenderRelaySigner, which enable interaction with the smart contract through a relayer. Subsequently, the main function is invoked, triggering the execution of the upgradeCustomers functions. This function serve as the core component where the automation logic is implemented and carried out.

Conclusion

In this article, we have explored how to build an automated Dynamic NFT system using Solidity and the OpenZeppelin Defender. The system allows customers to purchase products and earn NFTs based on their purchase history. The loyalty NFTs can be upgraded as customers accumulate more purchases. This application demonstrates the versatility of NFTs beyond art and collectibles and showcases the potential for automating one’s smart contract functionalities on the Celo blockchain.

Here is the link to the complete code. Here is the demo.

About the Author​

A software engineer, co-founder, Africinnovate, and a Web3 enthusiast. Connect with me on LinkedIn and Github

References​

4 Likes

Approved for you to get started. You can manage the tutorial here by changing the category to Proposals > In Progress then Proposals > Review as you complete the tutorial. Thanks!

2 Likes

Hi, i would be reviewing this

2 Likes

@cjustinobi remove the unnecessary space before celo composer

1 Like

Can you point me to the specific lines where to effect these changes. I’m yet to see the spaces.

1 Like

For context, Celo Composer is a tool that makes it easy to build and deploy decentralized applications on the Celo blockchain. It provides a number of features that make it a powerful tool for developers, including:

the spaces after this sentence

Oh I see. Suggestions effected

1 Like

ok great work, you can move to publsih

2 Likes

OpenZepelin places a strong emphasis on security and are trusted open source framework for building sexure and audited smart contracts, nice to see it integrated so well in this article.

2 Likes

@cjustinobi,
The below-quoted contract did not compile successfully. To help you fix it quickly, below is the error associated with it.

Error

from solidity:
TypeError: Function needs to specify overridden contract "ERC721URIStorage".
   --> contracts/Lv2.sol:167:64:
    |
167 |     function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) {
    |                                                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Note: This contract: 
  --> @openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol:12:1:
   |
12 | abstract contract ERC721URIStorage is IERC4906, ERC721 {
   | ^ (Relevant source part starts here and spans across multiple lines).
9 Likes