はじめに

初めまして。 『DApps開発入門』という本や色々記事を書いているAva Labsのかるでねです。

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!

今回は『Avalanche ICTT』についてまとめていきます。 Avalanche ICTTの各処理フローについて図を元にまとめているので、この記事を読むことで非エンジニアでも全体像が掴めます。 また、エンジニア向けにソースコードを元に詳しく説明しているので、技術的にも理解を深められると思います。

Avalanche ICTTについて、より詳しく知りたい場合は以下を読んでください。

ICTT概要

Avalanche Interchain Token Transfer (ICTT)は、異なるL1ブロックチェーン間でトークンを送付するためのアプリケーションです。 このシステムは、複数のL1にデプロイされた一連のスマートコントラクトによって実装されており、クロスチェーン通信にはICM(Inter-Chain Messaging) を利用します。

ICM(Inter-Chain Messaging)については以下の記事を読んでください。

ICTTは、1つのTokenHomeコントラクトと、1つ以上のTokenRemoteコントラクトから構成されます。

Homeコントラクトは、送付元となるトークンが存在するL1(Homeチェーン)上にデプロイされて、トークンの管理を担当します。 TokenRemoteコントラクトは、送付先となるL1(Remoteチェーン)上にデプロイされ、送付したいトークンのラップトークンのコントラクトです。

ICTT概要の図

コントラクト構成

コントラクトの継承関係は以下のようになっています。

コントラクト構成の図

TokenHomeコントラクトに、コアの実装やERC20とネイティブトークンで共通する機能の実装や必要な関数の定義などがされています。 TokenHomeコントラクトを継承している〇〇UpgradeableContractでは、ERC20とネイティブトークンそれぞれ特有の処理をしています。 さらにそれを継承しているERC20TokenHomeコントラクトとNativeTokenHomeコントラクトは、〇〇UpgradeableContractをそのまま実装してあり、カスタマイズ実装をしたい場合などにはここに実装を追加します。

トークン送付

トークン送付のプロセスは、Homeチェーンでトークンをロックし、 Remoteチェーンでそのトークンをラップしたトークンを新規に発行します。

トークン送付の図

  1. ユーザーが保有しているトークンの transfer する権限を TokenHomeコントラクトに付与するリクエスト。
  2. transfer する権限を付与。
  3. Remoteチェーン上にトークンを送付するリクエスト。
  4. トークンをTokenHomeコントラクトにロックして、Remoteチェーン上にトークンを送付。

TokenHomeコントラクトは、各L1上のTokenRemoteコントラクトに送付されたトークン残高を追跡し、トークンがTokenHomeコントラクトに戻される時には、TokenHomeコントラクトにロックされていた元のトークンをユーザーに返却します。

トークン送付の図

  1. ユーザーがRemoteチェーンに送付したトークンを戻すリクエスト。
  2. RemoteチェーンからHomeチェーンにトークンを送付。
  3. ロックしていたトークンをユーザーに返す。

TokenHomeコントラクトは、ERC20トークンかネイティブトークンのいずれかを送付し、事前に登録されている任意のTokenRemoteコントラクトにトークンを送付できます。 Remoteチェーン上のトークンもERC20かネイティブトークンのいずれかであり、ユーザーはHomeチェーンとRemoteチェーン間で以下のようなERC20とネイティブトークンの組み合わせでトークンを交換できます。

トークン送付の図

また、マルチホップTransferもサポートしています。 例えば、同じTokenHomeコントラクトに接続された2つのRemoteチェーンと各チェーン上のTokenRemoteコントラクトへの送付は、まず1つ目のRemoteチェーンのTokenRemoteコントラクトからTokenHomeコントラクトへリクエストが送られて、そのリクエストを受け取ったTokenHomeコントラクトが2つ目のRemoteチェーンのTokenRemoteコントラクトにトークンの送付を行います。

トークン送付の図

基本的なトークン送付に加えて、sendAndCallインターフェースが提供されており、これを利用するとトークンの送付とスマートコントラクトでの利用を単一のICMメッセージ内で完結させることができます。 もし宛先のスマートコントラクトの呼び出しが失敗した場合、送付されたトークンは送付先チェーン上のフォールバック用アドレスに送られます。 sendAndCallインターフェースにより、送付されたトークンを他のチェーン上のdAppsで直接使用すること(スワップの実行、サービス利用時の手数料支払いなど)が可能になります。

フロー

ICTTはICMを活用した仕組みのため、前提としてクロスチェーン通信を行うときには、以下のように「Teleporter」、「Warp Precompile」、「Relayer」を経由しています。 上記を含めたICMの詳しいフローは以下の記事を読んでください。

コントラクトのデプロイ

まずは、以下のコントラクトを一式デプロイしていきます。

コントラクトのデプロイの図

  • Homeチェーン
    • TokenHomeコントラクト
    • ERC20コントラクト
  • Remoteチェーン
    • TokenRemoteコントラクト

TokenHomeコントラクト

TokenHomeコントラクトをデプロイする時は、以下のようにTeleporterコントラクトの情報とERC20コントラクトのアドレスを渡します。

contracts/ictt/TokenHome/TokenHome.sol#L112-L124
        address teleporterRegistryAddress,
        address teleporterManager,
        uint256 minTeleporterVersion,
        address tokenAddress,
        uint8 tokenDecimals
    ) internal virtual onlyInitializing {
        __TeleporterRegistryOwnableApp_init(
            teleporterRegistryAddress, teleporterManager, minTeleporterVersion
        );
        __SendReentrancyGuard_init();
        __TokenHome_init_unchained(tokenAddress, tokenDecimals);
    }
ava-labs/icm-contracts の元のファイルを開く

teleporterRegistryAddress と teleporterManager は、Teleporterコントラクトのバージョン管理を行うコントラクトです。

