How to Interact with ERC-721 Tokens in Solidity
Learn how to interact with, and build on top of existing ERC-721 tokens using Solidity
One of the greatest benefits of NFTs (and smart contracts, in general) is universal interoperability. Any contract on a blockchain can theoretically interact and build on top of any other contract on the same blockchain.
Interoperability helps NFT collections provide continuing utility to their holders, allows other projects to entice top buyers with discounts and exclusive access, and in some cases allows creators to do damage control for security breaches in the original contract.
In a previous tutorial, we learned how to interact with existing ERC-721 tokens to create custom allowlists. In this tutorial, we will explore a more advanced case. We will write a smart contract using Solidity and Hardhat that allows users who have paid and minted NFTs from a compromised contract to mint the same token IDs from a new contract for free.
Creating the Interaction Contract
Imagine you are the creator of an NFT project. You launch a collection and your buyers start minting NFTs by paying a certain amount of ETH. However, after a point of time, you realize that you haven't added functionality to your contract to withdraw the ETH that buyers have paid.
You immediately halt any further sales. You decide to launch a new contract but you do not want buyers who have already paid and minted their NFTs to pay a price again. This project will explore how we go about handling this.
Interacting with other ERC-721 contracts
In order to interact with any ERC-721 contract deployed on the blockchain of your choice, you will need to setup the following in your own contract:
- Define a Solidity interface that lists all the functions (and their signatures) that you plan on calling.
- Define an instance of the interface using the address of the contract you want to interface with.
- Call the functions listed in the interface.
A very simple example would look something like this:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
interface OGContractInterface {
function functionToCall1() external view returns (uint);
function functionToCall2() external view returns (uint);
...
}
contract MyContract is Ownable, ERC721Enumerable {
// Deployed address of contract we want to interface with
address public ogContractAddress;
OGCollectionInterface ogContract;
constructor(address _ogContractAddress) ERC721("My Collection", "MC") {
// Connect to the original NFT smart contract and get an instance
ogContractAddress = _ogContractAddress;
ogContract = OGCollectionInterface(ogContractAddress);
}
// Call original contract's methods inside your contract
function myFunction() public onlyOwner {
ogContract.functionToCall1();
ogContract.functionToCall2();
}
}
In the following sections, we will implement a more concrete solution for the problem highlighted above.
Step 1: Install Node and npm
In case you haven't already, install node and npm on your local machine.
Make sure that node is at least v14 or higher by typing the following in your terminal:
node -v
Step 2: Create a Hardhat project
We're going to set up our project using Hardhat, the industry-standard development environment for Ethereum smart contracts. Additionally, we'll also install OpenZeppelin contracts.
To set up Hardhat, run the following commands in your terminal:
mkdir nft-interaction && cd nft-interaction
npm init -y
npm install --save-dev hardhat
npx hardhat
Choose Create a Javascript project
from the menu and accept all defaults. To ensure everything is installed correctly, run the following command in your terminal:
npx hardhat test
To install OpenZeppelin:
npm install @openzeppelin/contracts
Step 3: Write the incorrect NFT smart contract
Let's now create the original (but incorrect) NFT smart contract. This contract will have all the basic functionalities expected of an NFT PFP collection except the withdraw
function.
Open the nft-interaction
project in your favorite code editor (e.g. VS Code). Create a file named ICNft.sol
in the contracts
folder and add the following code:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
contract ICNft is Ownable, ERC721Enumerable {
uint private _tokenIds;
uint public constant price = 0.05 ether;
string public baseTokenURI;
bool public saleIsActive = true;
constructor() ERC721("Original Collection", "OGC") {
}
// Set Sale state
function setSaleState(bool _activeState) public onlyOwner {
saleIsActive = _activeState;
}
// Mint NFTs
function mintNfts(uint _count) public payable {
require(saleIsActive, "Sale is not currently active!");
require(msg.value >= price * _count, "Not enough ether to purchase.");
for (uint i = 0; i < _count; i++) {
_mintSingleNft();
}
}
// Mint a single NFT
function _mintSingleNft() private {
uint newTokenID = _tokenIds;
_safeMint(msg.sender, newTokenID);
_tokenIds = _tokenIds + 1;
}
// Get tokens of an owner
function tokensOfOwner(address _owner) public view returns (uint[] memory) {
uint tokenCount = balanceOf(_owner);
uint[] memory tokensId = new uint256[](tokenCount);
for (uint i = 0; i < tokenCount; i++) {
tokensId[i] = tokenOfOwnerByIndex(_owner, i);
}
return tokensId;
}
}
Step 4: Write the replacement NFT smart contract
Our replacement smart contract will implement all the functionalities of the original smart contract (plus withdrawal) and add a remint feature.
Remint will allow users who have NFTs from the original contract to mint the corresponding IDs on the new contract without having to pay the base price. This will be done by calling the tokensOfOwner
function in the original contract.
To make sure that our contract can actually call the aforementioned function, we need to provide it with the function’s signature. We will do this using Solidity interfaces.
In the contracts
folder, create another file named RCNft.sol
and add the following code:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
interface ICCollectionInterface {
// Define signature of tokensOfOwner
function tokensOfOwner(address _owner) external view returns (uint[] memory);
// Definte signature of totalSupply
function totalSupply() external view returns (uint);
}
contract RCNft is Ownable, ERC721Enumerable {
uint private _tokenIds;
uint public constant price = 0.05 ether;
string public baseTokenURI;
bool public saleIsActive = false;
// Mapping that keeps track of all reminted NFTs
mapping(uint => bool) private isReminted;
// Deployed address of original collection
address public icContractAddress;
ICCollectionInterface icContract;
constructor(address _icContractAddress) ERC721("Replacement Collection", "RC") {
// Connect to the original NFT smart contract and get an instance
icContractAddress = _icContractAddress;
icContract = ICCollectionInterface(icContractAddress);
// Set current token ID to total supply of old collection
// Done to allow only reminting of NFTs already minted in the old collection
_tokenIds = icContract.totalSupply();
}
// Set Sale state
function setSaleState(bool _activeState) public onlyOwner {
saleIsActive = _activeState;
}
// Mint NFTs
function mintNfts(uint _count) public payable {
require(saleIsActive, "Sale is not currently active!");
require(msg.value >= price * _count, "Not enough ether to purchase.");
for (uint i = 0; i < _count; i++) {
_mintSingleNft();
}
}
// Remint NFTs
function remintNfts() public {
uint[] memory ids = icContract.tokensOfOwner(msg.sender);
// Only remint if it hasn't been done already
for (uint i = 0; i < ids.length; i++) {
if (!isReminted[ids[i]]) {
_safeMint(msg.sender, ids[i]);
isReminted[ids[i]] = true;
}
}
}
// Mint a single NFT
function _mintSingleNft() private {
uint newTokenID = _tokenIds;
_safeMint(msg.sender, newTokenID);
_tokenIds + _tokenIds + 1;
}
// Get tokens of an owner
function tokensOfOwner(address _owner) external view returns (uint[] memory) {
uint tokenCount = balanceOf(_owner);
uint[] memory tokensId = new uint256[](tokenCount);
for (uint i = 0; i < tokenCount; i++) {
tokensId[i] = tokenOfOwnerByIndex(_owner, i);
}
return tokensId;
}
// Withdraw ether
function withdraw() public payable onlyOwner {
uint balance = address(this).balance;
require(balance > 0, "No ether left to withdraw");
(bool success, ) = (msg.sender).call{value: balance}("");
require(success, "Transfer failed.");
}
}
The contract ensures the following:
- NFTs that had already been minted in the old collection can only be reminted by existing owners. These NFTs won't be available for public sale.
- All NFTs that were not minted in the old collection will be available for public sale and will function as expected.
Compile the contracts and make sure everything is working by running:
npx hardhat compile
Step 5: Simulate functionality locally
Let's now simulate our functionality locally. We will write a script that sequentially does the following:
- Owner deploys the original (incorrect) NFT smart contract
- Wallet 1 mints three NFTs (IDs 0, 1, and 2) by paying 0.15 ETH.
- Wallet 2 mints two NFTs (IDs 3 and 4) by paying 0.1 ETH.
- Owner realizes mistake and halts sales.
- Owner deploys the replacement NFT smart contract.
- Wallet 2 mints NFTs 3 and 4 from the replacement contract without paying a price.
Create a new file called run.js
in the scripts folder, and add the following code:
const hre = require("hardhat");
const { utils } = require("ethers");
async function main() {
// Get wallet addresses from hardhat
const [owner, address1, address2] = await hre.ethers.getSigners();
// Deploy OG (incorrect) collection and get deployed contract address
const icFactory = await hre.ethers.getContractFactory("ICNft");
const icContract = await icFactory.deploy();
await icContract.deployed();
console.log("OG Collection deployed to: ", icContract.address, "\n");
icContractAddress = icContract.address
// Mint 3 OG NFTs to address1
let txn;
txn = await icContract.connect(address1).mintNfts(3, { value: utils.parseEther('0.15') });
await txn.wait()
console.log("3 OG NFTs minted to ", address1.address);
// Mint 2 OG NFTs to address2
txn = await icContract.connect(address2).mintNfts(2, { value: utils.parseEther('0.10') });
await txn.wait()
console.log("2 OG NFTs minted to ", address2.address);
// Freeze sales of OG collection
txn = await icContract.setSaleState(false);
await txn.wait();
console.log("OG Collection sales have been halted");
// Deploy replacement contract
const rcFactory = await hre.ethers.getContractFactory("RCNft");
const rcContract = await rcFactory.deploy(icContractAddress);
await rcContract.deployed();
console.log("\nReplacement Collection deployed to: ", rcContract.address, "\n");
// Remint NFTs belonging to address2
txn = await rcContract.connect(address2).remintNfts();
await txn.wait()
console.log("NFTs reminted to ", address2.address);
let ids = await rcContract.tokensOfOwner(address2.address);
ids = ids.map(x => x.toNumber())
console.log("Replacement NFTs reminted:", ids);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Run this script by running the following command in your terminal:
npx hardhat run scripts/run.js
You should see output that looks like this:
OG Collection deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
3 OG NFTs minted to 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
2 OG NFTs minted to 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
OG Collection sales have been halted
Replacement Collection deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
NFTs reminted to 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
Replacement NFTs reminted: [ 3, 4 ]
Conclusion
Congratulations! You now know how to interact with ERC-721 tokens and smart contracts on any EVM-based blockchain using Solidity.
If you enjoyed this tutorial about creating on-chain allowlists, tweet us at @AlchemyPlatform and give us a shoutout!
Don't forget to join our Discord server to meet other blockchain devs, builders, and entrepreneurs!
Ready to start building your NFT collection?
Create a free Alchemy account and do share your project with us!
Updated over 1 year ago