Unlocking Bitcoin's DeFi & NFT Frontier: Building Cross-Chain Applications with Rootstock and Router Protocol

Pranav KondePranav Konde
24 min read

The blockchain world is exploding with innovation, but its rapid expansion has brought about a significant challenge: fragmentation. Imagine a future where your digital assets, your DeFi investments, and even your unique NFTs are not confined to a single blockchain. This vision of seamless interoperability is rapidly becoming a reality, thanks to pioneering technologies like Rootstock and Router Protocol.

In this in-depth guide, we'll embark on a journey to understand how Rootstock provides a secure, Bitcoin-backed foundation for smart contracts, and how Router Protocol acts as the vital bridge, allowing developers to build truly interconnected dApps, particularly in the exciting realm of Cross-Chain NFTs. Whether you're a seasoned developer or just starting your Web3 journey, this guide aims to demystify the process and equip you with the knowledge to build the future of decentralized applications.

The Challenge of a Fragmented Blockchain Ecosystem

Today, the vast majority of dApps and digital assets reside on the specific blockchain where they were created. This chain-specific existence leads to several significant hurdles:

  • Siloed Liquidity: Capital is locked within individual networks, limiting its utility and efficient flow across the broader crypto economy.

  • Limited Asset Utility: Your NFT character from a game on one chain can't easily be used in a dApp or a marketplace on another. Its potential is capped by its native environment.

  • Complex User Experience: Moving assets between chains often involves cumbersome, multi-step bridging processes that are confusing, prone to error, and can incur significant fees and security risks.

  • Developer Barriers: Building applications that need to interact across different chains is incredibly complex, often requiring developers to build custom bridges, which can introduce new vulnerabilities.

This problem is particularly acute for Non-Fungible Tokens (NFTs). An NFT minted on Ethereum is typically confined to the Ethereum ecosystem. While wrapped versions exist, true cross-chain ownership and utility have been elusive. The ability to seamlessly transfer NFTs between different blockchain networks opens up a universe of possibilities:

  • Access Diverse Marketplaces: Sell your NFT on the marketplace with the most buyers, regardless of its original chain.

  • Optimize for Lower Fees: Move your NFT to a chain with cheaper transaction costs for trades or interactions.

  • Expand Audience: Reach collectors and users who prefer or primarily operate on different networks.

  • Leverage Chain-Specific Features: Use your NFT in dApps that might offer unique functionalities on specific chains.

Rootstock: Building Smart Contracts on Bitcoin's Foundation

Before we dive into cross-chain magic, let's understand why Rootstock is an increasingly vital component for any developer looking to build robust and secure dApps:

  1. Bitcoin's Unrivaled Security: Rootstock is a smart contract platform secured by the Bitcoin network itself. Through a process called merged mining, Bitcoin miners can simultaneously mine both Bitcoin and Rootstock blocks. This means that Rootstock inherits the unparalleled security, decentralization, and censorship resistance of Bitcoin, making it one of the most secure smart contract platforms available. For high-value dApps and valuable NFTs, this foundational security is a non-negotiable advantage.

  2. Full EVM Compatibility: For the vast majority of blockchain developers, Solidity is the language of choice, and the Ethereum Virtual Machine (EVM) is the execution environment they know. Rootstock is 100% EVM-compatible. This is a game-changer: if you're an Ethereum developer, you can deploy your existing Solidity smart contracts, including ERC-20 tokens, ERC-721 NFTs, and ERC-1155 multi-token contracts, on Rootstock with minimal to no modifications. This dramatically lowers the learning curve and accelerates development.

  3. Scalability and Cost Efficiency: While benefiting from Bitcoin's security, Rootstock offers significantly faster transaction finality and much lower gas fees compared to Ethereum mainnet. This makes it an ideal environment for dApps that require frequent user interactions, such as gaming, DeFi protocols with high transaction volumes, or dynamic NFTs that undergo frequent state changes, without imposing prohibitive costs on users.

  4. Growing DeFi Ecosystem: Rootstock is actively fostering a vibrant DeFi ecosystem, bringing Bitcoin-backed stablecoins (like rUSDT and rUSDC), lending protocols, and exchanges to the Bitcoin network. Building on Rootstock allows your dApp to integrate with and benefit from this unique and secure financial ecosystem.

Router Protocol: The Universal Fabric of Interoperability

