CCT - getCCIPAdmin() token with Burn and Mint Pool in forked environments

This tutorial will guide you through the process of testing the procedure of enabling your own tokens in CCIP. We will use the CCT-compatible ERC-20 token with getCCIPAdmin function and burning & minting capabilities. We will use Burn & Mint Pool for transferring this token across different blockchains using Chainlink CCIP.

Prerequisites

Before we start with this guide, let's recap parts of the CCT standard that we will need for it.

Requirements for Cross-Chain Tokens

Before enabling an ERC20-compatible token in CCIP, it's important to understand the requirements it must fulfill to integrate with CCIP.

  • Recommended Permissionless Token Administrator address registration methods: A token can utilize either of these supported function signatures to register permissionlessly:

    • owner(): This function returns the token contract owner's address.

    • getCCIPAdmin(): This function returns the token administrator's address and is recommended for new tokens, as it allows for abstraction of the CCIP Token Administrator role from other common roles, like owner().

  • Requirements for CCIP token transfers: The token's smart contract must meet minimum requirements to integrate with CCIP.

    • Burn & Mint Requirements:
      • The token smart contract must have the following functions:
        • mint(address account, uint256 amount): This function is used to mint the amount of tokens to a given account on the destination blockchain.
        • burn(uint256 amount): This function is used to burn the amount of tokens on the source blockchain.
        • decimals(): Returns the token's number of decimals.
        • balanceOf(address account): Returns the current token balance of the specified account.
        • burnFrom(address account, uint256 amount): This function burns a specified number of tokens from the provided account on the source blockchain. Note: This is an optional function. We generally recommend using the burn function, but if you use a tokenPool that calls burnFrom, your token contract will need to implement this function.
      • On the source and destination blockchains, the token contract must support granting mint and burn permissions. The token developers or another role (such as the token administrator) will grant these permissions to the token pool.
    • Lock & Mint Requirements:
      • The token smart contract must have the following functions:
        • decimals(): Returns the token's number of decimals.
        • balanceOf(address account): Returns the current token balance of the specified account.
      • On the destination blockchain, The token contract must support granting mint and burn permissions. The token developers or another role (such as the token administrator) will grant these permissions to the token pool.

If you don't have an existing token: For all blockchains where tokens need to be burned and minted (for example, the source or destination chain in the case of Burn and Mint, or the destination blockchain in the case of Lock and Mint), Chainlink provides a BurnMintERC677 contract that you can use to deploy your token in minutes. This token follows the ERC677 or ERC777, allowing you to use it as-is or extend it to meet your specific requirements.

Understanding the Procedure

It is also important first to understand the overall procedure for enabling your tokens in CCIP. This procedure involves deploying tokens and token pools, registering administrative roles, and configuring token pools to enable secure token transfers using CCIP. The steps in the diagram below highlight the flow of actions needed to enable a token for cross-chain transfers. Whether you're working with an Externally Owned Account (EOA) or a Smart Account (such as one using a multisig scheme), the overall logic remains the same. You'll follow the same process to enable cross-chain token transfers, configure pools, and register administrative roles.

The diagram below outlines the entire process:

Process for enabling a token in CCIP.

Before You Begin

  1. Install Foundry: If you haven't already, follow the instructions in the Foundry documentation to install Foundry.

  2. Create new Foundry project: Create a new Foundry project by running the following command:

    forge init
    
  3. Set up your environment: Create a .env file, and fill in the required values:

    Example .env file:

    ETHEREUM_SEPOLIA_RPC_URL=<ethereum_sepolia_rpc_url>
    BASE_SEPOLIA_RPC_URL=<base_sepolia_rpc_url>
    

Create the getCCIPAdmin() ERC-20 token

Inside the test folder create the new Solidity file and name it CCIPv1_5ForkBurnMintPoolFork.t.sol. We will use only this file through out the rest of this guide.

Create the CCT-compatible ERC-20 token with getCCIPAdmin() function and burning and minting capabilities.

// test/CCIPv1_5ForkBurnMintPoolFork.t.sol

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

import { ERC20, ERC20Burnable, IERC20 } from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import { AccessControl } from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/access/AccessControl.sol";

