Manual deployment

Let's manually build a smart contract that leverages Inco's random number generator in order to generate a random card using our external general message passing (GMP). For this tutorial we recommend having two different browser windows open with Metamask plugin installed on each.

Contract on Inco (A)

// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity >=0.8.13 <0.9.0;

import "fhevm/abstracts/EIP712WithModifier.sol";
import "fhevm/lib/TFHE.sol";

interface IInterchainExecuteRouter {
    function getRemoteInterchainAccount(uint32 _destination, address _owner) external view returns (address);
}

contract HiddenCard is EIP712WithModifier {
    uint32 ChainID;
    address public iexRouter;
    address public caller_contract;
    bool public isInitialized;
    
    constructor() EIP712WithModifier("Authorization token", "1") {
    }

    function initialize(uint32 _ChainID, address _iexRouter) public {
        require(isInitialized == false, "Bridge contract already initialized");
        ChainID = _ChainID;
        iexRouter = _iexRouter;
        caller_contract = msg.sender;
        isInitialized = true;
    }
    
    function setCallerContract(address _caller_contract) onlyCallerContract public {
        caller_contract = _caller_contract;
    }

    function getICA() public view returns(address) {
        return IInterchainExecuteRouter(iexRouter).getRemoteInterchainAccount(ChainID, address(this));
    }
    
    modifier onlyCallerContract() {
        require(caller_contract == msg.sender, "not right caller contract");
        _;
    }
    
    //Your hidden logic here
}

This boilerplate contract has two functions necessary for the initial "setup" that need to be called one after another. One is initialize(uint32, address, address) which takes in:

  • ChainID of target chain (84532 in case of Base Sepolia)

  • iexRouter address for Inco (0x015b8be6946ee593Ee2230E56221Db9cEE22aC20)

  • caller_contract, the address that is allowed to call the contract functions through the bridge (retreived by calling getICA() on the target contract)

Please refer to Cross-Chain gateways section to find your target chain router address and chainID.

pageCross-Chain gateways
InterchainAccountRouterInterchainExecuteRouterInterchainQueryRouterDomain/ChainID

Inco Gentry

0x86A5337E029B32BA1d34e89Ff9A96667583C7b4B

0x015b8be6946ee593Ee2230E56221Db9cEE22aC20

0xFD8992F4a09519d03071354683435c196c4b254c

9090

Base Sepolia

0x7867F458DBF31D9D9F7B1B758ea3847C3f7345fd

0xAC4fAb4c9E99606d255EB87cFAfAd4587801f743

0x24303e65069756C8A9ae73E5567d9B59176B7d75

84532

Edgeless

0xEA7E5a8Cb8741250326532b72c1bA05D067F8A61

0xc5F722b899dee3F01fC530f10664043aF0B927B2

0xC0F6aa9bA0f833d6a9e2c3AC2D3D7973Ba2984F2

202

The second function getICA(address) will return a contract address that belongs to the bridge, and it's the contract that will call our custom bridge functions on our behalf.

Let's continue writing the rest of our smart contract on Inco. We just need to write two custom functions, one for generating a random card, and the other for reveling user's card:

//Inside of HiddenCard contract add the following
    mapping (address => euint8) public encryptedCards;

    function returnCard(address user) external onlyCallerContract returns(uint8) {
        encryptedCards[user] = TFHE.rem(TFHE.randEuint8(), 52);
        return TFHE.decrypt(encryptedCards[user]);
    }

    function viewCard(address user) external view returns (uint8) {
        return TFHE.decrypt(encryptedCards[user]);
    }

Contract on target chain (B)

Now we need to write a second contract which will be deployed on your target chain that isn't Inco. The boilerplate code looks very similar to previous one:

// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity >=0.8.13 <0.9.0;

interface IInterchainExecuteRouter {
    function getRemoteInterchainAccount(uint32 _destination, address _owner) external view returns (address);
    // New
    function callRemote(uint32 _destination, address _to, uint256 _value, bytes calldata _data, bytes memory _callback) external returns (bytes32);
}