Once your dApp's core logic and assets are securely established on Rootstock, the challenge shifts to connecting it with the rest of the blockchain world. This is where Router Protocol truly shines, acting as the decentralized backbone for seamless cross-chain communication:

  1. Generic Cross-Chain Communication: Router Protocol is not just a simple asset bridge, it's a robust infrastructure for generic message passing. This means your dApp on Rootstock can send arbitrary data and execute functions on smart contracts residing on other chains (like BNB Smart Chain, Ethereum, Avalanche, Arbitrum, etc) and vice versa. This is crucial for complex dApps where actions on one chain need to trigger effects or updates on another.

  2. Seamless Asset Transfer: Beyond just messages, Router Protocol enables the secure and reliable transfer of tokens and NFTs between Rootstock and other supported chains. It handles the complexities of locking/burning on the source chain and minting/unlocking on the destination chain in a verified manner.

  3. Unified User Experience (UX): From the end-user's perspective, Router Protocol abstracts away the complexity of cross-chain interactions. A user can initiate a transaction from their preferred network (e.g., BNB Smart Chain), and Router transparently handles the secure routing and execution on the destination chain (Rootstock). This creates a smoother, more intuitive experience, making multi-chain interactions feel native.

  4. Enhanced Liquidity and Broader Reach: By opening up your dApp to multiple chains, you're no longer limited to the liquidity or user base of a single ecosystem. Router Protocol allows you to tap into the vast pools of capital and diverse user communities across the entire blockchain landscape, significantly expanding your dApp's potential for adoption and growth.

  5. Robust Decentralized Security: Router Protocol employs a decentralized network of validators, incentivized relayers, and a sophisticated cryptographic security model. This multi-layered approach ensures the integrity, authenticity, and reliability of all cross-chain transfers and messages, which is paramount for high-value digital assets and sensitive dApp operations.

Building a Cross-Chain NFT Smart Contract (XERC1155)

Let's dive into the code for a cross-chain compatible ERC-1155 NFT smart contract. This XERC1155 contract extends OpenZeppelin's robust ERC-1155 implementation with Router Protocol's IDapp and IGateway interfaces, enabling cross-chain functionality.

Complete Code Repository: XERC1155-Cross-Chain-NFT - Access all smart contracts, deployment scripts, and test cases

// SPDX-License-Identifier: Unlicensed
pragma solidity >=0.8.0 <0.9.0;

import "@routerprotocol/evm-gateway-contracts@1.1.11/contracts/IDapp.sol";
import "@routerprotocol/evm-gateway-contracts@1.1.11/contracts/IGateway.sol";
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";