Teleporterコントラクトについては以下の記事を読んでください。

ネイティブトークンを使用する場合は、以下のようにネイティブトークンのラップトークンコントラクトのアドレスを渡しています。

contracts/ictt/TokenHome/NativeTokenHomeUpgradeable.sol#L80-L89
    function initialize(
        address teleporterRegistryAddress,
        address teleporterManager,
        uint256 minTeleporterVersion,
        address wrappedTokenAddress
    ) public initializer {
        __NativeTokenHome_init(
            teleporterRegistryAddress, teleporterManager, minTeleporterVersion, wrappedTokenAddress
        );
    }
ava-labs/icm-contracts の元のファイルを開く

これは、ネイティブトークンでもERC20トークンと同じフローで処理されるように、ラップトークンに変換するようにして抽象化されています。

TokenRemoteコントラクト

TokenRemoteコントラクトのデプロイ時には、以下のようにERC20トークンを継承しているので、ERC20のトークン名やシンボルを渡す必要があります。

contracts/ictt/TokenRemote/ERC20TokenRemoteUpgradeable.sol#L30
contract ERC20TokenRemoteUpgradeable is IERC20TokenTransferrer, ERC20Upgradeable, TokenRemote {
ava-labs/icm-contracts の元のファイルを開く

以下の tokenName と tokenSymbol にERC20のトークン名とシンボルを渡しています。

contracts/ictt/TokenRemote/ERC20TokenRemoteUpgradeable.sol#L78-L85
    function initialize(
        TokenRemoteSettings memory settings,
        string memory tokenName,
        string memory tokenSymbol,
        uint8 tokenDecimals
    ) public initializer {
        __ERC20TokenRemote_init(settings, tokenName, tokenSymbol, tokenDecimals);
    }
ava-labs/icm-contracts の元のファイルを開く

ネイティブトークンの場合は、そのままネイティブトークンがMintされます。

また、TokenRemoteSettings には、以下のようにTeleporterコントラクトの情報とHomeチェーンのチェーンIDとTokenHomeコントラクトのアドレスを登録しています。 これはICM(Interchain Messaging)で必要になる情報です。

contracts/ictt/TokenRemote/interfaces/ITokenRemote.sol#L22-L29
struct TokenRemoteSettings {
    address teleporterRegistryAddress;
    address teleporterManager;
    uint256 minTeleporterVersion;
    bytes32 tokenHomeBlockchainID;
    address tokenHomeAddress;
    uint8 tokenHomeDecimals;
}
ava-labs/icm-contracts の元のファイルを開く

Remoteコントラクトの登録

TokenHomeコントラクトには、TokenRemoteコントラクトの情報を登録していないため、事前に登録しておく必要があります。

Remoteコントラクトの登録の図

TokenRemoteコントラクトの方は、TokenRemoteコントラクトデプロイ時に、TokenHomeコントラクトの登録は完了しています。

Remoteコントラクトの登録の図

TokenRemoteコントラクトの登録は、registerWithHome 関数を実行することで行われます。

contracts/ictt/TokenRemote/TokenRemote.sol#L207-L235
        TeleporterFeeInfo calldata feeInfo
    ) external virtual {
        TokenRemoteStorage storage $ = _getTokenRemoteStorage();
        require(!$._isRegistered, "TokenRemote: already registered");

        // Send a message to the token TokenHome instance to register this TokenRemote instance.
        RegisterRemoteMessage memory registerMessage = RegisterRemoteMessage({
            initialReserveImbalance: $._initialReserveImbalance,
            homeTokenDecimals: $._homeTokenDecimals,
            remoteTokenDecimals: $._tokenDecimals
        });
        TransferrerMessage memory message = TransferrerMessage({
            messageType: TransferrerMessageType.REGISTER_REMOTE,
            payload: abi.encode(registerMessage)
        });

        uint256 feeAmount = _handleFees(feeInfo.feeTokenAddress, feeInfo.amount);
        _sendTeleporterMessage(
            TeleporterMessageInput({
                destinationBlockchainID: $._tokenHomeBlockchainID,
                destinationAddress: $._tokenHomeAddress,
                feeInfo: TeleporterFeeInfo({feeTokenAddress: feeInfo.feeTokenAddress, amount: feeAmount}),
                requiredGasLimit: REGISTER_REMOTE_REQUIRED_GAS,
                allowedRelayerAddresses: new address[](0),
                message: abi.encode(message)
            })
        );
    }
ava-labs/icm-contracts の元のファイルを開く

ICMを使用した場合、クロスチェーン通信時に呼ばれる関数は、以下の_receiveTeleporterMessage 関数です。

