Exercise #2: Transfer Tokens & Data
Mint an NFT on one chain and pay for minting on another
Getting started
In this exercise, we are going to reuse and expand the smart contract from the previous exercise. We will send more than just a simple text message. To mint an NFT on the destination chain, we will send the minting price amount of tokens & ABI encoded NFT contract's mint
function signature as a data parameter of the CCIP Message.
Adjust CCIPTokenSender.sol from Exercise #1
Three adjustments that need to be made are:
Renaming
CCIPTokenSender.sol
toCCIPTokenAndDataSender.sol
Provide
abi.encodeWithSignature("mint(address)", msg.sender)
asdata
parameter of the CCIP Message instead of an empty stringIncrease
gasLimit
from 0 to 200_000.
Create a new file inside the contracts
folder and name it CCIPTokenAndDataSender.sol
, and after that, apply the above adjustments.
// 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 CCIPTokenAndDataSender 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: abi.encodeWithSignature("mint(address)", msg.sender),
tokenAmounts: tokenAmounts,
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 200_000, 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 file inside the src
folder and name it CCIPTokenAndDataSender.sol
, and after that, apply the above adjustments.
// 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 CCIPTokenAndDataSender 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: abi.encodeWithSignature("mint(address)", msg.sender),
tokenAmounts: tokenAmounts,
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 200_000, 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, name it CCIPTokenAndDataSender.sol
, and after that, apply the above adjustments.
// 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 CCIPTokenAndDataSender 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: abi.encodeWithSignature("mint(address)", msg.sender),
tokenAmounts: tokenAmounts,
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 200_000, 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);
}
}
Develop the CCIPTokenAndDataReceiver.sol smart contract
Let's now develop the CCIP Receiver smart contract alongside the simple NFT smart contract.
Create a new file inside the contracts
folder and name it CCIPTokenAndDataReceiver.sol
To compile the following contract, we must install the @openzeppelin/contracts
package first.
Run:
npm i --save-dev @openzeppelin/contracts
Start with the development by setting the Solidity compiler version and importing necessary contracts from the @chainlink/contracts-ccip
NPM package. Then implement a basic NFT smart contract using the OpenZeppelin library.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract MyNFT is ERC721URIStorage, OwnerIsCreator {
string constant TOKEN_URI =
"https://ipfs.io/ipfs/QmYuKY45Aq87LeL1R5dhb1hqHLp6ZFbJaCP8jxqKM1MX6y/babe_ruth_1.json";
uint256 internal tokenId;
constructor() ERC721("MyNFT", "MNFT") {}
function mint(address to) public onlyOwner {
_safeMint(to, tokenId);
_setTokenURI(tokenId, TOKEN_URI);
unchecked {
tokenId++;
}
}
}
Now we can add the CCIP Receiver contract logic
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract MyNFT is ERC721URIStorage, OwnerIsCreator {
string constant TOKEN_URI =
"https://ipfs.io/ipfs/QmYuKY45Aq87LeL1R5dhb1hqHLp6ZFbJaCP8jxqKM1MX6y/babe_ruth_1.json";
uint256 internal tokenId;
constructor() ERC721("MyNFT", "MNFT") {}
function mint(address to) public onlyOwner {
_safeMint(to, tokenId);
_setTokenURI(tokenId, TOKEN_URI);
unchecked {
tokenId++;
}
}
}
contract CCIPTokenAndDataReceiver is CCIPReceiver, OwnerIsCreator {
MyNFT public nft;
uint256 price;
event MintCallSuccessfull();
constructor(address router, uint256 _price) CCIPReceiver(router) {
nft = new MyNFT();
price = _price;
}
function _ccipReceive(
Client.Any2EVMMessage memory message
)
internal
override
{
require(message.destTokenAmounts[0].amount >= price, "Not enough CCIP-BnM for mint");
(bool success, ) = address(nft).call(message.data);
require(success);
emit MintCallSuccessfull();
}
}
CCIP Best Practice: Verify both sender & source chain
When implementing the ccipReceive
method in a contract residing on the destination chain, ensure to verify the source chain of the incoming CCIP message. It is also important to validate the sender of the incoming CCIP message. These verifications ensure that CCIP messages can only be received from trusted source chains and only from trusted sender addresses
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract MyNFT is ERC721URIStorage, OwnerIsCreator {
string constant TOKEN_URI =
"https://ipfs.io/ipfs/QmYuKY45Aq87LeL1R5dhb1hqHLp6ZFbJaCP8jxqKM1MX6y/babe_ruth_1.json";
uint256 internal tokenId;
constructor() ERC721("MyNFT", "MNFT") {}
function mint(address to) public onlyOwner {
_safeMint(to, tokenId);
_setTokenURI(tokenId, TOKEN_URI);
unchecked {
tokenId++;
}
}
}
contract CCIPTokenAndDataReceiver is CCIPReceiver, OwnerIsCreator {
MyNFT public nft;
uint256 price;
mapping(uint64 => bool) public whitelistedSourceChains;
mapping(address => bool) public whitelistedSenders;
event MintCallSuccessfull();
error SourceChainNotWhitelisted(uint64 sourceChainSelector);
error SenderNotWhitelisted(address sender);
modifier onlyWhitelistedSourceChain(uint64 _sourceChainSelector) {
if (!whitelistedSourceChains[_sourceChainSelector])
revert SourceChainNotWhitelisted(_sourceChainSelector);
_;
}
modifier onlyWhitelistedSenders(address _sender) {
if (!whitelistedSenders[_sender]) revert SenderNotWhitelisted(_sender);
_;
}
constructor(address router, uint256 _price) CCIPReceiver(router) {
nft = new MyNFT();
price = _price;
}
function whitelistSourceChain(
uint64 _sourceChainSelector
) external onlyOwner {
whitelistedSourceChains[_sourceChainSelector] = true;
}
function denylistSourceChain(
uint64 _sourceChainSelector
) external onlyOwner {
whitelistedSourceChains[_sourceChainSelector] = false;
}
function whitelistSender(address _sender) external onlyOwner {
whitelistedSenders[_sender] = true;
}
function denySender(address _sender) external onlyOwner {
whitelistedSenders[_sender] = false;
}
function _ccipReceive(
Client.Any2EVMMessage memory message
)
internal
onlyWhitelistedSourceChain(message.sourceChainSelector)
onlyWhitelistedSenders(abi.decode(message.sender, (address)))
override
{
require(message.destTokenAmounts[0].amount >= price, "Not enough CCIP-BnM for mint");
(bool success, ) = address(nft).call(message.data);
require(success);
emit MintCallSuccessfull();
}
}
Create a new file inside the src
folder and name it CCIPTokenAndDataReceiver.sol
To compile the following contract, we must install the @openzeppelin/contracts
package first.
Run:
forge install openzeppelin/openzeppelin-contracts
And then add this line: @openzeppelin/=lib/openzeppelin-contracts/
to the remappings.txt
folder.
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/
@openzeppelin/=lib/openzeppelin-contracts/
@chainlink/contracts/=node_modules/@chainlink/contracts
@chainlink/contracts-ccip/=node_modules/@chainlink/contracts-ccip
Run:
forge remappings
Start with the development by setting the Solidity compiler version and importing necessary contracts from the @chainlink/contracts-ccip
NPM package. Then implement a basic NFT smart contract using the OpenZeppelin library.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract MyNFT is ERC721URIStorage, OwnerIsCreator {
string constant TOKEN_URI =
"https://ipfs.io/ipfs/QmYuKY45Aq87LeL1R5dhb1hqHLp6ZFbJaCP8jxqKM1MX6y/babe_ruth_1.json";
uint256 internal tokenId;
constructor() ERC721("MyNFT", "MNFT") {}
function mint(address to) public onlyOwner {
_safeMint(to, tokenId);
_setTokenURI(tokenId, TOKEN_URI);
unchecked {
tokenId++;
}
}
}
Now we can add the CCIP Receiver contract logic
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract MyNFT is ERC721URIStorage, OwnerIsCreator {
string constant TOKEN_URI =
"https://ipfs.io/ipfs/QmYuKY45Aq87LeL1R5dhb1hqHLp6ZFbJaCP8jxqKM1MX6y/babe_ruth_1.json";
uint256 internal tokenId;
constructor() ERC721("MyNFT", "MNFT") {}
function mint(address to) public onlyOwner {
_safeMint(to, tokenId);
_setTokenURI(tokenId, TOKEN_URI);
unchecked {
tokenId++;
}
}
}
contract CCIPTokenAndDataReceiver is CCIPReceiver, OwnerIsCreator {
MyNFT public nft;
uint256 price;
event MintCallSuccessfull();
constructor(address router, uint256 _price) CCIPReceiver(router) {
nft = new MyNFT();
price = _price;
}
function _ccipReceive(
Client.Any2EVMMessage memory message
)
internal
override
{
require(message.destTokenAmounts[0].amount >= price, "Not enough CCIP-BnM for mint");
(bool success, ) = address(nft).call(message.data);
require(success);
emit MintCallSuccessfull();
}
}
CCIP Best Practice: Verify both sender & source chain
When implementing the ccipReceive
method in a contract residing on the destination chain, ensure to verify the source chain of the incoming CCIP message. It is also important to validate the sender of the incoming CCIP message. These verifications ensure that CCIP messages can only be received from trusted source chains and only from trusted sender addresses
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract MyNFT is ERC721URIStorage, OwnerIsCreator {
string constant TOKEN_URI =
"https://ipfs.io/ipfs/QmYuKY45Aq87LeL1R5dhb1hqHLp6ZFbJaCP8jxqKM1MX6y/babe_ruth_1.json";
uint256 internal tokenId;
constructor() ERC721("MyNFT", "MNFT") {}
function mint(address to) public onlyOwner {
_safeMint(to, tokenId);
_setTokenURI(tokenId, TOKEN_URI);
unchecked {
tokenId++;
}
}
}
contract CCIPTokenAndDataReceiver is CCIPReceiver, OwnerIsCreator {
MyNFT public nft;
uint256 price;
mapping(uint64 => bool) public whitelistedSourceChains;
mapping(address => bool) public whitelistedSenders;
event MintCallSuccessfull();
error SourceChainNotWhitelisted(uint64 sourceChainSelector);
error SenderNotWhitelisted(address sender);
modifier onlyWhitelistedSourceChain(uint64 _sourceChainSelector) {
if (!whitelistedSourceChains[_sourceChainSelector])
revert SourceChainNotWhitelisted(_sourceChainSelector);
_;
}
modifier onlyWhitelistedSenders(address _sender) {
if (!whitelistedSenders[_sender]) revert SenderNotWhitelisted(_sender);
_;
}
constructor(address router, uint256 _price) CCIPReceiver(router) {
nft = new MyNFT();
price = _price;
}
function whitelistSourceChain(
uint64 _sourceChainSelector
) external onlyOwner {
whitelistedSourceChains[_sourceChainSelector] = true;
}
function denylistSourceChain(
uint64 _sourceChainSelector
) external onlyOwner {
whitelistedSourceChains[_sourceChainSelector] = false;
}
function whitelistSender(address _sender) external onlyOwner {
whitelistedSenders[_sender] = true;
}
function denySender(address _sender) external onlyOwner {
whitelistedSenders[_sender] = false;
}
function _ccipReceive(
Client.Any2EVMMessage memory message
)
internal
onlyWhitelistedSourceChain(message.sourceChainSelector)
onlyWhitelistedSenders(abi.decode(message.sender, (address)))
override
{
require(message.destTokenAmounts[0].amount >= price, "Not enough CCIP-BnM for mint");
(bool success, ) = address(nft).call(message.data);
require(success);
emit MintCallSuccessfull();
}
}
Create a new file by clicking the "Create new file" button, and name it CCIPTokenAndDataReceiver.sol
Start with the development by setting the Solidity compiler version and importing necessary contracts from the @chainlink/contracts-ccip
NPM package. Then implement a basic NFT smart contract using the OpenZeppelin library.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract MyNFT is ERC721URIStorage, OwnerIsCreator {
string constant TOKEN_URI =
"https://ipfs.io/ipfs/QmYuKY45Aq87LeL1R5dhb1hqHLp6ZFbJaCP8jxqKM1MX6y/babe_ruth_1.json";
uint256 internal tokenId;
constructor() ERC721("MyNFT", "MNFT") {}
function mint(address to) public onlyOwner {
_safeMint(to, tokenId);
_setTokenURI(tokenId, TOKEN_URI);
unchecked {
tokenId++;
}
}
}
Now we can add the CCIP Receiver contract logic
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract MyNFT is ERC721URIStorage, OwnerIsCreator {
string constant TOKEN_URI =
"https://ipfs.io/ipfs/QmYuKY45Aq87LeL1R5dhb1hqHLp6ZFbJaCP8jxqKM1MX6y/babe_ruth_1.json";
uint256 internal tokenId;
constructor() ERC721("MyNFT", "MNFT") {}
function mint(address to) public onlyOwner {
_safeMint(to, tokenId);
_setTokenURI(tokenId, TOKEN_URI);
unchecked {
tokenId++;
}
}
}
contract CCIPTokenAndDataReceiver is CCIPReceiver, OwnerIsCreator {
MyNFT public nft;
uint256 price;
event MintCallSuccessfull();
constructor(address router, uint256 _price) CCIPReceiver(router) {
nft = new MyNFT();
price = _price;
}
function _ccipReceive(
Client.Any2EVMMessage memory message
)
internal
override
{
require(message.destTokenAmounts[0].amount >= price, "Not enough CCIP-BnM for mint");
(bool success, ) = address(nft).call(message.data);
require(success);
emit MintCallSuccessfull();
}
}
CCIP Best Practice: Verify both sender & source chain
When implementing the ccipReceive
method in a contract residing on the destination chain, ensure to verify the source chain of the incoming CCIP message. It is also important to validate the sender of the incoming CCIP message. These verifications ensure that CCIP messages can only be received from trusted source chains and only from trusted sender addresses
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
contract MyNFT is ERC721URIStorage, OwnerIsCreator {
string constant TOKEN_URI =
"https://ipfs.io/ipfs/QmYuKY45Aq87LeL1R5dhb1hqHLp6ZFbJaCP8jxqKM1MX6y/babe_ruth_1.json";
uint256 internal tokenId;
constructor() ERC721("MyNFT", "MNFT") {}
function mint(address to) public onlyOwner {
_safeMint(to, tokenId);
_setTokenURI(tokenId, TOKEN_URI);
unchecked {
tokenId++;
}
}
}
contract CCIPTokenAndDataReceiver is CCIPReceiver, OwnerIsCreator {
MyNFT public nft;
uint256 price;
mapping(uint64 => bool) public whitelistedSourceChains;
mapping(address => bool) public whitelistedSenders;
event MintCallSuccessfull();
error SourceChainNotWhitelisted(uint64 sourceChainSelector);
error SenderNotWhitelisted(address sender);
modifier onlyWhitelistedSourceChain(uint64 _sourceChainSelector) {
if (!whitelistedSourceChains[_sourceChainSelector])
revert SourceChainNotWhitelisted(_sourceChainSelector);
_;
}
modifier onlyWhitelistedSenders(address _sender) {
if (!whitelistedSenders[_sender]) revert SenderNotWhitelisted(_sender);
_;
}
constructor(address router, uint256 _price) CCIPReceiver(router) {
nft = new MyNFT();
price = _price;
}
function whitelistSourceChain(
uint64 _sourceChainSelector
) external onlyOwner {
whitelistedSourceChains[_sourceChainSelector] = true;
}
function denylistSourceChain(
uint64 _sourceChainSelector
) external onlyOwner {
whitelistedSourceChains[_sourceChainSelector] = false;
}
function whitelistSender(address _sender) external onlyOwner {
whitelistedSenders[_sender] = true;
}
function denySender(address _sender) external onlyOwner {
whitelistedSenders[_sender] = false;
}
function _ccipReceive(
Client.Any2EVMMessage memory message
)
internal
onlyWhitelistedSourceChain(message.sourceChainSelector)
onlyWhitelistedSenders(abi.decode(message.sender, (address)))
override
{
require(message.destTokenAmounts[0].amount >= price, "Not enough CCIP-BnM for mint");
(bool success, ) = address(nft).call(message.data);
require(success);
emit MintCallSuccessfull();
}
}
Deployment and Usage
Now that we know the basics of working with CCIP, how to deploy contracts, how to fund them, and how to monitor for CCIP requests, go through the following checklist to deploy & use these contracts.
Make sure you have at least 100 units of CCIP-BnM tokens on the Avalanche Fuji network
Make sure you have at least 1 LINK on the Avalanche Fuji network
Deploy the
CCIPTokenAndDataSender.sol
smart contract to the Avalanche Fuji network by providing the0x554472a2720E5E7D5D3C817529aBA05EEd5F82D8
address (Router.sol
on Avalanche Fuji) as the_router
parameter and the0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846
address as the_link
parameter. Save the contract address.On Avalanche Fuji, call the
whitelistChain
function of theCCIPTokenAndDataSender.sol
smart contract and pass16015286601757825753
(Ethereum Sepolia CCIP chain selector) as the_destinationChainSelector
parameter.On Avalanche Fuji, fund the
CCIPTokenAndDataSender.sol
smart contract with at least 100 units of CCIP-BnM tokens by sending them from your wallet toCCIPTokenAndDataSender.sol
smart contract's address you previously saved.On Avalanche Fuji, fund the
CCIPTokenAndDataSender.sol
smart contract with at least 1 LINK by sending tokens from your wallet toCCIPTokenAndDataSender.sol
smart contract's address you previously saved.Deploy the
CCIPTokenAndDataReceiver.sol
smart contract to the Ethereum Sepolia network by providing the0xD0daae2231E9CB96b94C8512223533293C3693Bf
address (Router.sol
on Ethereum Spolia) as the_router
parameter and100
as the_price
parameter. Save the contract address.On Ethereum Sepolia, call the
whitelistSourceChain
function of theCCIPTokenAndDataReceiver.sol
smart contract and provide the14767482510784806043
(Avalanche Fuji CCIP chain selector) as the_sourceChainSelector
parameter.On Ethereum Sepolia, call the
whitelistSender
function of theCCIPTokenAndDataReceiver.sol
smart contract and provide theCCIPTokenAndDataSender.sol
smart contract's address you previously saved as the_sender
parameter.Finally, go back to Avalanche Fuji and call the
transferTokens
function of theCCIPTokenAndDataSender.sol
smart contract by providing theCCIPTokenAndDataReceiver.sol
smart contract's address you previously saved as the_receiver
parameter,16015286601757825753
(Ethereum Sepolia CCIP chain selector) as the_destinationChainSelector
parameter,0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4
(CCIP-BnM token address on Avalanche Fuji) as the_token
parameter and100
as the_price
parameter.
You can now monitor the status of your CCIP Message using CCIP Explorer.
After the status of the message becomes 'Successful', you should be able to see your freshly minted NFT on OpenSea.
Last updated