LnM Custom Cross-Chain ERC20 TOKEN

Create a custom cross chain ERC20 using CCIP

CHAINLINK

Pedro Machado

5/7/20259 min read

A few days ago, I concluded my set of Masterclasses named Crafting CCIP. The journey explains how the Chainlink Cross Chain Interoperability Protocol works and how we can build Cross-Chain Decentralized Apps (CC-DAPPS). These are the new types of DApps that will revolutionize the market by transferring value from one chain to another. Inspired by this, I had the idea đź’ˇ to develop a simple Custom Cross-Chain ERC20 token. The distinctive feature of this token is that it enables the seamless flow of value between different chains.

Disclaimer

This article is intended for educational purposes only. While it provides insights into cross-chain token development, it does not comprehensively cover all security considerations. Developing blockchain solutions involves inherent risks and complexities. Readers are advised to conduct thorough security assessments, seek professional advice, and adhere to best practices. The authors do not assume responsibility for potential risks or losses. The content’s primary goal is to encourage learning and discussion within the blockchain community.

Understanding Cross-Chain Tokens

Nowadays, blockchains are like isolated islands in the middle of the ocean. Assets of one chain can freely flow just into its network. A Custom Cross-Chain Fungible Token breaks through this wall. This opens the door to move value from one chain to another one in a safe way, increasing the usability of the token and improving the user experience for Web3 users.

CCIP’s Token Types

Chainlink CCIP features two types of token categories:

1. CCIP-BnM (Burn and Mint):

- This type of token utilizes the Burn and Mint handling mechanism for transfer. Source chains burn the specified amount of tokens for transfer, and the destination chains mint an equivalent amount in the receiver’s address.

2. CCIP-LnM (Lock and Mint):

- Tokens in this category are locked on the source chain within Token Pools. Wrapped, synthetic, or derivative tokens representing the locked tokens are then minted on the destination chain.

To the purpose of this article, we will cover the LnM (Lock and Mint) handling mechanism.

Use Cases and Applications

The implementation of LnM Custom Cross-Chain ERC20 tokens unlocks diverse use cases and applications within the decentralized ecosystem:

1. Decentralized Exchanges (DEX):

Enhances liquidity and enables seamless cross-chain trading, contributing to the efficiency of decentralized exchanges.

2. Asset Management:

Facilitates the creation of diversified portfolios across multiple blockchain networks, fostering decentralized asset management solutions.

3. Cross-Chain Loans and Borrowing:

Empowers users to borrow and lend assets seamlessly across various blockchain networks, adding a new dimension to decentralized finance (DeFi). Users can leverage their assets across different blockchain networks, opening up new opportunities for lending protocols and decentralized borrowing. This functionality enhances the flexibility and accessibility of DeFi services, contributing to the evolution of cross-chain financial ecosystems.

4. Interoperable Smart Contracts:

Enables the creation of interoperable smart contracts, paving the way for complex DApps with cross-chain functionalities.

5. Token Swaps and Transfers:

Simplifies token swaps and transfers between different blockchains, expanding possibilities for utilizing tokens in various decentralized applications.

6. Cross-Chain Governance:

Extends decentralized governance models beyond individual blockchains, fostering collaborative and inclusive governance decisions for the entire cross-chain ecosystem.

These use cases demonstrate the versatility and transformative potential of LnM Custom Tokens in reshaping the decentralized landscape.

Invariants

When working with LnM (Lock and Mint) type of tokens, there are several invariants or key considerations to take into account. These invariants are essential to ensure the proper functioning, security, and usability of the token. Here are some important invariants for LnM tokens:

1. Locked Token Integrity:

The integrity of tokens locked on the source chain must be maintained. Any manipulation or compromise of the locked tokens could lead to security vulnerabilities and loss of value.

2. Minting Accuracy:

The minting process on the destination chain must accurately reflect the amount and ownership of the locked tokens on the source chain. Any discrepancies could result in a loss of trust and value for users.

3. Security of Locking Mechanism:

The mechanism used to lock tokens on the source chain must be secure and resistant to attacks. Proper encryption, hashing, and consensus mechanisms should be employed to safeguard the locked tokens.

4. Decentralization of Locking and Minting:

The process of locking tokens on the source chain and minting corresponding tokens on the destination chain should be decentralized. This ensures that the system is not reliant on a single point of failure and enhances overall security.

5. Interoperability with Source and Destination Chains:

LnM tokens should seamlessly operate and interact with both the source and destination chains. Compatibility and interoperability are crucial for the smooth transfer and utilization of tokens across different blockchain networks.

These invariants help establish a robust foundation for the implementation and utilization of LnM tokens, ensuring that they fulfill their intended purposes securely and effectively across different blockchain networks.