contracts/ictt/TokenHome/TokenHome.sol#L495-L595
        bytes32 sourceBlockchainID,
        address originSenderAddress,
        bytes memory message
    ) internal override {
        TokenHomeStorage storage $ = _getTokenHomeStorage();
        TransferrerMessage memory transferrerMessage = abi.decode(message, (TransferrerMessage));
        if (transferrerMessage.messageType == TransferrerMessageType.SINGLE_HOP_SEND) {
            SingleHopSendMessage memory payload =
                abi.decode(transferrerMessage.payload, (SingleHopSendMessage));

            uint256 homeAmount =
                _processSingleHopTransfer(sourceBlockchainID, originSenderAddress, payload.amount);

            // Send the tokens to the recipient.
            _withdraw(payload.recipient, homeAmount);
            return;
        } else if (transferrerMessage.messageType == TransferrerMessageType.SINGLE_HOP_CALL) {
            SingleHopCallMessage memory payload =
                abi.decode(transferrerMessage.payload, (SingleHopCallMessage));

            uint256 homeAmount =
                _processSingleHopTransfer(sourceBlockchainID, originSenderAddress, payload.amount);

            // Verify that the payload's source blockchain ID and origin token transferrer address matches the source blockchain ID
            // and origin sender address passed from Teleporter.
            require(
                payload.sourceBlockchainID == sourceBlockchainID,
                "TokenHome: mismatched source blockchain ID"
            );
            require(
                payload.originTokenTransferrerAddress == originSenderAddress,
                "TokenHome: mismatched origin sender address"
            );

            _handleSendAndCall(payload, homeAmount);
            return;
        } else if (transferrerMessage.messageType == TransferrerMessageType.MULTI_HOP_SEND) {
            MultiHopSendMessage memory payload =
                abi.decode(transferrerMessage.payload, (MultiHopSendMessage));

            (uint256 homeAmount, uint256 fee) = _processMultiHopTransfer(
                sourceBlockchainID, originSenderAddress, payload.amount, payload.secondaryFee
            );

            // For a multi-hop send, the fee token address has to be {tokenAddress},
            // because the fee is taken from the amount that has already been deposited.
            // For ERC20 tokens, the token address of the contract is directly passed.
            // For native assets, the contract address is the wrapped token contract.
            _routeMultiHop(
                SendTokensInput({
                    destinationBlockchainID: payload.destinationBlockchainID,
                    destinationTokenTransferrerAddress: payload.destinationTokenTransferrerAddress,
                    recipient: payload.recipient,
                    primaryFeeTokenAddress: $._tokenAddress,
                    primaryFee: fee,
                    secondaryFee: 0,
                    requiredGasLimit: payload.secondaryGasLimit,
                    multiHopFallback: payload.multiHopFallback
                }),
                homeAmount
            );
            return;
        } else if (transferrerMessage.messageType == TransferrerMessageType.MULTI_HOP_CALL) {
            MultiHopCallMessage memory payload =
                abi.decode(transferrerMessage.payload, (MultiHopCallMessage));

            (uint256 homeAmount, uint256 fee) = _processMultiHopTransfer(
                sourceBlockchainID, originSenderAddress, payload.amount, payload.secondaryFee
            );

            // For a multi-hop send, the fee token address has to be {tokenAddress},
            // because the fee is taken from the amount that has already been deposited.
            // For ERC20 tokens, the token address of the contract is directly passed.
            // For native assets, the contract address is the wrapped token contract.
            _routeMultiHopSendAndCall({
                sourceBlockchainID: sourceBlockchainID,
                originTokenTransferrerAddress: originSenderAddress,
                originSenderAddress: payload.originSenderAddress,
                input: SendAndCallInput({
                    destinationBlockchainID: payload.destinationBlockchainID,
                    destinationTokenTransferrerAddress: payload.destinationTokenTransferrerAddress,
                    recipientContract: payload.recipientContract,
                    recipientPayload: payload.recipientPayload,
                    requiredGasLimit: payload.secondaryRequiredGasLimit,
                    recipientGasLimit: payload.recipientGasLimit,
                    multiHopFallback: payload.multiHopFallback,
                    fallbackRecipient: payload.fallbackRecipient,
                    primaryFeeTokenAddress: $._tokenAddress,
                    primaryFee: fee,
                    secondaryFee: 0
                }),
                amount: homeAmount
            });
            return;
        } else if (transferrerMessage.messageType == TransferrerMessageType.REGISTER_REMOTE) {
            RegisterRemoteMessage memory payload =
                abi.decode(transferrerMessage.payload, (RegisterRemoteMessage));
            _registerRemote(sourceBlockchainID, originSenderAddress, payload);
        }
    }
ava-labs/icm-contracts の元のファイルを開く

受け取ったメッセージタイプ(今回は REGISTER_REMOTE )ごとに処理を分岐させており、TokenHomeコントラクトでTokenRemoteコントラクトの登録を行うときは _registerRemote 関数が実行されます。 引数には、送付元のL1チェーンのIDと送付元のコントラクト(TokenRemoteコントラクト)のアドレスを渡しています。

contracts/ictt/TokenHome/TokenHome.sol#L590-L594
            RegisterRemoteMessage memory payload =
                abi.decode(transferrerMessage.payload, (RegisterRemoteMessage));
            _registerRemote(sourceBlockchainID, originSenderAddress, payload);
        }
    }
ava-labs/icm-contracts の元のファイルを開く

_registerRemote 関数ではいくつかバリデーションを行った後に、TokenRemoteコントラクトの登録を行なっています。 この時、ERC20トークンの桁数が異なる場合に揃えるように

contracts/ictt/TokenHome/TokenHome.sol#L170-L225
        bytes32 remoteBlockchainID,
        address remoteTokenTransferrerAddress,
        RegisterRemoteMessage memory message
    ) internal {
        TokenHomeStorage storage $ = _getTokenHomeStorage();
        require(remoteBlockchainID != bytes32(0), "TokenHome: zero remote blockchain ID");
        require(
            remoteBlockchainID != $._blockchainID, "TokenHome: cannot register remote on same chain"
        );
        require(
            remoteTokenTransferrerAddress != address(0),
            "TokenHome: zero remote token transferrer address"
        );
        require(
            !$._registeredRemotes[remoteBlockchainID][remoteTokenTransferrerAddress].registered,
            "TokenHome: remote already registered"
        );
        require(
            message.remoteTokenDecimals <= TokenScalingUtils.MAX_TOKEN_DECIMALS,
            "TokenHome: remote token decimals too high"
        );
        require(
            message.homeTokenDecimals == $._tokenDecimals, "TokenHome: invalid home token decimals"
        );

        (uint256 tokenMultiplier, bool multiplyOnRemote) = TokenScalingUtils
            .deriveTokenMultiplierValues($._tokenDecimals, message.remoteTokenDecimals);

        // Calculate the collateral needed in home token denomination.
        uint256 collateralNeeded = TokenScalingUtils.removeTokenScale(
            tokenMultiplier, multiplyOnRemote, message.initialReserveImbalance
        );

        // Round up the collateral needed by 1 in the case that {multiplyOnRemote} is true and
        // {initialReserveImbalance} is not divisible by the {tokenMultiplier} to
        // ensure that the full amount is accounted for.
        if (multiplyOnRemote && message.initialReserveImbalance % tokenMultiplier != 0) {
            collateralNeeded += 1;
        }

        $._registeredRemotes[remoteBlockchainID][remoteTokenTransferrerAddress] =
        RemoteTokenTransferrerSettings({
            registered: true,
            collateralNeeded: collateralNeeded,
            tokenMultiplier: tokenMultiplier,
            multiplyOnRemote: multiplyOnRemote
        });

        emit RemoteRegistered(
            remoteBlockchainID,
            remoteTokenTransferrerAddress,
            collateralNeeded,
            message.remoteTokenDecimals
        );
    }