/// @title XERC1155
/// @notice A cross-chain ERC-1155 smart contract to demonstrate how one can create
/// cross-chain NFT contracts using Router CrossTalk.
contract XERC1155 is ERC1155, IDapp {
  address public owner; // Owner of the contract
  IGateway public gatewayContract; // Address of the Router Protocol Gateway contract

  // Mapping to store the addresses of our contract on other chains for access control
  mapping(string => string) public ourContractOnChains;

  // Struct to define the parameters for an NFT transfer
  struct TransferParams {
    uint256[] nftIds; // Array of NFT IDs to transfer
    uint256[] nftAmounts; // Array of corresponding amounts for each NFT ID
    bytes nftData; // Additional data for the NFT (e.g., metadata updates)
    bytes recipient; // Encoded address of the recipient on the destination chain
  }

  // Constructor: Initializes the ERC-1155 URI, sets the Gateway, owner, and mints initial NFTs
  constructor(
    string memory _uri,
    address payable gatewayAddress,
    string memory feePayerAddress // Router Chain fee payer (can be any EVM address or Router wallet address)
  ) ERC1155(_uri) {
    gatewayContract = IGateway(gatewayAddress);
    owner = msg.sender;

    // Minting initial NFTs for testing purposes
    _mint(msg.sender, 1, 10, "");

    // Set dapp metadata on the Gateway contract, typically for fee payment configuration
    gatewayContract.setDappMetadata(feePayerAddress);
  }

  /// @notice Function to set the fee payer address on Router Chain.
  /// @param feePayerAddress address of the fee payer on Router Chain.
  function setDappMetadata(string memory feePayerAddress) external {
    require(msg.sender == owner, "only owner");
    gatewayContract.setDappMetadata(feePayerAddress);
  }

  /// @notice Function to update the Router Gateway Contract address.
  /// @param gateway address of the new Gateway contract.
  function setGateway(address gateway) external {
    require(msg.sender == owner, "only owner");
    gatewayContract = IGateway(gateway);
  }

  /// @notice Function to mint new NFTs (only callable by the owner).
  /// @param account The address to mint NFTs to.
  /// @param nftIds An array of NFT IDs to mint.
  /// @param amounts An array of corresponding amounts for each NFT ID.
  /// @param nftData Additional data for the NFTs.
  function mint(
    address account,
    uint256[] memory nftIds,
    uint256[] memory amounts,
    bytes memory nftData
  ) external {
    require(msg.sender == owner, "only owner");
    _mintBatch(account, nftIds, amounts, nftData);
  }

  /// @notice Function to set the address of our NFT contracts on different chains.
  /// This is crucial for access control and verification when a cross-chain request is received.
  /// @param chainId Chain ID of the destination chain in string format (e.g., "31" for RSK).
  /// @param contractAddress Address of the NFT contract on the destination chain in string format.
  function setContractOnChain(
    string calldata chainId,
    string calldata contractAddress
  ) external {
    require(msg.sender == owner, "only owner");
    ourContractOnChains[chainId] = contractAddress;
  }

  /// @notice Function to generate a cross-chain NFT transfer request.
  /// This function burns NFTs on the source chain and initiates a request to mint them on the destination.
  /// @param destChainId Chain ID of the destination chain in string.
  /// @param transferParams Struct containing NFT IDs, amounts, data, and recipient address.
  /// @param requestMetadata abi-encoded metadata according to source and destination chains (gas limits, relayer fees, etc.)
  function transferCrossChain(
    string calldata destChainId,
    TransferParams calldata transferParams,
    bytes calldata requestMetadata
  ) public payable {
    // Ensure the contract address on the destination chain is set for verification
    require(
      keccak256(abi.encodePacked(ourContractOnChains[destChainId])) !=
        keccak256(abi.encodePacked("")),
      "contract on dest not set"
    );

    // Burn the NFTs from the user's address on the SOURCE chain.
    // This prevents double-spending and secures the transfer.
    _burnBatch(msg.sender, transferParams.nftIds, transferParams.nftAmounts);

    // Encode the TransferParams struct into a packet for the destination chain.
    bytes memory packet = abi.encode(transferParams);
    // Encode the destination contract address and the packet into the final requestPacket.
    bytes memory requestPacket = abi.encode(
      ourContractOnChains[destChainId], // The address of our XERC1155 contract on the destination chain
      packet
    );

    // Call Router Gateway's iSend function to initiate the cross-chain request.
    // 'value' is for relayer fees.
    gatewayContract.iSend{ value: msg.value }(
      1, // Type: 1 for cross-chain execution request
      0, // Route type (0 for default)
      string(""), // Request sender (empty for now)
      destChainId, // Destination chain ID
      requestMetadata, // Encoded metadata (gas limits, fees, etc.)
      requestPacket // The actual payload to be sent
    );
  }

  /// @notice Function to get the request metadata to be used while initiating a cross-chain request.
  /// This helps in calculating and encoding the necessary parameters for the cross-chain call.
  /// @return requestMetadata abi-encoded metadata according to source and destination chains
  function getRequestMetadata(
    uint64 destGasLimit, // Gas limit for execution on the destination chain
    uint64 destGasPrice, // Gas price for execution on the destination chain
    uint64 ackGasLimit,  // Gas limit for acknowledgment on the source chain
    uint64 ackGasPrice,  // Gas price for acknowledgment on the source chain
    uint128 relayerFees, // Fees for the Router Protocol relayer network
    uint8 ackType,       // Acknowledgment type (e.g., 0 for no ack, 1 for basic ack)
    bool isReadCall,     // True if the call is a read-only operation (doesn't change state)
    bytes memory asmAddress // Address for Additional Security Module (advanced use case)
  ) public pure returns (bytes memory) {
    bytes memory requestMetadata = abi.encodePacked(
      destGasLimit,
      destGasPrice,
      ackGasLimit,
      ackGasPrice,
      relayerFees,
      ackType,
      isReadCall,
      asmAddress
    );
    return requestMetadata;
  }

  /// @notice Function to handle the cross-chain request received from some other chain.
  /// This function is called by the Router Gateway contract on the destination chain.
  /// @param packet The payload sent by the source chain contract when the request was created.
  /// @param srcChainId Chain ID of the source chain in string.
  function iReceive(
    string memory /* requestSender */, // Sender of the request (ignored in this example)
    bytes memory packet,
    string memory srcChainId
  ) external override returns (bytes memory) {
    // IMPORTANT: Only the Router Gateway contract should be able to call this function.
    require(msg.sender == address(gatewayContract), "only gateway");

    // Decode our payload (the TransferParams struct).
    TransferParams memory transferParams = abi.decode(packet, (TransferParams));

    // Mint the NFTs to the specified recipient on the DESTINATION chain.
    _mintBatch(
      toAddress(transferParams.recipient), // Convert bytes to address
      transferParams.nftIds,
      transferParams.nftAmounts,
      transferParams.nftData
    );

    return abi.encode(srcChainId); // Return source chain ID as acknowledgment data
  }

  /// @notice Function to handle the acknowledgment received from the destination chain
  /// back on the source chain. This function is typically used for tracking purposes.
  /// @param requestIdentifier Event nonce which is received when we create a cross-chain request.
  /// @param execFlag A boolean value suggesting whether the call was successfully
  /// executed on the destination chain.
  /// @param execData Returning the data returned from the iReceive function of the destination chain.
  function iAck(
    uint256 requestIdentifier,
    bool execFlag,
    bytes memory execData
  ) external override {
    // This function can be implemented to update local state based on acknowledgment.
    // For this basic NFT transfer, we don't need complex logic here.
  }

  /// @notice Helper function to convert bytes to an address.
  /// @param _bytes Bytes to be converted.
  /// @return addr Address pertaining to the bytes.
  function toAddress(bytes memory _bytes) internal pure returns (address addr) {
    bytes20 srcTokenAddress;
    assembly {
      srcTokenAddress := mload(add(_bytes, 0x20))
    }
    addr = address(srcTokenAddress);
  }
}