Code

In the provided example, the illustration showcases a unidirectional transaction from the source chain, Polygon Mumbai, to the destination chain, Avalanche Fuji. It serves to highlight the concept of cross-chain functionality, emphasizing the seamless transfer of value from one specific source blockchain to a designated destination blockchain.

Source Chain Smart Contracts

Our contract CCIPTokenAndDataSender.sol should be deployed on Mumbai. This contract allows the sender to lock an amount of BCCLnM token and trigger the instructions to mirror the locked amount into the LOCK_ADDRESS address to be minted onto the destination chain to the beneficiary.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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/contracts/interfaces/IERC20.sol";
import {ChainsListerOperator} from "./ChainsListerOperator.sol";

contract CCIPTokenAndDataSender is ChainsListerOperator {
IRouterClient router;
IERC20 linkToken;

address public constant LOCK_ADDRESS =
address(uint160(uint256(keccak256(abi.encodePacked("LOCK_ADDRESS")))));
address public constant NATIVE_TOKEN =
address(uint160(uint256(keccak256(abi.encodePacked("NATIVE_TOKEN")))));

error InsufficientBalanceAtSourceChain();
error InsufficientBalanceToPayFees(uint256 currentBalance, uint256 calculatedFees);
error NothingToWithdraw();
error InvalidReceiverAddress();

event LockedERC20(
uint64 indexed destinationChainSelector,
address indexed owner,
address indexed token,
uint256 amount);

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.
);

event Withdrawal(
address indexed beneficiary,
address indexed token,
uint256 amount
);

constructor(address router, address linkToken) {
router = IRouterClient(_router);
linkToken = IERC20(_linkToken);
}

receive() external payable {}

function runMintTokens(address beneficiary, uint256 amount) public pure returns (bytes memory method) {
method = abi.encodeWithSignature("mint(address,uint256)",beneficiary,amount);
}


function transferTokensPayLinkToken(
uint64 destinationChainSelector,
address receiver,
address beneficiary,
address token,
uint256 amount
)
external
onlyOwner
onlyWhitelistedChain(destinationChainSelector)
returns (bytes32 messageId)
{
if (_receiver == address(0)) revert InvalidReceiverAddress();
Client.EVM2AnyMessage memory message = buildCcipMessage(
receiver,
_beneficiary,
_amount,
address(linkToken)
);

uint256 fees = ccipFeesManagement(false, destinationChainSelector, message);

_lockErc20(_destinationChainSelector, msg.sender, token, amount);
messageId = router.ccipSend(_destinationChainSelector, message);

emit TokensTransferred(
messageId,
_destinationChainSelector,
_receiver,
_token,
_amount,
address(linkToken),
fees
);
}

function transferTokensPayNative(
uint64 destinationChainSelector,
address receiver,
address beneficiary,
address token,
uint256 amount
)
external
onlyOwner
onlyWhitelistedChain(destinationChainSelector)
returns (bytes32 messageId)
{
if (_receiver == address(0)) revert InvalidReceiverAddress();
Client.EVM2AnyMessage memory message = buildCcipMessage(
receiver,
_beneficiary,
_amount,
address(0)
);

uint256 fees = ccipFeesManagement(true, destinationChainSelector, message);
_lockErc20(_destinationChainSelector, msg.sender, token, amount);
messageId = router.ccipSend{value:fees}(_destinationChainSelector, message);

emit TokensTransferred(
messageId,
_destinationChainSelector,
_receiver,
_token,
_amount,
address(0),
fees
);
}


function withdraw(address beneficiary) external {
uint256 amount = address(this).balance;
if (amount == 0) revert NothingToWithdraw();
payable(beneficiary).transfer(amount);
emit Withdrawal(_beneficiary, NATIVE_TOKEN, amount);
}

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);
emit Withdrawal(_beneficiary, token, amount);
}


function buildCcipMessage(
address receiver,
address beneficiary,
uint256 amount,
address feeTokenAddress
) private pure returns (Client.EVM2AnyMessage memory message) {

message = Client.EVM2AnyMessage({
receiver: abi.encode(_receiver),
data: runMintTokens(_beneficiary, amount),
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: Client.argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 200_000})
),
feeToken: feeTokenAddress
});
}

function ccipFeesManagement(bool payNative,
uint64 destinationChainSelector,
Client.EVM2AnyMessage memory message
) private returns (uint256 fees) {
fees = router.getFee(destinationChainSelector, message);
uint256 currentBalance;
if (payNative){
currentBalance = address(this).balance;
if (fees > currentBalance)
revert InsufficientBalanceToPayFees(currentBalance, fees);
}else {
currentBalance = linkToken.balanceOf(address(this));
if (fees > currentBalance)
revert InsufficientBalanceToPayFees(currentBalance, fees);
linkToken.approve(address(router), fees);
}
}

