13 minutes
ERC721 NFTs: Creating an Analogous Smart Contract on Flow Blockchain with Cadence
In this tutorial, we assume you have a basic understanding of decentralized application development, and we’ll compare the creation of an ERC721 NFT smart contract in Solidity with its Cadence counterpart.
Purple Dash
Table of Contents
The world of blockchain has been revolutionized by the emergence of non-fungible tokens (NFTs). These digital assets are unique and cannot be replicated, making them ideal for use cases such as art, collectibles, and gaming items. The ERC721 standard, developed by Ethereum, is one of the most popular NFT standards in use today. However, the limitations of the Ethereum network, such as high gas fees and scalability issues, have led to the emergence of alternative blockchain platforms for NFT development. One such platform is the Flow blockchain, which uses the Cadence programming language.
In this tutorial, we assume you have a basic understanding of decentralized application development, and we’ll compare the creation of an ERC721 NFT smart contract in Solidity with its Cadence counterpart.
What is an ERC721 NFT?
An ERC721 NFT is a unique digital asset that is stored on the Ethereum blockchain. Each NFT is represented by a smart contract that contains metadata and ownership information. Unlike traditional cryptocurrencies, such as Bitcoin or Ethereum, which are fungible and interchangeable, each ERC721 NFT is unique and cannot be replicated. This uniqueness makes them ideal for use cases such as art, collectibles, and gaming items, where the value of an asset is determined by its uniqueness.
ERC721 smart contracts define a standard interface that enables NFTs to be created, owned, and traded on the Ethereum network. The ERC721 standard includes functions such as minting new tokens, transferring ownership of tokens, and querying token metadata.
What is the Flow Blockchain?
The Flow blockchain is a fast, decentralized, and developer-friendly blockchain platform designed for building high-performance applications and digital assets. Flow uses a unique architecture that separates the computation and storage layers, allowing for more efficient use of resources and faster transaction processing times. Flow also supports smart contracts written in the Cadence programming language, which is designed to be safe, easy to use, and secure.
Why Use Flow for NFT Development?
While Ethereum is the most popular blockchain platform for NFT development, it has several limitations that have led to the emergence of alternative platforms such as Flow. One of the main issues with Ethereum is the high gas fees required to execute transactions on the network. Gas fees are paid in Ethereum’s native currency, Ether, and can vary greatly depending on network congestion and transaction complexity. This can make it expensive to create, trade, and transfer NFTs on the Ethereum network.
Flow, on the other hand, has lower transaction fees and faster transaction processing times than Ethereum. This makes it more suitable for use cases such as gaming, where high transaction volumes are required. Additionally, Flow’s architecture is designed to be developer-friendly, with a focus on ease of use and security.
ERC721 vs. Cadence NFTs
To provide a point of reference for Solidity, we’ll use a commonly used ERC721 implementation, such as the OpenZeppelin ERC721 contract. In Cadence, we’ll create a similar NFT smart contract to illustrate the fundamental differences between Solidity and Cadence.
While both Solidity and Cadence aim to achieve the same core functionality, which is to represent and manage ownership of unique digital assets, they differ significantly in their approach.
Solidity relies on address-based access and authorization, where interactions with the smart contract are gated by checking the sender’s address. In contrast, Cadence introduces a new paradigm of Capability-based access control, making token management more secure and developer-friendly.
Check out this article on the Flow blog for a deeper dive into the architectural differences between Solidity and Cadence, and how the Bored Ape Yacht Club NFT collection can be recreated on Flow.
Creating an ERC721 NFT in Solidity
In Solidity, creating an ERC721 NFT contract involves defining a contract that inherits from the ERC721 standard. Below is an example of a simplified ERC721 contract:
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract MyNFT is ERC721 {
constructor(string memory name, string memory symbol) ERC721(name, symbol) {}
function mint(address to, uint256 tokenId) public {
_mint(to, tokenId);
}
}
In this Solidity contract, we inherit from the ERC721 standard, allowing us to use its predefined functions for NFT management. We have a mint
function to create new NFTs and assign them to specific addresses.
Creating an Equivalent Smart Contract on Flow
To create an ERC721 NFT smart contract on the Flow blockchain, we will use Cadence. Cadence introduces a unique approach to smart contract development, emphasizing resource-oriented design, capability-based access control, and streamlined state management. This example will showcase the fundamental differences between Cadence and the more traditional Solidity paradigm by implementing a contract using Cadence conventions and standards to create the Flow equivalent of ERC-721.
The Flow Non-Fungible Token (NFT) Standard is a protocol on the Flow blockchain that defines how non-fungible tokens are created and interacted with. It is conceptually equivalent to the ERC-721 standard on Ethereum, but it offers several improvements.
Unlike ERC-721, tokens on Flow cannot be sent to contracts that don’t understand how to use them, because an account needs to have a Receiver or Collection in its storage to receive tokens. This prevents tokens from being lost in contracts that are not designed to handle them.
Additionally, the Flow NFT Standard supports batch transfers of NFTs within a transaction, which is not explicitly defined in the ERC-721 contract and which is generally handled through a different standard, ERC-1155. This allows for more efficient transfers of multiple tokens. Another advantage is that transfers can trigger actions because users can define custom Receivers to execute certain code when a token is sent.
Lastly, the Flow NFT Standard simplifies ownership indexing by storing all tokens in an account’s collection, allowing users to instantly get the list of the tokens they own, rather than having to iterate through all tokens to find which ones they own as in ERC-721.
import NonFungibleToken from 0x02
pub contract MyNFT: NonFungibleToken {
// Define the NFT type
pub resource NFT: NonFungibleToken.INFT {
pub let id: UInt64
init(id: UInt64) {
self.id = id
}
}
// Define the Collection type
pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver {
pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
// Deposit a new NFT to the collection
pub fun deposit(token: @NonFungibleToken.NFT) {
self.ownedNFTs[token.id] <-! token
}
// Withdraw an NFT from the collection
pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
let nft <- self.ownedNFTs.remove(key: withdrawID)
return <- nft
}
pub fun transferNFT(to: Address, id: UInt64) {
let recipient = getAccount(to)
let receiverRef = recipient.getCapability<&{NonFungibleToken.Receiver}>(/public/myNFTCollection).borrow()
?? panic("Could not borrow reference to the recipient's NFT Collection")
let token <- self.withdraw(withdrawID: id)
receiverRef.deposit(token: <-token)
}
// Mint a new NFT
pub fun createNFT(id: UInt64): @NFT {
return <- create NFT(id: id)
}
}
This example defines an NFT with a unique id
and a Collection
resource that can hold multiple NFTs. The deposit function allows you to add an NFT to a collection, and the withdraw function allows you to remove an NFT from a collection. The createNFT
function allows you to mint a new NFT.
Key features of the Cadence contract:
import NonFungibleToken from 0x02
: This line imports theNonFungibleToken
contract from its address on the blockchain. This contract provides the necessary interfaces for creating NFTs.pub contract MyNFT: NonFungibleToken
: This line declares a public contract namedMyNFT
that implements theNonFungibleToken
contract.pub resource NFT: NonFungibleToken.INFT
: This line declares a public resource (a type of object that has owners and can be moved between accounts) namedNFT
that implements theNonFungibleToken.INFT
interface. This resource represents an individual NFT.pub let id: UInt64
: This line declares a public constant namedid
of typeUInt64
. Thisid
is used to uniquely identify each NFT.pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver
: This line declares a public resource namedCollection
that implements both theNonFungibleToken.Provider
andNonFungibleToken.Receiver
interfaces. This resource represents a collection of NFTs owned by an account.pub fun deposit(token: @NonFungibleToken.NFT)
: This function allows an account to deposit an NFT into its collection.pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT
: This function allows an account to withdraw an NFT from its collection.pub fun createNFT(id: UInt64): @NFT
: This function allows the contract to create a new NFT with a specifiedid
.
To enhance the Cadence contract for ERC721 NFTs with robust access control, you can incorporate a capability field within the contract, enabling the assignment and revocation of specific access privileges to individual users. Presented below is an expanded iteration of the contract, integrating capability-based access control mechanisms:
pub contract MyNFT {
pub var totalSupply: UInt64
pub resource NFT {
pub let id: UInt64
init(id: UInt64) {
self.id = id
MyNFT.totalSupply = MyNFT.totalSupply + 1
}
}
pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver {
pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
pub fun deposit(token: @NonFungibleToken.NFT) {
self.ownedNFTs[token.id] <-! token
}
pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
let nft <- self.ownedNFTs.remove(key: withdrawID)
return <- nft
}
pub fun transferNFT(to: Address, id: UInt64) {
// Borrow a reference to the recipient's NFT collection
let recipient = getAccount(to)
let receiverRef = recipient.getCapability<&{NonFungibleToken.Receiver}>(/public/myNFTCollection).borrow()
?? panic("Could not borrow reference to the recipient's NFT Collection")
// Withdraw the NFT from the sender's collection
let token <- self.withdraw(withdrawID: id)
// Deposit the NFT to the recipient's collection
receiverRef.deposit(token: <-token)
}
}
access(self) var mintCapability: Capability<&{NonFungibleToken.Provider}>
init() {
self.totalSupply = 0
self.mintCapability = /public/mintNFT
}
pub fun createNFT(id: UInt64): @NFT {
let mintCapability = self.mintCapability.borrow()
?? panic("Could not borrow minting capability")
assert mintCapability.check(), message: "Caller does not have minting capability"
return <- create NFT(id: id)
}
}
In this extended contract:
- Capability for Minting: The contract introduces a mintCapability capability, which is used to control who can mint new NFTs.
- Initialization: In the contract’s
init
function, the minting capability is granted to the contract owner. The contract owner is typically the account that deploys the contract.
With these changes, the contract enforces capability-based access control for minting, providing enhanced security and control over NFT operations.
Let’s breakdown the contract components step by step:
Contract State
pub var totalSupply: UInt64
totalSupply
represents the total number of NFTs minted by this contract.
NFT Resource
pub resource NFT {
pub let id: UInt64
init(id: UInt64) {
self.id = id
MyNFT.totalSupply = MyNFT.totalSupply + 1
}
}
- Defines a resource
NFT
representing each unique token, with a uniqueid
. - The
init
function initializes the resource and increments thetotalSupply
of the contract by 1 each time a newNFT
is created.
Collection Resource
pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver {
pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
// … (functions)
}
- Defines a resource
Collection
representing a collection of NFTs owned by an account. - Implements
NonFungibleToken.Provider
andNonFungibleToken.Receiver
interfaces to allow depositing and withdrawing of NFTs. ownedNFTs
is a dictionary that holds the NFTs owned by an account, mapped by their unique IDs
Collection Resource Functions
pub fun deposit(token: @NonFungibleToken.NFT)
pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT
pub fun transferNFT(to: Address, id: UInt64)
deposit
allows a user to deposit an NFT to their collection.withdraw
allows a user to withdraw an NFT from their collection.transferNFT
allows a user to transfer an NFT from their collection to another user’s collection.
Minting Capability
access(self) var mintCapability: Capability<&{NonFungibleToken.Provider}>
- Represents a capability to control who can mint new NFTs.
Contract Initialization
init() {
self.totalSupply = 0
self.mintCapability = /public/mintNFT
}
- Initializes the contract state, setting
totalSupply
to 0 and creating a publicmintCapability
.
NFT Creation Function
pub fun createNFT(id: UInt64): @NFT {
// … (implementation)
}
- Allows the creation of a new NFT if the caller has the proper minting capability.
Highlighting Differences Between Cadence and Solidity Paradigms
The Cadence contract for an ERC721 NFT on the Flow blockchain diverges from the Solidity paradigm in several notable ways:
Resource-Oriented Design: Cadence introduces a resource-oriented design where assets like NFTs are treated as first-class “Resources.” These Resources in Cadence are unique, linear types, incapable of being copied or casually discarded, always stored within, and only transferred between, accounts. In contrast, Solidity employs a more data-centric approach, tracking ownership through data structures and mappings in a centralized, singular contract.
Ownership Model: Within the Cadence contract, ownership of NFTs is tied to the account in which they reside. Each account directly maintains its collection of NFTs. This simplifies the process of tracking ownership, eliminating the need for complex mappings as seen in Solidity.
Transfer Model: In Solidity, transferring NFTs usually involves invoking functions within a contract to update balances and ownership. Cadence simplifies this process significantly. Users can transfer NFTs between accounts by merely moving them from one account’s collection to another, as demonstrated in the straightforward transferNFT
function. This streamlined approach reduces the necessity for intricate authorization checks.
Capability-Based Access Control: Cadence introduces Capability-based access control. In the Cadence contract, access to functions and objects is governed by Capabilities. Capabilities are programmatic entities that grant bearers specific scoped access rights. This approach heightens security, ensuring that only authorized entities can interact with designated resources. In contrast, Solidity primarily relies on sender addresses for access control.
Composition Over Inheritance: While Solidity often resorts to inheritance for implementing token standards like ERC721, Cadence favors composition. In the Cadence contract, ERC721 functionality is composed by including the FungibleToken.Receiver interface. This composition-based approach allows for greater flexibility and simplification of the contract’s structure.
Simplified State Management: In the Cadence contract, NFTs are stored directly in user accounts, simplifying state management. In Solidity, state management can become more complex, typically involving the maintenance of balances, approvals, and mappings within a centralized contract.
Resource Creation: Cadence permits the creation of new resources using the create keyword. This feature simplifies the minting process, as evident in the mintNFT
function, where new NFTs are generated and added to the user’s collection.
Script vs. Transaction: Cadence distinguishes between scripts and transactions. Scripts are read-only and conduct queries against chain state, while transactions are ACID (Atomic, Consistent, Isolated, Durable) operations capable of mutating chain state. In contrast, Solidity contracts are primarily transaction-based.
Take a look at this Flow blog post for an in-depth exploration of the contrasting architectural aspects of Solidity and Cadence, including an example of how the Bored Ape Yacht Club NFT collection could be replicated on the Flow blockchain.
Deploying the Smart Contract to Flow
We can now deploy this ERC721 smart contract on the Flow blockchain using the Flow CLI. Here are the steps to deploy the contract:
- Install the Flow CLI by following the instructions on the official Flow documentation.
- Create a new directory for your project and navigate to it in your terminal.
- Initialize a new Flow project by running
flow init
in your terminal. This will create a newflow.json
file in your project directory. - Create a new Cadence file in your project directory called
MyNFT.cdc
and paste in the ERC721 smart contract implementation that we just created. - Compile your Cadence contract. You can use the
flow cadence check
command to ensure that your contract is free of syntax errors by running:flow cadence check MyNFT.cdc
. - Deploy the contract by running the following command in your terminal:
flow project deploy — network=emulator
. This will deploy the contract on the Flow emulator network.
Once the contract is deployed, you can interact with it using the Flow CLI. Here are some examples of how to use the contract:
- Mint a new token:
To mint a new NFT, you can use the Flow CLI to send a transaction. Create a script that defines a transaction for minting a new token. You can create a file named mint_token.cdc
with the following sample transaction. Ensure that you replace 0xCONTRACTADDRESS
with the actual contract address you obtained after deployment.
import MyNFT from 0xCONTRACTADDRESS
transaction(newID: UInt64) {
let collectionRef: &MyNFT.Collection
prepare(signer: AuthAccount) {
// Check if the account already has a collection, if not create a new one
if !signer.getCapability<&MyNFT.Collection>(/public/myNFTCollection).check() {
signer.save(<-MyNFT.Collection(), to: /storage/myNFTCollection)
signer.link<&MyNFT.Collection>(/public/myNFTCollection, target: /storage/myNFTCollection)
}
self.collectionRef = signer.borrow<&MyNFT.Collection>(from: /public/myNFTCollection)
?? panic("Could not borrow reference to the NFT Collection")
}
execute {
let newNFT <- MyNFT.createNFT(id: newID)
self.collectionRef.deposit(token: <-newNFT)
}
}
- This transaction script imports the
MyNFT
contract from its address. - In the prepare block, it checks if the signer has a collection and if not, it creates and links a new one.
- In the execute block, it calls the
createNFT
function from theMyNFT
contract to mint a new NFT and deposits it into the signer’s collection.
Run the following command to mint a new token using the Flow CLI:
flow transactions send ./mint_token.cdc
Make sure you’re in the project directory containing your minting script when you run this command. This command will send a transaction to the contract to mint a new token.
You’ll be prompted to sign the transaction with the account that owns the contract. Follow the prompts to sign the transaction.
After signing, the Flow CLI will submit the transaction to the Flow blockchain. You’ll need to wait for the transaction to be confirmed. The CLI will provide a transaction ID that you can use to check the status of the transaction.
2. Verify minting was successful by querying the NFT collection:
After minting the token, you can check the NFT collection to confirm that the minting was successful. Create a script (e.g., get_collection.cdc
) to query the collection. Here’s an example script:
import MyNFT from 0xCONTRACTADDRESS
pub fun main(accountAddress: Address, id: UInt64): Bool {
// Try to get a reference to the collection in the specified account
let collectionRef = getAccount(accountAddress)
.getCapability<&MyNFT.Collection{NonFungibleToken.CollectionPublic}>(/public/myNFTCollection)
.borrow()
?? panic("Could not borrow reference to the NFT Collection")
// Check if the NFT with the specified ID exists in the collection
return collectionRef.borrowNFT(id: id) != nil
}
Replace 0xCONTRACTADDRESS
with your contract’s address. This script imports the MyNFT
contract from its address, and tries to borrow a reference to the Collection
resource of the specified account. It then checks if an NFT with the specified ID exists in that collection and returns true if it does, and false otherwise.
Use the Flow CLI to execute the script and query the collection:
flow scripts execute ./get_collection.cdc \ - arg Address:0xYOURADDRESS
This command will execute a script to query the owner of a token.
3. Transfer ownership of a token:
To transfer an NFT, you can create a transaction script similar to the mint_token.cdc
script, but this time, it should call the transferNFT
function. Make sure you have the necessary capability to perform this action.
import MyNFT from 0xCONTRACTADDRESS // Replace with the actual address of your deployed contract
transaction(nftID: UInt64, recipientAddress: Address) {
let senderCollection: &MyNFT.Collection
prepare(signer: AuthAccount) {
// Borrow a reference to the sender's NFT collection
self.senderCollection = signer.borrow<&MyNFT.Collection>(from: /storage/myNFTCollection)
?? panic("Could not borrow reference to the sender's NFT Collection")
}
execute {
// Use the transferNFT function to transfer the NFT to the recipient
self.senderCollection.transferNFT(to: recipientAddress, id: nftID)
}
}
flow transactions send ./transfer.cdc
This command will send a transaction to the contract to transfer ownership of a token.
Conclusion
In conclusion, ERC721 tokens are a powerful tool for creating unique, indivisible digital assets on the blockchain. By using smart contracts, we can create ERC721 tokens that have specific properties and behaviors, such as being non-fungible, transferrable, and ownable. The Flow blockchain and the Cadence programming language provide an ideal environment for creating and deploying ERC721 contracts, with a high degree of security and performance.
I hope this article has been helpful in understanding how to create and deploy ERC721 smart contracts on the Flow blockchain using the Cadence programming language. By following the examples and guidelines presented here, you can create your own unique digital assets and interact with them on the blockchain.
It’s important to note that the implementation of ERC721 tokens can vary depending on the specific use case and requirements of the project. For example, some projects may require additional functionality such as the ability to burn tokens or to add metadata to each token. These features can be added to the contract by modifying the implementation or by creating additional functions.
Additionally, it’s important to thoroughly test your smart contracts before deploying them to the blockchain. This can be done by creating automated tests that cover all possible scenarios and edge cases. The Flow blockchain provides a robust testing framework that can be used to test your contracts before deploying them to the mainnet.
In summary, ERC721 tokens are a powerful tool for creating unique, indivisible digital assets on the blockchain. The Flow blockchain and the Cadence programming language provide an ideal environment for creating and deploying ERC721 contracts, with a high degree of security and performance. By following the guidelines and examples presented in this article, you can create your own ERC721 tokens and interact with them on the blockchain.
Tags:
Latest Articles
Stay up-to-date with the latest industry trends and insights by reading articles from our technical experts, providing expertise on cutting-edge technologies in the crypto and fintech space.