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;