Tagged

Writing and deploying your first NFT on Starknet

Learn how to build your first Cairo smart contract!

Darlington Nnam
Feb 7, 2023

We’re on a mission to bring developers into the Starknet ecosystem. To achieve this, we’re working on a content series that will educate and guide you through the basics of building on the network. 

Previously, we explained what an ERC20 token is and how to create and deploy an ERC20 token on Starknet. This week, we’ll focus on ERC721 tokens, also known as NFTs. 

By the end of this article, you’ll understand what an ERC721 token is and how to create one using OpenZeppelin’s library.

WTF are NFTs?

NFTs (Non-fungible tokens) are unique digital assets that live on a blockchain, like Starknet. 

Popularly known as NFTs, these tokens differ from their counterparts (Fungible tokens) because they cannot be exchanged or traded at equivalence for other tokens.

Unlike an ERC20 token which is fungible, meaning they are replaceable by the same token, similar to how money works. NFTs are non-fungible, meaning they cannot be replaced, as each has a unique identifier and metadata that distinguishes them from other tokens.

The ERC721 Standard

The ERC721 Standard was introduced by William Entriken, Dieter Shirley, Jacob Evans, and Nastassia Sachs and was inspired by the ERC20 Standard, containing ERC20-like methods such as:

  • name: This function defines the token’s name.
  • symbol: This function defines the token’s symbol.
  • balanceOf: This function returns number of NFTs owned by an address.

The ERC721 standard also introduces some new methods:

  • ownerOf: This function returns the address of the owner of a token.
  • supportsInterface: This function queries if a contract implements an interface.
  • transferFrom: Transfers token ownership from one account to another.
  • safeTransferFrom: Carries out safe transfer of token ownership from one account to another. A safe transfer means that it checks whether the receiver is valid. It can also accept additional data sent to the receiver.
  • approve: This function grants or approves another entity the permission to transfer tokens on the owner’s behalf.
  • setApprovalForAll: This function enables or disables approval for an operator to manage all owner’s asset.
  • getApproved: This function gets the approved address for a particular token ID.
  • isApprovedForAll: This function queries if an address is an authorized operator for another address.

Setting up Protostar Environment

To start building your first NFT on Starknet, you need to set up your developer framework. Our preferred option is Protostar. 

If you’re unfamiliar with a developer environment or want to learn more about Protostar, we recommend our article on creating a Cairo Development Environment

When you’ve set Protostar up, you’re ready to build your first Starknet NFT. Let’s call our project “argentERC721”. 

To do this, run:

protostar init

There will be a further request for the project’s name and the library’s directory name. This is needed to initialize your project successfully.

Creating a new file

We need to create a new file called ERC721.cairo in our src folder. This is where we’re going to be writing our contract code.

building an ERC721 token

Imports

Inside our new file, we’ll begin with the %lang starknet directive, which specifies that our file contains code for a Starknet contract.

After we’ve done that, we import all the necessary library functions. 

%lang starknet

from starkware.cairo.common.cairo_builtins import HashBuiltin

from starkware.cairo.common.uint256 import Uint256

from starkware.starknet.common.syscalls import get_caller_address

from cairo_contracts.src.openzeppelin.token.erc721.library import ERC721

from cairo_contracts.src.openzeppelin.introspection.erc165.library import ERC165

from cairo_contracts.src.openzeppelin.access.ownable.library import Ownable

From this code snippet, you can see that we import and will use the ERC721 and ERC165 namespaces from Openzeppelin’s ERC721 library. We also import the HashBuiltin, which is needed for pedersen hashing related operations, the Uint256 struct for creating Uint256 variables, and the get_caller_address for getting the address of the caller.

The ERC721 Contract Interface

Here’s the ERC721 contract interface, which exposes the methods we will need to implement while building our token:

%lang starknet

from starkware.cairo.common.uint256 import Uint256

@contract_interface

namespace IERC721 {

   func name() -> (name: felt) {

   }

   func symbol() -> (symbol: felt) {

   }

   func balanceOf(owner: felt) -> (balance: Uint256) {

   }

   func ownerOf(tokenId: Uint256) -> (owner: felt) {

   }

   func safeTransferFrom(from_: felt, to: felt, tokenId: Uint256, data_len: felt, data: felt*) {

   }

   func transferFrom(from_: felt, to: felt, tokenId: Uint256) {

   }

   func approve(approved: felt, tokenId: Uint256) {

   }

   func setApprovalForAll(operator: felt, approved: felt) {

   }

   func getApproved(tokenId: Uint256) -> (approved: felt) {

   }

   func isApprovedForAll(owner: felt, operator: felt) -> (isApproved: felt) {

   }

   func mint(to: felt) {

   }

} 

Writing the contract

Constructor

For our ERC721 token, we need to initialize certain variables on deployment, such as the name, symbol, and owner. To do this, our contract must implement a constructor:

@constructor

func constructor{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr} (

   name: felt, symbol: felt, owner: felt

) {

   ERC721.initializer(name, symbol);

   Ownable.initializer(owner);

   return ();

}

We also call the initializer internal function by using the ERC721 namespace we imported from Openzeppelin while passing the required function arguments [name and symbol]. We finally assign an owner to the contract by calling the initializer function of the Ownable namespace.

SupportsInterface

This function queries to check if the contract implements a certain interface.

@view

func supportsInterface{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr} (

   interfaceId: felt

) -> (success: felt) {

   let (success) = ERC165.supports_interface(interfaceId);

   return (success,);

}

Name

The name function is a view function that simply returns the token’s name when queried.

@view

func name{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr} () -> (name: felt) {

   let (name) = ERC721.name();

   return (name,);

}

Symbol

The symbol function returns the token’s symbol when queried.

@view