ava-labs/icm-contracts の元のファイルを開く

担保額の供給

担保額の供給の図

TokenRemoteコントラクトのデプロイ時に、initialReserveImbalance_ という値を指定することができます。 この値は、初期発行トークン量として指定することができる値です。

contracts/ictt/TokenRemote/TokenRemote.sol#L151-L163
        TokenRemoteSettings memory settings,
        uint256 initialReserveImbalance_,
        uint8 tokenDecimals
    ) internal onlyInitializing {
        __TeleporterRegistryOwnableApp_init(
            settings.teleporterRegistryAddress,
            settings.teleporterManager,
            settings.minTeleporterVersion
        );
        __SendReentrancyGuard_init();
        __TokenRemote_init_unchained(settings, initialReserveImbalance_, tokenDecimals);
    }
ava-labs/icm-contracts の元のファイルを開く

ここで指定された値は、TokenRemoteコントラクトをTokenHomeコントラクトへ登録するときに一緒に渡されます。

contracts/ictt/TokenRemote/TokenRemote.sol#L215
            homeTokenDecimals: $._homeTokenDecimals,
ava-labs/icm-contracts の元のファイルを開く

そして、TokenHomeコントラクトでは、_addCollateral 関数が実行されて、指定された値分のTokenHomeコントラクトに紐づいているERC20トークンを担保として預けることで、初期発行を満たしたと判断されます。

contracts/ictt/TokenHome/TokenHome.sol#L449-L486
        bytes32 remoteBlockchainID,
        address remoteTokenTransferrerAddress,
        uint256 amount
    ) internal sendNonReentrant {
        TokenHomeStorage storage $ = _getTokenHomeStorage();
        RemoteTokenTransferrerSettings memory remoteSettings =
            $._registeredRemotes[remoteBlockchainID][remoteTokenTransferrerAddress];
        require(remoteSettings.registered, "TokenHome: remote not registered");
        require(remoteSettings.collateralNeeded > 0, "TokenHome: zero collateral needed");

        // Deposit the full amount, and withdraw back to the sender if there is excess.
        amount = _deposit(amount);

        // Calculate the remaining collateral needed, any excess amount, and adjust
        // {amount} to represent the amount of tokens added as collateral.
        uint256 remainingCollateralNeeded;
        uint256 excessAmount;
        if (amount >= remoteSettings.collateralNeeded) {
            remainingCollateralNeeded = 0;
            excessAmount = amount - remoteSettings.collateralNeeded;
            amount = remoteSettings.collateralNeeded;
        } else {
            remainingCollateralNeeded = remoteSettings.collateralNeeded - amount;
        }

        // Update the remaining collateral needed.
        $._registeredRemotes[remoteBlockchainID][remoteTokenTransferrerAddress].collateralNeeded =
            remainingCollateralNeeded;
        emit CollateralAdded(
            remoteBlockchainID, remoteTokenTransferrerAddress, amount, remainingCollateralNeeded
        );

        // If there is excess amount, send it back to the sender.
        if (excessAmount > 0) {
            _withdraw(_msgSender(), excessAmount);
        }
    }
ava-labs/icm-contracts の元のファイルを開く

以下の collateralNeeded という値が true になっていれば、初期発行分の担保を預けてもらったと判断します。