Key Functions Explained:

  • XERC1155 Contract: This is our core NFT contract. It manages the creation, burning, and minting of ERC-1155 NFTs, extended with cross-chain capabilities.

  • transferCrossChain Function: This is the heart of the cross-chain transfer on the source chain.

    • It first burns the specified NFTs from the sender's address on the source chain. This is crucial for preventing double-spending and maintaining supply integrity across chains.

    • It then creates a payload containing all the necessary information about the NFT transfer (IDs, amounts, recipient address, etc.).

    • Finally, it calls gatewayContract.iSend(), which is Router Protocol's primary function for initiating cross-chain requests. This sends the payload to the destination chain.

  • iReceive Function: This function is the entry point for cross-chain requests on the destination chain.

    • It's designed to be called only by the Router Protocol Gateway contract, ensuring security and authenticity.

    • It decodes the payload received from the source chain.

    • Based on the decoded information, it then mints the NFTs to the specified recipient address on the destination chain.

  • IDapp and IGateway Interfaces: These are provided by Router Protocol.

    • IDapp is implemented by your dApp contract (XERC1155) to enable it to send and receive cross-chain messages.

    • IGateway is the interface for interacting with Router Protocol's main Gateway contract on each chain, facilitating iSend and acting as the caller for iReceive.

  • setContractOnChain: This crucial function allows you to register the address of your XERC1155 contract on other chains. When iReceive is called on a destination chain, it can verify that the original request came from a trusted counterpart of the same contract on the source chain.

  • getRequestMetadata: A utility function to properly encode parameters like gas limits, gas prices, and relayer fees, which are essential for Router Protocol to execute the cross-chain transaction successfully.

Cross-Chain NFT Flow Visualization

Deploying Your Cross-Chain NFT Contract

Now that we understand the contract, let's prepare for deployment on Rootstock Testnet and BNB Smart Chain (BSC) Testnet.

Setup Prerequisites:

  1. MetaMask Configuration:

    • Visit chainlist.org.

    • Connect your MetaMask wallet.

    • Enable Include testnets.

    • Search for and add:

      • Rootstock Testnet (Chain ID: 31)

      • BNB Smart Chain Testnet (Chain ID: 97)

  2. Obtain Testnet Tokens:

  3. Router Protocol Testnet ROUTE Tokens:

    • You'll also need test ROUTE tokens to pay for relayer fees (though Router Protocol often subsidizes testnet transactions or allows a zero fee for testing).

    • Go to the Router Protocol Faucet.

    • Connect your wallet and request test ROUTE tokens for both Rootstock and BSC Testnet.

