Hello Token
This tutorial contains a solidity contract that can be deployed onto many EVM chains to form a fully functioning cross-chain application with the ability for users to request, from one contract, that tokens are sent to an address on a different chain.
Here is an example of a cross-chain borrow lending application that uses the topics covered in this tutorial!
Summary
Included in this repository is:
Example Solidity Code
Example Forge local testing setup
Testnet Deploy Scripts
Example Testnet testing setup
Environment Setup
Node 16.14.1 or later, npm 8.5.0 or later: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
forge 0.2.0 or later: https://book.getfoundry.sh/getting-started/installation
Testing Locally
Clone down the repo, cd into it, then build and run unit tests:
Expected output is
Deploying to Testnet
You will need a wallet with at least 0.05 Testnet AVAX and 0.01 Testnet CELO.
Testing on Testnet
You will need a wallet with at least 0.02 Testnet AVAX. Obtain testnet AVAX here
You must have also deployed contracts onto testnet (as described in the above section).
To test sending and receiving a message on testnet, execute the test as such:
Getting Started
Let's write a HelloToken contract that lets users send an arbitrary amount of an IERC20 token to an address of their choosing on another chain.
Valid Tokens
Before getting started, it is important to note that we use Wormhole's TokenBridge to transfer tokens between chains!
So, in order to send a token using the method in this example, the token must be attested onto the Token Bridge contract that lives on our desired target blockchain.
In the test above, when you run npm run deploy
, a mock token contract was both deployed and attested onto the target chain's Token Bridge contract.
If you wish to attest a token yourself for the TokenBridge, you may use the attestWorkflow function.
To check if a token already is attested onto a TokenBridge, call the wrappedAsset(uint16 tokenChainId, bytes32 tokenAddress)
function on the TokenBridge - this will return, if attested, the address of the wrapped token on this blockchain corresponding to the given token (from the source blockchain), and the 0 address if the input token hasn't been attested yet.
Wormhole Solidity SDK
To ease development, we'll make use of the Wormhole Solidity SDK.
Include this SDK in your own cross-chain application by running:
and import it in your contract:
This SDK provides helpers that make cross-chain development with Wormhole easier, and specifically provides us with the TokenSender and TokenReceiver abstract classes with useful functionality for sending and receiving tokens using TokenBridge
Implement Sending Function
Lets start by writing a function to send some amount of a token to a specific recipient on a target chain.
The body of this function will send the token as well as a payload to the HelloToken contract on the target chain. For our application, the payload will contain the intended recipient of the token, so that the target chain HelloToken contract can send the token to the intended recipient.
Note: TokenBridge only supports sending IERC20 tokens, and specifically only up to 8 decimals of a token. So, if your IERC20 token has 18 decimals, and you send
amount
of a token, you will receiveamount
rounded down to the nearest multiple of 10^10.
To send the token and payload to the HelloToken contract, we make use of the sendTokenWithPayloadToEvm
helper from the Wormhole Solidity SDK.
For a successful transfer, several things need to happen:
The user (or contract) who calls
sendCrossChainDeposit
should approve theHelloToken
contract to useamount
of the user's tokens. See how that is done in the forge test hereWe must transfer
amount
of the token from the user to theHelloToken
source contractIERC20(token).transferFrom(msg.sender, address(this), amount);
We must encode the recipient address into a payload
bytes memory payload = abi.encode(recipient);
We must ensure the correct amount of
msg.value
was passed in to send the token and payload.The cost to send a token is provided by the value returned by
wormhole.messageFee()
Currently this is 0 but may change in the future, so don't assume it will always be 0.The cost to request a relay depends on the gas amount and receiver value you will need.
(deliveryCost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, 0, GAS_LIMIT);
Implement Receiving Function
Now, we'll implement the TokenReceiver
abstract class - which is also included in the Wormhole Solidity SDK
After we call sendTokenWithPayloadToEvm
on the source chain, the message goes through the standard Wormhole message lifecycle. Once a VAA is available, the delivery provider will call receivePayloadAndTokens
on the target chain and target address specified, with the appropriate inputs.
The arguments payload
, sourceAddress
, sourceChain
, and deliveryHash
are all the same as on the normal receiveWormholeMessages
endpoint.
Let's delve into the fields that are provided to us in the TokenReceived
struct:
tokenHomeAddress The same as the
token
field in the call tosendTokenWithPayloadToEvm
, as that is the original address of the token unless the original token sent is a wormhole-wrapped token. In the case a wrapped token is sent, this will be the address of the original version of the token (on it’s native chain) in wormhole address format - i.e. left-padded with 12 zerostokenHomeChain The chain (in wormhole chain ID format) corresponding to the home address above - this will be the source chain, unless if the original token sent is a wormhole-wrapped asset, in which case it will be the chain of the unwrapped version of the token.
tokenAddress This is the address of the IERC20 token on this chain (the target chain) that has been transferred to this contract. If tokenHomeChain == this chain, this will be the same as tokenHomeAddress; otherwise, it will be the wormhole-wrapped version of the token sent.
amount This is the amount of the token that has been sent to you - the units being the same as the original token. Note that since TokenBridge only sends with 8 decimals of precision, if your token had 18 decimals, this will be the ‘amount’ you sent, rounded down to the nearest multiple of 10^10.
amountNormalized This is the amount of token divided by (1 if decimals ≤ 8, else 10^(decimals - 8))
Since all we intend to do is send the received token to the recipient, our fields of interest are payload (containing recipient), receivedTokens[0].tokenAddress (token we received), and receivedTokens[0].amount (amount of token we received and that we must send)
We can complete the implementation as follows:
Note: In this case, we don't need to prevent duplicate deliveries using the delivery hash, because TokenBridge already provides a form of duplicate prevention when redeeming sent tokens
And voila! We have a complete working example of a cross-chain application that uses TokenBridge to send and receive tokens!
Try cloning and running HelloToken to see this example work for yourself!
How do these Solidity Helpers Work?
Let’s walk through the details of sendTokenWithPayloadToEvm
and receivePayloadAndTokens
to see how they make use of the IWormholeRelayer interface and IWormholeReceiver interface to send and receive tokens.
Sending a Token
To send a token, we make use of the EVM TokenBridge contract, specifically the transferTokensWithPayload
method (implementation)
Note: We leave the
payload
field here blank because we are using thepayload
field on the IWormholeRelayer interface instead
TokenBridge implements this function by publishing a wormhole message to the blockchain logs that indicates that amount
of the token
was sent (with the intended address being recipient
on recipientChain
). TokenBridge then returns the sequence number of this published wormhole message.
The transferTokens
function in the Wormhole Solidity SDK makes use of this TokenBridge endpoint by
approving the TokenBridge to spend
amount
of our ERC20token
calling
transferTokensWithPayload
with the appropriate inputsreturning a
VaaKey
struct containing information about the published wormhole message for the token transfer
Now, it is our task to get the signed VAA corresponding to this published token bridge wormhole message to be delivered to our target chain HelloToken contract. To do this, we make use of the sendVaasToEvm endpoint in the IWormholeRelayer interface.
This allows us to specify existing wormhole message(s) and get the signed VAA(s) corresponding to those messages delivered to the targetAddress (in the additionalVaas
field of receiveWormholeMessages
).
Note: If you wish to send multiple different tokens along with the payload, the
sendTokenWithPayloadToEvm
helper as currently implemented will not help (as it sends only one token). However, you can still calltransferToken
twice and request delivery of both of those TokenBridge wormhole messages by providing twoVaaKey
structs in thevaaKeys
array. See an example of HelloToken with more than one token here.
Receiving a Token
We know that our sendVaasToEvm
call will cause receiveWormholeMessages
on targetAddress
to be called with
The payload as the encoded
recipient
addressThe
additionalVaas
field being an array of length 1, with the first element being the signed VAA corresponding to our token bridge transfer
Crucially, we don't have the transferred tokens yet! There are a few things that we need to do before gaining access to these tokens.
We parse the signed VAA, and check that
The emitterAddress of the VAA is a valid token bridge - i.e. the message was published by one of the TokenBridge contracts
The transfer was sent to this address
note: this step isn’t strictly necessary because the call to
completeTransferWithPayload
would fail if these were not true**We call
tokenBridge.completeTransferWithPayload
, passing the VAA - this completes the transfer of the tokens and causes us to receive the (potentially wormhole-wrapped) transferred tokenWe return a
TokenReceived
struct containing useful information about the transferWe call
receivePayloadAndTokens
with the appropriate inputs
See the full implementation of the Wormhole Relayer SDK helpers here
Also, see a version of HelloToken implemented without any Wormhole Relayer SDK helpers here
as well as a version of HelloToken where native currency is deposited here
Wormhole integration complete?
Let us know so we can list your project in our ecosystem directory and introduce you to our global, multichain community!
Last updated