contracts/ictt/TokenHome/TokenHome.sol#L476-L477
            remainingCollateralNeeded;
        emit CollateralAdded(
ava-labs/icm-contracts の元のファイルを開く

TokenHomeコントラクト側では、Remoteチェーンのトークン(TokenRemoteコントラクト)を発行するリクエストを送る場合、以下のように collateralNeeded の値を確認しています。

contracts/ictt/TokenHome/TokenHome.sol#L766
ava-labs/icm-contracts の元のファイルを開く

そのため、ユーザーはこの初期発行が満たされないうちは、該当のRemoteチェーン上にトークンを送付することはできません。

0 を指定することもでき、この場合は初期発行はなしとみなされて、TokenRemoteコントラクトのデプロイ後直後にユーザーがRemoteチェーン上にトークンを送付することも可能です。

Approve

Homeチェーン上のERC20トークンをRemoteチェーンに送付する場合、事前にTokenHomeコントラクトに紐づいているERC20トークンをTokenHomeコントラクトに送付額分 approve しておく必要があります。 この approve をしておくことで、TokenHomeコントラクトがユーザーの資金を代わりに transfer (transferFrom)することができるようになります。

ネイティブトークンの場合は、approve は不要です。

送付の実行

ユーザーがHomeチェーンからRemoteチェーンにERC20トークンのリクエストを出します。

ネイティブトークンの送付もほとんど同じフローです。

Homeチェーン

TokenHomeコントラクトの send 関数を実行します。

contracts/ictt/TokenHome/ERC20TokenHomeUpgradeable.sol#L122-L124
    function send(SendTokensInput calldata input, uint256 amount) external {
        _send(input, amount);
    }
ava-labs/icm-contracts の元のファイルを開く

内部関数である _send を実行します。

contracts/ictt/TokenHome/TokenHome.sol#L240-L276
        _validateSendTokensInput(input);
        // Require that a single hop transfer does not have a multi-hop fallback recipient.
        require(input.multiHopFallback == address(0), "TokenHome: non-zero multi-hop fallback");

        (uint256 adjustedAmount, uint256 feeAmount) = _prepareSend({
            remoteBlockchainID: input.destinationBlockchainID,
            remoteTokenTransferrerAddress: input.destinationTokenTransferrerAddress,
            amount: amount,
            primaryFeeTokenAddress: input.primaryFeeTokenAddress,
            feeAmount: input.primaryFee
        });

        TransferrerMessage memory message = TransferrerMessage({
            messageType: TransferrerMessageType.SINGLE_HOP_SEND,
            payload: abi.encode(
                SingleHopSendMessage({recipient: input.recipient, amount: adjustedAmount})
            )
        });

        // Send message to the TokenRemote instance
        bytes32 messageID = _sendTeleporterMessage(
            TeleporterMessageInput({
                destinationBlockchainID: input.destinationBlockchainID,
                destinationAddress: input.destinationTokenTransferrerAddress,
                feeInfo: TeleporterFeeInfo({
                    feeTokenAddress: input.primaryFeeTokenAddress,
                    amount: feeAmount
                }),
                requiredGasLimit: input.requiredGasLimit,
                allowedRelayerAddresses: new address[](0),
                message: abi.encode(message)
            })
        );

        emit TokensSent(messageID, _msgSender(), input, adjustedAmount);
    }
ava-labs/icm-contracts の元のファイルを開く

実行条件を確認して、_prepareSend 関数でERC20トークンをTokenHomeコントラクトに送付してロックしています。

contracts/ictt/TokenHome/TokenHome.sol#L755-L788
        bytes32 remoteBlockchainID,
        address remoteTokenTransferrerAddress,
        uint256 amount,
        address primaryFeeTokenAddress,
        uint256 feeAmount
    ) private returns (uint256, uint256) {
        TokenHomeStorage storage $ = _getTokenHomeStorage();
        RemoteTokenTransferrerSettings memory remoteSettings =
            $._registeredRemotes[remoteBlockchainID][remoteTokenTransferrerAddress];
        require(remoteSettings.registered, "TokenHome: remote not registered");
        require(remoteSettings.collateralNeeded == 0, "TokenHome: collateral needed for remote");

        // Deposit the funds sent from the user to the token transferrer,
        // and set to adjusted amount after deposit.
        amount = _deposit(amount);

        if (feeAmount > 0) {
            feeAmount = SafeERC20TransferFrom.safeTransferFrom(
                IERC20(primaryFeeTokenAddress), _msgSender(), feeAmount
            );
        }

        // Scale the amount based on the token multiplier for the given TokenRemote instance.
        uint256 scaledAmount = TokenScalingUtils.applyTokenScale(
            remoteSettings.tokenMultiplier, remoteSettings.multiplyOnRemote, amount
        );
        require(scaledAmount > 0, "TokenHome: zero scaled amount");

        // Increase the balance of the TokenRemote instance by the scaled amount.
        $._transferredBalances[remoteBlockchainID][remoteTokenTransferrerAddress] += scaledAmount;

        return (scaledAmount, feeAmount);
    }
ava-labs/icm-contracts の元のファイルを開く

実行している safeTransferFrom 関数は、SafeERC20TransferFrom というライブラリの中で実装されています。 from に指定したアドレス(ユーザーのアドレス)から、address(this) (TokenHomeコントラクト)に amount で指定されたトークン量を送付しています。

contracts/utilities/SafeERC20TransferFrom.sol#L46-L60
    function safeTransferFrom(
        IERC20 erc20,
        address from,
        uint256 amount
    ) internal returns (uint256) {
        uint256 balanceBefore = erc20.balanceOf(address(this));
        erc20.safeTransferFrom(from, address(this), amount);
        uint256 balanceAfter = erc20.balanceOf(address(this));

        require(balanceAfter > balanceBefore, "SafeERC20TransferFrom: balance not increased");

        return balanceAfter - balanceBefore;
    }
    // solhint-enable private-vars-leading-underscore
}
ava-labs/icm-contracts の元のファイルを開く

その後、Teleporterコントラクト経由でRemoteチェーンにリクエストを送信しています。

contracts/ictt/TokenHome/TokenHome.sol#L261-L273
            TeleporterMessageInput({
                destinationBlockchainID: input.destinationBlockchainID,
                destinationAddress: input.destinationTokenTransferrerAddress,
                feeInfo: TeleporterFeeInfo({
                    feeTokenAddress: input.primaryFeeTokenAddress,
                    amount: feeAmount
                }),
                requiredGasLimit: input.requiredGasLimit,
                allowedRelayerAddresses: new address[](0),
                message: abi.encode(message)
            })
        );
ava-labs/icm-contracts の元のファイルを開く

この時、以下のように SINGLE_HOP_SEND というメッセージタイプを指定しています。 これは、Remoteチェーン側でどのリクエストが識別できるようにするためです。

contracts/ictt/TokenHome/TokenHome.sol#L253-L258
            messageType: TransferrerMessageType.SINGLE_HOP_SEND,
            payload: abi.encode(
                SingleHopSendMessage({recipient: input.recipient, amount: adjustedAmount})
            )
        });
ava-labs/icm-contracts の元のファイルを開く

Remoteチェーン

ICMでは、宛先チェーンの指定されたコントラクトの _receiveTeleporterMessage が呼び出されるようになっています。

