013
はじめに
015 初めまして。
『DApps開発入門』という本や色々記事を書いているAva Labsのかるでねです。
020 以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!
032 今回は『Avalanche ICTT』についてまとめていきます。
Avalanche ICTTの各処理フローについて図を元にまとめているので、この記事を読むことで非エンジニアでも全体像が掴めます。
また、エンジニア向けにソースコードを元に詳しく説明しているので、技術的にも理解を深められると思います。
036
Avalanche ICTTについて、より詳しく知りたい場合は以下を読んでください。
042
ICTT概要
044 Avalanche Interchain Token Transfer (ICTT)は、異なるL1ブロックチェーン間でトークンを送付するためのアプリケーションです。
このシステムは、複数のL1にデプロイされた一連のスマートコントラクトによって実装されており、クロスチェーン通信にはICM(Inter-Chain Messaging) を利用します。
047
ICM(Inter-Chain Messaging)については以下の記事を読んでください。
055 ICTTは、1つのTokenHome コントラクトと、1つ以上のTokenRemote コントラクトから構成されます。
057 Home コントラクトは、送付元となるトークンが存在するL1(Homeチェーン )上にデプロイされて、トークンの管理を担当します。
TokenRemote コントラクトは、送付先となるL1(Remoteチェーン )上にデプロイされ、送付したいトークンのラップトークンのコントラクトです。
060
062
コントラクト構成
064 コントラクトの継承関係は以下のようになっています。
066
068 TokenHome コントラクトに、コアの実装やERC20 とネイティブトークンで共通する機能の実装や必要な関数の定義などがされています。
TokenHome コントラクトを継承している〇〇UpgradeableContract では、ERC20 とネイティブトークンそれぞれ特有の処理をしています。
さらにそれを継承しているERC20TokenHome コントラクトとNativeTokenHome コントラクトは、〇〇UpgradeableContract をそのまま実装してあり、カスタマイズ実装をしたい場合などにはここに実装を追加します。
072
トークン送付
074 トークン送付のプロセスは、Homeチェーンでトークンをロックし、 Remoteチェーンでそのトークンをラップしたトークンを新規に発行します。
076
078
ユーザーが保有しているトークンの transfer する権限を TokenHome コントラクトに付与するリクエスト。
transfer する権限を付与。
Remoteチェーン上にトークンを送付するリクエスト。
トークンをTokenHome コントラクトにロックして、Remoteチェーン上にトークンを送付。
083 TokenHome コントラクトは、各L1上のTokenRemote コントラクトに送付されたトークン残高を追跡し、トークンがTokenHome コントラクトに戻される時には、TokenHome コントラクトにロックされていた元のトークンをユーザーに返却します。
085
087
ユーザーがRemoteチェーンに送付したトークンを戻すリクエスト。
RemoteチェーンからHomeチェーンにトークンを送付。
ロックしていたトークンをユーザーに返す。
091 TokenHome コントラクトは、ERC20 トークンかネイティブトークンのいずれかを送付し、事前に登録されている任意のTokenRemote コントラクトにトークンを送付できます。
Remoteチェーン上のトークンもERC20 かネイティブトークンのいずれかであり、ユーザーはHomeチェーンとRemoteチェーン間で以下のようなERC20 とネイティブトークンの組み合わせでトークンを交換できます。
094
096 また、マルチホップTransferもサポートしています。
例えば、同じTokenHome コントラクトに接続された2つのRemoteチェーンと各チェーン上のTokenRemote コントラクトへの送付は、まず1つ目のRemoteチェーンのTokenRemote コントラクトからTokenHome コントラクトへリクエストが送られて、そのリクエストを受け取ったTokenHome コントラクトが2つ目のRemoteチェーンのTokenRemote コントラクトにトークンの送付を行います。
099
101 基本的なトークン送付に加えて、sendAndCallインターフェースが提供されており、これを利用するとトークンの送付とスマートコントラクトでの利用を単一のICMメッセージ内で完結させることができます。
もし宛先のスマートコントラクトの呼び出しが失敗した場合、送付されたトークンは送付先チェーン上のフォールバック用アドレスに送られます。
sendAndCallインターフェースにより、送付されたトークンを他のチェーン上のdAppsで直接使用すること(スワップの実行、サービス利用時の手数料支払いなど)が可能になります。
105
フロー
107
ICTTはICMを活用した仕組みのため、前提としてクロスチェーン通信を行うときには、以下のように「Teleporter 」、「Warp Precompile 」、「Relayer 」を経由しています。
上記を含めたICMの詳しいフローは以下の記事を読んでください。
114
コントラクトのデプロイ
116 まずは、以下のコントラクトを一式デプロイしていきます。
118
120
Homeチェーン
TokenHome コントラクト
ERC20 コントラクト
Remoteチェーン
126
TokenHomeコントラクト
128 TokenHome コントラクトをデプロイする時は、以下のようにTeleporter コントラクトの情報とERC20 コントラクトのアドレスを渡します。
130 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 の元のファイルを開く
132
teleporterRegistryAddress と teleporterManager は、Teleporter コントラクトのバージョン管理を行うコントラクトです。
136
Teleporter コントラクトについては以下の記事を読んでください。
142 ネイティブトークンを使用する場合は、以下のようにネイティブトークンのラップトークンコントラクトのアドレスを渡しています。
144 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 の元のファイルを開く
146 これは、ネイティブトークンでもERC20 トークンと同じフローで処理されるように、ラップトークンに変換するようにして抽象化されています。
148
TokenRemoteコントラクト
150 TokenRemote コントラクトのデプロイ時には、以下のようにERC20 トークンを継承しているので、ERC20 のトークン名やシンボルを渡す必要があります。
152 contracts/ictt/TokenRemote/ERC20TokenRemoteUpgradeable.sol#L30 コピー
contract ERC20TokenRemoteUpgradeable is IERC20TokenTransferrer, ERC20Upgradeable, TokenRemote { ava-labs/icm-contracts の元のファイルを開く
154 以下の tokenName と tokenSymbol にERC20 のトークン名とシンボルを渡しています。
156 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 の元のファイルを開く
158 ネイティブトークンの場合は、そのままネイティブトークンがMintされます。
160 また、TokenRemoteSettings には、以下のようにTeleporter コントラクトの情報とHomeチェーンのチェーンIDとTokenHome コントラクトのアドレスを登録しています。
これはICM(Interchain Messaging)で必要になる情報です。
163 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 の元のファイルを開く
165
Remoteコントラクトの登録
167 TokenHome コントラクトには、TokenRemote コントラクトの情報を登録していないため、事前に登録しておく必要があります。
169
171 TokenRemote コントラクトの方は、TokenRemote コントラクトデプロイ時に、TokenHome コントラクトの登録は完了しています。
173
175 TokenRemote コントラクトの登録は、registerWithHome 関数を実行することで行われます。
177 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 の元のファイルを開く
179 ICMを使用した場合、クロスチェーン通信時に呼ばれる関数は、以下の_receiveTeleporterMessage 関数です。
181 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 の元のファイルを開く
183 受け取ったメッセージタイプ(今回は REGISTER_REMOTE )ごとに処理を分岐させており、TokenHome コントラクトでTokenRemote コントラクトの登録を行うときは _registerRemote 関数が実行されます。
引数には、送付元のL1チェーンのIDと送付元のコントラクト(TokenRemote コントラクト)のアドレスを渡しています。
186 contracts/ictt/TokenHome/TokenHome.sol#L590-L594 コピー
RegisterRemoteMessage memory payload =
abi.decode(transferrerMessage.payload, (RegisterRemoteMessage));
_registerRemote(sourceBlockchainID, originSenderAddress, payload);
}
} ava-labs/icm-contracts の元のファイルを開く
188 _registerRemote 関数ではいくつかバリデーションを行った後に、TokenRemote コントラクトの登録を行なっています。
この時、ERC20 トークンの桁数が異なる場合に揃えるように
191 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 の元のファイルを開く
193
担保額の供給
195
197 TokenRemote コントラクトのデプロイ時に、initialReserveImbalance_ という値を指定することができます。
この値は、初期発行トークン量として指定することができる値です。
200 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 の元のファイルを開く
202 ここで指定された値は、TokenRemote コントラクトをTokenHome コントラクトへ登録するときに一緒に渡されます。
206 そして、TokenHome コントラクトでは、_addCollateral 関数が実行されて、指定された値分のTokenHome コントラクトに紐づいているERC20 トークンを担保として預けることで、初期発行を満たしたと判断されます。
208 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 の元のファイルを開く
210 以下の collateralNeeded という値が true になっていれば、初期発行分の担保を預けてもらったと判断します。
214 TokenHome コントラクト側では、Remoteチェーンのトークン(TokenRemote コントラクト)を発行するリクエストを送る場合、以下のように collateralNeeded の値を確認しています。
218 そのため、ユーザーはこの初期発行が満たされないうちは、該当のRemoteチェーン上にトークンを送付することはできません。
220 0 を指定することもでき、この場合は初期発行はなしとみなされて、TokenRemote コントラクトのデプロイ後直後にユーザーがRemoteチェーン上にトークンを送付することも可能です。
222
Approve
224 Homeチェーン上のERC20 トークンをRemoteチェーンに送付する場合、事前にTokenHome コントラクトに紐づいているERC20 トークンをTokenHome コントラクトに送付額分 approve しておく必要があります。
この approve をしておくことで、TokenHome コントラクトがユーザーの資金を代わりに transfer (transferFrom)することができるようになります。
227
ネイティブトークンの場合は、approve は不要です。
231
送付の実行
233 ユーザーがHomeチェーンからRemoteチェーンにERC20 トークンのリクエストを出します。
235
ネイティブトークンの送付もほとんど同じフローです。
239
Homeチェーン
241 TokenHome コントラクトの send 関数を実行します。
243 contracts/ictt/TokenHome/ERC20TokenHomeUpgradeable.sol#L122-L124 コピー
function send(SendTokensInput calldata input, uint256 amount) external {
_send(input, amount);
} ava-labs/icm-contracts の元のファイルを開く
247 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 の元のファイルを開く
249 実行条件を確認して、_prepareSend 関数でERC20 トークンをTokenHome コントラクトに送付してロックしています。
251 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 の元のファイルを開く
253 実行している safeTransferFrom 関数は、SafeERC20TransferFrom というライブラリの中で実装されています。
from に指定したアドレス(ユーザーのアドレス)から、address(this) (TokenHome コントラクト)に amount で指定されたトークン量を送付しています。
256 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 の元のファイルを開く
258 その後、Teleporterコントラクト経由でRemoteチェーンにリクエストを送信しています。
260 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 の元のファイルを開く
262 この時、以下のように SINGLE_HOP_SEND というメッセージタイプを指定しています。
これは、Remoteチェーン側でどのリクエストが識別できるようにするためです。
265 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 の元のファイルを開く
267
Remoteチェーン
269 ICMでは、宛先チェーンの指定されたコントラクトの _receiveTeleporterMessage が呼び出されるようになっています。
271 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 の元のファイルを開く
273 SINGLE_HOP_SEND が指定されているので、以下のように _withdraw 関数が呼び出されます。
275 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 の元のファイルを開く
277 _withdraw 関数では、以下のように _mint 関数を呼び出して、指定されたアドレス(recipient)に指定されたトークン量(amount)のERC20 トークンを送付しています。
279 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 の元のファイルを開く
281 Teleporter コントラクトやRelayerを含めたフロー図は以下のようになります。
284
286
ICMのフローについて詳しくは以下の記事を読んでください。
292
トークンを戻す
294 Remoteチェーン上に存在するERC20 トークンを、Homeチェーン上のERC20 トークンに戻すフローを確認していきます。
296 実行する関数は、TokenHome コントラクトと同じで send 関数です。
298 contracts/ictt/TokenRemote/ERC20TokenRemoteUpgradeable.sol#L114-L116 コピー
function send(SendTokensInput calldata input, uint256 amount) external {
_send(input, amount);
} ava-labs/icm-contracts の元のファイルを開く
300 内部関数である _send 関数が呼び出されて、特定のL1チェーン宛の場合は _processSend が呼び出されます。
302 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 の元のファイルを開く
304 実行条件を確認して、ICMを活用してHomeチェーン上のTokenHome コントラクトにメッセージを送付しています。
306 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 の元のファイルを開く
308 TokenHome コントラクトでは、Teleporter コントラクトから _receiveTeleporterMessage 関数が呼び出され、メッセージタイプに SINGLE_HOP_SEND が指定されているため、以下の処理が実行されます。
310 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 の元のファイルを開く
312 内部では _withdraw 関数が呼び出されて、指定されたリクエスト元(ユーザー)のアドレス(recipient)に指定されたトークン量(amount )のERC20 を発行します。
314 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 の元のファイルを開く
316 フローとしては、TokenHome コントラクトからのトークン発行と逆になります。
318
320
追加処理実行
322 トークン送付に合わせて、任意の処理を実行することもできます。
この時には、以下の sendAndCall 関数を実行します。
実行したい情報(例えばRemoteチェーン上の特定のコントラクトの特定の関数を実行など)を input という引数に渡します。
326 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 の元のファイルを開く
328 input は以下の SendAndCallInput という情報を格納できるようになっています。
recipientContract に実行対象のコントラクトアドレスを渡し、recipientPayload に実行情報をエンコードしたバイトデータを渡します。
331 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 の元のファイルを開く
333 Remoteチェーンでは、TokenRemote コントラクトの _receiveTeleporterMessage 関数内で、メッセージタイプが SINGLE_HOP_CALL の処理(_handleSendAndCall 関数)が呼び出されます。
335 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 の元のファイルを開く
337 _handleSendAndCall 内では以下でトークン発行処理後に追加処理を実行しています。
339 contracts/ictt/TokenRemote/ERC20TokenRemoteUpgradeable.sol#L192-L194 コピー
bool success = CallUtils._callWithExactGas(
message.recipientGasLimit, message.recipientContract, payload
); ava-labs/icm-contracts の元のファイルを開く
341 RemoteチェーンのTokenRemote コントラクトからリクエストを送付する時も、同じように sendAndCall 関数を実行します。
343 contracts/ictt/TokenRemote/ERC20TokenRemoteUpgradeable.sol#L121-L123 コピー
function sendAndCall(SendAndCallInput calldata input, uint256 amount) external {
_sendAndCall(input, amount);
} ava-labs/icm-contracts の元のファイルを開く
345 フローは、以下のようにトークン送付時のフローに1つ処理が追加されます。
347
349
マルチホップ
351 2つのRemoteチェーン間でERC20 トークンの送付を行うフローを確認していきます。
353 まず、1つ目のRemoteチェーンのTokenRemote コントラクトの send 関数を実行します。
355 contracts/ictt/TokenRemote/ERC20TokenRemoteUpgradeable.sol#L114-L116 コピー
function send(SendTokensInput calldata input, uint256 amount) external {
_send(input, amount);
} ava-labs/icm-contracts の元のファイルを開く
357 この時、引数の input の destinationBlockchainID にもう1つのRemoteチェーンのチェーンIDを指定します。
合わせて、destinationTokenTransferrerAddress にももう1つにTokenRemote コントラクトのアドレスを指定します。
360 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 の元のファイルを開く
362 _send 関数内で、事前に登録されているHomeチェーンのチェーンIDと引数に設定されている input.destinationBlockchainID の値が異なるため、マルチホップTransferと判断されて _processSendMultiHop 関数が呼び出されます。
364 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 の元のファイルを開く
366 ICMメッセージの送付先は、事前に登録されているHomeチェーン上のTokenHome コントラクトになります。
TokenHome コントラクトがハブとなり、Remoteチェーン間の送付リクエストを中継します。
369 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 の元のファイルを開く
371 Homeチェーン上のTokenHome コントラクトでは、Teleporter コントラクトから _receiveTeleporterMessage 関数が呼び出されます。 今回はメッセージタイプに MULTI_HOP_CALL` が指定されているため、以下の処理が実行されます。
374 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 の元のファイルを開く
376 _routeMultiHopSendAndCall 関数が内部で呼び出されます。
380 渡された情報を元に、もう1つのRemoteチェーンのTokenRemote コントラクトにリクエストが送付されます。
384 このあとは、通常のTokenHome コントラクトからTokenRemote コントラクトへのリクエストと同じフローになります。
このように、TokenHome コントラクトを経由することで、常にTokenRemote コントラクトはTokenHome コントラクトをもとにトークンを発行する形式にできます。
389
391
最後に
393 今回は『Avalanche ICTT』についてまとめてきました。
395 他でも色々記事を書いているのでぜひよろしければ読んでいってください!