Compile Your Contract using Remix IDE:

  1. Navigate to Remix Ethereum IDE.

  2. Create a new workspace (select the Blank template).

  3. Create a new file named XERC1155.sol.

  4. Copy and paste the XERC1155 smart contract code provided above into this new file.

  5. Go to the Solidity compiler tab (the icon resembling a compass).

  6. Ensure the compiler version (0.8.0 to 0.9.0) matches your pragma statement.

  7. Click Compile XERC1155.sol.

Deploy the Contract on Both Chains:

You will deploy the same XERC1155 contract independently on both the Rootstock Testnet and the BSC Testnet.

  1. In Remix, go to the Deploy & run transactions tab (the icon resembling an Ethereum logo).

  2. Under Environment, select Injected Provider - MetaMask. Your MetaMask wallet should pop up, prompting you to connect.

  3. Deploy on Rootstock Testnet:

    • Switch your MetaMask network to Rootstock Testnet.

    • In Remix, select the XERC1155 contract from the dropdown.

    • Click the Deploy button. A MetaMask pop-up will appear asking for constructor arguments.

    • Constructor Parameters:

      • _uri: Provide a base URI for your NFTs (e.g., https://yournftproject.com/metadata/{id}.json). This is where your NFT metadata will be hosted.

      • gatewayAddress: Use the Router Protocol Gateway address for Rootstock Testnet. (Please always verify the latest Router Testnet Gateway addresses from Router Protocol's official documentation, as they can change. For a conceptual run, you might use a placeholder like 0x0000000000000000000000000000000000000000 but replace it with the actual address for deployment).

      • feePayerAddress: Your MetaMask wallet address (this will be the address paying for fees on the Router Chain, which can be an EVM address).

    • Confirm the transaction in MetaMask. Once confirmed, save the deployed contract address (e.g., XERC1155_RSK_ADDRESS).

  4. Deploy on BNB Smart Chain Testnet:

    • Switch your MetaMask network to BNB Smart Chain Testnet (BSC Testnet).

    • In Remix, ensure XERC1155 is still selected.

    • Click the Deploy button again.

    • Constructor Parameters:

      • _uri: Use the same base URI as before.

      • gatewayAddress: Use the Router Protocol Gateway address for BNB Smart Chain Testnet. (Again, verify this address from official Router Protocol documentation. Placeholder: 0x0000000000000000000000000000000000000000).

      • feePayerAddress: Your MetaMask wallet address.

    • Confirm the transaction in MetaMask. Save this deployed contract address (e.g., XERC1155_BSC_ADDRESS).

Router Protocol Gateway Addresses (Example for Testnets - ALWAYS VERIFY LATEST):

Network TypeChain IDChain NameGateway Address (Placeholder - VERIFY)
Testnet31RSK Testnet0xRSK_ROUTER_GATEWAY_ADDRESS_HERE
Testnet97BSC Testnet0xBSC_ROUTER_GATEWAY_ADDRESS_HERE

(Note: Always make sure to set an appropriate gas fee when deploying to ensure successful transaction processing, especially on testnets where default gas might be too low.)

Post-Deployment Configuration:

After deploying your identical XERC1155 contract on both Rootstock and BNB Smart Chain, you need to configure them to recognize each other through Router Protocol.

  1. Approve Fee-Payer Requests (Router Explorer):

    • Go to the Router Protocol Explorer (ensure you select Testnet).

    • Connect your MetaMask wallet.

    • You may need to approve any pending requests for your newly deployed contracts to allow them to interact with the Router network.

  2. Configure Cross-Chain Contract Recognition:

    • This is a critical step for security and proper routing. For each deployed XERC1155 contract, you'll call its setContractOnChain function.

    • On your Rootstock Testnet XERC1155_RSK_ADDRESS contract (via Remix or Etherscan):

      • Call setContractOnChain.

      • chainId: 97 (string representation of BSC Testnet's Chain ID).

      • contractAddress: XERC1155_BSC_ADDRESS (the address of your deployed contract on BSC Testnet).

    • On your BSC Testnet XERC1155_BSC_ADDRESS contract (via Remix or BscScan):

      • Call setContractOnChain.

      • chainId: 31 (string representation of Rootstock Testnet's Chain ID).

      • contractAddress: XERC1155_RSK_ADDRESS (the address of your deployed contract on Rootstock Testnet).

This step ensures that when a request comes in via Router, the receiving contract knows that the request originated from a trusted counterpart.

Part 3: Testing Cross-Chain NFT Transfers

Now, for the exciting part โ€“ transferring an NFT from Rootstock to BNB Smart Chain!

Scenario: You initially minted 10 NFTs of ID 1 to yourself in the XERC1155 contract on Rootstock (as per the constructor). You now want to transfer 5 of these NFTs (ID 1) to another address (e.g., 0xYourRecipientAddress) on BNB Smart Chain.

  1. Generate Request Metadata:

    • In Remix (or Etherscan/BscScan), switch your MetaMask to Rootstock Testnet.

    • Interact with your deployed XERC1155_RSK_ADDRESS contract.

    • Call the getRequestMetadata function (it's a pure function, so it's a local call).

    • Parameters (adjust based on current network conditions and Router Protocol recommendations):

      • destGasLimit: e.g., 300000 (sufficient gas for minting on BSC).

      • destGasPrice: e.g., 5 (gwei, check current BSC Testnet gas prices).

      • ackGasLimit: e.g., 100000 (for the acknowledgment on RSK).

      • ackGasPrice: e.g., 1 (gwei, check current RSK Testnet gas prices).

      • relayerFees: e.g., 10000000000000000 (0.01 ROUTE, or check Router Protocol's docs for testnet fee requirements, might be 0 for testing). This is the msg.value you send with transferCrossChain.

      • ackType: 1 (for a basic acknowledgment).

      • isReadCall: false (we are changing state - minting NFTs).

      • asmAddress: 0x0000000000000000000000000000000000000000 (for this basic example).

    • Copy the returned bytes memory value. This is your requestMetadata.

  2. Initiate the Cross-Chain Transfer (from Rootstock Testnet):

    • Still on RSK Testnet in MetaMask and Remix.

    • Interact with your XERC1155_RSK_ADDRESS contract.

    • Call the transferCrossChain function.

    • Parameters:

      • destChainId: 97 (string for BSC Testnet ID).

      • transferParams: This is a tuple. You need to encode it manually or use Remix's ABI encoder for tuple input.

        • nftIds: [1] (transferring NFT ID 1)

        • nftAmounts: [5] (transferring 5 units of NFT ID 1)

        • nftData: 0x (empty bytes, no extra data needed for this transfer)

        • recipient: The encoded address of the recipient on BSC Testnet. Use toAddress function in Remix or abi.encodePacked(recipientAddress) outside. E.g., if recipient is 0xYourRecipientAddressOnBSC, it's 0x000000000000000000000000YourRecipientAddressOnBSC.

      • requestMetadata: Paste the bytes memory value you generated in the previous step.

    • Value: Input the relayerFees amount (e.g., 0.01 ROUTE if that was the fee) into the Value field in Remix. This will be sent as msg.value.

    • Confirm the transaction in MetaMask.

  3. Track Your Transaction:

    • Monitor the status of your cross-chain transaction on the Router Explorer. You'll see the transaction originating on Rootstock, being processed by Router, and then executing on BSC.

    • You can also check the transaction hash on Rootstock Testnet Explorer for the initial burn and then on BSC Testnet Explorer for the eventual minting.

  4. Verify the Transfer:

    • Switch your MetaMask to BNB Smart Chain Testnet.

    • Connect your wallet to a testnet NFT explorer or directly query the balanceOf function of your XERC1155_BSC_ADDRESS contract for your recipient address and NFT ID 1.

    • You should see that your recipient address now holds 5 units of NFT ID 1 on the BSC Testnet! The NFTs were successfully burned on Rootstock and minted on BSC.

CONGRATULATIONSSS! ๐Ÿš€๐Ÿฅณ

If facing any issues, you can refer to my project on GitHub: XERC1155-Cross-Chain-NFT

Cross-Chain NFT Transfer Validation Guide

Step-by-Step Validation Process

1. Check Source Chain (Verify NFTs Were Burned)

After initiating a cross-chain transfer, first verify that the NFTs were properly burned on the source chain:

// Using web3.js
const sourceBalance = await xerc1155RSK.methods.balanceOf(userAddress, tokenId).call();
console.log("Remaining balance on RSK:", sourceBalance);

// Using ethers.js
const sourceBalance = await xerc1155RSK.balanceOf(userAddress, tokenId);
console.log("Remaining balance on RSK:", sourceBalance.toString());

Expected Result: The balance should be reduced by the amount you transferred.

2. Check Destination Chain (Verify NFTs Were Minted)

Next, verify that the NFTs were successfully minted on the destination chain:

// Using web3.js
const destBalance = await xerc1155BSC.methods.balanceOf(recipientAddress, tokenId).call();
console.log("New balance on BSC:", destBalance);

// Using ethers.js
const destBalance = await xerc1155BSC.balanceOf(recipientAddress, tokenId);
console.log("New balance on BSC:", destBalance.toString());

Expected Result: The recipient's balance should increase by the transferred amount.

3. Monitor Transfer Events

Listen for the transfer events emitted by the contract:

// Check for transfer initiation event on source chain
xerc1155RSK.events.CrossChainTransferInitiated({
    filter: { sender: userAddress },
    fromBlock: 'latest'
})
.on('data', function(event) {
    console.log('Transfer initiated:', event.returnValues);
});

// Check for transfer completion event on destination chain
xerc1155BSC.events.CrossChainTransferReceived({
    filter: { recipient: recipientAddress },
    fromBlock: 'latest'
})
.on('data', function(event) {
    console.log('Transfer received:', event.returnValues);
});

4. Track via Router Protocol Explorer

Use Router Protocol's explorer to monitor the cross-chain transaction:

  1. Visit Router Protocol Explorer

  2. Enter your transaction hash from the source chain

  3. Monitor the status through these stages:

    • Initiated: Transaction submitted on source chain

    • Validated: Router validators confirmed the transaction

    • Executed: Transaction completed on destination chain

5. Complete Validation Script

Here's a comprehensive validation function:

async function validateCrossChainTransfer(
    sourceContract,
    destContract,
    userAddress,
    recipientAddress,
    tokenId,
    transferAmount,
    txHash
) {
    console.log("๐Ÿ” Validating cross-chain transfer...");

    // 1. Check source chain balance
    const sourceBalance = await sourceContract.balanceOf(userAddress, tokenId);
    console.log(`๐Ÿ“‰ Source balance after transfer: ${sourceBalance}`);

    // 2. Wait for cross-chain processing (may take 2-5 minutes)
    console.log("โณ Waiting for cross-chain processing...");
    await new Promise(resolve => setTimeout(resolve, 180000)); // Wait 3 minutes

    // 3. Check destination chain balance
    const destBalance = await destContract.balanceOf(recipientAddress, tokenId);
    console.log(`๐Ÿ“ˆ Destination balance: ${destBalance}`);

    // 4. Validate transfer success
    const transferSuccessful = destBalance.gte(transferAmount);

    if (transferSuccessful) {
        console.log("โœ… Cross-chain transfer completed successfully!");
        return {
            success: true,
            sourceBalance: sourceBalance.toString(),
            destBalance: destBalance.toString()
        };
    } else {
        console.log("โŒ Cross-chain transfer may have failed or is still processing");
        console.log(`๐Ÿ”— Check Router Explorer: https://explorer.routerprotocol.com/tx/${txHash}`);
        return {
            success: false,
            sourceBalance: sourceBalance.toString(),
            destBalance: destBalance.toString()
        };
    }
}

6. Batch Balance Validation

For multiple NFT IDs transferred in a batch:

async function validateBatchTransfer(
    sourceContract,
    destContract,
    userAddress,
    recipientAddress,
    tokenIds,
    amounts
) {
    console.log("๐Ÿ” Validating batch cross-chain transfer...");

    // Check balances for all token IDs
    const sourceBalances = await sourceContract.balanceOfBatch(
        tokenIds.map(() => userAddress),
        tokenIds
    );

    const destBalances = await destContract.balanceOfBatch(
        tokenIds.map(() => recipientAddress),
        tokenIds
    );

    console.log("Source balances:", sourceBalances.map(b => b.toString()));
    console.log("Destination balances:", destBalances.map(b => b.toString()));

    // Validate each token transfer
    let allSuccessful = true;
    for (let i = 0; i < tokenIds.length; i++) {
        const expectedAmount = amounts[i];
        const actualAmount = destBalances[i];

        if (actualAmount.lt(expectedAmount)) {
            console.log(`โŒ Token ID ${tokenIds[i]} transfer incomplete`);
            allSuccessful = false;
        } else {
            console.log(`โœ… Token ID ${tokenIds[i]} transferred successfully`);
        }
    }

    return allSuccessful;
}

Troubleshooting Failed Validations

If Source Balance Unchanged

  • Transaction may have failed on source chain

  • Check transaction receipt and revert reasons

  • Ensure sufficient gas and correct parameters

If Destination Balance Unchanged

  • Cross-chain processing may still be in progress (wait 5-10 minutes)

  • Check Router Protocol explorer for status

  • Verify destination chain gateway is operational

If Partial Transfer Detected

  • Some tokens in batch may have failed

  • Check individual token balances

  • Review transfer events for specific failures

Considerations for Developers: Building Robust Cross-Chain DApps

Building interconnected dApps, while revolutionary, comes with its own set of challenges. Here are key considerations for developers:

  1. Security First: Cross-chain bridges and communication protocols are prime targets for attacks.

    • Audit Your Contracts: Thoroughly audit your smart contracts for vulnerabilities.

    • Verify Router Integration: Ensure your iReceive function strictly checks msg.sender == address(gatewayContract).

    • Trust Assumptions: Understand Router Protocol's security model (decentralized validators, multi-party computation, etc.) and your dApp's reliance on it.

  2. Gas Management Across Chains:

  3. Error Handling and Monitoring:

  4. State Synchronization and Consistency: For complex dApps that maintain state across multiple chains, ensure strong logic for synchronizing data and handling potential inconsistencies or race conditions. The generic message passing of Router Protocol is powerful but requires careful architectural planning.

  5. User Experience (UX) Design:

    • Transparency: Inform users about the cross-chain nature of transactions, even if seamless.

    • Feedback: Provide clear feedback on the status of cross-chain transactions (e.g., Pending cross-chain transfer, NFT minted on destination chain).

    • Wallet Network Switching: Guide users on when they need to switch networks in their wallet (e.g., initiating on BSC, verifying on Rootstock).

  6. Upgradeability: Plan for future upgrades to your dApp's smart contracts on both Rootstock and other connected chains. Proxy patterns are often used for this.

Benefits of Cross-Chain NFTs with Router Protocol on Rootstock

Implementing cross-chain NFTs using Router Protocol, with Rootstock as a core component, offers a multitude of advantages:

  • Unleashing Bitcoin's Potential for NFTs: By leveraging Rootstock, your NFTs benefit from the most secure blockchain in existence, adding a layer of trust and longevity that is unmatched. This brings a new paradigm of high-assurance NFTs to the Bitcoin ecosystem.

  • Enhanced Liquidity & Market Reach: Access to multiple marketplaces across various chains (e.g., minting on Rootstock, listing on OpenSea via a wrapped version on Ethereum) dramatically increases the potential for NFT sales and trading.

  • Optimal Transaction Costs: Transfer NFTs to chains with lower gas fees (like BSC Testnet for interactions, or Rootstock for core game logic) for more cost-effective minting, transfers, and in-dApp transactions, making NFTs more accessible to a broader audience.

  • Broader Ecosystem Access: Allow NFT holders to interact with diverse DeFi protocols, gaming dApps, and metaverses across multiple chains, unlocking new utility for their digital assets.

  • Increased Resilience & Decentralization: Reduce dependence on a single blockchain network, enhancing your project's robustness, censorship resistance, and overall longevity.

  • Expanded User Base & Global Reach: Appeal to users who prefer specific chains, have assets on different networks, or are new to Web3, breaking down geographical and technical barriers.

  • Future-Proofing Your Project: As the blockchain space continues its multi-chain evolution, building with Router Protocol ensures your project remains adaptable, scalable, and relevant.

Conclusion: The Future is Interconnected

In today's increasingly complex and fragmented blockchain ecosystem, the ability to seamlessly move digital assets and data across different chains is no longer a luxury; it's a necessity. Router Protocol provides a robust, secure, and decentralized infrastructure that empowers developers to build truly cross-chain applications, creating unified and fluid experiences for users.

By combining the unparalleled security and EVM compatibility of Rootstock for your core NFT logic and backend operations with the expansive reach and seamless communication capabilities of Router Protocol, you are equipped to build the next generation of dApps. This combination not only enhances the utility and value of your digital assets but also plays a crucial role in bridging the gaps between different chains, creating a more cohesive and unified blockchain ecosystem.

Remember, rigorous testing on testnets is crucial before deploying to mainnet. Always prioritize security best practices when developing cross-chain applications. The future of decentralized applications is interconnected, and with tools like Rootstock and Router Protocol, you are at the forefront of building it.

If facing any errors, join Rootstock Discord and ask under the respective channel.

Until then, dive deeper into Rootstock by exploring its official documentation. Keep experimenting, and happy coding!

0
Subscribe to my newsletter

Read articles from Pranav Konde directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Pranav Konde
Pranav Konde