Site icon Chainstack

Deploying an NFT staking contract on the Gnosis chain

Nft Staking Gnosis 1024x542 logo

Table of contents

Introduction

Gnosis is an EVM-based blockchain that is currently secured by over 100k validators. It aims for fast transaction times and low transaction fees. Gnosis mainnet and its corresponding Chiado testnet use xDai and the Chiado xDai as native tokens for their respective blockchain networks.

In this tutorial, we will deploy an NFT staking smart contract to the Gnosis Chain. Our project consists of two contracts: 

Prerequisites 

Setting up a dev environment with Truffle

Before we begin writing our smart contracts, we need to have a dev environment set up which allows us to compile and test our smart contracts before we deploy them to a live network. While there are many great smart contract frameworks available, we will be going with Truffle in this tutorial.
To set up a dev environment with Truffle:

  1. Create a folder named nftGnosis (or any other name you like).
  1. Open the folder in your terminal, and run the command:
    npm init -y
    This command will initialize an empty package.json in your directory.
  1. To install Truffle in your system, run:
    npm install -g truffle
    This command will install the latest version of Truffle into your system.
  1. Let us check if Truffle was correctly installed in our system. To do so, run:
    truffle version
    This command should return the version of Truffle installed in your system.
  1. To initialize a basic Truffle project, first, make sure that your terminal is pointing correctly to the root of your project directory. Now, in the terminal, run:
    truffle init
    This command will install a barebones Truffle project into your local directory.
  1. This is what your terminal and project directory should look like right now:

We are using VS Code as our editor, but any good code editor will do.

Taking a look at Truffle’s project structure

After you’ve initialized a boilerplate Truffle project, your directory should look something like this:

├── contracts 
   └── .gitkeep
├── migrations 
   └── .gitkeep 
├── test
└── .gitkeep
├── package.json 
├── truffle-config.js

Let us quickly go over this:

Writing and compiling smart contracts in Truffle

Before getting down to writing Solidity code, let us learn a bit about OpenZeppelin contracts.
OpenZeppelin offers us a variety of products, but their contracts library is the one we will be using in this tutorial.
Solidity as a language supports inheritance. This means we can use pre-built Solidity components to augment our own code. OpenZeppelin on its part has a huge library of Solidity contracts and interfaces that we can use to build ERC-20 and ERC-721 smart contracts in a secure way.
Sure we could do the same without using OpenZeppelin, but why not use a library that has been thoroughly audited and optimized, thus saving us both time and money?

To get started with OpenZepplin contracts, open your terminal and run:

npm i @openzeppelin/contracts

This command will install the @openzeppelin/contracts library into your local directory, and you can check your package.json to make sure it is installed correctly.
We can now inherit OpenZeppelin contracts into our own solidity code.

We are now ready to start writing some Solidity code. In the contracts folder, create 2 files:

Fuel.sol

In an empty Fuel.sol, paste the following code:

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract Fuel is ERC721, ERC721Burnable, Ownable {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    constructor() ERC721("Fuel", "FUEL") {}

    function safeMint(address to) public onlyOwner {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
    }
}

This is a small, but quite effective ERC-721 contract that does the following:

Rewards.sol

Now open Rewards.sol, and paste the following code inside it:

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