func symbol{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr} () -> (symbol: felt) {

   let (symbol) = ERC721.symbol();

   return (symbol,);

}

BalanceOf

The balance of function returns the number of NFTs owned by an address.

@view

func balanceOf{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr} (owner: felt) -> (balance: Uint256) {

   let (balance: Uint256) = ERC721.balance_of(owner);

   return (balance,);

}

OwnerOf

This function returns the address of the owner of a token.

@view

func ownerOf{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr} (tokenId: Uint256) -> (owner: felt) {

   let (owner) = ERC721.owner_of(tokenId);

   return (owner,);

}

GetApproved

This function gets the approved address for a particular token ID.

@view

func getApproved{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr} (tokenId: Uint256) -> (approved: felt) {

   let (approved) = ERC721.get_approved(tokenId);

   return (approved,);

}

IsApprovedForAll

This function queries if an address is an authorized operator for another address.

@view

func isApprovedForAll{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr} (owner: felt, operator: felt) -> (isApproved: felt) {

   let (isApproved) = ERC721.is_approved_for_all(owner, operator);

   return (isApproved,);

}

TransferFrom

This function Transfers token ownership from one account to another.

@external

func transferFrom{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(

   _from: felt, to: felt, tokenId: Uint256

) {

   ERC721.transfer_from(_from, to, tokenId);

   return ();

}

SafeTransferFrom

Carries out safe transfer of token ownership from one account to another.

@external

func safeTransferFrom{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(

   _from: felt, to: felt, tokenId: Uint256, data_len: felt, data: felt*

) {

   ERC721.safe_transfer_from(_from, to, tokenId, data_len, data);

   return ();

}

Approve

This function grants or approves another entity the permission to transfer tokens on the owner’s behalf.

@external

func approve{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(

   account: felt, tokenId: Uint256

) {

   ERC721.approve(account, tokenId);

   return ();

}

SetApprovalForAll

This function enables or disables approval for an operator to manage all owner’s asset.

@external

func setApprovalForAll{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(

   operator: felt, approved: felt

) {

   ERC721.set_approval_for_all(operator, approved);

   return ();

}

Congratulations, you just completed your first ERC721 contract! You can find the full contract code here.

Minting NFTs

While we’ve completed our NFT contract, we would need an extra function for minting new tokens with different token IDs to a user. To do this, we will implement a storage variable token_counter and an external function mint.

token_counter

The token_counter is a storage variable that keeps track of the number of tokens created in order to determine the next token ID.

@storage_var

 func token_counter() -> (id: felt) {

}

mint

The mint function is an external function that implements the minting logic. It first checks that the caller is the contract owner, then calculates the new token ID, mints the new token to the recipient, and finally updates the token_countervariable.

@external

func mint{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(

   to: felt

) {

   Ownable.assert_only_owner();

   let (prevTokenId) = token_counter.read();

   let tokenId = prevTokenId + 1;

   ERC721._mint(to, Uint256(tokenId, 0));

   token_counter.write(tokenId);

   return ();

}

Deployment

Finally, following the usual steps to deploy our contracts, we will first build/compile, declare, and deploy.

Building

When you build your Starknet NFT, you need to specify the correct path to your contract in your protostar.toml and enable Protostar to locate your lib folder, which contains your Openzeppelin contracts. To do this,  add the snippet:

cairo-path = ["lib"]

Your protostar.toml file should look like this:

[project]

protostar-version = "0.9.1"

lib-path = "lib"

cairo-path = ["lib"]

[contracts]

ERC721 = ["src/ERC721.cairo"]

To build your contract, simply run the following:

protostar build
Building your ERC721 contract

Declaring

Before executing the “declare” command, you need to set the private key associated with the specified account address in a file or the terminal. 

To set your private key in the terminal, run the command:

export PROTOSTAR_ACCOUNT_PRIVATE_KEY=[YOUR PRIVATE KEY HERE]

Do not share your private key with anyone. Not even Argent. It should be for your eyes only.

To declare your contract, simply run the command:

protostar declare ./build/ERC721.json --network testnet --account 0x0691622bBFD29e835bA4004e7425A4e9630840EbD11c5269DE51C16774585b16 --max-fee auto
Declaring your ERC721 Starknet contract

Deploying

Finally, we need to call the “deploy” command passing in our contract class hash to deploy our contract.

protostar deploy 0x04dae654c7b6707667a178729b512d61494fe590ab4accc46923d6409b97e617 --network testnet --account 0x0691622bBFD29e835bA4004e7425A4e9630840EbD11c5269DE51C16774585b16 --max-fee auto --inputs 71959616777844 4280903 0x0691622bBFD29e835bA4004e7425A4e9630840EbD11c5269DE51C16774585b16
Deploying your ERC721 Starknet contract

Conclusion

Congratulations! You just wrote and deployed your first ERC721 contract on StarkNet.

To interact with the deployed contract, check Starkscan here.

If you have any questions as regards this, reach out to me @0xdarlington, I’d love to help you build on StarkNet with Argent X.

For more developer resources, follow us across our socials:

Twitter — @argentHq

Engineering Twitter — @argentDeveloper

LinkedIn — @argentHq

Youtube — @argentHQ

Interested in the topic? Join us!

We’re always looking for outstanding engineers to help us pioneer better UX and security in crypto. We work remotely across Europe.

Argent Careers

Related Blogs

Getting started with StarkNet

The StarkNet ecosystem is exploding and here's how you can get involved

Understanding The Universal Deployer Contract And Deploying your Contracts through Argent X

Deploying contracts on Starknet using the Universal deployer contract

Part I: WTF is Account Abstraction

Learn why it’s a game changer for crypto's adoption

Own It

We use 🍪 cookies to personalise your experience on Argent. Privacy Policy

Accept

HQ London, made with ❤️ across Europe