contract MockERC20BurnAndMintToken is IBurnMintERC20, ERC20Burnable, AccessControl {
  address internal immutable i_CCIPAdmin;
  bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
  bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

  constructor() ERC20("MockERC20BurnAndMintToken", "MTK") {
    _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
    _grantRole(MINTER_ROLE, msg.sender);
    _grantRole(BURNER_ROLE, msg.sender);
    i_CCIPAdmin = msg.sender;
  }

  function mint(address account, uint256 amount) public onlyRole(MINTER_ROLE) {
    _mint(account, amount);
  }

  function burn(uint256 amount) public override(IBurnMintERC20, ERC20Burnable) onlyRole(BURNER_ROLE) {
    super.burn(amount);
  }

  function burnFrom(
    address account,
    uint256 amount
  ) public override(IBurnMintERC20, ERC20Burnable) onlyRole(BURNER_ROLE) {
    super.burnFrom(account, amount);
  }

  function burn(address account, uint256 amount) public virtual override {
    burnFrom(account, amount);
  }

  function getCCIPAdmin() public view returns (address) {
    return i_CCIPAdmin;
  }
}

Test the CCT enabling procedure

Expand the existing CCIPv1_5ForkBurnMintPoolFork.t.sol file to create and set up our basic test.

// test/CCIPv1_5ForkBurnMintPoolFork.t.sol

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

import { Test, Vm } from "forge-std/Test.sol";
import { CCIPLocalSimulatorFork, Register } from "../../../src/ccip/CCIPLocalSimulatorFork.sol";
import { BurnMintTokenPool, TokenPool } from "@chainlink/contracts-ccip/src/v0.8/ccip/pools/BurnMintTokenPool.sol";
import { LockReleaseTokenPool } from "@chainlink/contracts-ccip/src/v0.8/ccip/pools/LockReleaseTokenPool.sol"; // not used in this test
import { IBurnMintERC20 } from "@chainlink/contracts-ccip/src/v0.8/shared/token/ERC20/IBurnMintERC20.sol";
import { RegistryModuleOwnerCustom } from "@chainlink/contracts-ccip/src/v0.8/ccip/tokenAdminRegistry/RegistryModuleOwnerCustom.sol";
import { TokenAdminRegistry } from "@chainlink/contracts-ccip/src/v0.8/ccip/tokenAdminRegistry/TokenAdminRegistry.sol";
import { RateLimiter } from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/RateLimiter.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";

/**
 * The token code part from previous section goes here...
 */

contract CCIPv1_5BurnMintPoolFork is Test {
  CCIPLocalSimulatorFork public ccipLocalSimulatorFork;
  MockERC20BurnAndMintToken public mockERC20TokenEthSepolia;
  MockERC20BurnAndMintToken public mockERC20TokenBaseSepolia;
  BurnMintTokenPool public burnMintTokenPoolEthSepolia;
  BurnMintTokenPool public burnMintTokenPoolBaseSepolia;

  Register.NetworkDetails ethSepoliaNetworkDetails;
  Register.NetworkDetails baseSepoliaNetworkDetails;

  uint256 ethSepoliaFork;
  uint256 baseSepoliaFork;

  address alice;

  function setUp() public {
    alice = makeAddr("alice");

    string memory ETHEREUM_SEPOLIA_RPC_URL = vm.envString("ETHEREUM_SEPOLIA_RPC_URL");
    string memory BASE_SEPOLIA_RPC_URL = vm.envString("BASE_SEPOLIA_RPC_URL");
    ethSepoliaFork = vm.createSelectFork(ETHEREUM_SEPOLIA_RPC_URL);
    baseSepoliaFork = vm.createFork(BASE_SEPOLIA_RPC_URL);

    ccipLocalSimulatorFork = new CCIPLocalSimulatorFork();
    vm.makePersistent(address(ccipLocalSimulatorFork));
  }
}

Step 1) Deploy token on Ethereum Sepolia

contract CCIPv1_5BurnMintPoolFork is Test {
  function setUp() public {
    // Code from previous section goes here...

    // Step 1) Deploy token on Ethereum Sepolia
    vm.startPrank(alice);
    mockERC20TokenEthSepolia = new MockERC20BurnAndMintToken();
    vm.stopPrank();
  }
}

Step 2) Deploy token on Base Sepolia

contract CCIPv1_5BurnMintPoolFork is Test {
  function setUp() public {
    // Code from previous section goes here...

    // Step 2) Deploy token on Base Sepolia
    vm.selectFork(baseSepoliaFork);

    vm.startPrank(alice);
    mockERC20TokenBaseSepolia = new MockERC20BurnAndMintToken();
    vm.stopPrank();
  }
}

