Exercise #1: Transfer Tokens
Transfer tokens between chains using Chainlink CCIP
Last updated
Transfer tokens between chains using Chainlink CCIP
Last updated
Now that we learned the basics of Chainlink CCIP let's use that knowledge to bridge tokens from one blockchain to another. In exercise number one of this Masterclass, you will use Chainlink CCIP to transfer tokens from a smart contract to an EOA on a different blockchain.
To transfer tokens using CCIP, token pools on both blockchains must exist. That's why, if you remember the first chapter of this Masterclass, we said that you can transfer only supported tokens, not all of them.
We are going to cover Token Pools later in the CCIP Architecture in Depth chapter, but briefly, the CCIP token bridge can support multiple token handling mechanisms at source & destination blockchains. Token handling mechanisms are a key aspect of how token transfers work. They each have different characteristics with trade-offs for issuers, holders, and DeFi applications.
Burn & Mint Tokens are burned on the source chain and minted natively on the destination chain
Lock & Mint (Reverse: Burn & Unlock) Tokens are locked on the source chain (in Token Pools), and wrapped/synthetic/derivative tokens that represent the locked tokens are minted on the destination chain.
Lock & Unlock [ON THE ROADMAP] Transferred tokens are locked on the source chain (in Token Pools) and unlocked from Token Pools on the destination chain. This feature is not live yet.
Public faucets sometimes limit how many tokens a user can create and token pools might not have enough liquidity. To resolve these issues, CCIP supports two test tokens that you can mint permissionlessly so you don't run out of tokens while testing different scenarios.
As already said, there are two ERC20 test tokens currently available on each testnet - CCIP-BnM and CCIP-LnM (and its wrapped/synthetic representation, clCCIP-LnM).
Name | Decimals | Type |
---|---|---|
These tokens are minted natively on each testnet because the token contract is deployed on each testnet. When transferring these tokens between testnet blockchains, CCIP burns the tokens on the source chain and mints them on the destination chain.
These tokens are only minted on Ethereum Sepolia because the token contract is only deployed to Sepolia. On other testnet blockchains, the token is represented as a wrapped/synthetic asset called clCCIP-LnM because it is not natively deployed to other testnets. When transferring these tokens from Ethereum Sepolia to another testnet, CCIP locks the CCIP-LnM tokens on the source chain and mints the wrapped representation clCCIP-LnM on the destination chain. Between non-Ethereum Sepolia chains, CCIP burns and mints the wrapped representation clCCIP-LnM.
You can mint both of these tokens using the drip
function call on the token contract. This function acts like a faucet. Each call mints 10**18 units of a token to the specified address.
For CCIP-BnM, you can call drip
on all testnet blockchains.
For CCIP-LnM, you can call drip
only on Ethereum Sepolia.
The drip
function is implemented on these tokens as follows:
function drip(address to) external {
_mint(to, 1e18);
}
To call this function, you can use Block Explorer like Etherscan, Foundry's cast send
command, and more... But the easiest way to get those tokens is probably through the Official Chainlink Documentation. Navigate to the linked documentation page and connect your wallet by clicking the "Connect Wallet" button.
Once connected, switch to Avalanche Fuji Testnet and mint 1 CCIP-BnM token. You should also add the CCIP-BnM token to your MetaMask using the button.
In this exercise, you will transfer CCIP-BnM tokens from a smart contract on Avalanche Fuji to an Externally Owned Account on Ethereum Sepolia. Obviously, you can reuse this contract for different lanes and for transferring other supported tokens as well.
You should already have @chianlink/contracts
and @chainlink/contracts-ccip
NPM packages installed. If not, please install them by referring to The @chainlink/contracts-ccip NPM package and Develop CCIP Sender contract subchapters.
Create a new file inside the contracts
folder and name it CCIPTokenSender.sol
Start with the development by setting the Solidity compiler version and importing necessary contracts from the @chainlink/contracts-ccip
NPM package.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
contract CCIPTokenSender {}
Now let's add storage variables for the Router.sol
smart contract address and theLINK
token, which we will use for fees. Also, we imported the OwnerIsCreator
smart contract because later, we will want to allow onlyOwner
to withdraw tokens from this contract (both CCIP-BnM & LINK).
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
contract CCIPTokenSender is OwnerIsCreator {
IRouterClient router;
LinkTokenInterface linkToken;
constructor(address _router, address _link) {
router = IRouterClient(_router);
linkToken = LinkTokenInterface(_link);
}
}
Let's develop a function for sending tokens
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
contract CCIPTokenSender is OwnerIsCreator {
IRouterClient router;
LinkTokenInterface linkToken;
error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees);
event TokensTransferred(
bytes32 indexed messageId, // The unique ID of the message.
uint64 indexed destinationChainSelector, // The chain selector of the destination chain.
address receiver, // The address of the receiver on the destination chain.
address token, // The token address that was transferred.
uint256 tokenAmount, // The token amount that was transferred.
address feeToken, // the token address used to pay CCIP fees.
uint256 fees // The fees paid for sending the message.
);
constructor(address _router, address _link) {
router = IRouterClient(_router);
linkToken = LinkTokenInterface(_link);
}
function transferTokens(
uint64 _destinationChainSelector,
address _receiver,
address _token,
uint256 _amount
)
external
returns (bytes32 messageId)
{
Client.EVMTokenAmount[]
memory tokenAmounts = new Client.EVMTokenAmount[](1);
Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
token: _token,
amount: _amount
});
tokenAmounts[0] = tokenAmount;
// Build the CCIP Message
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(_receiver),
data: "",
tokenAmounts: tokenAmounts,
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 0, strict: false})
),
feeToken: address(linkToken)
});
// CCIP Fees Management
uint256 fees = router.getFee(_destinationChainSelector, message);
if (fees > linkToken.balanceOf(address(this)))
revert NotEnoughBalance(linkToken.balanceOf(address(this)), fees);
linkToken.approve(address(router), fees);
// Approve Router to spend CCIP-BnM tokens we send
IERC20(_token).approve(address(router), _amount);
// Send CCIP Message
messageId = router.ccipSend(_destinationChainSelector, message);
emit TokensTransferred(
messageId,
_destinationChainSelector,
_receiver,
_token,
_amount,
address(linkToken),
fees
);
}
}
Before calling the Router's ccipSend
function, ensure that your code allows users to send CCIP messages to trusted destination chains. We will also restrict that only Owner can transfer tokens from this contract.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
contract CCIPTokenSender is OwnerIsCreator {
IRouterClient router;
LinkTokenInterface linkToken;
mapping(uint64 => bool) public whitelistedChains;
error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees);
error DestinationChainNotWhitelisted(uint64 destinationChainSelector);
event TokensTransferred(
bytes32 indexed messageId, // The unique ID of the message.
uint64 indexed destinationChainSelector, // The chain selector of the destination chain.
address receiver, // The address of the receiver on the destination chain.
address token, // The token address that was transferred.
uint256 tokenAmount, // The token amount that was transferred.
address feeToken, // the token address used to pay CCIP fees.
uint256 fees // The fees paid for sending the message.
);
modifier onlyWhitelistedChain(uint64 _destinationChainSelector) {
if (!whitelistedChains[_destinationChainSelector])
revert DestinationChainNotWhitelisted(_destinationChainSelector);
_;
}
constructor(address _router, address _link) {
router = IRouterClient(_router);
linkToken = LinkTokenInterface(_link);
}
function whitelistChain(
uint64 _destinationChainSelector
) external onlyOwner {
whitelistedChains[_destinationChainSelector] = true;
}
function denylistChain(
uint64 _destinationChainSelector
) external onlyOwner {
whitelistedChains[_destinationChainSelector] = false;
}
function transferTokens(
uint64 _destinationChainSelector,
address _receiver,
address _token,
uint256 _amount
)
external
onlyOwner
onlyWhitelistedChain(_destinationChainSelector)
returns (bytes32 messageId)
{
Client.EVMTokenAmount[]
memory tokenAmounts = new Client.EVMTokenAmount[](1);
Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
token: _token,
amount: _amount
});
tokenAmounts[0] = tokenAmount;
// Build the CCIP Message
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(_receiver),
data: "",
tokenAmounts: tokenAmounts,
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 0, strict: false})
),
feeToken: address(linkToken)
});
// CCIP Fees Management
uint256 fees = router.getFee(_destinationChainSelector, message);
if (fees > linkToken.balanceOf(address(this)))
revert NotEnoughBalance(linkToken.balanceOf(address(this)), fees);
linkToken.approve(address(router), fees);
// Approve Router to spend CCIP-BnM tokens we send
IERC20(_token).approve(address(router), _amount);
// Send CCIP Message
messageId = router.ccipSend(_destinationChainSelector, message);
emit TokensTransferred(
messageId,
_destinationChainSelector,
_receiver,
_token,
_amount,
address(linkToken),
fees
);
}
}
Finally, let's develop a function to withdraw tokens from this contract.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
contract CCIPTokenSender is OwnerIsCreator {
IRouterClient router;
LinkTokenInterface linkToken;
mapping(uint64 => bool) public whitelistedChains;
error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees);
error DestinationChainNotWhitelisted(uint64 destinationChainSelector);
error NothingToWithdraw();
event TokensTransferred(
bytes32 indexed messageId, // The unique ID of the message.
uint64 indexed destinationChainSelector, // The chain selector of the destination chain.
address receiver, // The address of the receiver on the destination chain.
address token, // The token address that was transferred.
uint256 tokenAmount, // The token amount that was transferred.
address feeToken, // the token address used to pay CCIP fees.
uint256 fees // The fees paid for sending the message.
);
modifier onlyWhitelistedChain(uint64 _destinationChainSelector) {
if (!whitelistedChains[_destinationChainSelector])
revert DestinationChainNotWhitelisted(_destinationChainSelector);
_;
}
constructor(address _router, address _link) {
router = IRouterClient(_router);
linkToken = LinkTokenInterface(_link);
}
function whitelistChain(
uint64 _destinationChainSelector
) external onlyOwner {
whitelistedChains[_destinationChainSelector] = true;
}
function denylistChain(
uint64 _destinationChainSelector
) external onlyOwner {
whitelistedChains[_destinationChainSelector] = false;
}
function transferTokens(
uint64 _destinationChainSelector,
address _receiver,
address _token,
uint256 _amount
)
external
onlyOwner
onlyWhitelistedChain(_destinationChainSelector)
returns (bytes32 messageId)
{
Client.EVMTokenAmount[]
memory tokenAmounts = new Client.EVMTokenAmount[](1);
Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
token: _token,
amount: _amount
});
tokenAmounts[0] = tokenAmount;
// Build the CCIP Message
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(_receiver),
data: "",
tokenAmounts: tokenAmounts,
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 0, strict: false})
),
feeToken: address(linkToken)
});
// CCIP Fees Management
uint256 fees = router.getFee(_destinationChainSelector, message);
if (fees > linkToken.balanceOf(address(this)))
revert NotEnoughBalance(linkToken.balanceOf(address(this)), fees);
linkToken.approve(address(router), fees);
// Approve Router to spend CCIP-BnM tokens we send
IERC20(_token).approve(address(router), _amount);
// Send CCIP Message
messageId = router.ccipSend(_destinationChainSelector, message);
emit TokensTransferred(
messageId,
_destinationChainSelector,
_receiver,
_token,
_amount,
address(linkToken),
fees
);
}
function withdrawToken(
address _beneficiary,
address _token
) public onlyOwner {
uint256 amount = IERC20(_token).balanceOf(address(this));
if (amount == 0) revert NothingToWithdraw();
IERC20(_token).transfer(_beneficiary, amount);
}
}
You should already have @chianlink/contracts
and @chainlink/contracts-ccip
NPM packages installed. If not, please install them by referring to The @chainlink/contracts-ccip NPM package and Develop CCIP Sender contract subchapters.
Create a new file inside the src
folder and name it CCIPTokenSender.sol
Start with the development by setting the Solidity compiler version and importing necessary contracts from the @chainlink/contracts-ccip
NPM package.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
contract CCIPTokenSender {}
Now let's add storage variables for the Router.sol
smart contract address and theLINK
token, which we will use for fees. Also, we imported the OwnerIsCreator
smart contract because later, we will want to allow onlyOwner
to withdraw tokens from this contract (both CCIP-BnM & LINK).
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
contract CCIPTokenSender is OwnerIsCreator {
IRouterClient router;
LinkTokenInterface linkToken;
constructor(address _router, address _link) {
router = IRouterClient(_router);
linkToken = LinkTokenInterface(_link);
}
}
Let's develop a function for sending tokens
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
contract CCIPTokenSender is OwnerIsCreator {
IRouterClient router;
LinkTokenInterface linkToken;
error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees);
event TokensTransferred(
bytes32 indexed messageId, // The unique ID of the message.
uint64 indexed destinationChainSelector, // The chain selector of the destination chain.
address receiver, // The address of the receiver on the destination chain.
address token, // The token address that was transferred.
uint256 tokenAmount, // The token amount that was transferred.
address feeToken, // the token address used to pay CCIP fees.
uint256 fees // The fees paid for sending the message.
);
constructor(address _router, address _link) {
router = IRouterClient(_router);
linkToken = LinkTokenInterface(_link);
}
function transferTokens(
uint64 _destinationChainSelector,
address _receiver,
address _token,
uint256 _amount
)
external
returns (bytes32 messageId)
{
Client.EVMTokenAmount[]
memory tokenAmounts = new Client.EVMTokenAmount[](1);
Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
token: _token,
amount: _amount
});
tokenAmounts[0] = tokenAmount;
// Build the CCIP Message
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(_receiver),
data: "",
tokenAmounts: tokenAmounts,
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 0, strict: false})
),
feeToken: address(linkToken)
});
// CCIP Fees Management
uint256 fees = router.getFee(_destinationChainSelector, message);
if (fees > linkToken.balanceOf(address(this)))
revert NotEnoughBalance(linkToken.balanceOf(address(this)), fees);
linkToken.approve(address(router), fees);
// Approve Router to spend CCIP-BnM tokens we send
IERC20(_token).approve(address(router), _amount);
// Send CCIP Message
messageId = router.ccipSend(_destinationChainSelector, message);
emit TokensTransferred(
messageId,
_destinationChainSelector,
_receiver,
_token,
_amount,
address(linkToken),
fees
);
}
}
Before calling the Router's ccipSend
function, ensure that your code allows users to send CCIP messages to trusted destination chains. We will also restrict that only Owner can transfer tokens from this contract.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
contract CCIPTokenSender is OwnerIsCreator {
IRouterClient router;
LinkTokenInterface linkToken;
mapping(uint64 => bool) public whitelistedChains;
error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees);
error DestinationChainNotWhitelisted(uint64 destinationChainSelector);
event TokensTransferred(
bytes32 indexed messageId, // The unique ID of the message.
uint64 indexed destinationChainSelector, // The chain selector of the destination chain.
address receiver, // The address of the receiver on the destination chain.
address token, // The token address that was transferred.
uint256 tokenAmount, // The token amount that was transferred.
address feeToken, // the token address used to pay CCIP fees.
uint256 fees // The fees paid for sending the message.
);
modifier onlyWhitelistedChain(uint64 _destinationChainSelector) {
if (!whitelistedChains[_destinationChainSelector])
revert DestinationChainNotWhitelisted(_destinationChainSelector);
_;
}
constructor(address _router, address _link) {
router = IRouterClient(_router);
linkToken = LinkTokenInterface(_link);
}
function whitelistChain(
uint64 _destinationChainSelector
) external onlyOwner {
whitelistedChains[_destinationChainSelector] = true;
}
function denylistChain(
uint64 _destinationChainSelector
) external onlyOwner {
whitelistedChains[_destinationChainSelector] = false;
}
function transferTokens(
uint64 _destinationChainSelector,
address _receiver,
address _token,
uint256 _amount
)
external
onlyOwner
onlyWhitelistedChain(_destinationChainSelector)
returns (bytes32 messageId)
{
Client.EVMTokenAmount[]
memory tokenAmounts = new Client.EVMTokenAmount[](1);
Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
token: _token,
amount: _amount
});
tokenAmounts[0] = tokenAmount;
// Build the CCIP Message
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(_receiver),
data: "",
tokenAmounts: tokenAmounts,
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 0, strict: false})
),
feeToken: address(linkToken)
});
// CCIP Fees Management
uint256 fees = router.getFee(_destinationChainSelector, message);
if (fees > linkToken.balanceOf(address(this)))
revert NotEnoughBalance(linkToken.balanceOf(address(this)), fees);
linkToken.approve(address(router), fees);
// Approve Router to spend CCIP-BnM tokens we send
IERC20(_token).approve(address(router), _amount);
// Send CCIP Message
messageId = router.ccipSend(_destinationChainSelector, message);
emit TokensTransferred(
messageId,
_destinationChainSelector,
_receiver,
_token,
_amount,
address(linkToken),
fees
);
}
}
Finally, let's develop a function to withdraw tokens from this contract.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
contract CCIPTokenSender is OwnerIsCreator {
IRouterClient router;
LinkTokenInterface linkToken;
mapping(uint64 => bool) public whitelistedChains;
error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees);
error DestinationChainNotWhitelisted(uint64 destinationChainSelector);
error NothingToWithdraw();
event TokensTransferred(
bytes32 indexed messageId, // The unique ID of the message.
uint64 indexed destinationChainSelector, // The chain selector of the destination chain.
address receiver, // The address of the receiver on the destination chain.
address token, // The token address that was transferred.
uint256 tokenAmount, // The token amount that was transferred.
address feeToken, // the token address used to pay CCIP fees.
uint256 fees // The fees paid for sending the message.
);
modifier onlyWhitelistedChain(uint64 _destinationChainSelector) {
if (!whitelistedChains[_destinationChainSelector])
revert DestinationChainNotWhitelisted(_destinationChainSelector);
_;
}
constructor(address _router, address _link) {
router = IRouterClient(_router);
linkToken = LinkTokenInterface(_link);
}
function whitelistChain(
uint64 _destinationChainSelector
) external onlyOwner {
whitelistedChains[_destinationChainSelector] = true;
}
function denylistChain(
uint64 _destinationChainSelector
) external onlyOwner {
whitelistedChains[_destinationChainSelector] = false;
}
function transferTokens(
uint64 _destinationChainSelector,
address _receiver,
address _token,
uint256 _amount
)
external
onlyOwner
onlyWhitelistedChain(_destinationChainSelector)
returns (bytes32 messageId)
{
Client.EVMTokenAmount[]
memory tokenAmounts = new Client.EVMTokenAmount[](1);
Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
token: _token,
amount: _amount
});
tokenAmounts[0] = tokenAmount;
// Build the CCIP Message
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(_receiver),
data: "",
tokenAmounts: tokenAmounts,
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 0, strict: false})
),
feeToken: address(linkToken)
});
// CCIP Fees Management
uint256 fees = router.getFee(_destinationChainSelector, message);
if (fees > linkToken.balanceOf(address(this)))
revert NotEnoughBalance(linkToken.balanceOf(address(this)), fees);
linkToken.approve(address(router), fees);
// Approve Router to spend CCIP-BnM tokens we send
IERC20(_token).approve(address(router), _amount);
// Send CCIP Message
messageId = router.ccipSend(_destinationChainSelector, message);
emit TokensTransferred(
messageId,
_destinationChainSelector,
_receiver,
_token,
_amount,
address(linkToken),
fees
);
}
function withdrawToken(
address _beneficiary,
address _token
) public onlyOwner {
uint256 amount = IERC20(_token).balanceOf(address(this));
if (amount == 0) revert NothingToWithdraw();
IERC20(_token).transfer(_beneficiary, amount);
}
}
Create a new Solidity file by clicking on the "Create new file" button and name it CCIPTokenSender.sol
Start with the development by setting the Solidity compiler version and importing necessary contracts from the @chainlink/contracts-ccip
NPM package.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
contract CCIPTokenSender {}
Now let's add storage variables for the Router.sol
smart contract address and theLINK
token, which we will use for fees. Also, we imported the OwnerIsCreator
smart contract because later, we will want to allow onlyOwner
to withdraw tokens from this contract (both CCIP-BnM & LINK).
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
contract CCIPTokenSender is OwnerIsCreator {
IRouterClient router;
LinkTokenInterface linkToken;
constructor(address _router, address _link) {
router = IRouterClient(_router);
linkToken = LinkTokenInterface(_link);
}
}
Let's develop a function for sending tokens
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
contract CCIPTokenSender is OwnerIsCreator {
IRouterClient router;
LinkTokenInterface linkToken;
error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees);
event TokensTransferred(
bytes32 indexed messageId, // The unique ID of the message.
uint64 indexed destinationChainSelector, // The chain selector of the destination chain.
address receiver, // The address of the receiver on the destination chain.
address token, // The token address that was transferred.
uint256 tokenAmount, // The token amount that was transferred.
address feeToken, // the token address used to pay CCIP fees.
uint256 fees // The fees paid for sending the message.
);
constructor(address _router, address _link) {
router = IRouterClient(_router);
linkToken = LinkTokenInterface(_link);
}
function transferTokens(
uint64 _destinationChainSelector,
address _receiver,
address _token,
uint256 _amount
)
external
returns (bytes32 messageId)
{
Client.EVMTokenAmount[]
memory tokenAmounts = new Client.EVMTokenAmount[](1);
Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
token: _token,
amount: _amount
});
tokenAmounts[0] = tokenAmount;
// Build the CCIP Message
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(_receiver),
data: "",
tokenAmounts: tokenAmounts,
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 0, strict: false})
),
feeToken: address(linkToken)
});
// CCIP Fees Management
uint256 fees = router.getFee(_destinationChainSelector, message);
if (fees > linkToken.balanceOf(address(this)))
revert NotEnoughBalance(linkToken.balanceOf(address(this)), fees);
linkToken.approve(address(router), fees);
// Approve Router to spend CCIP-BnM tokens we send
IERC20(_token).approve(address(router), _amount);
// Send CCIP Message
messageId = router.ccipSend(_destinationChainSelector, message);
emit TokensTransferred(
messageId,
_destinationChainSelector,
_receiver,
_token,
_amount,
address(linkToken),
fees
);
}
}
Before calling the Router's ccipSend
function, ensure that your code allows users to send CCIP messages to trusted destination chains. We will also restrict that only Owner can transfer tokens from this contract.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
contract CCIPTokenSender is OwnerIsCreator {
IRouterClient router;
LinkTokenInterface linkToken;
mapping(uint64 => bool) public whitelistedChains;
error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees);
error DestinationChainNotWhitelisted(uint64 destinationChainSelector);
event TokensTransferred(
bytes32 indexed messageId, // The unique ID of the message.
uint64 indexed destinationChainSelector, // The chain selector of the destination chain.
address receiver, // The address of the receiver on the destination chain.
address token, // The token address that was transferred.
uint256 tokenAmount, // The token amount that was transferred.
address feeToken, // the token address used to pay CCIP fees.
uint256 fees // The fees paid for sending the message.
);
modifier onlyWhitelistedChain(uint64 _destinationChainSelector) {
if (!whitelistedChains[_destinationChainSelector])
revert DestinationChainNotWhitelisted(_destinationChainSelector);
_;
}
constructor(address _router, address _link) {
router = IRouterClient(_router);
linkToken = LinkTokenInterface(_link);
}
function whitelistChain(
uint64 _destinationChainSelector
) external onlyOwner {
whitelistedChains[_destinationChainSelector] = true;
}
function denylistChain(
uint64 _destinationChainSelector
) external onlyOwner {
whitelistedChains[_destinationChainSelector] = false;
}
function transferTokens(
uint64 _destinationChainSelector,
address _receiver,
address _token,
uint256 _amount
)
external
onlyOwner
onlyWhitelistedChain(_destinationChainSelector)
returns (bytes32 messageId)
{
Client.EVMTokenAmount[]
memory tokenAmounts = new Client.EVMTokenAmount[](1);
Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
token: _token,
amount: _amount