contract Rewards is ERC20, ERC721Holder, Ownable {
    IERC721 public nft;
    mapping(uint256 => address) public tokenOwnerOf;
    mapping(uint256 => uint256) public tokenStakedAt;
    mapping(uint256 => bool) public isStaked;
    uint256 public rewardsPerHour = (1 * 10 ** decimals()) / 1 hours;

    constructor(address _nft) ERC20("Reward", "RWD") {
        nft = IERC721(_nft);
    }

    function stake(uint256 tokenId) external {
        nft.safeTransferFrom(msg.sender, address(this), tokenId);
        tokenOwnerOf[tokenId] = msg.sender;
        tokenStakedAt[tokenId] = block.timestamp;
        isStaked[tokenId] = true;
    }

    function calculateRewards(uint256 tokenId) public view returns (uint256) {
        require(isStaked[tokenId], "This Token id was never staked");
        uint timeElapsed = block.timestamp - tokenStakedAt[tokenId];
        return timeElapsed * rewardsPerHour;
    }

    function unstake(uint256 tokenId) external {
        uint timeElapsed = block.timestamp - tokenStakedAt[tokenId];
        uint minimumTime = 7 days;

        require(tokenOwnerOf[tokenId] == msg.sender, "You can't unstake because you are not the owner");
        require(timeElapsed >= minimumTime, "You need to stake for at least 7 days");

        _mint(msg.sender, calculateRewards(tokenId));
        nft.transferFrom(address(this), msg.sender, tokenId);
        delete tokenOwnerOf[tokenId];
        delete tokenStakedAt[tokenId];
        delete isStaked[tokenId];
    }
}

This is our main staking smart contract. Let us go over the main concepts of this contract carefully:

That was quite a lot to digest, but we now have our staking smart contract ready to go.

To compile smart contracts in Truffle, we need to run the compile command in our terminal:

truffle compile

This command compiles all the smart contracts in the contracts directory, and generates artifacts for them in the build/contracts directory. Subsequent invocations of the command will only compile those smart contracts that have undergone any changes since the last compile command.

Setting up a dotenv file

We will soon be deploying and verifying the NFT staking smart contract. To do so, we need three main values:

It is possible to plugin these values directly into the truffle-config file to get up and running, but we would highly discourage you from doing so. Putting these values directly into the config file can lead to you pushing them onto a remote directory which could potentially compromise your funds. We can instead export our sensitive data securely using the dotenv package. That is what we will do now.

Once you have these three variables, open the terminal, make sure it is pointing to the root directory, and run:

npm i dotenv

This will install the dotenv package into your local directory.
Again, in your terminal run:

touch .env

This will create an empty .env file inside your project. Paste the following code inside the file:

PRIVATE_KEY="0x0000000000000000000"
CHIADO_RPC_URL=https://123-456-789
GNOSIS_RPC_URL=https://987-654-321
API_KEY=ABCDEFGHIJKLMNOPQRSTUVWXYZ

Simply replace the mock data with your actual keys and URLs. Note that we are putting two different RPC URLs into our dotenv file. This is because we will define two different networks in our truffle-config file, one for the Gnosis mainnet, and the other for the Chiado testnet.

Once you are ready, save the dotenv file.

Setting up truffle-config

We need to install two packages to power our truffle-config file:

To install these plugins, run the following commands in your terminal:

npm i @truffle/hdwallet-provider
npm i truffle-plugin-verify

Now that you have all the required plugins, go to your truffle-config file and delete everything inside of it.
Paste the following code into the file:

require('dotenv').config();
const HDWalletProvider = require("@truffle/hdwallet-provider");

module.exports = {
  networks: {
    GnosisMainnet: {
      provider: () => {
        return new HDWalletProvider([process.env.PRIVATE_KEY], `${process.env.GNOSIS_RPC_URL}`);
      },
      network_id: 100,
    },
    ChiadoTestnet: {
      provider: () =>
        new HDWalletProvider([process.env.PRIVATE_KEY], `${process.env.CHIADO_RPC_URL}`),
      network_id: 10200,
    },
  },

  compilers: {
    solc: {
      version: "0.8.17",
      settings: {
        optimizer: {
          enabled: true,
          runs: 50,
        },
      },
    },
  },

  plugins: ['truffle-plugin-verify'],
  api_keys: {
    gnosisscan: `${process.env.API_KEY}`
  },
  
};

Let us go over the major configurations we define here:

Deploying and verifying our smart contract

Before moving further, please note that Gnosis mainnet recently went through the merge. If you don’t know what the merge is, or what it meanse for the Gnosis chain, you can refer to this article from our blog.

Gnosis Merge