contracts/ictt/TokenRemote/TokenRemote.sol#L329-L368
        bytes32 sourceBlockchainID,
        address originSenderAddress,
        bytes memory message
    ) internal override {
        TokenRemoteStorage storage $ = _getTokenRemoteStorage();
        require(
            sourceBlockchainID == $._tokenHomeBlockchainID,
            "TokenRemote: invalid source blockchain ID"
        );
        require(
            originSenderAddress == $._tokenHomeAddress, "TokenRemote: invalid origin sender address"
        );
        TransferrerMessage memory transferrerMessage = abi.decode(message, (TransferrerMessage));

        // If the contract was not previously known to be registered or collateralized, it is now given that
        // the home has sent a message to mint funds.
        if (!$._isRegistered || !$._isCollateralized) {
            $._isRegistered = true;
            $._isCollateralized = true;
        }

        // Remote contracts should only ever receive single-hop messages because
        // multi-hop messages are always routed through the home contract.
        if (transferrerMessage.messageType == TransferrerMessageType.SINGLE_HOP_SEND) {
            SingleHopSendMessage memory payload =
                abi.decode(transferrerMessage.payload, (SingleHopSendMessage));
            _withdraw(payload.recipient, payload.amount);
        } else if (transferrerMessage.messageType == TransferrerMessageType.SINGLE_HOP_CALL) {
            // The {sourceBlockchainID}, and {originSenderAddress} specified in the message
            // payload will not match the sender of this Teleporter message in the case of a
            // multi-hop message. Since Teleporter messages are only received from the specified
            // token TokenHome instance, no additional authentication is needed on the payload values.
            SingleHopCallMessage memory payload =
                abi.decode(transferrerMessage.payload, (SingleHopCallMessage));
            _handleSendAndCall(payload, payload.amount);
        } else {
            revert("TokenRemote: invalid message type");
        }
    }
ava-labs/icm-contracts の元のファイルを開く

SINGLE_HOP_SEND が指定されているので、以下のように _withdraw 関数が呼び出されます。

contracts/ictt/TokenRemote/TokenRemote.sol#L353-L356
            SingleHopSendMessage memory payload =
                abi.decode(transferrerMessage.payload, (SingleHopSendMessage));
            _withdraw(payload.recipient, payload.amount);
        } else if (transferrerMessage.messageType == TransferrerMessageType.SINGLE_HOP_CALL) {
ava-labs/icm-contracts の元のファイルを開く

_withdraw 関数では、以下のように _mint 関数を呼び出して、指定されたアドレス(recipient)に指定されたトークン量(amount)のERC20トークンを送付しています。

contracts/ictt/TokenRemote/ERC20TokenRemoteUpgradeable.sol#L136-L139
    function _withdraw(address recipient, uint256 amount) internal virtual override {
        emit TokensWithdrawn(recipient, amount);
        _mint(recipient, amount);
    }
ava-labs/icm-contracts の元のファイルを開く

TeleporterコントラクトやRelayerを含めたフロー図は以下のようになります。

Remoteチェーンの図

ICMのフローについて詳しくは以下の記事を読んでください。

トークンを戻す

Remoteチェーン上に存在するERC20トークンを、Homeチェーン上のERC20トークンに戻すフローを確認していきます。

実行する関数は、TokenHomeコントラクトと同じで send 関数です。

contracts/ictt/TokenRemote/ERC20TokenRemoteUpgradeable.sol#L114-L116
    function send(SendTokensInput calldata input, uint256 amount) external {
        _send(input, amount);
    }
ava-labs/icm-contracts の元のファイルを開く

内部関数である _send 関数が呼び出されて、特定のL1チェーン宛の場合は _processSend が呼び出されます。

contracts/ictt/TokenRemote/TokenRemote.sol#L291-L299
        TokenRemoteStorage storage $ = _getTokenRemoteStorage();
        _validateSendTokensInput(input);
        if (input.destinationBlockchainID == $._tokenHomeBlockchainID) {
            _processSend(input, amount);
        } else {
            _processSendMultiHop(input, amount);
        }
    }
ava-labs/icm-contracts の元のファイルを開く

実行条件を確認して、ICMを活用してHomeチェーン上のTokenHomeコントラクトにメッセージを送付しています。

contracts/ictt/TokenRemote/TokenRemote.sol#L449-L483
        TokenRemoteStorage storage $ = _getTokenRemoteStorage();
        _validateSingleHopInput(
            input.destinationTokenTransferrerAddress, input.secondaryFee, input.multiHopFallback
        );

        uint256 primaryFee;
        (amount, primaryFee) = _prepareSend({
            amount: amount,
            primaryFeeTokenAddress: input.primaryFeeTokenAddress,
            primaryFee: input.primaryFee,
            secondaryFee: input.secondaryFee
        });

        TransferrerMessage memory message = TransferrerMessage({
            messageType: TransferrerMessageType.SINGLE_HOP_SEND,
            payload: abi.encode(SingleHopSendMessage({recipient: input.recipient, amount: amount}))
        });

        bytes32 messageID = _sendTeleporterMessage(
            TeleporterMessageInput({
                destinationBlockchainID: $._tokenHomeBlockchainID,
                destinationAddress: $._tokenHomeAddress,
                feeInfo: TeleporterFeeInfo({
                    feeTokenAddress: input.primaryFeeTokenAddress,
                    amount: primaryFee
                }),
                requiredGasLimit: input.requiredGasLimit,
                allowedRelayerAddresses: new address[](0),
                message: abi.encode(message)
            })
        );

        emit TokensSent(messageID, _msgSender(), input, amount);
    }
ava-labs/icm-contracts の元のファイルを開く