Step 3) Deploy BurnMintTokenPool on Ethereum Sepolia

contract CCIPv1_5BurnMintPoolFork is Test {
  // Code from previous section goes here...

  function test_forkSupportNewCCIPToken() public {
    // Step 3) Deploy BurnMintTokenPool on Ethereum Sepolia
    vm.selectFork(ethSepoliaFork);
    ethSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid);
    address[] memory allowlist = new address[](0);
    uint8 localTokenDecimals = 18;

    vm.startPrank(alice);
    burnMintTokenPoolEthSepolia = new BurnMintTokenPool(
      IBurnMintERC20(address(mockERC20TokenEthSepolia)),
      localTokenDecimals,
      allowlist,
      ethSepoliaNetworkDetails.rmnProxyAddress,
      ethSepoliaNetworkDetails.routerAddress
    );
    vm.stopPrank();
  }
}

Step 4) Deploy BurnMintTokenPool on Base Sepolia

contract CCIPv1_5BurnMintPoolFork is Test {
  function test_forkSupportNewCCIPToken() public {
    // Code from previous section goes here...

    // Step 4) Deploy BurnMintTokenPool on Base Sepolia
    vm.selectFork(baseSepoliaFork);
    baseSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid);

    vm.startPrank(alice);
    burnMintTokenPoolBaseSepolia = new BurnMintTokenPool(
      IBurnMintERC20(address(mockERC20TokenBaseSepolia)),
      localTokenDecimals,
      allowlist,
      baseSepoliaNetworkDetails.rmnProxyAddress,
      baseSepoliaNetworkDetails.routerAddress
    );
    vm.stopPrank();
  }
}

Step 5) Grant Mint and Burn roles to BurnMintTokenPool on Ethereum Sepolia

contract CCIPv1_5BurnMintPoolFork is Test {
  function test_forkSupportNewCCIPToken() public {
    // Code from previous section goes here...

    // Step 5) Grant Mint and Burn roles to BurnMintTokenPool on Ethereum Sepolia
    vm.selectFork(ethSepoliaFork);

    vm.startPrank(alice);
    mockERC20TokenEthSepolia.grantRole(mockERC20TokenEthSepolia.MINTER_ROLE(), address(burnMintTokenPoolEthSepolia));
    mockERC20TokenEthSepolia.grantRole(mockERC20TokenEthSepolia.BURNER_ROLE(), address(burnMintTokenPoolEthSepolia));
    vm.stopPrank();
  }
}

Step 6) Grant Mint and Burn roles to BurnMintTokenPool on Base Sepolia

contract CCIPv1_5BurnMintPoolFork is Test {
  function test_forkSupportNewCCIPToken() public {
    // Code from previous section goes here...

    // Step 6) Grant Mint and Burn roles to BurnMintTokenPool on Base Sepolia
    vm.selectFork(baseSepoliaFork);

    vm.startPrank(alice);
    mockERC20TokenBaseSepolia.grantRole(mockERC20TokenBaseSepolia.MINTER_ROLE(), address(burnMintTokenPoolBaseSepolia));
    mockERC20TokenBaseSepolia.grantRole(mockERC20TokenBaseSepolia.BURNER_ROLE(), address(burnMintTokenPoolBaseSepolia));
    vm.stopPrank();
  }
}

Step 7) Claim Admin role on Ethereum Sepolia

contract CCIPv1_5BurnMintPoolFork is Test {
  function test_forkSupportNewCCIPToken() public {
    // Code from previous section goes here...

    // Step 7) Claim Admin role on Ethereum Sepolia
    vm.selectFork(ethSepoliaFork);

    RegistryModuleOwnerCustom registryModuleOwnerCustomEthSepolia = RegistryModuleOwnerCustom(
      ethSepoliaNetworkDetails.registryModuleOwnerCustomAddress
    );

    vm.startPrank(alice);
    registryModuleOwnerCustomEthSepolia.registerAdminViaGetCCIPAdmin(address(mockERC20TokenEthSepolia));
    vm.stopPrank();
  }
}

Step 8) Claim Admin role on Base Sepolia

