6. Fungible Token Tutorial
In this tutorial, we're going to deploy, store, and transfer fungible tokens.
Instructions that require you to take action are always included in a callout box like this one. These highlighted actions are all that you need to do to get your code running, but reading the rest is necessary to understand the language's design.
Some of the most popular contract classes on blockchains today are fungible tokens. These contracts create homogeneous tokens that can be transferred to other users and spent as currency (e.g., ERC-20 on Ethereum).
In traditional software and smart contracts, balances for each user are tracked by a central ledger, such as a dictionary:
_13// DO NOT USE THIS CODE FOR YOUR PROJECT_13contract LedgerToken {_13 // Tracks every user's balance_13 access(contract) let balances: {Address: UFix64}_13_13 // Transfer tokens from one user to the other_13 // by updating their balances in the central ledger_13 access(all)_13 fun transfer(from: Address, to: Address, amount: UFix64) {_13 balances[from] = balances[from] - amount_13 balances[to] = balances[to] + amount_13 }_13}
With Cadence, we use the new resource-oriented paradigm to implement fungible tokens and avoid using a central ledger because there are inherent problems with using a central ledger that are detailed in the Fungible Tokens section below.
Flow Network Token
In Flow, the native network token (FLOW) is implemented as a normal fungible token smart contract using a smart contract similar to the one in this tutorial.
There are special transactions and hooks that allow it to be used for transaction execution fees, storage fees, and staking, but besides that, developers and users are able to treat it and use it just like any other token in the network!
It is important to remember that while this tutorial implements a working fungible token, it has been simplified for educational purposes and is not what any project should use in production. See the Flow Fungible Token standard for the standard interface and example implementation. You can also see the Fungible Token Developer Guide for a guide for how to create a production ready version of a Fungible Token contract.
We're going to take you through these steps to get comfortable with the fungible token:
- Deploy the fungible token contract to account
0x06
- Create a fungible token object and store it in your account storage.
- Create a reference to your tokens that others can use to send you tokens.
- Set up another account the same way.
- Transfer tokens from one account to another.
- Use a script to read the accounts' balances.
Before proceeding with this tutorial, we recommend following the instructions in Getting Started and Hello, World! to learn the basics of the language and the playground.
Fungible Tokens on the Flow Emulator
Open the account 0x06
tab to see the file named
BasicToken.cdc
. BasicToken.cdc
should contain the full code for the
fungible token, which provides the core functionality to store fungible tokens
in your account and transfer to and accept tokens from other users.
The concepts involved in implementing a fungible token in Cadence can be unfamiliar at first. If you haven't completed and understood the previous tutorials, please go back and complete those first because this tutorial will assume that you understand a lot of the concepts explored in those tutorials.
For an in-depth explanation of this functionality and code, continue reading the next section.
Or, if you'd like to go immediately into deploying it and using it in the playground, you can skip to the Interacting with the Fungible Token section of this tutorial.
Fungible Tokens: An In-Depth Exploration
How Flow implements fungible tokens is different from other programming languages. As a result:
- Ownership is decentralized and does not rely on a central ledger
- Bugs and exploits present less risk for users and less opportunity for attackers
- There is no risk of integer underflow or overflow
- Assets cannot be duplicated, and it is very hard for them to be lost, stolen, or destroyed
- Code can be composable
- Rules can be immutable
- Code is not unintentionally made public
Decentralizing Ownership
Instead of using a central ledger system, Flow ties ownership to each account via a new paradigm for asset ownership. The example below showcases how Solidity (the smart contract language for the Ethereum Blockchain, among others) implements fungible tokens, with only the code for storage and transferring tokens shown for brevity.
As you can see, Solidity uses a central ledger system for its fungible tokens. There is one contract that manages the state of the tokens and every time that a user wants to do anything with their tokens, they have to interact with the central ERC20 contract, calling its functions to update their balance. This contract handles access control for all functionality, implements all of its own correctness checks, and enforces rules for all of its users.
Instead of using a central ledger system, Flow utilizes a few different concepts to provide better safety, security, and clarity for smart contract developers and users. In this section, we'll show how Flow's resources, interfaces, and other features are employed via a fungible token example.
Intuiting Ownership with Resources
An important concept in Cadence is Resources, which are linear types.
A resource is a composite type (like a struct) that has its own defined fields and functions.
The difference is that resource objects have special rules that keep them from being copied or lost.
Resources are a new paradigm for asset ownership. Instead of representing token ownership in a central ledger smart contract,
each account owns its own resource object in its account storage that records the number of tokens they own.
This way, when users want to transact with each other, they can do so peer-to-peer without having to interact with a central token contract.
To transfer tokens to each other, they call a transfer
function (or something equivalent)
on their own resource object and other users' resources, instead of a central transfer
function.
This approach simplifies access control because instead of a central contract having to check the sender of a function call, most function calls happen on resource objects stored in users' account, and each user controls who is able to call the functions on resources in their account. This concept, called Capability-based security, will be explained more in a later section.
This approach also helps protect against potential bugs. In a Solidity contract with all the logic and state contained in a central contract, an exploit is likely to affect all users who are involved in the contract.
In Cadence, if there is a bug in the resource logic, an attacker would have to exploit the bug in each token holder's account individually, which is much more complicated and time-consuming than it is in a central ledger system.
Below is an example of a resource for a fungible token vault. Every user who owns these tokens would have this resource stored in their account.
It is important to remember that each account stores only a copy of the Vault
resource, and not a copy of the entire ExampleToken
contract.
The ExampleToken
contract only needs to be stored in the initial account that manages the token definitions.
This piece of code is for educational purposes and is not comprehensive. However, it still showcases how a resource for a token works.
Token Balances and Initialization
Each token resource object has a balance and associated functions (e.g., deposit
, withdraw
, etc).
When a user wants to use these tokens, they instantiate a zero-balance copy of this resource in their account storage.
The language requires that the initialization function init
, which is only run once, must initialize all member variables.
_10// Balance of a user's Vault_10// we use unsigned fixed-point integers for balances because they do not require the_10// concept of a negative number and allow for more clear precision_10access(all) var balance: UFix64_10_10init(balance: UFix64) {_10 self.balance = balance_10}
If you remove the init
function from your ExampleToken
contract, it will cause an error because
the balance field is no longer initialized.
Deposit
Then, the deposit function is available for any account to transfer tokens to.
_10access(all) fun deposit(from: @Vault) {_10 self.balance = self.balance + from.balance_10 destroy from_10}
Transferring Tokens
When an account wants to send tokens to a different account, the sending account calls their own withdraw function first, which subtracts tokens from their resource’s balance and temporarily creates a new resource object that holds this balance:
_10// Withdraw tokens from the signer's stored vault_10let sentVault <- vaultRef.withdraw(amount: amount)
The sending account then calls the recipient account’s deposit function, which literally moves the resource instance to the other account, adds it to their balance, and then destroys the used resource:
_10// Deposit the withdrawn tokens in the recipient's receiver_10receiverRef.deposit(from: <-sentVault)
The resource needs to be destroyed because Cadence enforces strict rules around resource interactions. A resource can never be left hanging in a piece of code. It either needs to be explicitly destroyed or stored in an account's storage.
When interacting with resources, you use the @
symbol to specify the type, and a special “move operator” <-
when moving the resource, such as assigning the resource, when passing it as an argument to a function, or when returning it from a function.
_10access(all) fun withdraw(amount: UInt64): @Vault {
This @
symbol is required when specifying a resource type for a field, an argument, or a return value.
The move operator <-
makes it clear that when a resource is used in an assignment, parameter, or return value,
it is moved to a new location and the old location is invalidated. This ensures that the resource only ever exists in one location at a time.
If a resource is moved out of an account's storage, it either needs to be moved to an account’s storage or explicitly destroyed.
_10destroy from
This rule ensures that resources, which often represent real value, do not get lost because of a coding error.
You’ll notice that the arithmetic operations aren't explicitly protected against overflow or underflow.
_10self.balance = self.balance - amount
In Solidity, this could be a risk for integer overflow or underflow, but Cadence has built-in overflow and underflow protection, so it is not a risk. We are also using unsigned numbers in this example, so as mentioned earlier, the vault`s balance cannot go below 0.
Additionally, the requirement that an account contains a copy of the token’s resource type in its storage ensures that funds cannot be lost by being sent to the wrong address.
If an address doesn’t have the correct resource type imported, the transaction will revert, ensuring that transactions sent to the wrong address are not lost.
Important note: This protection is not in place for the Flow network currency, because every Flow account is initialized with a default Flow Token Vault in order to pay for storage fees and transaction fees.
Function Parameters
The line in withdraw
that creates a new Vault
has the parameter name balance
specified in the function call.
_10return <-create Vault(balance: amount)
This is another feature that Cadence uses to improve the clarity of code. All function calls are required to specify the names of the arguments they are sending unless the developer has specifically overridden the requirement in the funtion declaration.
Interacting with the Fungible Token in the Flow Playground
Now that you have read about how the Fungible Token works, we can deploy a basic version of it to your account and send some transactions to interact with it.
Make sure that you have opened the Fungible Token templates in the playground
by following the link at the top of this page. You should have Account 0x06
open and should see the code below.
_101/// BasicToken.cdc_101///_101/// The BasicToken contract is a sample implementation of a fungible token on Flow._101///_101/// Fungible tokens behave like everyday currencies -- they can be minted, transferred or_101/// traded for digital goods._101///_101/// This is a basic implementation of a Fungible Token and is NOT meant to be used in production_101/// See the Flow Fungible Token standard for real examples: https://github.com/onflow/flow-ft_101_101access(all) contract BasicToken {_101_101 access(all) entitlement Withdraw_101_101 access(all) let VaultStoragePath: StoragePath_101 access(all) let VaultPublicPath: PublicPath_101_101 /// Vault_101 ///_101 /// Each user stores an instance of only the Vault in their storage_101 /// The functions in the Vault are governed by the pre and post conditions_101 /// in the interfaces when they are called._101 /// The checks happen at runtime whenever a function is called._101 ///_101 /// Resources can only be created in the context of the contract that they_101 /// are defined in, so there is no way for a malicious user to create Vaults_101 /// out of thin air. A special Minter resource or constructor function needs to be defined to mint_101 /// new tokens._101 ///_101 access(all) resource Vault {_101_101 /// keeps track of the total balance of the account's tokens_101 access(all) var balance: UFix64_101_101 /// initialize the balance at resource creation time_101 init(balance: UFix64) {_101 self.balance = balance_101 }_101_101 /// withdraw_101 ///_101 /// Function that takes an integer amount as an argument_101 /// and withdraws that amount from the Vault._101 ///_101 /// It creates a new temporary Vault that is used to hold_101 /// the money that is being transferred. It returns the newly_101 /// created Vault to the context that called so it can be deposited_101 /// elsewhere._101 ///_101 access(Withdraw) fun withdraw(amount: UFix64): @Vault {_101 pre {_101 self.balance >= amount:_101 "BasicToken.Vault.withdraw: Cannot withdraw tokens! "_101 .concat("The amount requested to be withdrawn (").concat(amount.toString())_101 .concat(") is greater than the balance of the Vault (")_101 .concat(self.balance.toString()).concat(").")_101 }_101 self.balance = self.balance - amount_101 return <-create Vault(balance: amount)_101 }_101_101 /// deposit_101 ///_101 /// Function that takes a Vault object as an argument and adds_101 /// its balance to the balance of the owners Vault._101 ///_101 /// It is allowed to destroy the sent Vault because the Vault_101 /// was a temporary holder of the tokens. The Vault's balance has_101 /// been consumed and therefore can be destroyed._101 access(all) fun deposit(from: @Vault) {_101 self.balance = self.balance + from.balance_101 destroy from_101 }_101 }_101_101 /// createVault_101 ///_101 /// Function that creates a new Vault with an initial balance_101 /// and returns it to the calling context. A user must call this function_101 /// and store the returned Vault in their storage in order to allow their_101 /// account to be able to receive deposits of this token type._101 ///_101 access(all) fun createVault(): @Vault {_101 return <-create Vault(balance: 30.0)_101 }_101_101 /// The init function for the contract. All fields in the contract must_101 /// be initialized at deployment. This is just an example of what_101 /// an implementation could do in the init function. The numbers are arbitrary._101 init() {_101 self.VaultStoragePath = /storage/CadenceFungibleTokenTutorialVault_101 self.VaultPublicPath = /public/CadenceFungibleTokenTutorialReceiver_101 // create the Vault with the initial balance and put it in storage_101 // account.save saves an object to the specified `to` path_101 // The path is a literal path that consists of a domain and identifier_101 // The domain must be `storage`, `private`, or `public`_101 // the identifier can be any name_101 let vault <- self.createVault()_101 self.account.storage.save(<-vault, to: self.VaultStoragePath)_101 }_101}
Click the Deploy
button at the top right of the editor to deploy the code.
This deployment stores the contract for the basic fungible token
in the selected account (account 0x06
) so that it can be imported into transactions.
A contract's init
function runs at contract creation, and never again afterwards.
In our example, this function stores an instance of the Vault
object with an initial balance of 30.
_10// create the Vault with the initial balance and put it in storage_10// account.save saves an object to the specified `to` path_10// The path is a literal path that consists of a domain and identifier_10// The domain must be `storage` or `public`_10// the identifier can be any string_10let vault <- self.createVault()_10self.account.save(<-vault, to: self.VaultStoragePath)
This line saves the new @Vault
object to storage.
Account storage is indexed with paths,
which consist of a domain and identifier. /domain/identifier
.
Only two domains are allowed for paths:
storage
: The place where all objects are stored. Only accessible by the owner of the account.public
: Stores links to objects in storage: Accessible by anyone in the network.
Contracts have access to the private &Account
object of the account it is deployed to, using self.account
.
This object has methods that can modify storage in many ways.
See the account documentation for a list of all the methods it can call.
In this line, we call the storage.save
method to store an object in storage.
The first argument is the value to store, and the second argument is the path where the value is being stored.
For storage.save
the path has to be in the /storage/
domain.
You are now ready to run transactions that use the fungible tokens!
Perform a Basic Transfer
As we talked about above, a token transfer with resources is not a simple update to a ledger. In Cadence, you have to first withdraw tokens from your vault, then deposit them to the vault that you want to transfer to. We'll start a simple transaction that withdraws tokens from a vault and deposits them back into the same vault.
Open the transaction named Basic Transfer
.
Basic Transfer
should contain the following code for withdrawing and depositing with a stored Vault:
Select account 0x06
as the only signer.
You can enter any number less than 30.0 for the amount of tokens to transfer.
Click the Send
button to submit the transaction.
This transaction withdraws tokens from the main vault and deposits them back to it.
This transaction is a basic example of a transfer within an account. It withdraws tokens from the main vault and deposits back to the main vault. It is simply to illustrate the basic functionality of how transfers work.
You'll see in this transaction that you can borrow a reference directly from an object in storage.
_10// Borrow a Withdraw reference to the signer's vault_10// Remember to always have descriptive error messages!_10let vaultRef = signer.storage.borrow<auth(BasicToken.Withdraw) &BasicToken.Vault>_10 (from: ExampleToken.VaultStoragePath)_10 ?? panic("Could not borrow a vault reference to 0x06's BasicToken.Vault"_10 .concat(" from the path ")_10 .concat(BasicToken.VaultStoragePath.toString())_10 .concat(". Make sure account 0x06 has set up its account ")_10 .concat("with an BasicToken Vault."))
This allows you to efficiently access objects in storage without having to load them, which is a much more costly interaction.
This code also uses entitlements (auth(BasicToken.Withdraw)
)
to access the withdraw functionality through a reference.
Without entitlements, any privileged functionality would be able to be accessed
via a public capability because reference can be downcasted to their concrete reference types.
Therefore, functions with privileged functionality, like withdraw()
here,
should have entitlements in order to be secure.
In production code, you'll likely be transferring tokens to other accounts. Capabilities allow us to accomplish this safely.
Ensuring Security in Public: Capability Security
Another important feature in Cadence is its utilization of Capability-Based Security.
Cadence's security model ensures that objects stored in an account's storage can only be accessed by the account that owns them. If a user wants to give another user access to their stored objects, they can link a public capability, which is like an "API" that allows others to call specified functions on their objects.
An account only has access to the fields and methods of an object in a different account if they hold a capability to that object that explicitly allows them to access those fields and methods with entitlements.
Only the owner of an object can create a capability for it and only the owner can add entitlements to a capability.
Therefore, when a user creates a Vault in their account, they publish a capability
that exposes the access(all)
fields and functions on the resource.
Here, those are balance
and deposit()
.
The withdraw function can remain hidden as a function that only the owner can call.
This removes the need to check the address of the account that made the function call
(msg.sender
in Ethereum) for access control purposes, because this functionality
is handled by the protocol and the language's strong static type system.
If you aren't the owner of an object or don't have a valid reference to it
that was created by the owner, you cannot access the object at all!
Using Pre and Post-Conditions to Secure Implementations
The next important concept in Cadence is design-by-contract, which uses pre-conditions and post-conditions to document and programmatically assert the change in state caused by a piece of a program. These conditions are usually specified in interfaces that enforce rules about how types are defined and behave. They can be stored on-chain in an immutable fashion so that certain pieces of code can import and implement them to ensure that they meet certain standards.
In our example, we don't use interfaces for simplicity,
but here is an example of how interfaces for the Vault
resource we defined above would look.
In production code, the Vault
resource implements all three of these interfaces.
The interfaces ensure that specific fields and functions are present in the resource implementation
and that the function arguments, fields of the resource,
and any return value are in a valid state before and/or after execution.
These interfaces can be deployed on-chain and imported into other contracts or resources so that these requirements are enforced by an immutable source of truth that is not susceptible to human error.
See the Flow Fungible Token standard for the interfaces that are used for real Fungible Token implementations!