NOTE: The designs of the new standards have evolved significantly since this was originally posted, so many of the discussion points and code samples are out-of-date. Please refer to the FLIPs and PRs for the latest versions of the v2 standards.
Streamlined Token Standards
Stable Cadence refers to a major milestone in the evolution of the Cadence programming language where there will no longer be any more breaking changes to Cadence. There are a number of changes that the Cadence community would like to make to Cadence before this milestone. Several proposals can be found in this forum post. We hope to see the Cadence programming language (and apps that use it) last for many many years, so it is important that we set everything up to be as safe and easy to use as possible in the long term.
One of the biggest changes we would like to see made is a refactoring and streamlining of the fungible and non-fungible token standards with the goal of making them cleaner and more powerful.
The current fungible and non-fungible token standards for Flow (henceforth referred to as “the token standards”) were designed in mid 2019, at a time when Cadence itself was still being designed. The current standards, though functional, leave much to be desired. (we believe can be improved upon) We have learned a lot in the three years since and want to make a fundamental improvement to them, as well as some other smaller changes.
First, we’ll discuss the shortcomings of the current standards, then we’ll address the core update proposal, and finally mention other smaller proposals at the end.
These changes would be breaking for all fungible tokens and NFTs on Flow, but we will provide a clear and easy upgrade and migration path for all projects to follow, as well as provide assistance in the migration process. To make the process easier, our plan is to provide a window for projects to upgrade their contracts and downstream dependencies before the old version of Cadence is phased out.
We’ll also propose a feature to allow developers to pre- upload revised contracts addressing the changes. These contracts will become effective in the next spork which updates the Cadence version.
Current Token Standards
Before reading about the proposal, please make sure you are familiar with the current token standards for Flow:
The current token standards use contract interfaces. They are designed in a way which requires each concrete contract to provide exactly one Vault
or NFT
type. This means that any project that needs multiple tokens must deploy multiple contracts. In the case of very simple tokens, this is a lot of complexity for very little value. Additionally, projects cannot give a custom name for their NFT because of these type requirements.
// Reduced Example.
// Contracts that import that standard must define one vault that
// implements FungibleToken.Vault and can’t add any more
pub contract interface FungibleToken {
// Can only define one Vault that is compatible with the standard
pub resource Vault: Provider, Receiver, Balance {
pub var balance: UFix64
pub fun withdraw(amount: UFix64): @Vault { /* … */ }
pub fun deposit(from: @Vault) { /* … */ }
}
}
Additionally, implementing the NFT standard is more complex than it needs to be, since each NFT implementor also needs to implement their own Collection
resource. (The Collection implementation of the Example NFT contract is 5x as long as the NFT implementation itself!)
// Reduced example of NonFungibleToken
pub contract interface NonFungibleToken {
// Projects can only define one token type that conforms to the standard
pub resource NFT {
pub let id: UInt64
}
// Projects must also implement a custom collection type,
// for the specific new NFT.
// This shouldn’t be necessary,
// a collection should be able to be heterogeneous
pub resource Collection: Provider, Receiver, CollectionPublic {
pub var ownedNFTs: @{UInt64: NFT}
pub fun withdraw(withdrawID: UInt64): @NFT
pub fun deposit(token: @NFT)
pub fun getIDs(): [UInt64]
pub fun borrowNFT(id: UInt64): &NFT
}
The motivation for using contract interfaces in the first place was in the hope that the Receiver
and Provider
types in the concrete token interfaces would be statically checkable, e.g. that FlowToken.Provider
would have FlowToken.Vault
as its return type, instead of FungibleToken.Vault
. Thanks to the vagaries of “variance” in programming language type systems this isn’t actually possible.
So, the value of using contract interfaces and especially type requirements was never realized, and now we are just stuck with the cost.
Nested Type Requirements in Cadence are a fairly advanced concept. Just like an interface may require a conforming type to provide a certain field or function, it may also require the conforming type to provide a nested type. This is uncommon in other programming languages, so newcomers to Cadence need to learn and understand this feature to understand how a simple Fungible Token can be implemented.
Fungible token contracts need to provide a way to create an empty vault. As resources may only be created/constructed inside the contract they are defined in, the fungible token standard requires fungible token contracts to provide/implement a function createEmptyVault
. This makes it hard to reason about the code of a fungible token, as code related to the creation of a vault is partially implemented in the vault resource (the initializer), and partially implemented in the contract (the createEmptyVault
function). This is also the reason why a vault type currently needs to be defined inside of a contract, and cannot be defined alongside it.
Potential Solutions
There are several potential ways the current standards could be improved. We have come up with a proposal that we believe addresses these issues. However, we are not fully committed to the specific proposed changes and would also like to strongly encourage improvements to this suggestion, as well as alternative solutions.
This proposal tries to address some of the issues described by encapsulating most of the functionality of tokens in the resources and/or resource interfaces themselves, instead of as nested type requirements in the contract. This should simplify adoption of standards, making it possible to define multiple vaults and NFTs per contract, with specific names instead of generic “NFT
.”
We also propose defining event types within the vaults themselves. This also requires an update to Cadence to allow type definitions within resources, which will be a separate FLIP.
By introducing static functions as a language feature (like in the suggested standard below), the function to create an empty vault could be added to the Vault resource itself, instead of being defined in the contract. This allows keeping code related to vault creation in one place. The addition of static functions to Cadence will also be in its own FLIP.
The proposal also includes some other changes that have been proposed in the past, such as:
- introducing a transfer function
- using default implementations for functions (specifically related to NFT metadata)
- not requiring an NFT ID field to be a specific value or type
- using a reusable NFT collection instead of requiring each project to implement a collection for a specific NFT.
New Token Standards
Below are examples of what the token standards could look like if we implemented the changes proposed above.
Proposed Fungible Token Interfaces
The current contract interface would be replaced by something similar to the following:
// A shared contract, deployed in a well-known location.
pub contract FungibleToken {
pub resource interface Provider {
pub fun withdraw(amount: UFix64): @{Vault} {
post {
result.balance == amount
}
}
pub fun transfer(amount: UFix64, recipient: &AnyResource{Receiver})
}
pub resource interface Receiver {
pub fun deposit(from: @{Vault})
}
pub resource interface Balance {
pub fun getBalance(): UFix64
}
pub resource interface Vault: Provider, Receiver {
// The proposal only requires a function to get the balance
// of a vault because some projects may want to compute a balance
// instead of storing it as a single field
pub fun getBalance(): UFix64
pub fun withdraw(amount: UFix64): @{Vault} {
pre {
self.getBalance() >= amount
}
post {
self.getBalance() == before(self.getBalance()) - amount
}
}
pub fun deposit(from: @{Vault}) {
post {
self.getBalance() == before(self.getBalance()) + before(from.getBalance())
}
}
pub fun transfer(amount: UFix64, recipient: &AnyResource{Receiver}) {}
}
}
New Fungible Token Example Implementation
// A specific contract, deployed into a user account
import FungibleToken from 0x02
import StandardMetadata from 0x04
pub contract TokenExample {
pub resource ExampleVault: FungibleToken.Vault {
// events could be defined in the contract, or potentially
// in the resource itself. This is an implementation detail
pub event TokensWithdrawn(amount: UFix64, from: Address?)
pub event TokensDeposited(amount: UFix64, to: Address?)
access(self) var balance: UFix64
init(balance: UFix64) {
self.balance = balance
}
// Code for creating new vaults can now be in the vault
// instead of separate
pub static fun createEmpty(): @ExampleVault {}
return <-create ExampleVault(balance: 0.0)
}
pub fun withdraw(amount: UFix64): @ExampleVault {
self.balance = self.balance - amount
return <-create Vault(balance: amount)
}
pub fun deposit(from: @ExampleVault) {
self.balance = self.balance + from.balance
from.balance = 0.0
destroy from
}
pub fun transfer(amount: UFix64, recipient: &AnyResource{Receiver}) {
let tokens <- self.withdraw(amount: amount)
recipient.deposit(from: <-tokens)
}
pub fun getBalance(): UFix64 {
return self.balance
}
}
Proposed NonFungibleToken Interfaces
// A shared contract, deployed in a well-known location.
pub contract NonFungibleToken {
// We will use a generic NFT collection, so we can use standard paths
// in the standard contract
pub let collectionStoragePath: StoragePath
pub let collectionPublicPath: PublicPath
pub event Withdraw(type: Type, id: UFix64, from: Address?)
pub event Deposit(type: Type, id: UFix64, to: Address?)
pub event Transfer(type: Type, id: UFix64, from: Address?, to: Address)
pub resource interface NFT {
// We also don’t want to restrict how an NFT defines its ID
// They can store it, compute it, or use the UUID
pub fun getID(): UInt64
// Two functions for the NFT Metadata Standard
pub fun getViews() : [Type]
pub fun resolveView(_ view:Type): AnyStruct?
}
// This is not the final proposal for provider. Ideally, we could have a provider
// interface that is only valid for a specific NFT type of array of IDs so that the owner
// can protect NFTs from being accessed unintentionally
pub resource interface Provider {
pub fun withdraw(type: Type, withdrawID: UInt64): @{NFT}? {
post {
result.getID() == withdrawID
}
}
pub fun transfer(type: Type, withdrawID: UInt64, recipient: &AnyResource{Receiver})
}
pub resource interface Receiver {
pub fun deposit(token: @{NFT})
}
pub resource interface CollectionPublic: Receiver {
// A user will likely have a way to restrict which NFTs
// they want to receive so they can’t get spammed
pub fun deposit(token: @AnyResource{NFT})
pub fun getIDs(): {Type: [UInt64]}
pub fun borrowNFT(id: UInt64): &{NFT}?
}
// A full implementation of NFT collection that
// *is not* specific to one NFT type
// Users could just use an instance of this to store all their
// NFTs instead of having to use a specific one for each project
//
pub resource NFTCollection: CollectionPublic, Receiver, Provider {
access(self) let ownedNFTs: {Type: {UInt64: @{NFT}}
pub fun deposit(token: @AnyResource{NFT}) {
pre {
// Potentially allow users to specify which NFT types
// they are comfortable receiving so they don’t
// get spammed with NFTs
}
}
pub fun withdraw(type: Type, withdrawID: UInt64): @{NFT}? {
}
pub fun transfer(type: Type, withdrawID: UInt64, recipient: &AnyResource{Receiver}) {
}
// Also could potentially include batch withdraw, batch deposit,
// and batch transfer
pub fun getIDs(): {Type: [UInt64] {
}
// Could also include a method that includes a subset of the IDs
pub fun getIDsPaginated(subset: {???}): {Type: [UInt64]} {}
pub fun borrowNFT(id: UInt64): &{NFT}? {
}
}
}
New Non-Fungible Token Example Implementation
// A specific contract, deployed into a user account
import NonFungibleToken from 0x03
import StandardMetadata from 0x04
pub contract ExampleTokenImplementation
pub resource ExampleNFT: NonFungibleToken.NFT {
// Could also use uuid as the unique identifier
access(self) let name: String
pub fun getID(): UInt64 {
// this could also use a id field if needed
return self.uuid
}
// From the NFT Metadata Standard
pub fun getViews(): [Type] {
return [StandardMetadata.SimpleName]
}
// From the NFT Metadata Standard
pub fun resolveView(_ view:Type): AnyStruct? {
if view == StandardMetadata.SimpleName {
return StandardMetadata.SimpleName(name: self.name)
}
return nil
}
init(initID: UInt64) {
self.id = initID
self.name = "Token name"
}
}
}
Some things worth noting from the examples:
- The project doesn’t have to worry about adding boilerplate code for implementing an nft collection resource. Since the standard does not use a contract interface any more and makes NFTs more generic, users can use a single collection for all their NFTs.
- We no longer use a contract interface for FungibleToken or NonFungibleToken, so some standard pieces like the totalSupply field and the standard events are not included in the contract. The totalSupply field could potentially be handled by a simple contract interface that only specifies the totalSupply field. Replacing the standard events is more difficult because we’ve decided in Cadence that we’d like to avoid type requirements in interfaces. To address this, we propose that projects define their own event types in their contract. There will still be a standard for event names and parameters, but it will not be enforced by the contract interface anymore. This is definitely a topic that is still up for debate.
- Another benefit of using resource interfaces instead of contract interfaces is that a project can define any number of fungible tokens and/or non-fungible tokens in the same contract, allowing a lot more flexibility and efficiency for developers.
- We have included standard storage and public paths for the nft collection. Since each user will use the same collection, there is no need for projects to have their own paths.
- We have also included a transfer method to make transfers a bit more straightforward, a feature requested by many community members.
- There is an open issue for improvements to the existing NFT standard that was discussed in 2021. Many of those improvements are addressed by this standard, but not all. Some of them are no longer relevant because of the nature of the refactoring of the standard, while some of them require additional changes to Cadence not in the scope of these upgrades.
Some of these improvements that are out of scope are:- Enumerating resources in an account
- Using generics
- Requiring that a certain event is emitted in a specific function.
- NFT nonce
We recognize these are not trivial changes, but we are confident that by collaborating with the community, we can devise an upgrade path for everyone that minimizes the difficulty associated with upgrading. If this initial proposal gets good feedback, we’ll move forward with defining a detailed upgrade path and migration plan. This will also need to be approved by the community before committing to the upgrade. The Flow team is sure that the effort required by everyone will be worth it in the long run and produce clearer, safer, and more composable token standards.