contract CCIPv1_5BurnMintPoolFork is Test {
  function test_forkSupportNewCCIPToken() public {
    // Code from previous section goes here...

    // Step 8) Claim Admin role on Base Sepolia
    vm.selectFork(baseSepoliaFork);

    RegistryModuleOwnerCustom registryModuleOwnerCustomBaseSepolia = RegistryModuleOwnerCustom(
      baseSepoliaNetworkDetails.registryModuleOwnerCustomAddress
    );

    vm.startPrank(alice);
    registryModuleOwnerCustomBaseSepolia.registerAdminViaGetCCIPAdmin(address(mockERC20TokenBaseSepolia));
    vm.stopPrank();
  }
}

Step 9) Accept Admin role on Ethereum Sepolia

contract CCIPv1_5BurnMintPoolFork is Test {
  function test_forkSupportNewCCIPToken() public {
    // Code from previous section goes here...

    // Step 9) Accept Admin role on Ethereum Sepolia
    vm.selectFork(ethSepoliaFork);

    TokenAdminRegistry tokenAdminRegistryEthSepolia = TokenAdminRegistry(
      ethSepoliaNetworkDetails.tokenAdminRegistryAddress
    );

    vm.startPrank(alice);
    tokenAdminRegistryEthSepolia.acceptAdminRole(address(mockERC20TokenEthSepolia));
    vm.stopPrank();
  }
}

Step 10) Accept Admin role on Base Sepolia

contract CCIPv1_5BurnMintPoolFork is Test {
  function test_forkSupportNewCCIPToken() public {
    // Code from previous section goes here...

    // Step 10) Accept Admin role on Base Sepolia
    vm.selectFork(baseSepoliaFork);

    TokenAdminRegistry tokenAdminRegistryBaseSepolia = TokenAdminRegistry(
      baseSepoliaNetworkDetails.tokenAdminRegistryAddress
    );

    vm.startPrank(alice);
    tokenAdminRegistryBaseSepolia.acceptAdminRole(address(mockERC20TokenBaseSepolia));
    vm.stopPrank();
  }
}
contract CCIPv1_5BurnMintPoolFork is Test {
  function test_forkSupportNewCCIPToken() public {
    // Code from previous section goes here...

    // Step 11) Link token to pool on Ethereum Sepolia
    vm.selectFork(ethSepoliaFork);

    vm.startPrank(alice);
    tokenAdminRegistryEthSepolia.setPool(address(mockERC20TokenEthSepolia), address(burnMintTokenPoolEthSepolia));
    vm.stopPrank();
  }
}
contract CCIPv1_5BurnMintPoolFork is Test {
  function test_forkSupportNewCCIPToken() public {
    // Code from previous section goes here...

    // Step 12) Link token to pool on Base Sepolia
    vm.selectFork(baseSepoliaFork);

    vm.startPrank(alice);
    tokenAdminRegistryBaseSepolia.setPool(address(mockERC20TokenBaseSepolia), address(burnMintTokenPoolBaseSepolia));
    vm.stopPrank();
  }
}

Step 13) Configure Token Pool on Ethereum Sepolia

contract CCIPv1_5BurnMintPoolFork is Test {
  function test_forkSupportNewCCIPToken() public {
    // Code from previous section goes here...

    // Step 13) Configure Token Pool on Ethereum Sepolia
    vm.selectFork(ethSepoliaFork);

    vm.startPrank(alice);
    TokenPool.ChainUpdate[] memory chains = new TokenPool.ChainUpdate[](1);
    bytes[] memory remotePoolAddressesEthSepolia = new bytes[](1);
    remotePoolAddressesEthSepolia[0] = abi.encode(address(burnMintTokenPoolEthSepolia));
    chains[0] = TokenPool.ChainUpdate({
      remoteChainSelector: baseSepoliaNetworkDetails.chainSelector,
      remotePoolAddresses: remotePoolAddressesEthSepolia,
      remoteTokenAddress: abi.encode(address(mockERC20TokenBaseSepolia)),
      outboundRateLimiterConfig: RateLimiter.Config({ isEnabled: true, capacity: 100_000, rate: 167 }),
      inboundRateLimiterConfig: RateLimiter.Config({ isEnabled: true, capacity: 100_000, rate: 167 })
    });
    uint64[] memory remoteChainSelectorsToRemove = new uint64[](0);
    burnMintTokenPoolEthSepolia.applyChainUpdates(remoteChainSelectorsToRemove, chains);
    vm.stopPrank();
  }
}

Step 14) Configure Token Pool on Base Sepolia