We have now configured everything in our truffle-config file. To actually deploy a contract in Truffle, however, we need to write a basic deployment script.
Go to the migrations directory, and create a file named 1_deploy_contract.js. Inside the file, paste the following code:

const Fuel = artifacts.require("Fuel");
const Rewards = artifacts.require("Rewards");

module.exports = async function (deployer) {

  await deployer.deploy(Fuel);
  await deployer.deploy(Rewards, Fuel.address);
  await Fuel.setApprovalForAll(Rewards.address, true);
};

This is a very simple deployment script. We import the ABIs of our smart contracts from the artifacts folder. After that, we call the deploy function on all three of the contracts in an async function.
Please note that the NFTstake contract requires the addresses of both Fuel.sol and Rewards.sol in its constructor. Therefore, those not only have to be deployed first, but we also need to pass their respective addresses in NFTstake’s parameters.

Please note that Truffle has a very specific naming convention for its migration files. We strongly recommend you follow this naming convention or the deployment script may not work as intended. You can read more about these conventions on Truffle’s docs.

To deploy our smart contracts to a live network, open the terminal and run:

truffle migrate --network GnosisMainnet

This will engage 1_deploy_Contract.js and deploy the contract to the Gnosis mainnet as specified in truffle-config.js.

Now onto the verification part. Please note that the Gnosisscan API key that we added to our project does not support contract verification on the Chiado testnet. We can only use it to verify contracts on the Gnosis mainnet.
To verify a smart contract on the Gnosis mainnet using a Gnosisscan API key in Truffle:

  1. Deploy a smart contract to Gnosis mainnet using the migrate command with the corresponding network flag as shown above.
  1. Run the following command in your terminal after your contract has been deployed successfully.
truffle run verify <CONTRACT_NAME>@<CONTRACT_ADDRESS> --network GnosisMainnet

This will verify your smart contract on the Gnosis network and return a success message. Your smart contract is now verified!
Verify both the smart contracts on the Mainnet, and you can now interact with them right from Gnosisscan explorer.

If you don’t have xDAI tokens from the mainnet, you can still follow through the tutorial by deploying the contracts to the Chiado testnet. It is just that you won’t be able to use Truffle to verify your contracts. You can still verify them by going to the Chiado explorer. If you need some testnet xDAI tokens, you can get them from the official faucet

Logic flow for staking an NFT

Both of our smart contracts are now deployed and verified on the Gnosis Mainnet. In this last section, we will briefly go over the logic flow for the staking mechanism:

  1. Deploy Fuel.sol. Now Copy Fuel.sol‘s contract address and pass it to the constructor of Rewards.sol. Now deploy the second smart contract. We already completed this step through the migrations script.
  1. Call the safe mint function and mint an NFT each to at least 2 unique addresses. You can check that the NFTs were minted properly by calling the currentTokenID() and ownerOf() functions.
  1. Now go to Rewards.sol and try staking an NFT to it by calling the stake function with a token ID parameter. It won’t work.
    Why? Because we never approved the Rewards.sol as an eligible recipient of a Fuel NFT.
    When you have an ERC721 NFT, you as the owner can hand over custody of your NFT by calling the setApprovalForAll() function, thus approving a particular address as an authorized handler of your NFTs. Go to Fuel.sol and approve the Rewards contract as a recipient by calling the function.
  1. Now all the NFTs of a particular wallet can be staked at Rewards.sol. Please note that only the NFTs of those addresses can be staked at the Rewards contract, those who authorized the rewards contract as an approved handler by calling the setApprovalForAll()function.
  1. If you want to stake NFTs from 10 different addresses at Rewards.sol, you will have to call the setApprovalForAll() from those 10 different addresses before staking their NFTs with the rewards contract.

Conclusion

In this tutorial, you learned how to compile, test and deploy smart contracts using the Truffle framework.
We deployed an NFT staking smart contract on Gnosis mainnet through a migrations script in Truffle and then verified it using an API key from Gnosisscan.
You can go to our blog for more tutorials like this.

Have you already explored what you can achieve with Chainstack? Get started for free today.

Exit mobile version