Hello Token
Last updated
Was this helpful?
Last updated
Was this helpful?
This tutorial contains a 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 that uses the topics covered in this tutorial!
Included in this is:
Example Solidity Code
Example Forge local testing setup
Testnet Deploy Scripts
Example Testnet testing setup
Node 16.14.1 or later, npm 8.5.0 or later:
forge 0.2.0 or later:
Clone down the repo, cd into it, then build and run unit tests:
Expected output is
You will need a wallet with at least 0.05 Testnet AVAX and 0.01 Testnet CELO.
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:
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.
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.
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
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:
We must transfer amount
of the token from the user to the HelloToken
source contract IERC20(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);
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:
tokenHomeChain 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
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.
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 ERC20 token
calling transferTokensWithPayload
with the appropriate inputs
returning a VaaKey
struct containing information about the published wormhole message for the token transfer
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
).
We know that our sendVaasToEvm
call will cause receiveWormholeMessages
on targetAddress
to be called with
The payload as the encoded recipient
address
The 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 token
We return a TokenReceived
struct containing useful information about the transfer
We call receivePayloadAndTokens
with the appropriate inputs
You will need a wallet with at least 0.02 Testnet AVAX.
Let's write a that lets users send an arbitrary amount of an IERC20 token to an address of their choosing on another chain.
If you wish to attest a token yourself for the TokenBridge, you may use the function.
Off chain: using the Wormhole Chain ID, Emitter address (TokenBridge address) and sequence number from the LogMessage
event.
To ease development, we'll make use of the .
The user (or contract) who calls sendCrossChainDeposit
should approve the HelloToken
contract to use amount
of the user's tokens. See how that is done in the forge test
tokenHomeAddress The same as the token
field in the call to sendTokenWithPayloadToEvm
, 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 - i.e. left-padded with 12 zeros
And voila! We have a of a cross-chain application that uses TokenBridge to send and receive tokens!
Try to see this example work for yourself!
To send a token, we make use of the EVM TokenBridge contract, specifically the transferTokensWithPayload
method ()
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 endpoint in the IWormholeRelayer interface.
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 call transferToken
twice and request delivery of both of those TokenBridge wormhole messages by providing two VaaKey
structs in the vaaKeys
array. See an example of HelloToken with more than one token .
See the full implementation of the Wormhole Relayer SDK helpers
Also, see a version of HelloToken implemented without any Wormhole Relayer SDK helpers
as well as a version of HelloToken where native currency is deposited