contract CCIPv1_5BurnMintPoolFork is Test {
  function test_forkSupportNewCCIPToken() public {
    // Code from previous section goes here...

    // Step 14) Configure Token Pool on Base Sepolia
    vm.selectFork(baseSepoliaFork);

    vm.startPrank(alice);
    chains = new TokenPool.ChainUpdate[](1);
    bytes[] memory remotePoolAddressesBaseSepolia = new bytes[](1);
    remotePoolAddressesBaseSepolia[0] = abi.encode(address(burnMintTokenPoolEthSepolia));
    chains[0] = TokenPool.ChainUpdate({
      remoteChainSelector: ethSepoliaNetworkDetails.chainSelector,
      remotePoolAddresses: remotePoolAddressesBaseSepolia,
      remoteTokenAddress: abi.encode(address(mockERC20TokenEthSepolia)),
      outboundRateLimiterConfig: RateLimiter.Config({ isEnabled: true, capacity: 100_000, rate: 167 }),
      inboundRateLimiterConfig: RateLimiter.Config({ isEnabled: true, capacity: 100_000, rate: 167 })
    });
    burnMintTokenPoolBaseSepolia.applyChainUpdates(remoteChainSelectorsToRemove, chains);
    vm.stopPrank();
  }
}

Step 15) Mint tokens on Ethereum Sepolia and transfer them to Base Sepolia

contract CCIPv1_5BurnMintPoolFork is Test {
  function test_forkSupportNewCCIPToken() public {
    // Code from previous section goes here...

    // Step 15) Mint tokens on Ethereum Sepolia and transfer them to Base Sepolia
    vm.selectFork(ethSepoliaFork);

    address linkSepolia = ethSepoliaNetworkDetails.linkAddress;
    ccipLocalSimulatorFork.requestLinkFromFaucet(address(alice), 20 ether);

    uint256 amountToSend = 100;
    Client.EVMTokenAmount[] memory tokenToSendDetails = new Client.EVMTokenAmount[](1);
    Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
      token: address(mockERC20TokenEthSepolia),
      amount: amountToSend
    });
    tokenToSendDetails[0] = tokenAmount;

    vm.startPrank(alice);
    mockERC20TokenEthSepolia.mint(address(alice), amountToSend);

    mockERC20TokenEthSepolia.approve(ethSepoliaNetworkDetails.routerAddress, amountToSend);
    IERC20(linkSepolia).approve(ethSepoliaNetworkDetails.routerAddress, 20 ether);

    uint256 balanceOfAliceBeforeEthSepolia = mockERC20TokenEthSepolia.balanceOf(alice);

    IRouterClient routerEthSepolia = IRouterClient(ethSepoliaNetworkDetails.routerAddress);
    routerEthSepolia.ccipSend(
      baseSepoliaNetworkDetails.chainSelector,
      Client.EVM2AnyMessage({
        receiver: abi.encode(address(alice)),
        data: "",
        tokenAmounts: tokenToSendDetails,
        extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({ gasLimit: 0 })),
        feeToken: linkSepolia
      })
    );

    uint256 balanceOfAliceAfterEthSepolia = mockERC20TokenEthSepolia.balanceOf(alice);
    vm.stopPrank();

    assertEq(balanceOfAliceAfterEthSepolia, balanceOfAliceBeforeEthSepolia - amountToSend);

    ccipLocalSimulatorFork.switchChainAndRouteMessage(baseSepoliaFork);

    uint256 balanceOfAliceAfterBaseSepolia = mockERC20TokenBaseSepolia.balanceOf(alice);
    assertEq(balanceOfAliceAfterBaseSepolia, amountToSend);
  }
}

Final code - full example

// test/CCIPv1_5ForkBurnMintPoolFork.t.sol

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