function lockErc20(uint64 destinationChainSelector, address owner, address token, uint256 amount) private {
IERC20(token).transfer(LOCK_ADDRESS,_amount);
emit LockedERC20(_destinationChainSelector, owner, token, _amount);
}
}

Destination Chain Smart Contracts

Our contract CCIPTokenAndDataReceiver is the one that will deploy our BCCLnM token ledger onto the destination chain (Avalanche Fuji). This allows it to be the owner, and it has authorization to mint tokens only when it receives the amount of tokens locked in the source chain by the CCIPTokenAndDataSender contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;


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";
import {ChainsListerOperator} from "./ChainsListerOperator.sol";
import {BCCLnM} from "./BCCLnM.sol";

contract CCIPTokenAndDataReceiver is CCIPReceiver, ChainsListerOperator {

BCCLnM public bccLnM;

event MintCallSuccessfull(bytes4 function_selector);

constructor(address router, address airdrop) CCIPReceiver(_router) {
bccLnM = new BCCLnM(_airdrop);
}

function _ccipReceive(
Client.Any2EVMMessage memory message
)
internal
onlyWhitelistedChain(message.sourceChainSelector)
onlyWhitelistedSenders(abi.decode(message.sender, (address)))
override
{
bytes memory runMint = message.data;
(bool success, ) = address(bccLnM).call(runMint);
require(success);
emit MintCallSuccessfull(bytes4(runMint));
}
}

BCCInM is our ERC20 token ledger that was deployed by an EOA in Polygon Mumbai to facilitate funding for CCIPTokenAndDataSender. Additionally, it is utilized by CCIPTokenAndDataReceiver on Avalanche Fuji to grant ownership, enabling CCIPTokenAndDataReceiver to call the mint function.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract BCCLnM is ERC20, OwnerIsCreator {

constructor(address airdrop) ERC20("Blockitus Cross Chain LnM", "BCCLnM") {
mint(_airdrop, 100 ether);
}

function mint(address beneficiary, uint256 amount) onlyOwner public {
_mint(beneficiary, amount);
}

}

Token Flow Walkthrough

For the purpose of our article, we just want to send 1 BCCLnM from Mumbai to a beneficiary on Avalanche Fuji. This process happens following the steps below.

  1. Sender fund CCIPTokenAndDataSender with 1 Link token to pay the fees of the transaction.

  2. Sender fund CCIPTokenAndDataSender with 1 BCCLnM token to be later transferred to the beneficiary.

  3. Sender call “CCIPTokenAndDataSender::transferTokensPayLinkToken(uint64 destinationChainSelector, address receiver, address beneficiary, address token, uint256 _amount)”.

_destinationChainSelector: `14767482510784806043`

receiver: `<CCIPTokenAndDataReceiverADDRESS>`

_beneficiary: `0x8d98929F87cd5169b30b1C0be585685bF8ba1198`

token: `<BCCLnMADDRESS>`

_amount: `1000000000000000000`

4. Wait for transaction been finalized into the CCIP Explorer.

5. When the transaction’s status changes to success, the beneficiary on the destination chain, Avalanche Fuji, should have received 1 BCCLnM ready to spend on this blockchain.

Security Considerations

There are some security considerations that we need to take into account for the purpose of this article:

  1. The Sender, as the owner of the CCIPTokenAndDataSender, is the only account that can call the `transferTokensPayLinkToken(uint64 destinationChainSelector, address receiver, address beneficiary, address token, uint256 _amount)` function.

  2. On the side of the destination chain, only the CCIPTokenAndDataReceiver can call the `mint(address beneficiary, uint256 amount)` function in the BCCLnM token ledger.

  3. The CCIPTokenAndDataSender can send tokens only to whitelisted destination chains.

  4. The CCIPTokenAndDataReceiver can handle instructions only from whitelisted chains.

  5. The CCIPTokenAndDataReceiver can handle instructions only from whitelisted senders.

  6. When the Sender sends tokens, it firstly locks the amount it wants to send into a deterministic LOCK_ADDRESS.

  7. The function called by the Sender checks the necessary fees to pay the transaction.

  8. The CCIPTokenAndDataSender can operate with both fee payment options: Link Token or Native Coin.

