Transferring Fungible Tokens
In this tutorial, you'll learn how to implement the core standards into your smart contract. You'll implement the logic that allows you to transfer and receive tokens. If you're joining us for the first time, feel free to clone this repository and follow along in the 4.storage
folder.
If you wish to see the finished code for this Core tutorial, you can find it in the 5.transfers
folder.
Introduction
Up until this point, you've created a simple FT smart contract that allows the owner to mint a total supply of tokens and view information about the Fungible Token itself. In addition, you've added the functionality to register accounts and emit events. Today, you'll expand your smart contract to allow for users to transfer and receive fungible tokens.
The logic for doing a simple transfer is quite easy to understand. Let's say Benji wants to transfer Mike 100 of his fungible tokens. The contract should do a few things:
- Check if Benji owns at least 100 tokens.
- Make sure Benji is calling the function.
- Ensure Mike is registered on the contract.
- Take 100 tokens out of Benji's account.
- Put 100 tokens into Mike's account.
At this point, you're ready to move on and make the necessary modifications to your smart contract.
Modifications to the contract
Let's start our journey in the src/ft_core.rs
file.
Transfer function
You'll start by implementing the ft_transfer
logic which is found in the src/ft_core.rs
file. This function will transfer the specified amount
to the receiver_id
with an optional memo
such as "Happy Birthday Mike!"
.
Loading...
There are a couple things to notice here.
-
We've introduced a new function called
assert_one_yocto()
. This method will ensure that the user is signing the transaction with a full access key by requiring a deposit of exactly 1 yoctoNEAR, the smallest possible amount of $NEAR that can be transferred. Since the transfer function is potentially transferring very valuable assets, you'll want to make sure that whoever is calling the function has a full access key. -
We've introduced an
internal_transfer
method. This will perform all the logic necessary to transfer the tokens internally.
Internal helper functions
Let's quickly move over to the ft-contract/src/internal.rs
file so that you can implement the internal_transfer
method which is the core of this tutorial. This function will take the following parameters:
- sender_id: the account that's attempting to transfer the tokens.
- receiver_id: the account that's receiving the tokens.
- amount: the amount of FTs being transferred.
- memo: an optional memo to include.
The first thing you'll want to do is make sure the sender isn't sending tokens to themselves and that they're sending a positive number. After that, you'll want to withdraw the tokens from the sender's balance and deposit them into the receiver's balance. You've already written the logic to deposit FTs by using the internal_deposit
function.
Let's use similar logic to implement internal_withdraw
:
Loading...
In this case, the contract will get the account's balance and ensure they are registered by calling the internal_unwrap_balance_of
function. It will then subtract the amount from the user's balance and re-insert them into the map.
Using the internal_deposit
and internal_withdraw
functions together, the core of the internal_transfer
function is complete.
There's only one more thing you need to do. When transferring the tokens, you need to remember to emit a log as per the events standard:
Loading...
Now that this is finished, the simple transfer case is done! You can now transfer FTs between registered users!
Transfer call function
In this section, you'll learn about a new function ft_transfer_call
. This will transfer FTs to a receiver and also call a method on the receiver's contract all in the same transaction.
Let's consider the following scenario. An account wants to transfer FTs to a smart contract for performing a service. The traditional approach would be to perform the service and then ask for the tokens in two separate transactions. If we introduce a “transfer and call” workflow baked into a single transaction, the process can be greatly improved:
Loading...
This function will do several things:
- Ensures the caller attached exactly 1 yocto for security purposes.
- Transfer the tokens using the
internal_transfer
you just wrote. - Creates a promise to call the method
ft_on_transfer
on thereceiver_id
's contract. - After the promise finishes executing, the function
ft_resolve_transfer
is called.
This is a very common workflow when dealing with cross contract calls. You first initiate the call and wait for it to finish executing. Then, you invoke a function that resolves the result of the promise and act accordingly. Learn more here.
When calling ft_on_transfer
, it will return how many tokens the contract should refund the original sender.
This is important for a couple of reasons:
- If you send the receiver too many FTs and their contract wants to refund the excess.
- If any of the logic panics, all of the tokens should be refunded back to the sender.
This logic will all happen in the ft_resolve_transfer
function:
Loading...
The first thing you'll do is check the status of the promise. If anything failed, you'll refund the sender for the full amount of tokens. If the promise succeeded, you'll extract the amount of tokens to refund the sender based on the value returned from ft_on_transfer
. Once you have the amount needed to be refunded, you'll perform the actual refund logic by using the internal_transfer
function you wrote previously.
You'll then return the net amount of tokens that were transferred to the receiver. If the sender transferred 100 tokens but 20 were refunded, this function should return 80.
With that finished, you've now successfully added the necessary logic to allow users to transfer FTs. It's now time to deploy and do some testing.
Deploying the contract
Let's create a new sub-account to deploy the contract to. Since these changes are only additive and non state breaking, you could have deployed a patch fix to the contract you deployed in the storage section as well. To learn more about upgrading contracts, see the upgrading a contract section in the NFT Zero to Hero tutorial.
Creating a sub-account
Run the following command to create a sub-account transfer
of your main account with an initial balance of 3 NEAR which will be transferred from the original to your new account.
near account create-account fund-myself transfer.$FT_CONTRACT_ID '3 NEAR' autogenerate-new-keypair save-to-legacy-keychain sign-as $FT_CONTRACT_ID network-config testnet sign-with-legacy-keychain send
Next, you'll want to export an environment variable for ease of development:
export TRANSFER_FT_CONTRACT_ID=transfer.$FT_CONTRACT_ID
Build the contract as you did in the previous tutorials:
cd 4.storage
cargo near build
Deployment and Initialization
It's time to deploy the contract, initialize it and mint the total supply. Let's once again create an initial supply of 1000 gtNEAR
.
cargo near deploy $TRANSFER_FT_CONTRACT_ID with-init-call new_default_meta json-args '{"owner_id": "'$TRANSFER_FT_CONTRACT_ID'", "total_supply": "1000000000000000000000000000"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send