import { Test, Vm } from "forge-std/Test.sol";
import { CCIPLocalSimulatorFork, Register } from "../../../src/ccip/CCIPLocalSimulatorFork.sol";
import { BurnMintTokenPool, TokenPool } from "@chainlink/contracts-ccip/src/v0.8/ccip/pools/BurnMintTokenPool.sol";
import { LockReleaseTokenPool } from "@chainlink/contracts-ccip/src/v0.8/ccip/pools/LockReleaseTokenPool.sol"; // not used in this test
import { IBurnMintERC20 } from "@chainlink/contracts-ccip/src/v0.8/shared/token/ERC20/IBurnMintERC20.sol";
import { RegistryModuleOwnerCustom } from "@chainlink/contracts-ccip/src/v0.8/ccip/tokenAdminRegistry/RegistryModuleOwnerCustom.sol";
import { TokenAdminRegistry } from "@chainlink/contracts-ccip/src/v0.8/ccip/tokenAdminRegistry/TokenAdminRegistry.sol";
import { RateLimiter } from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/RateLimiter.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 { ERC20, ERC20Burnable, IERC20 } from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import { AccessControl } from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/access/AccessControl.sol";

contract MockERC20BurnAndMintToken is IBurnMintERC20, ERC20Burnable, AccessControl {
  address internal immutable i_CCIPAdmin;
  bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
  bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

  constructor() ERC20("MockERC20BurnAndMintToken", "MTK") {
    _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
    _grantRole(MINTER_ROLE, msg.sender);
    _grantRole(BURNER_ROLE, msg.sender);
    i_CCIPAdmin = msg.sender;
  }

  function mint(address account, uint256 amount) public onlyRole(MINTER_ROLE) {
    _mint(account, amount);
  }

  function burn(uint256 amount) public override(IBurnMintERC20, ERC20Burnable) onlyRole(BURNER_ROLE) {
    super.burn(amount);
  }

  function burnFrom(
    address account,
    uint256 amount
  ) public override(IBurnMintERC20, ERC20Burnable) onlyRole(BURNER_ROLE) {
    super.burnFrom(account, amount);
  }

  function burn(address account, uint256 amount) public virtual override {
    burnFrom(account, amount);
  }

  function getCCIPAdmin() public view returns (address) {
    return i_CCIPAdmin;
  }
}