The security considerations outlined in the provided code aim to address several potential vulnerabilities. Firstly, the ownership restriction on the `transferTokensPayLinkToken` function ensures that only the designated Sender can initiate token transfers, preventing unauthorized access. Similarly, on the destination chain, the limitation to the CCIPTokenAndDataReceiver calling the `mint` function safeguards against unauthorized minting. The whitelisting mechanisms for both destination chains and senders enhance security by allowing only authorized entities to interact with the system. The locking of tokens using a deterministic LOCK_ADDRESS before transfer adds an additional layer of security, preventing potential exploits during the transfer process. The inclusion of fee checks and the flexibility to operate with both Link Token and Native Coin for fee payments contribute to a robust and adaptable security posture. Overall, these measures collectively address potential vulnerabilities, establishing a more secure cross-chain token transfer system.

Future Developments and Considerations: Enabling Bidirectional Token Flow

To enhance the system’s functionality and facilitate bidirectional token transfers, several security considerations and best practices should be implemented:

  1. Bidirectional Token Flow:
    Modify the system to allow bidirectional token transfers, enabling seamless movement of tokens between source and destination chains.

  2. Sender and Receiver Authorization:
    Implement robust authorization mechanisms for both the CCIPTokenAndDataSender and CCIPTokenAndDataReceiver to ensure secure bidirectional interactions.

  3. Enhanced Whitelisting:
    Strengthen whitelisting protocols for destination chains and senders to manage bidirectional flows securely. Verify the authenticity of both the sender and receiver addresses.

  4. Amount Limits:
    Introduce limits on the token amounts that can be moved bidirectionally to prevent potential exploits or unintended large transfers. Establishing maximum and minimum thresholds enhances control and security.

  5. Liquidity Management:
    Develop mechanisms to manage liquidity on both chains effectively. Maintain balanced token reserves to facilitate bidirectional transfers without causing liquidity issues on either side.

  6. Dynamic Fee Calculations:
    Implement dynamic fee calculations based on bidirectional transfer parameters. Adjust fees according to the direction and volume of token transfers to ensure fair compensation for network resources.

  7. Transaction Auditing:
    Introduce comprehensive transaction auditing to monitor bidirectional token flows. Implement logs and alerts for unusual or potentially malicious activities, enabling proactive security measures.

  8. Smart Contract Upgradability:
    Design smart contracts with upgradability in mind, allowing for future enhancements and security updates without compromising the integrity of existing token flows.

  9. Community Engagement:
    Engage the community in discussions regarding bidirectional token flow features. Gather feedback, conduct security audits, and encourage community-driven security initiatives to fortify the system against potential vulnerabilities.

  10. Regular Security Audits:
    Conduct regular security audits by third-party experts to identify and address any vulnerabilities. Stay abreast of evolving security standards and implement necessary updates to maintain a resilient bidirectional token transfer system.

By incorporating these considerations, the system can evolve to support bidirectional token flows securely, preventing potential exploits, maintaining liquidity, and ensuring the overall robustness of cross-chain interactions.

Test Transactions Demonstration: Moving Tokens Across Chains

If you want to see all the processes committed, you can go to the Blockitus GitHub Repository to walk through the process of Lock-And-Mint from the source chain to the destination chain. However, we have provided you with the addresses of the smart contracts’ core and the BCCLnM transfer transactions to check on the Source Chain Polygon Mumbai, the destination chain Avalanche Fuji, and in the CCIP Explorer to observe the movement of value from one chain to another.

SOURCE CHAIN: Polygon Mumbai

BCCLnM_ADDRESS: 0xdbF38F247C9981e14D79F3C73c98C737d0FA486b

CCIPTokenAndDataSender_ADDRESS: 0xb92b74f8E02848e2538873584254FBeea9eEE52a

DESTINATION CHAIN: Avalanche Fuji

BCCLnM_ADDRESS: 0x15BED01B3Ce3c0886Ec5E80A8ce09FF0C0F23FEd

CCIPTokenAndDataReceiver: 0x24f7EDA7c8A14CddCd77AFe25E479d459aEdc626

Transaction that executed the movement

0xdf50bcea8452dabc7beb9e8e95d24476e16afa699242f5b292aacc713d12a0bc

Check into CCIP-Explorer

Conclusion: Transforming Decentralized Transfers

In summary, the LnM Custom Cross-Chain ERC20 Token revolutionizes decentralized value transfers. From overcoming blockchain silos to enabling secure bidirectional flows, this article provides a concise guide to its features and applications. Whether enhancing liquidity or facilitating cross-chain loans, this token embodies the future of decentralized interoperability. Looking forward, considerations for bidirectional flows pave the way for a dynamic and secure cross-chain landscape. As we navigate this evolving space, the LnM token exemplifies a key step toward efficient, interconnected decentralized systems, where continuous exploration and community collaboration drive innovation.

Medium Link