TokenHomeコントラクトでは、Teleporterコントラクトから _receiveTeleporterMessage 関数が呼び出され、メッセージタイプに SINGLE_HOP_SEND が指定されているため、以下の処理が実行されます。

contracts/ictt/TokenHome/TokenHome.sol#L495-L512
        bytes32 sourceBlockchainID,
        address originSenderAddress,
        bytes memory message
    ) internal override {
        TokenHomeStorage storage $ = _getTokenHomeStorage();
        TransferrerMessage memory transferrerMessage = abi.decode(message, (TransferrerMessage));
        if (transferrerMessage.messageType == TransferrerMessageType.SINGLE_HOP_SEND) {
            SingleHopSendMessage memory payload =
                abi.decode(transferrerMessage.payload, (SingleHopSendMessage));

            uint256 homeAmount =
                _processSingleHopTransfer(sourceBlockchainID, originSenderAddress, payload.amount);

            // Send the tokens to the recipient.
            _withdraw(payload.recipient, homeAmount);
            return;
        } else if (transferrerMessage.messageType == TransferrerMessageType.SINGLE_HOP_CALL) {
            SingleHopCallMessage memory payload =
ava-labs/icm-contracts の元のファイルを開く

内部では _withdraw 関数が呼び出されて、指定されたリクエスト元(ユーザー)のアドレス(recipient)に指定されたトークン量(amount )のERC20を発行します。

contracts/ictt/TokenHome/ERC20TokenHomeUpgradeable.sol#L163-L167
    function _withdraw(address recipient, uint256 amount) internal virtual override {
        ERC20TokenHomeStorage storage $ = _getERC20TokenHomeStorage();
        emit TokensWithdrawn(recipient, amount);
        $._token.safeTransfer(recipient, amount);
    }
ava-labs/icm-contracts の元のファイルを開く

フローとしては、TokenHomeコントラクトからのトークン発行と逆になります。

トークンを戻すの図

追加処理実行

トークン送付に合わせて、任意の処理を実行することもできます。 この時には、以下の sendAndCall 関数を実行します。 実行したい情報(例えばRemoteチェーン上の特定のコントラクトの特定の関数を実行など)を input という引数に渡します。

contracts/ictt/TokenHome/ERC20TokenHomeUpgradeable.sol#L129-L137
    function sendAndCall(SendAndCallInput calldata input, uint256 amount) external {
        _sendAndCall({
            sourceBlockchainID: getBlockchainID(),
            originTokenTransferrerAddress: address(this),
            originSenderAddress: _msgSender(),
            input: input,
            amount: amount
        });
    }
ava-labs/icm-contracts の元のファイルを開く

input は以下の SendAndCallInput という情報を格納できるようになっています。 recipientContract に実行対象のコントラクトアドレスを渡し、recipientPayload に実行情報をエンコードしたバイトデータを渡します。

contracts/ictt/interfaces/ITokenTransferrer.sol#L64-L76
struct SendAndCallInput {
    bytes32 destinationBlockchainID;
    address destinationTokenTransferrerAddress;
    address recipientContract;
    bytes recipientPayload;
    uint256 requiredGasLimit;
    uint256 recipientGasLimit;
    address multiHopFallback;
    address fallbackRecipient;
    address primaryFeeTokenAddress;
    uint256 primaryFee;
    uint256 secondaryFee;
}
ava-labs/icm-contracts の元のファイルを開く

Remoteチェーンでは、TokenRemoteコントラクトの _receiveTeleporterMessage 関数内で、メッセージタイプが SINGLE_HOP_CALL の処理(_handleSendAndCall 関数)が呼び出されます。

contracts/ictt/TokenRemote/TokenRemote.sol#L357-L368
            // The {sourceBlockchainID}, and {originSenderAddress} specified in the message
            // payload will not match the sender of this Teleporter message in the case of a
            // multi-hop message. Since Teleporter messages are only received from the specified
            // token TokenHome instance, no additional authentication is needed on the payload values.
            SingleHopCallMessage memory payload =
                abi.decode(transferrerMessage.payload, (SingleHopCallMessage));
            _handleSendAndCall(payload, payload.amount);
        } else {
            revert("TokenRemote: invalid message type");
        }
    }
ava-labs/icm-contracts の元のファイルを開く

_handleSendAndCall 内では以下でトークン発行処理後に追加処理を実行しています。

contracts/ictt/TokenRemote/ERC20TokenRemoteUpgradeable.sol#L192-L194
        bool success = CallUtils._callWithExactGas(
            message.recipientGasLimit, message.recipientContract, payload
        );
ava-labs/icm-contracts の元のファイルを開く

RemoteチェーンのTokenRemoteコントラクトからリクエストを送付する時も、同じように sendAndCall 関数を実行します。

contracts/ictt/TokenRemote/ERC20TokenRemoteUpgradeable.sol#L121-L123
    function sendAndCall(SendAndCallInput calldata input, uint256 amount) external {
        _sendAndCall(input, amount);
    }
ava-labs/icm-contracts の元のファイルを開く

フローは、以下のようにトークン送付時のフローに1つ処理が追加されます。

追加処理実行の図

マルチホップ

2つのRemoteチェーン間でERC20トークンの送付を行うフローを確認していきます。

まず、1つ目のRemoteチェーンのTokenRemoteコントラクトの send 関数を実行します。

contracts/ictt/TokenRemote/ERC20TokenRemoteUpgradeable.sol#L114-L116
    function send(SendTokensInput calldata input, uint256 amount) external {
        _send(input, amount);
    }
ava-labs/icm-contracts の元のファイルを開く

この時、引数の input の destinationBlockchainID にもう1つのRemoteチェーンのチェーンIDを指定します。 合わせて、destinationTokenTransferrerAddress にももう1つにTokenRemoteコントラクトのアドレスを指定します。

