Non-Fungible Tokens in Cadence 1.0
In 2024, the network will be upgrading to Cadence 1.0. In addition to many changes to the Cadence programming language, the Cadence token standards are also being streamlined and improved. All applications will need to prepare and migrate their existing Cadence smart contracts, scripts, and transactions for the update. If you do not update your code, your applications will become non-functional after the network upgrade.
This document describes the changes to the Cadence Non-Fungible Token (NFT) standard and gives a step-by-step guide for how to upgrade your NFT contract from Cadence 0.42 to Cadence 1.0.
We'll be using the ExampleNFT
contract
as an example. Many projects have used ExampleNFT
as a starting point for their projects,
so it is widely applicable to most NFT developers on Flow.
The upgrades required for ExampleNFT
will cover 90%+ of what you'll
need to do to update your contract. Each project most likely has
additional logic or features that aren't included in ExampleNFT
,
but hopefully after reading this guide, you'll understand Cadence 1.0
well enough that you can easily make any other changes that are necessary.
Additionally, most of the changes described here also apply to anyone who is updating a Fungible Token contract or interacting with one, so keep that in mind while reading if that applies to you.
As always, there are plenty of people on the Flow team and in the community who are happy to help answer any questions you may have, so please reach out in Discord if you need any help.
Important Info
Please read the FLIP
that describes the changes to the NonFungibleToken
standard first.
The updated code for the V2 Non-Fungible Token standard is located in the
standard-v2
branch of the flow-nft repo.
Please look at the changes there to understand how the standard and examples have changed.
This branch includes the updated versions of NonFungibleToken
, MetadataViews
, ViewResolver
,
and NFTForwarding
.
Please see the latest post in this forum thread to find the latest version of the CLI and emulator that you should be testing with.
It is also important to remember that after you've made your changes to your contracts, you will have to stage the upgrades on testnet and mainnet in order for them to be upgraded and migrated properly. You can find informaion about how to do that here: https://github.com/onflow/contract-updater
Additionally, here are the import addresses for all of the important contracts related to non-fungible tokens. The second column is the import address if you are testing with a basic version of the emulator. The third column contains the import addresses if you are using the Cadence testing framework.
Contract | Emulator Import Address | Testing Framework |
---|---|---|
NonFungibleToken | 0xf8d6e0586b0a20c7 | 0x0000000000000001 |
FungibleToken | 0xee82856bf20e2aa6 | 0x0000000000000002 |
ViewResolver | 0xf8d6e0586b0a20c7 | 0x0000000000000001 |
Burner | 0xf8d6e0586b0a20c7 | 0x0000000000000001 |
MetadataViews | 0xf8d6e0586b0a20c7 | 0x0000000000000001 |
See the other guides in this section of the docs for the import addresses of other important contracts in the emulator.
As for contracts that are important for NFT developers but aren't "core contracts", here is information about where to find the Cadence 1.0 Versions of Each:
NFT Catalog: See the feature/cadence-1.0
branch of the NFT Catalog Repo
for the updated versions of NFT Catalog contracts.
NFT Storefront: See the cadence-1.0
branch in the NFT Storefront Repo
for the updated versions of the NFTStorefront
and NFTStorefrontV2
contracts.
USDC: See this PR in the USDC repo for updated USDC contracts.
Account Linking and Hybrid Custody: See this PR in the hybrid custody repo for updated hybrid custody contracts.
This discord announcement also contains versions of a lot of important contracts.
For any others, search for their github repo and there will likely be a PR or feature branch with the Cadence 1.0 changes. If there isn't, please create an issue in the repo or reach out to that team directly via their support or Discord channel to ask them about their plans to update their contracts.
A note for newcomers​
This guide is primarily for developers who have existing contracts
deployed to Flow mainnet that they need to update for Cadence 1.0.
If you don't have any contracts deployed yet, it is recommended that
you start an NFT contract from scratch by either copying the ExampleNFT
contract or the BasicNFT
contract
from the standard-v2
branch of the flow-nft
github repo and wait to deploy it until Flow has been upgraded for Cadence 1.0.
BasicNFT and UniversalCollection​
As part of the improvements to the NFT standard, there is now a new NFT contract
example in the flow-nft
github repo: BasicNFT
.
BasicNFT
defines a Cadence NFT in as few lines of code as possible, 137 at the moment!
This is possible because the contract basically only defines the NFT resource,
the essential metadata views, and a minter resource.
It doesn't have to define a collection! Most collection resources are 99% boilerplate
code, so it really doesn't make sense for most projects to have to define their own
collection.
Instead, BasicNFT
uses UniversalCollection
,
a contract that defines a collection resource
that has all of the standard functionality that a collection needs and nothing else.
From now on, any project that doesn't want to do anything unique with their collection
can just import UniversalCollection
and call it from their createEmptyCollection
function:
_10access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {_10 return <- UniversalCollection.createEmptyCollection(identifier: "flowBasicNFTCollection", type: Type<@BasicNFT.NFT>())_10}
All they have to provide is a type and an identifier for the collection.
UniversalCollection.Collection
will enforce that only NFTs of the given type
can be accepted by the collection:
_10access(all) fun deposit(token: @{NonFungibleToken.NFT}) {_10 if self.supportedType != token.getType() {_10 panic("Cannot deposit an NFT of the given type")_10 }
It also constructs standard paths based on the identifier provided.
UniversalCollection
will be deployed to all the networks soon after the Cadence 1.0 upgrade,
so developers will be able to import from it after that point.
We'll be putting out more information and guides for BasicNFT
and UniversalCollection
in the near future, but keep it in mind if you are thinking about deploying
any new NFT contracts in the future!
Migration Guide
This guide will cover changes that are required because of upgrades to the Cadence Language as well as the token standard. The improvements will be described here as they apply to specific changes that projects need to make in order to be ready for the upgrade, but it is good to read all sources to fully understand the changes.
Please read the motivation section of the NFT-V2 FLIP to learn about why most of the changes to the standard were needed or desired.
First, we will cover the changes that come from the new token standards and then we will cover the changes that come from Cadence.
Previous Non-Permitted Changes​
Until now, there were many restrictions on what changes are allowed in upgrades to Cadence smart contracts, like not being allowed to change the type of fields, not being able to change interface conformance, and more. Many of the Cadence 1.0 changes require updates that break some of these rules, but the upgrade checker will be relaxed in order to allow these changes to be possible. All of these changes have already been tested with the Cadence 1.0 upgrades for all of the token standards and all of the protocol smart contracts which cover a huge amount of changes that contracts can go through, so the Flow team is confident that the restrictions have been relaxed enough to allow the upgrades.
Automatic State Migrations​
Some of these changes require that types for stored values are updated to more or less restrictive types. The work for updating the code that refers to these types is up to the developer, but the work for migrating the stored state to reflect these two types will be handled automatically by the Flow teams custom state migrations. These migrations only expect a restricted subset of type changes, but it is important to make sure that you don't introduce any new business logic as part of your Cadence 1.0 upgrades because the migrations will only understand how to migrate state for existing code. More information will be given about this in the sections of this doc that will be affected by the custom migrations, such as with entitlements and the removal of nested type requirements and restricted types.
Token Standard Changes​
Continue to implement NonFungibleToken​
Make sure your contract still implements the NonFungibleToken
interface:
_10access(all) contract YourContract: NonFungibleToken {
This won't be a change for most contracts because this is how tokens are implemented
currently, but there was a period of time when the new standards defined NonFungibleToken
as a contract instead of an interface, so anyone who was testing with those early versions
should make sure that their contract still implements NonFungibleToken
.
This will ensure that the correct metadata view methods are enforced from ViewResolver
and that your contract has the correct createEmptyCollection(nftType: Type)
method defined.
It is important to understand that the new NonFungibleToken
no longer specifies types
that you need to define like NFT
and Collection
. These were changed to interfaces
because now the token standards support defining multiple token types in a single contract.
Most projects won't utilize this, but it is important to understand in relation
to some of the other changes that are needed, like the next one for example.
Add type argument to contract.createEmptyCollection()​
Because contracts can now define multiple token types, all contract.createEmptyCollection()
functions now have a nftType
argument:
_10/// createEmptyCollection creates an empty Collection for the specified NFT type_10/// and returns it to the caller so that they can own NFTs_10access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {_10 return <- create Collection()_10}
As is shown here, if you only have a single collection type defined in your contract, you can just return that collection type regardless of what the type argument is, but you could also make sure that the caller provides the correct type before returning the collection.
Your NFT implements NonFungibleToken.NFT​
NonFungibleToken.NFT
used to be a nested type specification, but now it is an interface!
This means that in your contract, your NFT
resource needs to implement it
in order to be considered compatible with the standard!
_10access(all) contract ExampleNFT: NonFungibleToken {_10_10 /// We choose the name NFT here, but this type can have any name now_10 /// because the interface does not require it to have a specific name any more_10 access(all) resource NFT: NonFungibleToken.NFT {
This will ensure that your NFT
resource has all the correct fields and functions.
As part of this upgrade, you should remove the NonFungibleToken.INFT
implementation specification
from the declaration of your NFT
because the INFT
interface has been removed.
In your code, any instance that refers
to @NonFungibleToken.NFT
or &NonFungibleToken.NFT
need to be updated to
@{NonFungibleToken.NFT}
or &{NonFungibleToken.NFT}
respectively.
Example in deposit()
:
_10/// deposit now accepts a resource that implements the `NonFungibleToken.NFT` interface type_10access(all) fun deposit(token: @{NonFungibleToken.NFT})
Note for Custom Migrations: All stored objects that currently use the concrete type
NonFungibleToken.NFT
will be automatically migrated to use the interface type {NonFungibleToken.NFT}
as part of the Flow team's custom state migrations. Your code still needs to be updated to reflect this though.
Your Collection implements NonFungibleToken.Collection​
Similar to NFT
, NonFungibleToken.Collection
is now an interface,
so your Collection
resource type needs to implement it in order to be conformant.
_10/// In the `ExampleToken` smart contract_10access(all) resource Collection: NonFungibleToken.Collection {
In addition, since Collection
is an interface, you will need to update every instance in your code
that refers to @NonFungibleToken.Collection
or &NonFungibleToken.Collection
to
@{NonFungibleToken.Collection}
or &{NonFungibleToken.Collection}
respectively to show
that it is now an interface specification instead of a concrete type specification.
Note for Custom Migrations: All stored objects that currently use the concrete type
NonFungibleToken.Collection
will be automatically migrated to use the interface type {NonFungibleToken.Collection}
as part of the Flow team's custom state migrations. Your code still needs to be updated to reflect this though.
Remove Project-Specific Events​
Standard events are being added to the token standards! These are events that are defined in the contract interface and are emitted during pre and post-conditions every time an important event happens like a Deposit or a Withdrawal. The events include all the important information and metadata about the action, are emitted automatically from the interface, and are unable to be spoofed!
This means that you can get rid of the Deposit and Withdraw events in your contracts completely! You don't have to obviously, but the standard events will be much more reliable and will be emitted anyway, so your custom events are redundant unless they contain some information that is not included in the standard events.
The definitions
and emissions
for these standard events is in the new NonFungibleToken
standard.
_15/// Event that is emitted when a token is withdrawn,_15/// indicating the type, id, uuid, the owner of the collection that it was withdrawn from,_15/// and the UUID of the resource it was withdrawn from, usually a collection._15///_15/// If the collection is not in an account's storage, `from` will be `nil`._15///_15access(all) event Withdrawn(type: String, id: UInt64, uuid: UInt64, from: Address?, providerUUID: UInt64)_15_15/// Event that emitted when a token is deposited to a collection._15/// Indicates the type, id, uuid, the owner of the collection that it was deposited to,_15/// and the UUID of the collection it was deposited to_15///_15/// If the collection is not in an account's storage, `from`, will be `nil`._15///_15access(all) event Deposited(type: String, id: UInt64, uuid: UInt64, to: Address?, collectionUUID: UInt64)
As you can see in the ExampleNFT
diff,
the events have been removed completely.
For event listeners, the events will have this format:
_10A.f8d6e0586b0a20c7.NonFungibleToken.Deposited(...)
Where the address is whatever address the NonFungibleToken
contract interface is deployed to.
Implement ViewResolver​
The new standard enforces that implementations also implement
the ViewResolver
functions,
which are standard functions for returning metadata about a given
token or smart contract.
If you were using these contract-level functions before, you will need to update them to have the correct names and arguments. They were changed because now that contracts can define multiple token types, the metadata getter functions need to be able to return information about any of the token types. Therefore, here are the new definitions. (The comments explain the design and some suggestions for how to implement them, so it is recommend that you read them.)
_25 /// Function that returns all the Metadata Views implemented by the resolving contract._25 /// Some contracts may have multiple resource types that support metadata views_25 /// so there there is an optional parameter for specify which resource type the caller_25 /// is looking for views for._25 /// Some contract-level views may be type-agnostic. In that case, the contract_25 /// should return the same views regardless of what type is passed in._25 ///_25 /// @param resourceType: An optional resource type to return views for_25 /// @return An array of Types defining the implemented views. This value will be used by_25 /// developers to know which parameter to pass to the resolveView() method._25 ///_25 access(all) view fun getContractViews(resourceType: Type?): [Type]_25_25 /// Function that resolves a metadata view for this token._25 /// Some contracts may have multiple resource types that support metadata views_25 /// so there there is an optional parameter for specify which resource type the caller_25 /// is looking for views for._25 /// Some contract-level views may be type-agnostic. In that case, the contract_25 /// should return the same views regardless of what type is passed in._25 ///_25 /// @param resourceType: An optional resource type to return views for_25 /// @param view: The Type of the desired view._25 /// @return A structure representing the requested view._25 ///_25 access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct?
You can see how ExampleNFT
implements them here.
Keep NFT ID Usage Consistent​
In the new standard examples, we often use UUID for NFT IDs. Many early Flow projects used a project-specific ID system for their NFTs. It is important that you stick with whatever ID system your project used from the beginning so NFT IDs don't get mixed up.
Add createEmptyCollection() to NFT and Collection.​
These function requirements were added to NFT
and Collection
so that holders of any of those objects could create a new collection of the correct type,
no matter if they imported the contract or knew the type ahead of time.
Add getSupportedNFTTypes() and isSupportedNFTType()​
All resources that implement NonFungibleToken.Receiver
now have to include these
two functions that indicate which types they are able to receive in their deposit()
calls.
Since Collection
implements Receiver
, your Collection
will need implementations
for both of these functions.
As is done in the ExampleNFT.Collection
,
if your Collection
can only accept a single NFT type, then the implementation is simple.
Add getLength()​
Add a getLength()
function
to your Collection
resource so that callers can quickly
get an idea of the size of your collection.
_10/// Gets the amount of NFTs stored in the collection_10 access(all) view fun getLength(): Int {_10 return self.ownedNFTs.length_10 }
Update borrowNFT() to Return an Optional​
The borrowNFT()
method is used to get a reference to any NFT in the collection.
It is a common best practice in Cadence smart contracts for getter functions
(functions that only return a piece of informaion instead of modifying state)
to never panic or revert if the request is invalid. Getter functions should just
return nil
if the request is invalid.
Therefore. The borrowNFT
method should be modified
to return an optional reference and return nil
if the NFT ID doesn't exist in the collection.
_10/// In the Collection resource_10access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {_10 return (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)_10}
Additionally, any projects that have borrowNFTSafe
or a project-specific borrow
like borrowMoment()
in NBA Top Shot can safely remove those and also remove
any usage of them from transactions and scripts.
Remove Private Path and Type fields​
Since private paths were removed in Cadence 1.0, these fields are no longer needed,
so remove the code that returns them in your resolveView
method for NFTCollectionData
:
_13case Type<MetadataViews.NFTCollectionData>():_13 let collectionData = MetadataViews.NFTCollectionData(_13 storagePath: /storage/cadenceExampleNFTCollection,_13 publicPath: /public/cadenceExampleNFTCollection,_13 // REMOVED: providerPath_13 publicCollection: Type<&ExampleNFT.Collection>(),_13 publicLinkedType: Type<&ExampleNFT.Collection>(),_13 // REMOVED: providerLinkedType_13 createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {_13 return <-ExampleNFT.createEmptyCollection(nftType: Type<@ExampleNFT.NFT>())_13 })_13 )_13 return collectionData
Private paths are no longer able to be used in Cadence across the board, so you'll need to find other ways to do what you were doing with them before. This will likely involve Capability Controllers.
Use the NonFungibleToken.emitNFTUpdated() function​
This is an optional change and only applies to projects that have functionality
that updates the metadata of NFTs periodically. It allows those projects to emit
the standard Updated
event
so that event listeners can know when NFTs have been updated
so they can query collections to get the updated metadata to show in their user interfaces.
_10 access(all) event Updated(type: String, id: UInt64, uuid: UInt64, owner: Address?)_10 access(all) view fun emitNFTUpdated(_ nftRef: auth(Update) &{NonFungibleToken.NFT})_10 {_10 emit Updated(type: nftRef.getType().identifier, id: nftRef.id, uuid: nftRef.uuid, owner: nftRef.owner?.address)_10 }
As you can see, it requires an authorized reference to an NFT, so only the owner of and NFT can call this to emit an event. Additionally, as is noted in the example below, you have to use your own contract's name to call the function because the code that emits the event is a default implementation that can only be accessed from an implementation.
DO NOT Re-implement the emitNFTUpdated
function
in your contract or you will lose access to the ability to emit the standard event.
This function could be called from within a Collection
resource when a piece of metadata on an owned NFT is updated. For example,
if a developer wanted to track the time of the latest transfer for each NFT,
they could do it in the deposit()
function:
_36access(all) contract ExampleNFT {_36 access(all) resource Collection: NonFungibleToken.Collection {_36_36 access(all) var ownedNFTs: @{UInt64: ExampleNFT.NFT}_36_36 ..._36_36 /// deposit takes a NFT and adds it to the collections dictionary_36 /// and adds the ID to the id array_36 access(all) fun deposit(token: @{NonFungibleToken.NFT}) {_36 let token <- token as! @ExampleNFT.NFT_36_36 let id = token.id_36_36 // add the new token to the dictionary which removes the old one_36 let oldToken <- self.ownedNFTs[token.id] <- token_36 destroy oldToken_36_36 // Get an authorized reference to the NFT so that_36 // the update transfer date function can be called_36 // and the emitNFTUpdated function can be called_36 let authTokenRef = (&self.ownedNFTs[id] as auth(NonFungibleToken.Update) &{NonFungibleToken.NFT}?)!_36 authTokenRef.updateTransferDate(date: getCurrentBlock().timestamp)_36_36 // EMIT THE UPDATED EVENT_36 // Note: You have to use your own contract's name for the call_36 // because the code that emits the event is a default implementation_36 // DO NOT Re-implement the `emitNFTUpdated` function or you will lose_36 // access to the ability to emit the standard event_36 ExampleNFT.emitNFTUpdated(authTokenRef)_36 }_36_36 ..._36 }_36 ..._36}
Cadence Changes​
Update all pub access modfiers​
The pub
access modifier was removed from the language to better support unified
representation of access control, especially now that entitlements exist.
IMPORTANT SECURITY NOTICE
Please familiarize yourself with the new entitlements feature because it is extremely important for you to understand in order to build safe smart contracts.
If you change pub
to access(all)
without paying attention to potential downcasting from public interfaces, you might expose private
functions like withdraw
that will cause security problems for your contract.
Learn more about this here.
Most contracts can update and pub
access modifiers to access(all),
but there are some functions, such as withdraw
, that need to have entitled access.
These privledge functions need to have entitled access now because as part of Cadence 1.0,
any restricted reference can be downcast to the concrete reference type without
needing to be an authorized reference. This means for example, that a &{NonFungibleToken.Receiver}
reference can be cast to a &{NonFungibleToken.Collection}
reference, which would give
access to the withdraw()
method if it is access(all)
!
You can read more about how references work in Cadence 1.0 here.
A good rule to follow is that if there is a resource that will ever have a reference created for it (such as for a public or private capability), any functions that you don't want everyone in the network to be able to have access to should be restricted by an entitlement so that people cannot downcast the reference to access these privledged functions.
Add Withdraw Entitlement to withdraw()​
Now that unrestricted casting is possible in Cadence, it is necessary to use entitlements to restrict access to privledged functions in any composite type.
The only default method that needs to be restricted is the withdraw
method:
_10access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @ExampleNFT.NFT {
This means that you can only call the withdraw
method if you control the actual object
or if you have an auth(NonFungibleToken.Withdraw)
entitled reference to it.
So in a typical transfer transaction when you need to withdraw from a vault, you would get the reference like this:
_10// borrow a reference to the signer's NFT collection_10self.withdrawRef = signer.storage.borrow<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>(_10 from: collectionData.storagePath_10 ) ?? panic("Account does not store an object at the specified path")
From the flow-nft transfer_nft.cdc
transaction.
Note on Custom State Migrations: You may be wondering
how you can get these entitlements onto Provider
capabilities
that already exist in a contract or in an account. As part of the automatic
migrations, all existing capabilities will be automatically migrated to
use a type that offers the same level of access. In the case of Capabilities
that provide access to entitled functions, the relevant entitlements will be added.
Update all getter functions to view​
Cadence 1.0 introduces view functions which enforce that a function does not modify any state.
The default view functions will be enforced by the token standard,
but if your project has any other getter functions that aren't in the standard
and don't modify any state, then you should add view
to these functions.
Style Tip: The recommended style for view functions is to put the view
keyword
after the access specification instead of before, like this:
_10/// Recommended_10access(all) view fun getIDs(): [UInt64] {_10_10/// NOT Recommended_10view access(all) fun getIDs(): [UInt64] {
Remove Restricted Types​
Cadence 1.0 makes it so restricted types
(for example: @ExampleNFT.Collection{NonFungibleToken.Receiver}
instead of
@ExampleNFT.Collection
or {NonFungibleToken.Receiver}
) are no longer permitted.
See the FLIP to get more context on why these were removed.
Note on Custom State Migrations: Developers are required to update
any code that refers to a restricted type to either refer to the resource type
OR the interface type, but the migration of any stored values that use a restricted
type will be handled by the Flow team's custom migrations. Restricted types
will be migrated to be the unrestricted type. For example, a capability with the type
&ExampleNFT.Collection{NonFungibleToken.Receiver}
will be changed to have the type
&ExampleNFT.Collection
.
Use Correct Capability Syntax​
Cadence 1.0 introduces Capability Controllers a more sophisticated and easy to use way of handling capabilities. It is important to understand how these work in order to use them properly.
As part of these changes, projects need to update the syntax for how they
manage capabilities. You can see the setup_account.cdc
transaction
for the proper syntax for creating public capabilities for example.
_10// create a public capability for the collection_10signer.capabilities.unpublish(collectionData.publicPath)_10let collectionCap = signer.capabilities.storage.issue<&ExampleNFT.Collection>(collectionData.storagePath)_10signer.capabilities.publish(collectionCap, at: collectionData.publicPath)
Additionally, private paths have been removed, so any code that references private paths needs to be changed to use capability controllers instead.
Use Proper Entitlements for Accounts​
AuthAccount
objects are not referred to as Account
and there are now more restrictions on how accounts can be used.
See the Cadence 1.0 Account documentation for more information.
Most of the functionality on Account
objects is now hidden by entitlements,
so a transaction has to declare what Account
functionality the transaction will access
in the prepare
block.
For example, in the transfer_nft
transaction,
these are the entitlements that are required:
_10prepare(signer: auth(BorrowValue) &Account) {
The transaction needs to borrow a value from storage to withdraw the NFT,
so the BorrowValue
entitlement is required.
Each transaction is different, so different entitlements will be required depending on what is happening. It is important for developers to make sure that only the minimum set of entitlements that are required for the transaction are given. This allows wallets to more accurately show users what the transactions they are signing will have acccess to, which helps users have more confidence and safety about what transactions they sign.
Conclusion​
This guide covered the most important changes that are required for the Cadence 1.0 upgrades to NFT contracts. Please ask any questions about the migrations in the #developer-questions channel in discord and good luck with your upgrades!