Fungible Tokens
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:
_12// Solidity Example_12// SIMPLIFIED ERC20 EXAMPLE. DO NOT USE THIS CODE FOR YOUR PROJECT_12contract LedgerToken {_12 mapping (address => uint) public balances;_12 uint public supply;_12_12 function transfer(address _to, uint _amount) public {_12 require(_balances[msg.sender] >= amount, "Insufficent funds");_12 balances[msg.sender] -= _amount;_12 balances[_to] += _amount;_12 }_12}
_14// Cadence Example_14// BAD CODE EXAMPLE. DO NOT USE THIS CODE FOR YOUR PROJECT_14contract LedgerToken {_14 // Tracks every user's balance_14 access(contract) let balances: {Address: UFix64}_14_14 // Transfer tokens from one user to the other_14 // by updating their balances in the central ledger_14 access(all)_14 fun transfer(from: Address, to: Address, amount: UFix64) {_14 balances[from] = balances[from] - amount_14 balances[to] = balances[to] + amount_14 }_14}
This is an immensely dangerous pattern - all the funds are stored in one account, the contract itself, which means that they can all be stolen at once if a vulnerability is found.
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].
This tutorial implements a working fungible token, but it has been simplified for educational purposes and is not what you should use in production.
If you've found this tutorial looking for information on how to implement a real token, see the Flow Fungible Token standard for the standard interface and example implementation, and the Fungible Token Developer Guide for a details on creating a production ready version of a Fungible Token contract.
In this tutorial, we're going to deploy, store, and transfer fungible tokens.
Objectives
After completing this tutorial, you'll be able to:
- Compare and contrast how tokens are stored in Flow Cadence compared to Ethereum.
- Utilize the
UFix64
type to allow decimals without converting back and forth with 10^18. - Implement a vault resource to manage the functionality needed for fungible tokens
- Use interfaces to enforce the presence of specified functions and fields.
- Write transactions to transfer tokens safely from one account to another.
- Develop scripts to read account balances.
- Use preconditions and postconditions to perform checks before or after a function call completes.
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 you'll build 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!
Fungible Tokens on Flow
Flow implements fungible tokens differently than 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
Fungible tokens on Ethereum
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. This contract handles access control for all functionality, implements all of its own correctness checks, and enforces rules for all of its users.
If there's a bug, such as accidentally making the _transfer
function accessible to the wrong user, a reentrancy issue, or another bug, an attacker can immediately exploit the entire contract and the tokens owned by all users.
_10// BAD CODE - DO NOT USE_10// Anyone can transfer funds out of any account!_10function exploitableTransfer(address, _from, address _to, uint _amount) public {_10 balances[_from] -= _amount;_10 balances[_to] += _amount;_10}
Intuiting Ownership with Resources
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. Primarily, tokens are stored in each user's vault, which is a resource similar to the collection you created to store NFTs in the previous tutorial.
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' accounts, and each user natively has sole control over the resources stored in their accounts.
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.
Constructing a Vault
Our vault will be a simplified version of the one found in the Flow Fungible Token standard. We'll follow some of the same practices, including using interfaces to standardize the properties of our vault and make it easier for other contracts to interact with it.
Open the starter code for this tutorial in the Flow Playground:
https://play.flow.com/359cf1a1-63cc-4774-9c09-1b63ed83379bIn ExampleToken.cdc
, you'll see:
Before you can add your vault, you'll need to implement the various pieces it will depend on.
Supply and Balance
The two most basic pieces of information for a fungible token are a method of tracking the balance of a given user, and the total supply for the token. In Cadence, you'll usually want to use UFix64
- a fixed-point number.
Fixed-point numbers are essentially integers with a scale, represented by a decimal point. They make it much easier to work with money-like numbers as compared to endlessly handling conversions to and from the 10^18 or 10^9 representation of a value.
Implement a contract-level fixed-point number to track the totalSupply
of the token, and init
it.
_10access(all) var totalSupply: UFix64
_10self.totalSupply = 0.0;
Interfaces
You'll also need a place to store the balance
of any given user's vault. You could simply add a variable in the vault resource definition to do this and it would work, but it's not the best option for composability.
Instead, let's use this opportunity to create some [interface]s.
In Cadence, interfaces are abstract types used to specify behavior in types that implement the interface. Using them helps compatibility and composability by breaking larger constructions down into standardized parts that can be used by more than one contract for more than one use case. For example, the interface we'll create for Receiver
is used by the vault, but it's also something you'd use for any other resource that needs to be able to receive tokens - such as a contract that pools a collection of tokens and splits them between several addresses.
You'll create three interfaces, to handle the three functional areas of the vault:
- A
Balance
interface for the balance of tokens stored in the vault - A
Provider
interface that can provide tokens by withdrawing them from the vault - A
Receiver
interface that can safely deposit tokens from one vault into another
First, create a Balance
interface, requiring a public UFix64
called balance
. It should be public.
_10access(all) resource interface Balance {_10 access(all) var balance: UFix64_10}
This one is pretty simple. It just defines the type of variable anything implementing it will need to have to keep track of a token balance.
Next, create the Provider
interface
. In it, define a withdraw
function. It should have the Withdraw
access entitlement, accept an argument for amount
, and return a Vault
resource type. This should also be public.
To prevent an error, stub out the Vault
resource as well.
_10access(all) resource interface Provider {_10 access(Withdraw) fun withdraw(amount: UFix64): @Vault_10}_10_10access(all) resource Vault {}
This [interface] will require resources implementing it to have a withdraw
function, but it doesn't provide any limitations to how that function works. For example, it could be implemented such that the amount of tokens returned is double the withdrawn amount. While there might be a use case for that effect, it's not what you want for a normal token standard.
You can allow for flexibility, such as allowing a Provider
to select randomly from several vaults to determine the payer, while enforcing that the amount withdrawn is appropriate by adding a post
condition to the function. Function preconditions and postconditions can be used to restrict the inputs and outputs of a function.
In a postcondition, the special constant result
is used to reference the return
of the function. They're written following the rules of statements and can contain multiple conditions. Optionally, a :
can be added after the last statement, containing an error message to be passed if the postcondition fails.
Add a post
condition that returns a descriptive and nicely formatted error if the amount returned in the vault from the function doesn't match the amount
passed into the function.
_10access(Withdraw) fun withdraw(amount: UFix64): @Vault {_10 post {_10 result.balance == amount:_10 "ExampleToken.Provider.withdraw: Cannot withdraw tokens!"_10 .concat("The balance of the withdrawn tokens (").concat(result.balance.toString())_10 .concat(") is not equal to the amount requested to be withdrawn (")_10 .concat(amount.toString()).concat(")")_10 }_10}
This post
condition will be added automatically to the withdraw
function in a resource implementing Provider
.
Finally, implement an [interface] called Receiver
, containing a function called deposit
that accepts a Vault
.
::
_10access(all) resource interface Receiver {_10 access(all) fun deposit(from: @Vault)_10}
Implementing the Vault
You're finally ready to implement the vault.
Start by updating the stub for a Vault
to implement Balance
, Provider
, and Receiver
.
_10access(all) resource Vault: Balance, Provider, Receiver {_10 // TODO_10}
You'll get errors:
_10resource `ExampleToken.Vault` does not conform to resource interface `ExampleToken.Balance`. `ExampleToken.Vault` is missing definitions for members: Balance
And similar errors for Provider
and Receiver
. Similar to inheriting from a virtual class in other languages, implementing the interfaces requires you to implement the properties from those interfaces in your resource.
Implement balance
in the vault
. You'll also need to initialize it. Initialize it with the balance
passed into the init
for the resource itself.
_10access(all) var balance: UFix64_10_10init(balance: UFix64) {_10 self.balance = balance_10}
The pattern we're setting up here lets us create vaults and give them a balance
in one go. Doing so is useful for several tasks - creating a temporary Vault
to hold a balance during a transaction also creates most of the functionality you need to do complex tasks with that balance.
For example, you might want to set up a conditional transaction that deposit
s the balance in the vaults in different addresses based on whether or not a part of the transaction is successful.
Next, implement withdraw
function for the vault
. It should contain a precondition that validates that the user actually possesses equal to or greater the number of tokens they are withdrawing.
While this functionality is probably something we'd want in every vault, we can't put the requirement in the [interface], because the interface doesn't have access to the balance
.
_11access(Withdraw) fun withdraw(amount: UFix64): @Vault {_11 pre {_11 self.balance >= amount:_11 "ExampleToken.Vault.withdraw: Cannot withdraw tokens! "_11 .concat("The amount requested to be withdrawn (").concat(amount.toString())_11 .concat(") is greater than the balance of the Vault (")_11 .concat(self.balance.toString()).concat(").")_11 }_11 self.balance = self.balance - amount_11 return <-create Vault(balance: amount)_11}
Finally, implement the deposit
function for the vault
. Depositing should move the entire balance from the provided vault, and then destroy
that vault
. Remember, we're transferring tokens by creating a vault and funding it with the amount to transfer. It's not needed once the deposit has emptied it.
_10access(all) fun deposit(from: @Vault) {_10 self.balance = self.balance + from.balance_10 destroy from_10}
You must do something with the Vault
resource after it's moved into the function. Again, you can safely destroy
it, because it's now empty, and you don't need it anymore.
Vault Creation
We'll also need a way to create empty vaults to onboard new users, or to create vaults for a variety of other uses.
Add a function to the contract to create
an empty Vault
.
_10access(all) fun createEmptyVault(): @Vault {_10 return <-create Vault(balance: 0.0)_10}
We'll use this when we create a transaction to set up new users.
Error Handling
As before, you can anticipate some of the errors that other developers building transactions and scripts that interact with your contract might encounter. At the very least, it's likely that there will be many instances that an attempt is made to borrow a Vault
that is not present, or lacks a capability for the caller to borrow it.
Add a function to generate a helpful error if an attempt to borrow a Vault
fails.
_10access(all) fun vaultNotConfiguredError(address: Address): String {_10 return "Could not borrow a collection reference to recipient's ExampleToken.Vault"_10 .concat(" from the path ")_10 .concat(ExampleToken.VaultPublicPath.toString())_10 .concat(". Make sure account ")_10 .concat(address.toString())_10 .concat(" has set up its account ")_10 .concat("with an ExampleToken Vault.")_10}
Minting
Next, you need a way to actually create, or mint, tokens. For this example, we'll define a VaultMinter
resource that has the power to mint and airdrop tokens to any address that possesses a vault, or at least something with the Receiver
[interface] for this token.
Only the owner of this resource will be able to mint tokens.
To do so, we use capability with a reference to the resource or interface we want to require as the type: Capability<&{Receiver}>
Define a public resource with a public function mintTokens
that accepts an amount
of tokens to mint, and a recipient
that must possess the Receiver
capability.
_10access(all) resource VaultMinter {_10 access(all) fun mintTokens(amount: UFix64, recipient: Capability<&{Receiver}>) {_10 let recipientRef = recipient.borrow()_10 ?? panic(ExampleToken.vaultNotConfiguredError(address: recipient.address))_10_10 ExampleToken.totalSupply = ExampleToken.totalSupply + UFix64(amount)_10 recipientRef.deposit(from: <-create Vault(balance: amount))_10 }_10}
Don't be misled by the access(all)
entitlement for this resource. This entitlement only allows public access to the VaultMinter
type. It does not give access to the instance we'll create in a moment. That instance will be owned by the publisher of the contract and is the only one that can be created since there isn't a function to create more and VaultMinter
does not have a public init
function.
Final Contract Setup
The last task with the contract is to update the init
function in your contract to save yourself a little bit of time and create and create a VaultMinter
in your account.
Update the contract init
function to create
and save
an instance of VaultMinter
:
_10self_10 .account_10 .storage_10 .save(<-create VaultMinter(),_10 to: /storage/CadenceFungibleTokenTutorialMinter_10 )
After doing all of this, your contract should be similar to:
_92access(all) contract ExampleToken {_92_92 access(all) entitlement Withdraw_92_92 access(all) let VaultStoragePath: StoragePath_92 access(all) let VaultPublicPath: PublicPath_92_92 access(all) var totalSupply: UFix64_92_92 access(all) resource interface Balance {_92 access(all) var balance: UFix64_92 }_92_92 access(all) resource interface Provider {_92 access(Withdraw) fun withdraw(amount: UFix64): @Vault {_92 post {_92 result.balance == amount:_92 "ExampleToken.Provider.withdraw: Cannot withdraw tokens!"_92 .concat("The balance of the withdrawn tokens (").concat(result.balance.toString())_92 .concat(") is not equal to the amount requested to be withdrawn (")_92 .concat(amount.toString()).concat(")")_92 }_92 }_92 }_92_92 access(all) resource interface Receiver {_92 access(all) fun deposit(from: @Vault)_92 }_92_92 access(all) resource Vault: Balance, Provider, Receiver {_92 access(all) var balance: UFix64_92_92 init(balance: UFix64) {_92 self.balance = balance_92 }_92_92 access(Withdraw) fun withdraw(amount: UFix64): @Vault {_92 pre {_92 self.balance >= amount:_92 "ExampleToken.Vault.withdraw: Cannot withdraw tokens! "_92 .concat("The amount requested to be withdrawn (").concat(amount.toString())_92 .concat(") is greater than the balance of the Vault (")_92 .concat(self.balance.toString()).concat(").")_92 }_92 self.balance = self.balance - amount_92 return <-create Vault(balance: amount)_92 }_92_92 access(all) fun deposit(from: @Vault) {_92 self.balance = self.balance + from.balance_92 destroy from_92 }_92 }_92_92 access(all) fun createEmptyVault(): @Vault {_92 return <-create Vault(balance: 0.0)_92 }_92_92 access(all) fun vaultNotConfiguredError(address: Address): String {_92 return "Could not borrow a collection reference to recipient's ExampleToken.Vault"_92 .concat(" from the path ")_92 .concat(ExampleToken.VaultPublicPath.toString())_92 .concat(". Make sure account ")_92 .concat(address.toString())_92 .concat(" has set up its account ")_92 .concat("with an ExampleToken Vault.")_92 }_92_92 access(all) resource VaultMinter {_92 access(all) fun mintTokens(amount: UFix64, recipient: Capability<&{Receiver}>) {_92 let recipientRef = recipient.borrow()_92 ?? panic(ExampleToken.vaultNotConfiguredError(address: recipient.address))_92_92 ExampleToken.totalSupply = ExampleToken.totalSupply + UFix64(amount)_92 recipientRef.deposit(from: <-create Vault(balance: amount))_92 }_92 }_92_92 init() {_92 self.VaultStoragePath = /storage/CadenceFungibleTokenTutorialVault_92 self.VaultPublicPath = /public/CadenceFungibleTokenTutorialReceiver_92_92 self_92 .account_92 .storage_92 .save(<-create VaultMinter(),_92 to: /storage/CadenceFungibleTokenTutorialMinter_92 )_92_92 self.totalSupply = 0.0;_92 }_92}
Deploy the ExampleToken
contract with account 0x06
.
Set Up Account Transaction
We'll now need to create several transactions and scripts to manage interactions with the vault. The first of these is one to set up a user's account. It needs to:
- Create an empty vault
- Save that vault in the caller's storage
- Issue a capability for the vault
- Publish that capability
You've already learned how to do everything you need for this, so you should be able to implement it on your own.
Implement the Set Up Account
transaction.
You should end up with something similar to:
_17import ExampleToken from 0x06_17_17transaction {_17 prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) {_17 // You may wish to check if a vault already exists here_17_17 let vaultA <- ExampleToken.createEmptyVault()_17_17 signer.storage.save(<-vaultA, to: ExampleToken.VaultStoragePath)_17_17 let receiverCap = signer.capabilities.storage.issue<&ExampleToken.Vault>(_17 ExampleToken.VaultStoragePath_17 )_17_17 signer.capabilities.publish(receiverCap, at: ExampleToken.VaultPublicPath)_17 }_17}
Minting Tokens
The next transaction is another one that you should be able to implement on your own. Give it a try, and check the solution if you need to. Your transaction should:
- Accept an
Address
for therecipient
and anamount
- Store transaction-level references to the
VaultMinter
andReceiver
- Borrow a reference to the
VaultMinter
in the caller's storage - Get the
recipient
'sReceiver
capability - Use the above to call the
mintTokens
function in the minter
Implement the Mint Tokens
transaction.
You should end up with something similar to:
_26import ExampleToken from 0x06_26_26transaction(recipient: Address, amount: UFix64) {_26 let mintingRef: &ExampleToken.VaultMinter_26 var receiver: Capability<&{ExampleToken.Receiver}>_26_26 prepare(signer: auth(BorrowValue) &Account) {_26 self.mintingRef = signer.storage.borrow<&ExampleToken.VaultMinter>(from: /storage/CadenceFungibleTokenTutorialMinter)_26 ?? panic(ExampleToken.vaultNotConfiguredError(address: recipient))_26_26 let recipient = getAccount(recipient)_26_26 // Consider further error handling if this fails_26 self.receiver = recipient.capabilities.get<&{ExampleToken.Receiver}>_26 (ExampleToken.VaultPublicPath)_26_26 }_26_26 execute {_26 // Mint 30 tokens and deposit them into the recipient's Vault_26 self.mintingRef.mintTokens(amount: 30.0, recipient: self.receiver)_26_26 log("30 tokens minted and deposited to account "_26 .concat(self.receiver.address.toString()))_26 }_26}
Test out your minting function by attempting to mint tokens to accounts that do and do not have vaults.
Checking Account Balances
You can mint tokens now. Probably. But it's hard to tell if you have a bug without a way to check an accounts balance. You can do this with a script, using techniques you've already learned.
Write a script to check the balance of an address. It should accept an argument for an address
. In this script,get
and borrow
a reference to that address's Vault
from the VaultPublicPath
, and return a nicely formatted string containing the balance
.
You should end up with something similar to:
_15import ExampleToken from 0x06_15_15access(all)_15fun main(address: Address): String {_15 let account = getAccount(address)_15_15 let accountReceiverRef = account.capabilities.get<&{ExampleToken.Balance}>(ExampleToken.VaultPublicPath)_15 .borrow()_15 ?? panic(ExampleToken.vaultNotConfiguredError(address: address))_15_15 return("Balance for "_15 .concat(address.toString())_15 .concat(": ").concat(accountReceiverRef.balance.toString())_15 )_15}
Transferring Tokens
Transferring tokens from one account to another takes a little more coordination and a more complex transaction. 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.
Initialize a transaction-level variable to hold a temporary vault. Borrow a reference for the sender's vault with the Withdraw
entitlement and send it to the temporary vault.
_13import ExampleToken from 0x06_13_13transaction(recipient: Address, amount: UFix64) {_13 var temporaryVault: @ExampleToken.Vault_13_13 prepare(signer: auth(BorrowValue) &Account) {_13 let vaultRef = signer.storage.borrow<auth(ExampleToken.Withdraw) &ExampleToken.Vault>(_13 from: ExampleToken.VaultStoragePath)_13 ?? panic(ExampleToken.vaultNotConfiguredError(address: signer.address))_13_13 self.temporaryVault <- vaultRef.withdraw(amount: amount)_13 }_13}
The sending account then gets a reference to the recipients published capability and calls the recipient account's deposit function, which literally moves the resource instance to the other account, adds the temporary vault's balance to their balance, and then destroys the used resource.
Use the execute
phase to deposit
the tokens in the temporaryVault
into the recipient's vault.
_12execute{_12 let receiverAccount = getAccount(recipient)_12_12 let receiverRef = receiverAccount_12 .capabilities_12 .borrow<&ExampleToken.Vault>(ExampleToken.VaultPublicPath)_12 ?? panic(ExampleToken.vaultNotConfiguredError(address: recipient))_12_12 receiverRef.deposit(from: <-self.temporaryVault)_12_12 log("Withdraw/Deposit succeeded!")_12}
The resource is destroyed by the deposit
function. It 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.
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
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.
Every Flow account is initialized with a default Flow Token Vault in order to pay for storage fees and transaction fees. If an address is in use, it will be able to accept Flow tokens, without a user or developer needing to take further action. If that account becomes lost, any tokens inside will be lost as well.
Reviewing Fungible Tokens
In this tutorial, you learned how to create a simplified version of fungible tokens on Flow. You build a vault resource to safely store tokens inside the owner's storage, and used interfaces to define and enforce the properties a vault should have. By using interfaces, your definition is flexible and composable. Other developers can use all or parts of these definitions to build new apps and contracts that are compatible with yours.
You also practiced writing transactions on your own, and learned some new techniques, such as writing error messages more easily, using paths stored in the contract, and separating different parts of the transaction into their appropriate sections - prepare
and execute
.
Now that you have completed the tutorial, you should be able to:
- Compare and contrast how tokens are stored in Flow Cadence compared to Ethereum.
- Utilize the
UFix64
type to allow decimals without converting back and forth with 10^18. - Implement a vault resource to manage the functionality needed for fungible tokens
- Use interfaces to enforce the presence of specified functions and fields.
- Write transactions to transfer tokens safely from one account to another.
- Develop scripts to read account balances.
- Use preconditions and postconditions to perform checks before or after a function call completes.
If you're ready to try your hand at implementing a production-quality token, head over to the Fungible Token Developer Guide.
In the next tutorial, you'll combine the techniques and patterns you've learned for the classic challenge - building an NFT marketplace!
Reference Solution
You are not saving time by skipping the the reference implementation. You'll learn much faster by doing the tutorials as presented!
Reference solutions are functional, but may not be optimal.