Deploying an NFT staking contract on the Gnosis chain
Table of contents
- Introduction
- Prerequisites
- Setting up a dev environment with Truffle
- Taking a look at Truffle’s project structure
- Writing and compiling smart contracts in Truffle
- Setting up a dotenv file
- Setting up truffle-config
- Deploying and verifying our smart contract
- Logic flow for staking an NFT
- Conclusion
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:
Fuel.sol
, will represent our NFT smart contract (ERC-721)
Rewards.sol
, will be our staking contract that will receive new NFTs and issue rewards in the form of ERC-20 tokens.
Prerequisites
- Any code editor of your choice on a machine that has NodeJs installed.
- A grasp of the basic concepts of Solidity and Javascript, since all of our code will be written in those two languages.
- We will be using Truffle to compile and deploy our smart contracts. It will be helpful, but not necessary to be familiar with the framework.
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:
- Create a folder named nftGnosis (or any other name you like).
- Open the folder in your terminal, and run the command:
npm init -y
This command will initialize an empty package.json in your directory.
- To install Truffle in your system, run:
npm install -g truffle
This command will install the latest version of Truffle into your system.
- 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.
- 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.
- 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:
- The .gitkeep file is simply used as a placeholder in empty directories since Git does not allow us to push empty directories to remote repositories. This file does not affect our project in any way.
- The contracts directory contains all of our smart contracts.
- The migrations directory contains JavaScript files that are used to deploy our contracts to live networks.
- The test directory contains code that tests our smart contracts. Truffle supports smart contract testing in JavaScript, TypeScript, and Solidity. You can read more about this in their official docs.
- The package.json contains a reference to all the npm packages that we have installed in our project.
- The truffle-config file, as the name suggests, contains all the configurations for our Truffle project.
This is a JavaScript file that exports an object that contains all of our configurations. We can define a wide variety of parameters within this object, including the networks, the Solidity version, and the provider RPC URLs, amongst many more.
We will look into this in more detail below.
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
: This will be our ERC-721 contract
Rewards.sol
: This contract will contain the logic for our staking system.
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:
- The constructor initializes the contract name and symbol at the time of deployment.
- The
counters.sol
contract helps us keep track of all the NFTs that have been minted from our contract. Every time a new NFT is minted, the token ID is incremented. This means each NFT minted from our contract has a unique token ID that can be used to uniquely identify it.
- The
safeMint()
function uses the_safeMint()
function from the ERC-721 contract, which allows us to conveniently mint a new NFT to a specific address. Please note that while we define the ‘safeMint()
‘ function, the ‘_safeMint()
‘ function is inherited from the ERC-721 contract. You can read more about the ERC-721 template from OpenZeppelin’s official docs.
TheonlyOwner
modifier helps us in preventing unauthorized people from calling sensitive functions.
ERC721Burnable.sol
gives us access to the burn function, which can be used to ‘burn’ a specific NFT by basically transferring it over to the ‘zero’ address, which is basically a generic null address that has no owner.
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:
- The contract uses four OpenZeppelin contract templates, namely:
ERC20.sol
: The ERC-20 contract, allows us to create a smart contract for fungible tokens. The contract constructor specifies the name, symbol, and initial minting quantity of our token.
Ownable.so
l: The ownable contract allows us to manage access to our contract functions in a simplified manner. Whenever a smart contract inherits the ownable contract, the deployer of the contract is set as its owner.
ERC721Holder.sol
: This is an interesting one. When we finally deploy our smart contracts, we will need to approve theRewards.sol
contract to be able to transfer NFTs fromFuel.sol
to its own address. That is what we call ‘staking an NFT‘. But for a contract to be able to call the transfer function on an NFT from another contract, it must support theIERC721Receiver
interface. TheERC721Holder.sol
is simply a convenient implementation of the interface. We just need to inherit the contract into our main Rewards contract. If that was a bit confusing, we recommend you take a look at what interfaces do in Solidity. A look at OpenZeppelin’s official docs will be helpful too.IERC721.sol
: This is simply the interface that ERC721 contract is based on. Since our Fuel smart contract is an ERC721 implementation, we can initiate an instance of the Fuel contract in our Rewards contract by wrapping its address inside the IERC721 interface. As you can see in the constructor, we will be providing the address of the Fuel contract to the Rewards contract while deployment.
- The constructor simply accepts an address argument and initializes an ERC20 token with the symbol “RWD“.
- The staking function is quite simple. We call the
safeTransferFrom()
function on the instance of the Fuel contract, which transfers an NFT of a specific token ID to the Rewards contract. Once this is done, we update three mappings in our contract. These mappings keep track of the owners of the NFTs, the specific timestamps when they were staked, and a list of valid token IDs.
Please note that the function will only work if two conditions are fulfilled-- The token ID being passed as the parameter should actually be a valid, minted token ID.
- And, the address calling the function should actually own the NFT of that specific token ID.
- The function
calculateRewards()
simply calculates the amount of tokens we need to give to the owner of a staked NFT based on the amount of time an NFT has been staked for. The function calculates the amount for only valid token IDs, which we keep track of with the help ofisStaked[]
mapping.
- The unstake function will allow an owner to unstake an NFT after a minimum of 7 days. The function mints an exact amount of RWD tokens and transfers them to the NFT owner, following which the NFT is returned to the original owner. All data corresponding to that token ID is then deleted.
- Please feel free to tinker around with the logic of the contract. You can calculate rewards based on other parameters. You can change the minimum staking time or perhaps remove it altogether.
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:
- RPC_URL: Truffle needs to connect to a blockchain node to deploy our contract. We can connect to a blockchain using an HTTPS endpoint for a particular blockchain. For a fast and reliable connection to a live network, we recommend you use an endpoint from a provider like Chainstack that is run and maintained by a dedicated team behind the scenes. If you have never set up a node using Chainstack before, feel free to go through this tutorial from our blog.
- PRIVATE_KEY: Truffle needs access to a wallet’s private key to be able to sign transactions and send them to the blockchain. In this tutorial, we will be deploying our smart contract to the Gnosis mainnet, so make sure to use the private key of a wallet that has some xDAI tokens from the Gnosis chain.
- API_KEY: We will be verifying our deployed smart contract from Truffle’s command line. To do so, we need an API key from Gnosisscan. You can get started by signing up here.
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:
- HD wallet provider: We need to define a provider to sign transactions through truffle. A provider in Truffle is basically a combination of a private key/mnemonic with an RPC URL. These two values used together can sign transactions to a particular blockchain from a particular wallet address.
- truffle-plugin-verify: Truffle allows us to install a variety of officially supported plugins into our project. This plugin can be used to verify smart contracts on certain blockchains.
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:
- Networks: We can define multiple networks inside truffle-config. Each network has to contain at least one provider along with a chain ID. A new provider can be declared as a new instance of the
hdwallet-provider
object that contains at least one private key and an HTTPS or WSS URL corresponding to a particular blockchain.
- The compiler object supports various tweaks to the Solidity compiler. Here we simply define the Solidity version and ask the compiler to optimize the contract deployment for an expected 50 calls throughout its lifetime. You can read more about the supported configurations on Truffle’s reference page.
- plugins: Truffle allows us to use a variety of plugins that can be installed as NPM dependencies. In this tutorial, we are using only one plugin as explained previously.
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.
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:
- Deploy a smart contract to Gnosis mainnet using the migrate command with the corresponding network flag as shown above.
- 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:
- Deploy
Fuel.sol
. Now CopyFuel.sol
‘s contract address and pass it to the constructor ofRewards.sol
. Now deploy the second smart contract. We already completed this step through the migrations script.
- 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()
andownerOf()
functions.
- 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 theRewards.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 toFuel.sol
and approve the Rewards contract as a recipient by calling the function.
- 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 thesetApprovalForAll()
function.
- If you want to stake NFTs from 10 different addresses at
Rewards.sol
, you will have to call thesetApprovalForAll()
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.
- Discover how you can save thousands in infra costs every month with our unbeatable pricing on the most complete Web3 development platform.
- Input your workload and see how affordable Chainstack is compared to other RPC providers.
- Connect to Ethereum, Solana, BNB Smart Chain, Polygon, Arbitrum, Base, Optimism, Avalanche, TON, Ronin, zkSync Era, Starknet, Scroll, Aptos, Fantom, Cronos, Gnosis Chain, Klaytn, Moonbeam, Celo, Aurora, Oasis Sapphire, Polygon zkEVM, Bitcoin and Harmony mainnet or testnets through an interface designed to help you get the job done.
- To learn more about Chainstack, visit our Developer Portal or join our Discord server and Telegram group.
- Are you in need of testnet tokens? Request some from our faucets. Multi-chain faucet, Sepolia faucet, Holesky faucet, BNB faucet, zkSync faucet, Scroll faucet.
Have you already explored what you can achieve with Chainstack? Get started for free today.