概述

了解代币 Transfer 模块的作用。

什么是 Transfer 模块?

Transfer 是 Cosmos SDK 对 ICS-20 协议的实现,该协议支持跨链同质化代币转移。

概念

确认

ICS20 使用 ICS 04 指定的推荐确认格式。 成功接收转账数据包后,会写入一个结果确认,并在 Response 字段中写入值 []byte{byte(1)}。 接收转账数据包失败时,会写入一个错误确认,并在 Response 字段中写入错误信息。

面额追踪

面额追踪对应的是能够将代币追溯回其源链的信息。它包含一系列端口和通道标识符,并按照转账时间线从最近到最早的顺序排列。
当使用 IBC v2 的 transfer 连接到例如 Ethereum 时,源通道标识符将改为源客户端标识符。
为防止面额长度无限增长,这些信息会以哈希形式包含在代币的基础面额字段中。例如,代币 transfer/channelToA/uatom 会显示为 ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2。可读的面额会通过 x/bank 模块的 denom metadata 功能保存。你可以像下面这样,在查询余额时使用 --resolve-denom 标志来显示人类可读的面额:
simd query bank balances [address] --resolve-denom
每当代币被发送到除上一次接收它的链之外的其他链时,都会沿着代币时间线向前移动。这会将追踪信息添加到代币历史中,并把目标端口和目标通道作为前缀附加到面额上。在这些情况下,发送方链充当“源区”。当代币被发回到它上一次接收自的那条链时,该前缀会被移除。这属于沿时间线向后移动,此时发送方链充当“汇区”。 强烈建议你充分理解 IBC 代币表示形式的影响和上下文。

面向客户端的 UX 建议

对于希望展示代币来源的客户端(钱包、交易所、应用、区块浏览器等),建议在下列不同场景中采用以下方式:

直接连接

如果面额追踪只包含一组标识符前缀对(如上例所示),那么获取链和轻客户端标识符的最简单方式就是直接映射该追踪信息。简而言之,这需要先根据面额追踪中的标识符查询通道,然后再使用所获取通道中的对手方端口和通道标识符查询对手方客户端状态。 一个通用的伪算法如下:
  1. 查询完整的面额追踪。
  2. 使用 portID/channelID 这一对标识符查询通道,它对应代币的第一个目标位置。
  3. 使用该标识符对查询客户端状态。注意,如果当前链未连接到该通道,此查询会返回一个 "Not Found" 响应。
  4. 从客户端状态中获取客户端标识符或链标识符(例如在 Tendermint 客户端中),并将其存储在本地。