contract CCIPv1_5BurnMintPoolFork is Test {
  CCIPLocalSimulatorFork public ccipLocalSimulatorFork;
  MockERC20BurnAndMintToken public mockERC20TokenEthSepolia;
  MockERC20BurnAndMintToken public mockERC20TokenBaseSepolia;
  BurnMintTokenPool public burnMintTokenPoolEthSepolia;
  BurnMintTokenPool public burnMintTokenPoolBaseSepolia;

  Register.NetworkDetails ethSepoliaNetworkDetails;
  Register.NetworkDetails baseSepoliaNetworkDetails;

  uint256 ethSepoliaFork;
  uint256 baseSepoliaFork;

  address alice;

  function setUp() public {
    alice = makeAddr("alice");

    string memory ETHEREUM_SEPOLIA_RPC_URL = vm.envString("ETHEREUM_SEPOLIA_RPC_URL");
    string memory BASE_SEPOLIA_RPC_URL = vm.envString("BASE_SEPOLIA_RPC_URL");
    ethSepoliaFork = vm.createSelectFork(ETHEREUM_SEPOLIA_RPC_URL);
    baseSepoliaFork = vm.createFork(BASE_SEPOLIA_RPC_URL);

    ccipLocalSimulatorFork = new CCIPLocalSimulatorFork();
    vm.makePersistent(address(ccipLocalSimulatorFork));

    // Step 1) Deploy token on Ethereum Sepolia
    vm.startPrank(alice);
    mockERC20TokenEthSepolia = new MockERC20BurnAndMintToken();
    vm.stopPrank();

    // Step 2) Deploy token on Base Sepolia
    vm.selectFork(baseSepoliaFork);

    vm.startPrank(alice);
    mockERC20TokenBaseSepolia = new MockERC20BurnAndMintToken();
    vm.stopPrank();
  }

  function test_forkSupportNewCCIPToken() public {
    // Step 3) Deploy BurnMintTokenPool on Ethereum Sepolia
    vm.selectFork(ethSepoliaFork);
    ethSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid);
    address[] memory allowlist = new address[](0);
    uint8 localTokenDecimals = 18;

    vm.startPrank(alice);
    burnMintTokenPoolEthSepolia = new BurnMintTokenPool(
      IBurnMintERC20(address(mockERC20TokenEthSepolia)),
      localTokenDecimals,
      allowlist,
      ethSepoliaNetworkDetails.rmnProxyAddress,
      ethSepoliaNetworkDetails.routerAddress
    );
    vm.stopPrank();

    // Step 4) Deploy BurnMintTokenPool on Base Sepolia
    vm.selectFork(baseSepoliaFork);
    baseSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid);

    vm.startPrank(alice);
    burnMintTokenPoolBaseSepolia = new BurnMintTokenPool(
      IBurnMintERC20(address(mockERC20TokenBaseSepolia)),
      localTokenDecimals,
      allowlist,
      baseSepoliaNetworkDetails.rmnProxyAddress,
      baseSepoliaNetworkDetails.routerAddress
    );
    vm.stopPrank();

    // Step 5) Grant Mint and Burn roles to BurnMintTokenPool on Ethereum Sepolia
    vm.selectFork(ethSepoliaFork);

    vm.startPrank(alice);
    mockERC20TokenEthSepolia.grantRole(mockERC20TokenEthSepolia.MINTER_ROLE(), address(burnMintTokenPoolEthSepolia));
    mockERC20TokenEthSepolia.grantRole(mockERC20TokenEthSepolia.BURNER_ROLE(), address(burnMintTokenPoolEthSepolia));
    vm.stopPrank();

    // Step 6) Grant Mint and Burn roles to BurnMintTokenPool on Base Sepolia
    vm.selectFork(baseSepoliaFork);

    vm.startPrank(alice);
    mockERC20TokenBaseSepolia.grantRole(mockERC20TokenBaseSepolia.MINTER_ROLE(), address(burnMintTokenPoolBaseSepolia));
    mockERC20TokenBaseSepolia.grantRole(mockERC20TokenBaseSepolia.BURNER_ROLE(), address(burnMintTokenPoolBaseSepolia));
    vm.stopPrank();

    // Step 7) Claim Admin role on Ethereum Sepolia
    vm.selectFork(ethSepoliaFork);

    RegistryModuleOwnerCustom registryModuleOwnerCustomEthSepolia = RegistryModuleOwnerCustom(
      ethSepoliaNetworkDetails.registryModuleOwnerCustomAddress
    );

    vm.startPrank(alice);
    registryModuleOwnerCustomEthSepolia.registerAdminViaGetCCIPAdmin(address(mockERC20TokenEthSepolia));
    vm.stopPrank();

    // Step 8) Claim Admin role on Base Sepolia
    vm.selectFork(baseSepoliaFork);

    RegistryModuleOwnerCustom registryModuleOwnerCustomBaseSepolia = RegistryModuleOwnerCustom(
      baseSepoliaNetworkDetails.registryModuleOwnerCustomAddress
    );

    vm.startPrank(alice);
    registryModuleOwnerCustomBaseSepolia.registerAdminViaGetCCIPAdmin(address(mockERC20TokenBaseSepolia));
    vm.stopPrank();

    // Step 9) Accept Admin role on Ethereum Sepolia
    vm.selectFork(ethSepoliaFork);

    TokenAdminRegistry tokenAdminRegistryEthSepolia = TokenAdminRegistry(
      ethSepoliaNetworkDetails.tokenAdminRegistryAddress
    );

    vm.startPrank(alice);
    tokenAdminRegistryEthSepolia.acceptAdminRole(address(mockERC20TokenEthSepolia));
    vm.stopPrank();

    // Step 10) Accept Admin role on Base Sepolia
    vm.selectFork(baseSepoliaFork);

    TokenAdminRegistry tokenAdminRegistryBaseSepolia = TokenAdminRegistry(
      baseSepoliaNetworkDetails.tokenAdminRegistryAddress
    );

    vm.startPrank(alice);
    tokenAdminRegistryBaseSepolia.acceptAdminRole(address(mockERC20TokenBaseSepolia));
    vm.stopPrank();

    // Step 11) Link token to pool on Ethereum Sepolia
    vm.selectFork(ethSepoliaFork);

    vm.startPrank(alice);
    tokenAdminRegistryEthSepolia.setPool(address(mockERC20TokenEthSepolia), address(burnMintTokenPoolEthSepolia));
    vm.stopPrank();

    // Step 12) Link token to pool on Base Sepolia
    vm.selectFork(baseSepoliaFork);

    vm.startPrank(alice);
    tokenAdminRegistryBaseSepolia.setPool(address(mockERC20TokenBaseSepolia), address(burnMintTokenPoolBaseSepolia));
    vm.stopPrank();

    // Step 13) Configure Token Pool on Ethereum Sepolia
    vm.selectFork(ethSepoliaFork);

    vm.startPrank(alice);
    TokenPool.ChainUpdate[] memory chains = new TokenPool.ChainUpdate[](1);
    bytes[] memory remotePoolAddressesEthSepolia = new bytes[](1);
    remotePoolAddressesEthSepolia[0] = abi.encode(address(burnMintTokenPoolEthSepolia));
    chains[0] = TokenPool.ChainUpdate({
      remoteChainSelector: baseSepoliaNetworkDetails.chainSelector,
      remotePoolAddresses: remotePoolAddressesEthSepolia,
      remoteTokenAddress: abi.encode(address(mockERC20TokenBaseSepolia)),
      outboundRateLimiterConfig: RateLimiter.Config({ isEnabled: true, capacity: 100_000, rate: 167 }),
      inboundRateLimiterConfig: RateLimiter.Config({ isEnabled: true, capacity: 100_000, rate: 167 })
    });
    uint64[] memory remoteChainSelectorsToRemove = new uint64[](0);
    burnMintTokenPoolEthSepolia.applyChainUpdates(remoteChainSelectorsToRemove, chains);
    vm.stopPrank();

    // Step 14) Configure Token Pool on Base Sepolia
    vm.selectFork(baseSepoliaFork);

    vm.startPrank(alice);
    chains = new TokenPool.ChainUpdate[](1);
    bytes[] memory remotePoolAddressesBaseSepolia = new bytes[](1);
    remotePoolAddressesBaseSepolia[0] = abi.encode(address(burnMintTokenPoolEthSepolia));
    chains[0] = TokenPool.ChainUpdate({
      remoteChainSelector: ethSepoliaNetworkDetails.chainSelector,
      remotePoolAddresses: remotePoolAddressesBaseSepolia,
      remoteTokenAddress: abi.encode(address(mockERC20TokenEthSepolia)),
      outboundRateLimiterConfig: RateLimiter.Config({ isEnabled: true, capacity: 100_000, rate: 167 }),
      inboundRateLimiterConfig: RateLimiter.Config({ isEnabled: true, capacity: 100_000, rate: 167 })
    });
    burnMintTokenPoolBaseSepolia.applyChainUpdates(remoteChainSelectorsToRemove, chains);
    vm.stopPrank();

    // Step 15) Mint tokens on Ethereum Sepolia and transfer them to Base Sepolia
    vm.selectFork(ethSepoliaFork);

    address linkSepolia = ethSepoliaNetworkDetails.linkAddress;
    ccipLocalSimulatorFork.requestLinkFromFaucet(address(alice), 20 ether);

    uint256 amountToSend = 100;
    Client.EVMTokenAmount[] memory tokenToSendDetails = new Client.EVMTokenAmount[](1);
    Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
      token: address(mockERC20TokenEthSepolia),
      amount: amountToSend
    });
    tokenToSendDetails[0] = tokenAmount;

    vm.startPrank(alice);
    mockERC20TokenEthSepolia.mint(address(alice), amountToSend);

    mockERC20TokenEthSepolia.approve(ethSepoliaNetworkDetails.routerAddress, amountToSend);
    IERC20(linkSepolia).approve(ethSepoliaNetworkDetails.routerAddress, 20 ether);

    uint256 balanceOfAliceBeforeEthSepolia = mockERC20TokenEthSepolia.balanceOf(alice);

    IRouterClient routerEthSepolia = IRouterClient(ethSepoliaNetworkDetails.routerAddress);
    routerEthSepolia.ccipSend(
      baseSepoliaNetworkDetails.chainSelector,
      Client.EVM2AnyMessage({
        receiver: abi.encode(address(alice)),
        data: "",
        tokenAmounts: tokenToSendDetails,
        extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({ gasLimit: 0 })),
        feeToken: linkSepolia
      })
    );

    uint256 balanceOfAliceAfterEthSepolia = mockERC20TokenEthSepolia.balanceOf(alice);
    vm.stopPrank();

    assertEq(balanceOfAliceAfterEthSepolia, balanceOfAliceBeforeEthSepolia - amountToSend);

    ccipLocalSimulatorFork.switchChainAndRouteMessage(baseSepoliaFork);

    uint256 balanceOfAliceAfterBaseSepolia = mockERC20TokenBaseSepolia.balanceOf(alice);
    assertEq(balanceOfAliceAfterBaseSepolia, amountToSend);
  }
}

Get the latest Chainlink content straight to your inbox.