contracts/ictt/interfaces/ITokenTransferrer.sol#L33-L42
struct SendTokensInput {
    bytes32 destinationBlockchainID;
    address destinationTokenTransferrerAddress;
    address recipient;
    address primaryFeeTokenAddress;
    uint256 primaryFee;
    uint256 secondaryFee;
    uint256 requiredGasLimit;
    address multiHopFallback;
}
ava-labs/icm-contracts の元のファイルを開く

_send 関数内で、事前に登録されているHomeチェーンのチェーンIDと引数に設定されている input.destinationBlockchainID の値が異なるため、マルチホップTransferと判断されて _processSendMultiHop 関数が呼び出されます。

contracts/ictt/TokenRemote/TokenRemote.sol#L291-L299
        TokenRemoteStorage storage $ = _getTokenRemoteStorage();
        _validateSendTokensInput(input);
        if (input.destinationBlockchainID == $._tokenHomeBlockchainID) {
            _processSend(input, amount);
        } else {
            _processSendMultiHop(input, amount);
        }
    }
ava-labs/icm-contracts の元のファイルを開く

ICMメッセージの送付先は、事前に登録されているHomeチェーン上のTokenHomeコントラクトになります。 TokenHomeコントラクトがハブとなり、Remoteチェーン間の送付リクエストを中継します。

contracts/ictt/TokenRemote/TokenRemote.sol#L488-L534
        TokenRemoteStorage storage $ = _getTokenRemoteStorage();
        _validateMultiHopInput(
            input.destinationBlockchainID,
            input.destinationTokenTransferrerAddress,
            input.multiHopFallback
        );

        uint256 primaryFee;
        (amount, primaryFee) = _prepareSend({
            amount: amount,
            primaryFeeTokenAddress: input.primaryFeeTokenAddress,
            primaryFee: input.primaryFee,
            secondaryFee: input.secondaryFee
        });

        TransferrerMessage memory message = TransferrerMessage({
            messageType: TransferrerMessageType.MULTI_HOP_SEND,
            payload: abi.encode(
                MultiHopSendMessage({
                    destinationBlockchainID: input.destinationBlockchainID,
                    destinationTokenTransferrerAddress: input.destinationTokenTransferrerAddress,
                    recipient: input.recipient,
                    amount: amount,
                    secondaryFee: input.secondaryFee,
                    secondaryGasLimit: input.requiredGasLimit,
                    multiHopFallback: input.multiHopFallback
                })
            )
        });

        bytes32 messageID = _sendTeleporterMessage(
            TeleporterMessageInput({
                destinationBlockchainID: $._tokenHomeBlockchainID,
                destinationAddress: $._tokenHomeAddress,
                feeInfo: TeleporterFeeInfo({
                    feeTokenAddress: input.primaryFeeTokenAddress,
                    amount: primaryFee
                }),
                requiredGasLimit: MULTI_HOP_SEND_REQUIRED_GAS,
                allowedRelayerAddresses: new address[](0),
                message: abi.encode(message)
            })
        );

        emit TokensSent(messageID, _msgSender(), input, amount);
    }
ava-labs/icm-contracts の元のファイルを開く

Homeチェーン上のTokenHomeコントラクトでは、Teleporterコントラクトから _receiveTeleporterMessage 関数が呼び出されます。 今回はメッセージタイプに MULTI_HOP_CALL` が指定されているため、以下の処理が実行されます。

contracts/ictt/TokenHome/TokenHome.sol#L558-L590
            MultiHopCallMessage memory payload =
                abi.decode(transferrerMessage.payload, (MultiHopCallMessage));

            (uint256 homeAmount, uint256 fee) = _processMultiHopTransfer(
                sourceBlockchainID, originSenderAddress, payload.amount, payload.secondaryFee
            );

            // For a multi-hop send, the fee token address has to be {tokenAddress},
            // because the fee is taken from the amount that has already been deposited.
            // For ERC20 tokens, the token address of the contract is directly passed.
            // For native assets, the contract address is the wrapped token contract.
            _routeMultiHopSendAndCall({
                sourceBlockchainID: sourceBlockchainID,
                originTokenTransferrerAddress: originSenderAddress,
                originSenderAddress: payload.originSenderAddress,
                input: SendAndCallInput({
                    destinationBlockchainID: payload.destinationBlockchainID,
                    destinationTokenTransferrerAddress: payload.destinationTokenTransferrerAddress,
                    recipientContract: payload.recipientContract,
                    recipientPayload: payload.recipientPayload,
                    requiredGasLimit: payload.secondaryRequiredGasLimit,
                    recipientGasLimit: payload.recipientGasLimit,
                    multiHopFallback: payload.multiHopFallback,
                    fallbackRecipient: payload.fallbackRecipient,
                    primaryFeeTokenAddress: $._tokenAddress,
                    primaryFee: fee,
                    secondaryFee: 0
                }),
                amount: homeAmount
            });
            return;
        } else if (transferrerMessage.messageType == TransferrerMessageType.REGISTER_REMOTE) {
            RegisterRemoteMessage memory payload =
ava-labs/icm-contracts の元のファイルを開く

_routeMultiHopSendAndCall 関数が内部で呼び出されます。

渡された情報を元に、もう1つのRemoteチェーンのTokenRemoteコントラクトにリクエストが送付されます。

このあとは、通常のTokenHomeコントラクトからTokenRemoteコントラクトへのリクエストと同じフローになります。 このように、TokenHomeコントラクトを経由することで、常にTokenRemoteコントラクトはTokenHomeコントラクトをもとにトークンを発行する形式にできます。

フロー図は以下のようになります。

マルチホップの図

最後に

今回は『Avalanche ICTT』についてまとめてきました。

他でも色々記事を書いているのでぜひよろしければ読んでいってください!