使用 gRPC gateway 客户端服务时,以上步骤在给定 IBC 代币 ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2 存储于 chainB 上的情况下如下所示:
  1. GET /ibc/apps/transfer/v1/denom_traces/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2 -> {"path": "transfer/channelToA", "base_denom": "uatom"}
  2. GET /ibc/apps/transfer/v1/channels/channelToA/ports/transfer/client_state" -> {"client_id": "clientA", "chain-id": "chainA", ...}
  3. GET /ibc/apps/transfer/v1/channels/channelToA/ports/transfer" -> {"channel_id": "channelToA", port_id": "transfer", counterparty: {"channel_id": "channelToB", port_id": "transfer"}, ...}
  4. GET /ibc/apps/transfer/v1/channels/channelToB/ports/transfer/client_state" -> {"client_id": "clientB", "chain-id": "chainB", ...}
那么,uatom 面额对应的代币转移链路径将是:chainA -> chainB。

多跳

多通道跳转场景适用于代币在原始源链和最终目标链之间经过了多条链的情况。 IBC 协议并不知道整个网络的拓扑结构,也就是说,它不知道链之间的连接关系以及各自使用的标识符名称。因此,在多跳场景下,单次转账时间线中的某一条链无法查询其他链的链标识符和客户端标识符。 例如,某个 IBC 代币经历了如下转账序列:A -> B -> C,其最终前缀路径(追踪信息)为 transfer/channelChainC/transfer/channelChainB。上面这段话的意思是,即使链 C 直接连接到了链 A,链 B 用来连接链 A 的端口和通道标识符(例如 transfer/channelChainA)也可能与链 C 用来连接链 A 的标识符(例如 transfer/channelToChainA)完全不同。 因此,IBC 团队为客户端推荐的解决方案如下:
  • 连接所有链:连接时间线中的所有链后,客户端就可以在每一条相关链上执行直接连接部分所述的查询。通过反复沿着端口、通道和面额追踪的转账时间线向前追踪,客户端应当始终能够找到所有相关标识符。代价是客户端必须连接到这些链上的节点才能执行查询。
  • Relayer as a Service (RaaS):一个更长期的方案是使用或构建中继服务,将面额追踪映射为每个代币的链路径时间线(即 源链 -> 第 1 条链 -> ... -> 第 (n-1) 条链 -> 最终链)。这些服务可以提供 merkle 证明,使客户端能够通过运行轻客户端,自行选择性验证路径时间线的正确性。如果不验证这些证明,就应将这些服务视为受信任的第三方服务。此外,未来也会建议客户端优先使用支持生态中最多链间连接的 RaaS。遗憾的是,现有公开中继器(Golang 和 Rust 实现)都没有向客户端提供这项服务。
对于经过多次连接跳转的代币,在本文撰写时,客户端唯一可行的替代方案是直接连接所有相关链,并按顺序对每条链执行相应查询。

锁定资金

在某些特殊情况下,与某个通道关联的客户端状态无法更新。这会导致该通道中的同质化代币资金被永久锁定,从而无法继续转移。 为缓解这一问题,可以提交客户端更新治理提案,使用新的有效头信息更新被冻结的客户端。提案通过后,客户端状态将解除冻结,相关通道中的资金也会随之解锁。该机制仅适用于允许通过治理进行更新的客户端,例如 Tendermint 客户端。 除此之外,还需要特别说明的是,代币必须沿着它最初经过的完全相同路径发送回去,才能在源链上恢复为原始形式(例如 uatom 在 Cosmos Hub 上的原始形式)。如果通过不同通道将代币发回同一条链,代币不会沿着其时间线回退。如果链历史中的某个通道在代币能够沿该通道发回之前就已关闭,那么该代币将无法恢复为其原始形式。

安全注意事项

出于安全考虑,任何其他模块都不应能够铸造带有 ibc/ 前缀的代币。IBC transfer 模块需要占用一部分仅允许自身创建代币的面额空间。

通道关闭

IBC transfer 模块不支持关闭通道。

Synopsis

Learn about what the token Transfer module is

What is the Transfer module?

Transfer is the Cosmos SDK implementation of the ICS-20 protocol, which enables cross-chain fungible token transfers.

Concepts

Acknowledgements

ICS20 uses the recommended acknowledgement format as specified by ICS 04. A successful receive of a transfer packet will result in a Result Acknowledgement being written with the value []byte{byte(1)} in the Response field. An unsuccessful receive of a transfer packet will result in an Error Acknowledgement being written with the error message in the Response field.

Denomination trace

The denomination trace corresponds to the information that allows a token to be traced back to its origin chain. It contains a sequence of port and channel identifiers ordered from the most recent to the oldest in the timeline of transfers.
When using transfer with IBC v2 connecting to e.g. Ethereum, the source channel identifier will be the source client identifier instead.
This information is included on the token’s base denomination field in the form of a hash to prevent an unbounded denomination length. For example, the token transfer/channelToA/uatom will be displayed as ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2. The human readable denomination is stored using x/bank module’s denom metadata feature. You may display the human readable denominations by querying balances with the --resolve-denom flag, as in:
simd query bank balances [address] --resolve-denom
Each send to any chain other than the one it was previously received from is a movement forwards in the token’s timeline. This causes trace to be added to the token’s history and the destination port and destination channel to be prefixed to the denomination. In these instances the sender chain is acting as the “source zone”. When the token is sent back to the chain it previously received from, the prefix is removed. This is a backwards movement in the token’s timeline and the sender chain is acting as the “sink zone”. It is strongly recommended to understand the implications and context of the IBC token representations.

UX suggestions for clients

For clients (wallets, exchanges, applications, block explorers, etc) that want to display the source of the token, it is recommended to use the following alternatives for each of the cases below:

Direct connection

If the denomination trace contains a single identifier prefix pair (as in the example above), then the easiest way to retrieve the chain and light client identifier is to map the trace information directly. In summary, this requires querying the channel from the denomination trace identifiers, and then the counterparty client state using the counterparty port and channel identifiers from the retrieved channel. A general pseudo algorithm would look like the following:
  1. Query the full denomination trace.
  2. Query the channel with the portID/channelID pair, which corresponds to the first destination of the token.
  3. Query the client state using the identifiers pair. Note that this query will return a "Not Found" response if the current chain is not connected to this channel.
  4. Retrieve the client identifier or chain identifier from the client state (eg: on Tendermint clients) and store it locally.
Using the gRPC gateway client service the steps above would be, with a given IBC token ibc/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2 stored on chainB:
  1. GET /ibc/apps/transfer/v1/denom_traces/7F1D3FCF4AE79E1554D670D1AD949A9BA4E4A3C76C63093E17E446A46061A7A2 -> {"path": "transfer/channelToA", "base_denom": "uatom"}
  2. GET /ibc/apps/transfer/v1/channels/channelToA/ports/transfer/client_state" -> {"client_id": "clientA", "chain-id": "chainA", ...}
  3. GET /ibc/apps/transfer/v1/channels/channelToA/ports/transfer" -> {"channel_id": "channelToA", port_id": "transfer", counterparty: {"channel_id": "channelToB", port_id": "transfer"}, ...}
  4. GET /ibc/apps/transfer/v1/channels/channelToB/ports/transfer/client_state" -> {"client_id": "clientB", "chain-id": "chainB", ...}
Then, the token transfer chain path for the uatom denomination would be: chainA -> chainB.

Multiple hops

The multiple channel hops case applies when the token has passed through multiple chains between the original source and final destination chains. The IBC protocol doesn’t know the topology of the overall network (i.e connections between chains and identifier names between them). For this reason, in the multiple hops case, a particular chain in the timeline of the individual transfers can’t query the chain and client identifiers of the other chains. Take for example the following sequence of transfers A -> B -> C for an IBC token, with a final prefix path (trace info) of transfer/channelChainC/transfer/channelChainB. What the paragraph above means is that even in the case that chain C is directly connected to chain A, querying the port and channel identifiers that chain B uses to connect to chain A (eg: transfer/channelChainA) can be completely different from the one that chain C uses to connect to chain A (eg: transfer/channelToChainA). Thus the proposed solution for clients that the IBC team recommends are the following:
  • Connect to all chains: Connecting to all the chains in the timeline would allow clients to perform the queries outlined in the direct connection section to each relevant chain. By repeatedly following the port and channel denomination trace transfer timeline, clients should always be able to find all the relevant identifiers. This comes at the tradeoff that the client must connect to nodes on each of the chains in order to perform the queries.
  • Relayer as a Service (RaaS): A longer term solution is to use/create a relayer service that could map the denomination trace to the chain path timeline for each token (i.e origin chain -> chain #1 -> ... -> chain #(n-1) -> final chain). These services could provide merkle proofs in order to allow clients to optionally verify the path timeline correctness for themselves by running light clients. If the proofs are not verified, they should be considered as trusted third parties services. Additionally, client would be advised in the future to use RaaS that support the largest number of connections between chains in the ecosystem. Unfortunately, none of the existing public relayers (in Golang and Rust), provide this service to clients.
The only viable alternative for clients (at the time of writing) to tokens with multiple connection hops, is to connect to all chains directly and perform relevant queries to each of them in the sequence.

Locked funds

In some exceptional cases, a client state associated with a given channel cannot be updated. This causes that funds from fungible tokens in that channel will be permanently locked and thus can no longer be transferred. To mitigate this, a client update governance proposal can be submitted to update the frozen client with a new valid header. Once the proposal passes the client state will be unfrozen and the funds from the associated channels will then be unlocked. This mechanism only applies to clients that allow updates via governance, such as Tendermint clients. In addition to this, it’s important to mention that a token must be sent back along the exact route that it took originally in order to return it to its original form on the source chain (eg: the Cosmos Hub for the uatom). Sending a token back to the same chain across a different channel will not move the token back across its timeline. If a channel in the chain history closes before the token can be sent back across that channel, then the token will not be returnable to its original form.

Security considerations

For safety, no other module must be capable of minting tokens with the ibc/ prefix. The IBC transfer module needs a subset of the denomination space that only it can create tokens in.

Channel Closure

The IBC transfer module does not support channel closure.