Writing the Contract
First, head over to this GitHub template and create a new repository using it.
Clone the repository and set up the starter code in your workspace:
git clone git@github.com:<your username>/xToken.git
cd xToken/
npm i
Don’t worry about the deployment scripts and tests; we’ve got you covered. 🎉 Now, let’s get to the fun part—bridging tokens!
Step 1: Setting Up the Basics 📝
Kick things off by declaring the license and importing the necessary libraries:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "wormhole-solidity-sdk/WormholeRelayerSDK.sol";
Just like in our Cross Chain Mailbox tutorial, we’re using the wormhole-solidity-sdk
for seamless cross-chain functionality.
Step 2: Crafting the Function Signature 🛠️
Now, let’s define the function that will handle the cross-chain deposit quotes:
function quoteCrossChainDeposit(uint16 targetChain) public view returns (uint256 cost) {
// Function Body
}
This function is public and read-only, meaning it won’t alter the blockchain’s state. It accepts a uint16
parameter, targetChain
, and returns a uint256
variable named cost
.
Step 3: Introducing Local Variables 📦
uint256 deliveryCost;
Within the function, go ahead and declare a local variable named deliveryCost
of type uint256
. This variable will store the expense associated with transferring the token and payload to the designated chain.
Step 4: Fetch the Delivery Cost 📬
(deliveryCost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, 0, GAS_LIMIT);
Utilize the quoteEVMDeliveryPrice
method from the wormholeRelayer
object to fetch the delivery cost. This function requires three arguments: targetChain
, 0
, and GAS_LIMIT
. The result will be stored in deliveryCost
.
Step 5: Crunching the Numbers for Total Cost 🧮
Now, let’s compute the total cost of the cross-chain deposit by summing up deliveryCost
and wormhole.messageFee()
.
function quoteCrossChainDeposit(uint16 targetChain) public view returns (uint256 cost) {
uint256 deliveryCost;
(deliveryCost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, 0, GAS_LIMIT);
cost = deliveryCost + wormhole.messageFee();
}
Step 6: Function Signature and Cost Calculation 🖋️
Begin by setting the function signature to public payable
and outline its parameters. Then, invoke the quoteCrossChainDeposit
function to obtain the total cost for the cross-chain deposit.
function sendCrossChainDeposit(
uint16 targetChain,
address targetHelloToken,
address recipient,
uint256 amount,
address token
) public payable {
uint256 cost = quoteCrossChainDeposit(targetChain);
}
Use the require
statement to confirm that the Ether sent (msg.value
) aligns with the quoted cost. This validation ensures that the user has dispatched the accurate amount of Ether to cover the transaction fees.
require(msg.value == cost, "msg.value must be quoteCrossChainDeposit(targetChain)");
Step 7: Token Transfer and Payload Encoding 🔄
IERC20(token).transferFrom(msg.sender, address(this), amount);
Leverage the transferFrom
function from the IERC20
interface to move tokens from the sender to this contract. Next, encode the recipient
address into bytes using abi.encode
.
bytes memory payload = abi.encode(recipient);
Step 8: Sending Tokens and Payload to Target Chain 🚀
sendTokenWithPayloadToEvm(
targetChain,
targetHelloToken,
payload,
0,
GAS_LIMIT,
token,
amount
);
Utilize the sendTokenWithPayloadToEvm
function to dispatch the token and payload to the destination chain. Pass in all the necessary parameters like targetChain
, targetHelloToken
, payload
, 0
for receiver value, GAS_LIMIT
, token
, and amount
.
Step 9: Internal Function with Security Measures 🛡️
function receivePayloadAndTokens(
bytes memory payload,
TokenReceived[] memory receivedTokens,
bytes32, // sourceAddress
uint16, // sourceChain
bytes32 // deliveryHash
) internal override onlyWormholeRelayer {
require(receivedTokens.length == 1, "Expected 1 token transfers");
}
Declare the function as internal
and override
, and specify its parameters. Add the onlyWormholeRelayer
modifier to make sure that only the Wormhole Relayer can invoke this function. Use a require
statement to validate that only a single token transfer occurs in the receivedTokens
array.
Step 10: Decoding Payload and Final Transfer 📦
address recipient = abi.decode(payload, (address));
Decode the payload
to retrieve the recipient
address using abi.decode
. Then, execute the transfer
function from the IERC20
interface to send the received token to the designated recipient.
IERC20(receivedTokens[0].tokenAddress).transfer(recipient, receivedTokens[0].amount);
And finally close your contract with a }
.
If everything’s been good so far, here’s what your final code should look like:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "wormhole-solidity-sdk/WormholeRelayerSDK.sol";
contract HelloToken is TokenSender, TokenReceiver {
uint256 constant GAS_LIMIT = 250_000;
constructor(address _wormholeRelayer, address _tokenBridge, address _wormhole)
TokenBase(_wormholeRelayer, _tokenBridge, _wormhole)
{}
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);
// Total cost: delivery cost + cost of publishing the 'sending token' wormhole message
cost = deliveryCost + wormhole.messageFee();
}
function sendCrossChainDeposit(
uint16 targetChain,
address targetHelloToken,
address recipient,
uint256 amount,
address token
) public payable {
uint256 cost = quoteCrossChainDeposit(targetChain);
require(msg.value == cost, "msg.value must be 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 to
payload,
0, // receiver value
GAS_LIMIT,
token, // address of IERC20 token contract
amount
);
}
function receivePayloadAndTokens(
bytes memory payload,
TokenReceived[] memory receivedTokens,
bytes32, // sourceAddress
uint16,
bytes32 // deliveryHash
) internal override onlyWormholeRelayer {
require(receivedTokens.length == 1, "Expected 1 token transfers");
address recipient = abi.decode(payload, (address));
IERC20(receivedTokens[0].tokenAddress).transfer(recipient, receivedTokens[0].amount);
}
}