Purple Dash

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
27/09/2023 7:35 AM

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 the NonFungibleToken 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 named MyNFT that implements the NonFungibleToken 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) named NFT that implements the NonFungibleToken.INFT interface. This resource represents an individual NFT.
  • pub let id: UInt64: This line declares a public constant named id of type UInt64. This id is used to uniquely identify each NFT.
  • pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver: This line declares a public resource named Collection that implements both the NonFungibleToken.Provider and NonFungibleToken.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 specified id.

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 unique id.
  • The init function initializes the resource and increments the totalSupply of the contract by 1 each time a new NFT 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 and NonFungibleToken.Receiverinterfaces 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 public mintCapability.

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:

  1. Install the Flow CLI by following the instructions on the official Flow documentation.
  2. Create a new directory for your project and navigate to it in your terminal.
  3. Initialize a new Flow project by running flow init in your terminal. This will create a new flow.json file in your project directory.
  4. Create a new Cadence file in your project directory called MyNFT.cdc and paste in the ERC721 smart contract implementation that we just created.
  5. 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 .
  6. 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:

  1. 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 the MyNFTcontract 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:
Flow
Blockchain Networks
Blockchain Technology
DApp development
Decentralized applications
Ethereum
Ethereum network
NFTs
Smart Contract Development
Smart Contracts
Use cases for developers
Web3

Purple Dash

We are a team of seasoned developers, blockchain architects, and financial experts, passionate about building cutting-edge solutions that revolutionize how businesses operate in the digital economy.


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.

View All