contract Card {
    uint32 ChainID;
    address hiddencard; // HiddenCard contract in Inco Network
    address iexRouter; // InterchainExcuteRouter contract address in current chain
    address caller_contract;
    bool public isInitialized;

    function initialize(uint32 _ChainID, address _hiddencard, address _iexRouter) public {
        require(isInitialized == false, "Bridge contract already initialized");
        ChainID = _ChainID;
        hiddencard = _hiddencard;
        iexRouter = _iexRouter;
        caller_contract = msg.sender;
        isInitialized = true;
    }
    
    function setCallerContract(address _caller_contract) onlyCallerContract public {
        caller_contract = _caller_contract;
    }

    function getICA() public view returns(address) {
        return IInterchainExecuteRouter(iexRouter).getRemoteInterchainAccount(ChainID, address(this));
    }
    
    modifier onlyCallerContract() {
        require(caller_contract == msg.sender, "not right caller contract");
        _;
    }
    
    //Your public logic here
}

In order to interact with our Inco contract, we use Interchain Execution Router to call our code on Inco by passing arguments like chainID, contract address containing our hidden logic on Inco, rlp encoded function signature with arguments that will be passed in as calldata, and an rlp encoded func signature of a callback function that will be invoked containing our return values (in our case it's cardReceive(uint256, uint8) function).

interface HiddenCard {
    function returnCard(address user) external returns(uint8);
}

contract Card {
    ...
    mapping (address => uint8) public Cards;
    
    function CardGet(address user) public {
        HiddenCard _Hiddencard = HiddenCard(hiddencard);

        bytes memory _callback = abi.encodePacked(this.cardReceive.selector, (uint256(uint160(user))));

        IInterchainExecuteRouter(iexRouter).callRemote(
            ChainID,
            address(_Hiddencard),
            0,
            abi.encodeCall(_Hiddencard.returnCard, (user)),
            _callback
        );
    }
    
    function cardReceive(uint256 user, uint8 _card) external {
        require(caller_contract == msg.sender, "not right caller contract");
        Cards[address(uint160(user))] = _card;
    }
    
    function CardView(address user) public view returns(uint8) {
        return Cards[user];
    }
}

Deployment

When deploying both contracts on their respective chains, we first initialize each contract manually, starting with contract on Inco, since we need to pass it's address to the contract on Base.

For the left window A we use the following highlighted values from the table to initialize the contract on Inco:

InterchainAccountRouterInterchainExecuteRouterInterchainQueryRouterDomain/ChainID

Inco Gentry

0x86A5337E029B32BA1d34e89Ff9A96667583C7b4B

0x015b8be6946ee593Ee2230E56221Db9cEE22aC20

0xFD8992F4a09519d03071354683435c196c4b254c

9090

Base Sepolia

0x7867F458DBF31D9D9F7B1B758ea3847C3f7345fd

0xAC4fAb4c9E99606d255EB87cFAfAd4587801f743

0x24303e65069756C8A9ae73E5567d9B59176B7d75

84532

For the right window B we use the following values instead to initialize the contract on Base:

InterchainAccountRouterInterchainExecuteRouterInterchainQueryRouterDomain/ChainID

Inco Gentry

0x86A5337E029B32BA1d34e89Ff9A96667583C7b4B

0x015b8be6946ee593Ee2230E56221Db9cEE22aC20

0xFD8992F4a09519d03071354683435c196c4b254c

9090

Base Sepolia

0x7867F458DBF31D9D9F7B1B758ea3847C3f7345fd

0xAC4fAb4c9E99606d255EB87cFAfAd4587801f743

0x24303e65069756C8A9ae73E5567d9B59176B7d75

84532

Please refer to Cross-Chain gateways table to find correct DestinationDomain as well as iexRouter address.

pageCross-Chain gateways

Next we call getICA() followed by setCallerContract() on Inco to get the caller contract address that will be allowed to call to it, and for the one on base we just set the same value we used for iexRouter:

Now both bridge contracts should be able to talk to each other.

Using the bridge

In order to use the bridge, we simply interact with the contract deployed on Base. To check if everything works correctly, we first call GetCard(address) function in order to generate a card (address we pass as argument doesn't matter), then we call CardView(address) with the same address in order to reveal the card stored on Inco:

And now in order to reveal the card that we have just generated, we call CardView() function, passing the same ethereum address as argument:

And that is it for now. Hope you find this tutorial helpful, and happy bridging!

Last updated