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!
- 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: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
Clone down the repo, cd into it, then build and run unit tests:
git clone https://github.com/wormhole-foundation/hello-token.git
cd hello-token
npm run build
forge test
Expected output is
Running 1 test for test/HelloToken.t.sol:HelloTokenTest
[PASS] testCrossChainDeposit() (gas: 1338038)
Test result: ok. 1 passed; 0 failed; finished in 5.64s
You will need a wallet with at least 0.05 Testnet AVAX and 0.01 Testnet CELO.
EVM_PRIVATE_KEY=your_wallet_private_key npm run deploy
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:
EVM_PRIVATE_KEY=your_wallet_private_key npm run test
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.
Include this SDK in your own cross-chain application by running:
forge install wormhole-foundation/wormhole-solidity-sdk
and import it in your contract:
import "wormhole-solidity-sdk/WormholeRelayerSDK.sol";
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.
function sendCrossChainDeposit(
uint16 targetChain, // A wormhole chain id
address targetHelloToken, // address of HelloToken contract on targetChain
address recipient,
uint256 amount,
address token
) public payable;
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 sendamount
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 here - We 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);
function sendCrossChainDeposit(
uint16 targetChain,
address targetHelloToken,
address recipient,
uint256 amount,
address token
) public payable {
uint256 cost = quoteCrossChainDeposit(targetChain);
require(msg.value == cost,
"msg.value != quoteCrossChainDeposit(targetChain)");
IERC20(token).transferFrom(msg.sender, address(this), amount);
bytes memory payload = abi.encode(recipient);
sendTokenWithPayloadToEvm(
targetChain,
targetHelloToken, // address (on targetChain) to send token and payload
payload,
0, // receiver value
GAS_LIMIT,
token, // address of IERC20 token contract
amount
);
}
function quoteCrossChainDeposit(uint16 targetChain)
public view returns (uint256 cost) {
// Cost of delivering token and payload to targetChain
uint256 deliveryCost;
(deliveryCost,) =
wormholeRelayer.quoteEVMDeliveryPrice(targetChain, 0, GAS_LIMIT);