变更日志

  • 09-07-2020: 初始草案
  • 11-08-2020: 实现变更

状态

已接受,已实现

背景

IBC 跨链同质化代币转账的规范 (ICS20) 需要 感知任意代币面额的来源,才能中继一个包含发送方和接收方地址的 Packet,这些地址位于 FungibleTokenPacketData 中。 Packet 中继发送基于两种情况运作(依据 规范 和 Colin Axnér 的说明):
  1. 发送链充当源区(source zone)。代币会被转入发送链上的托管地址(即锁定),然后通过 IBC TAO 逻辑转移到接收链。预期接收链会向接收地址铸造凭证代币。
  2. 发送链充当汇区(sink zone)。代币(凭证)会在发送链上销毁,然后通过 IBC TAO 逻辑转移到接收链。预期此前发送过原始面额的接收链会将该同质化代币解托管,并发送到接收地址。
理解源区和汇区的另一种方式,是从代币的时间线来思考。每当代币被发送到一个并非其上一次接收来源的链时,就表示它在代币时间线上向前移动。这会把追踪信息加入代币历史中,并将目标端口和目标通道前缀添加到面额上。在这些情况下,发送链充当源区。当代币被发回它上一次接收来源的链时,这个前缀会被移除。这表示代币时间线上的一次向后移动,而发送链则充当汇区。

示例

假设存在如下通道连接,并且所有通道都使用端口 ID transfer:
  • 链 A 与链 B、链 C 分别有通道,ID 分别为 channelToB 和 channelToC
  • 链 B 与链 A、链 C 分别有通道,ID 分别为 channelToA 和 channelToC
  • 链 C 与链 A、链 B 分别有通道,ID 分别为 channelToA 和 channelToB
链之间按如下顺序发生转账:A -> B -> C -> A -> C。具体如下:
  1. A -> B:发送链是源区。A 发送携带 denom 的 packet(在 A 上托管),B 接收 denom 并铸造、发送凭证 transfer/channelToA/denom 给接收方。
  2. B -> C:发送链是源区。B 发送携带 transfer/channelToA/denom 的 packet(在 B 上托管),C 接收 transfer/channelToA/denom 并铸造、发送凭证 transfer/channelToB/transfer/channelToA/denom 给接收方。
  3. C -> A:发送链是源区。C 发送携带 transfer/channelToB/transfer/channelToA/denom 的 packet(在 C 上托管),A 接收 transfer/channelToB/transfer/channelToA/denom 并铸造、发送凭证 transfer/channelToC/transfer/channelToB/transfer/channelToA/denom 给接收方。
  4. A -> C:发送链是汇区。A 发送携带 transfer/channelToC/transfer/channelToB/transfer/channelToA/denom 的 packet(在 A 上销毁),C 接收 transfer/channelToC/transfer/channelToB/transfer/channelToA/denom,并将 transfer/channelToB/transfer/channelToA/denom 解托管后发送给接收方。
该代币在链 C 上的最终面额为 transfer/channelToB/transfer/channelToA/denom,其中 transfer/channelToB/transfer/channelToA 是追踪信息。 在这个上下文中,当接收到跨链同质化代币转账时,如果发送链是该代币的源链,协议会按照以下格式,将端口和通道标识符前缀添加到面额前:
prefix + denom = {destPortN}/{destChannelN}/.../{destPort0}/{destChannel0}/denom
示例:将 100 uatom 从 Hub 上的端口 HubPort 和通道 HubChannel 转到 Ethermint 的端口 EthermintPort 和通道 EthermintChannel,结果会得到 100 EthermintPort/EthermintChannel/uatom,其中 EthermintPort/EthermintChannel/uatom 是接收链上的新面额。 如果这些代币被转回 Hub(即 源 链),前缀会被裁剪掉,代币面额也会更新回原始值。

问题

向代币面额中添加额外信息的问题有两个方面:
  1. 如果代币被转移到源链以外的其他 zone,长度会持续增长:
如果一个代币通过 IBC 向某个汇链转移了 n 次,那么该代币的面额将包含 n 对前缀,如上面的格式示例所示。这会带来问题,因为虽然端口和通道标识符各自的最大长度都是 64,但 SDK 的 Coin 类型只接受长度不超过 64 个字符的面额。因此,一个跨链代币本身由端口与通道标识符再加上基础面额组成,就可能超过 SDK Coins 的长度校验限制。 这可能导致一些不期望的行为,例如如果面额超长,代币将无法继续转到多个汇链,或者由于接收链上的面额校验失败而出现意外的 panics。
  1. 面额中存在特殊字符和大写字母:
在 SDK 中,每次通过构造函数 NewCoin 初始化一个 Coin 时,都会根据一个 Regex 对币种面额进行校验,其中只接受小写字母数字字符。对于原生面额来说,这有利于保持良好的 UX,但对于 IBC 来说则带来了挑战,因为根据 ICS 024 - Host Requirements 规范,端口和通道可能会随机生成,并包含特殊字符和大写字符。

决策

上述问题只适用于基于 SDK 的链,因此所提出的解决方案不需要修改规范,也不会要求调整 ICS20 规范的其他实现。 提议的方案不是直接把标识符加到代币面额上,而是对面额前缀进行哈希,以便为所有跨链同质化代币得到一致的长度。 这只会用于内部存储;而当通过 IBC 转移到另一条链时,packet 数据中指定的面额仍会是完整的标识符前缀路径,以便按照 ICS20 的规定将代币追溯回其源链。 新的建议格式如下:
ibcDenom = "ibc/" + hash(trace path + "/" + base denom)
哈希函数将是对 DenomTrace 字段进行 SHA256 哈希:
// DenomTrace contains the base denomination for ICS20 fungible tokens and the source tracing
// information
message DenomTrace {
  // chain of port/channel identifiers used for tracing the source of the fungible token
  string path = 1;
  // base denomination of the relayed fungible token
  string base_denom = 2;
}
IBCDenom 函数会在创建 ICS20 同质化代币 packet 数据时,构造所使用的 Coin 面额:
// Hash returns the hex bytes of the SHA256 hash of the DenomTrace fields using the following formula:
//
// hash = sha256(tracePath + "/" + baseDenom)
func (dt DenomTrace) Hash() tmbytes.HexBytes {
  return tmhash.Sum(dt.Path + "/" + dt.BaseDenom)
}

// IBCDenom a coin denomination for an ICS20 fungible token in the format 'ibc/{hash(tracePath + baseDenom)}'. 
// If the trace is empty, it will return the base denomination.
func (dt DenomTrace) IBCDenom() string {
  if dt.Path != "" {
    return fmt.Sprintf("ibc/%s", dt.Hash())
  }
  return dt.BaseDenom
}

x/ibc-transfer 变更

为了从 IBC 面额中取回 trace 信息,需要在 ibc-transfer 模块中新增一个查找表。 这些值还需要在升级之间持久化,这意味着需要在模块中新增一个 []DenomTrace 的 GenesisState 字段:
// GetDenomTrace retrieves the full identifiers trace and base denomination from the store.
func (k Keeper) GetDenomTrace(ctx Context, denomTraceHash []byte) (DenomTrace, bool) {
  store := ctx.KVStore(k.storeKey)
  bz := store.Get(types.KeyDenomTrace(traceHash))
  if bz == nil {
    return &DenomTrace, false
  }

  var denomTrace DenomTrace
  k.cdc.MustUnmarshalBinaryBare(bz, &denomTrace)
  return denomTrace, true
}

// HasDenomTrace checks if a the key with the given trace hash exists on the store.
func (k Keeper) HasDenomTrace(ctx Context, denomTraceHash []byte)  bool {
  store := ctx.KVStore(k.storeKey)
  return store.Has(types.KeyTrace(denomTraceHash))
}

// SetDenomTrace sets a new {trace hash -> trace} pair to the store.
func (k Keeper) SetDenomTrace(ctx Context, denomTrace DenomTrace) {
  store := ctx.KVStore(k.storeKey)
  bz := k.cdc.MustMarshalBinaryBare(&denomTrace)
  store.Set(types.KeyTrace(denomTrace.Hash()), bz)
}
MsgTransfer 会校验 Token 字段中的 Coin 面额:如果提供了 trace 信息,则其中必须包含有效哈希;否则必须与基础面额匹配:
func (msg MsgTransfer) ValidateBasic() error {
  // ...
  return ValidateIBCDenom(msg.Token.Denom)
}
// ValidateIBCDenom validates that the given denomination is either:
//
//  - A valid base denomination (eg: 'uatom')
//  - A valid fungible token representation (i.e 'ibc/{hash}') per ADR 001 https://github.com/cosmos/ibc-go/blob/main/docs/architecture/adr-001-coin-source-tracing.md
func ValidateIBCDenom(denom string) error {
  denomSplit := strings.SplitN(denom, "/", 2)

  switch {
  case strings.TrimSpace(denom) == "",
    len(denomSplit) == 1 && denomSplit[0] == "ibc",
    len(denomSplit) == 2 && (denomSplit[0] != "ibc" || strings.TrimSpace(denomSplit[1]) == ""):
    return sdkerrors.Wrapf(ErrInvalidDenomForTransfer, "denomination should be prefixed with the format 'ibc/{hash(trace + \"/\" + %s)}'", denom)

  case denomSplit[0] == denom && strings.TrimSpace(denom) != "":
    return sdk.ValidateDenom(denom)
  }

  if _, err := ParseHexHash(denomSplit[1]); err != nil {
    return Wrapf(err, "invalid denom trace hash %s", denomSplit[1])
  }

  return nil
}
只有在接收代币时才需要更新面额 trace 信息:
  • 接收方是源链:接收方创建了该代币,因此必须已经存储了 trace 查找信息(如有必要;例如原生代币场景就不需要查找)。
  • 接收方不是源链:存储收到的信息。例如,在步骤 1 中,当链 B 接收到 transfer/channelToA/denom 时。
// SendTransfer
// ...

  fullDenomPath := token.Denom

// deconstruct the token denomination into the denomination trace info
// to determine if the sender is the source chain
if strings.HasPrefix(token.Denom, "ibc/") {
  fullDenomPath, err = k.DenomPathFromHash(ctx, token.Denom)
  if err != nil {
    return err
  }
}

if types.SenderChainIsSource(sourcePort, sourceChannel, fullDenomPath) {
//...
// DenomPathFromHash returns the full denomination path prefix from an ibc denom with a hash
// component.
func (k Keeper) DenomPathFromHash(ctx sdk.Context, denom string) (string, error) {
  hexHash := denom[4:]
  hash, err := ParseHexHash(hexHash)
  if err != nil {
    return "", Wrap(ErrInvalidDenomForTransfer, err.Error())
  }

  denomTrace, found := k.GetDenomTrace(ctx, hash)
  if !found {
    return "", Wrap(ErrTraceNotFound, hexHash)
  }

  fullDenomPath := denomTrace.GetFullDenomPath()
  return fullDenomPath, nil
}
// OnRecvPacket
// ...

// This is the prefix that would have been prefixed to the denomination
// on sender chain IF and only if the token originally came from the
// receiving chain.
//
// NOTE: We use SourcePort and SourceChannel here, because the counterparty
// chain would have prefixed with DestPort and DestChannel when originally
// receiving this coin as seen in the "sender chain is the source" condition.
if ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), data.Denom) {
  // sender chain is not the source, unescrow tokens

  // remove prefix added by sender chain
  voucherPrefix := types.GetDenomPrefix(packet.GetSourcePort(), packet.GetSourceChannel())
  unprefixedDenom := data.Denom[len(voucherPrefix):]
  token := sdk.NewCoin(unprefixedDenom, sdk.NewIntFromUint64(data.Amount))

  // unescrow tokens
  escrowAddress := types.GetEscrowAddress(packet.GetDestPort(), packet.GetDestChannel())
  return k.bankKeeper.SendCoins(ctx, escrowAddress, receiver, sdk.NewCoins(token))
}

// sender chain is the source, mint vouchers

// since SendPacket did not prefix the denomination, we must prefix denomination here
sourcePrefix := types.GetDenomPrefix(packet.GetDestPort(), packet.GetDestChannel())
// NOTE: sourcePrefix contains the trailing "/"
prefixedDenom := sourcePrefix + data.Denom

// construct the denomination trace from the full raw denomination
denomTrace := types.ParseDenomTrace(prefixedDenom)

// set the value to the lookup table if not stored already
traceHash := denomTrace.Hash()
if !k.HasDenomTrace(ctx, traceHash) {
  k.SetDenomTrace(ctx, traceHash, denomTrace)
}

voucherDenom := denomTrace.IBCDenom()
voucher := sdk.NewCoin(voucherDenom, sdk.NewIntFromUint64(data.Amount))

// mint new tokens if the source of the transfer is the same chain
if err := k.bankKeeper.MintCoins(
  ctx, types.ModuleName, sdk.NewCoins(voucher),
); err != nil {
  return err
}

// send to receiver
return k.bankKeeper.SendCoinsFromModuleToAccount(
  ctx, types.ModuleName, receiver, sdk.NewCoins(voucher),
)
func NewDenomTraceFromRawDenom(denom string) DenomTrace{
  denomSplit := strings.Split(denom, "/")
  trace := ""
  if len(denomSplit) > 1 {
    trace = strings.Join(denomSplit[:len(denomSplit)-1], "/")
  }
  return DenomTrace{
    BaseDenom: denomSplit[len(denomSplit)-1],
    Trace:     trace,
  }
}
最后还需要说明一点:FungibleTokenPacketData 将保持不变,也就是说仍然使用带前缀的完整面额,因为接收链可能不是基于 SDK 的链。

Coin 变更

Coin 面额校验需要更新以反映这些变化。具体来说,面额校验函数现在将:
  • 接受斜杠分隔符("/")和大写字符(因为 HexBytes 格式)
  • 将最大字符长度提升到 128,因为 Tendermint 的 HexBytes 类型所使用的十六进制表示包含 64 个字符。
如果未来 custom base denomination validation 被集成到 SDK 中,还可以在 bank 模块中加入额外的校验逻辑,例如校验哈希长度。

优点

  • 更清晰地区分代币的来源追踪行为(transfer 前缀)与原始 Coin 面额
  • Coin 字段的校验更加一致(即不包含特殊字符、最大长度固定)
  • IBC 的 Coin 和标准面额更简洁
  • SDK 的 Coin 无需新增字段

缺点

  • 需要在 ibc-transfer 模块存储中保存每一组 trace 面额标识
  • 客户端每次通过 IBC 接收到新的中继同质化代币时,都必须获取其基础面额。可以通过在客户端侧使用映射或缓存已见过的哈希来缓解。其他缓解方式还包括建立 websocket 连接并订阅传入事件。

中性影响

  • 与 ICS20 规范存在轻微差异
  • 在 ibc-transfer 模块中增加了针对 IBC Coin 的额外校验逻辑
  • 增加了额外的创世字段
  • 由于需要访问存储,跨链转账的 gas 使用量会略有增加。如果转账频繁,这部分应可通过区块间缓存进行优化。

参考资料


Changelog

  • 09-07-2020: Initial Draft
  • 11-08-2020: Implementation changes

Status

Accepted, Implemented

Context

The specification for IBC cross-chain fungible token transfers (ICS20), needs to be aware of the origin of any token denomination in order to relay a Packet which contains the sender and recipient addresses in the FungibleTokenPacketData. The Packet relay sending works based in 2 cases (per specification and Colin Axnér’s description):
  1. Sender chain is acting as the source zone. The coins are transferred to an escrow address (i.e locked) on the sender chain and then transferred to the receiving chain through IBC TAO logic. It is expected that the receiving chain will mint vouchers to the receiving address.
  2. Sender chain is acting as the sink zone. The coins (vouchers) are burned on the sender chain and then transferred to the receiving chain through IBC TAO logic. It is expected that the receiving chain, which had previously sent the original denomination, will unescrow the fungible token and send it to the receiving address.
Another way of thinking of source and sink zones is through the token’s timeline. 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.

Example

Assume the following channel connections exist and that all channels use the port ID transfer:
  • chain A has channels with chain B and chain C with the IDs channelToB and channelToC, respectively
  • chain B has channels with chain A and chain C with the IDs channelToA and channelToC, respectively
  • chain C has channels with chain A and chain B with the IDs channelToA and channelToB, respectively
These steps of transfer between chains occur in the following order: A -> B -> C -> A -> C. In particular:
  1. A -> B: sender chain is source zone. A sends packet with denom (escrowed on A), B receives denom and mints and sends voucher transfer/channelToA/denom to recipient.
  2. B -> C: sender chain is source zone. B sends packet with transfer/channelToA/denom (escrowed on B), C receives transfer/channelToA/denom and mints and sends voucher transfer/channelToB/transfer/channelToA/denom to recipient.
  3. C -> A: sender chain is source zone. C sends packet with transfer/channelToB/transfer/channelToA/denom (escrowed on C), A receives transfer/channelToB/transfer/channelToA/denom and mints and sends voucher transfer/channelToC/transfer/channelToB/transfer/channelToA/denom to recipient.
  4. A -> C: sender chain is sink zone. A sends packet with transfer/channelToC/transfer/channelToB/transfer/channelToA/denom (burned on A), C receives transfer/channelToC/transfer/channelToB/transfer/channelToA/denom, and unescrows and sends transfer/channelToB/transfer/channelToA/denom to recipient.
The token has a final denomination on chain C of transfer/channelToB/transfer/channelToA/denom, where transfer/channelToB/transfer/channelToA is the trace information. In this context, upon a receive of a cross-chain fungible token transfer, if the sender chain is the source of the token, the protocol prefixes the denomination with the port and channel identifiers in the following format:
prefix + denom = {destPortN}/{destChannelN}/.../{destPort0}/{destChannel0}/denom
Example: transferring 100 uatom from port HubPort and channel HubChannel on the Hub to Ethermint’s port EthermintPort and channel EthermintChannel results in 100 EthermintPort/EthermintChannel/uatom, where EthermintPort/EthermintChannel/uatom is the new denomination on the receiving chain. In the case those tokens are transferred back to the Hub (i.e the source chain), the prefix is trimmed and the token denomination updated to the original one.

Problem

The problem of adding additional information to the coin denomination is twofold:
  1. The ever increasing length if tokens are transferred to zones other than the source:
If a token is transferred n times via IBC to a sink chain, the token denom will contain n pairs of prefixes, as shown on the format example above. This poses a problem because, while port and channel identifiers have a maximum length of 64 each, the SDK Coin type only accepts denoms up to 64 characters. Thus, a single cross-chain token, which again, is composed by the port and channels identifiers plus the base denomination, can exceed the length validation for the SDK Coins. This can result in undesired behaviours such as tokens not being able to be transferred to multiple sink chains if the denomination exceeds the length or unexpected panics due to denomination validation failing on the receiving chain.
  1. The existence of special characters and uppercase letters on the denomination:
In the SDK every time a Coin is initialized through the constructor function NewCoin, a validation of a coin’s denom is performed according to a Regex, where only lowercase alphanumeric characters are accepted. While this is desirable for native denominations to keep a clean UX, it presents a challenge for IBC as ports and channels might be randomly generated with special and uppercase characters as per the ICS 024 - Host Requirements specification.

Decision

The issues outlined above, are applicable only to SDK-based chains, and thus the proposed solution are do not require specification changes that would result in modification to other implementations of the ICS20 spec. Instead of adding the identifiers on the coin denomination directly, the proposed solution hashes the denomination prefix in order to get a consistent length for all the cross-chain fungible tokens. This will be used for internal storage only, and when transferred via IBC to a different chain, the denomination specified on the packed data will be the full prefix path of the identifiers needed to trace the token back to the originating chain, as specified on ICS20. The new proposed format will be the following:
ibcDenom = "ibc/" + hash(trace path + "/" + base denom)
The hash function will be a SHA256 hash of the fields of the DenomTrace:
// DenomTrace contains the base denomination for ICS20 fungible tokens and the source tracing
// information
message DenomTrace {
  // chain of port/channel identifiers used for tracing the source of the fungible token
  string path = 1;
  // base denomination of the relayed fungible token
  string base_denom = 2;
}
The IBCDenom function constructs the Coin denomination used when creating the ICS20 fungible token packet data:
// Hash returns the hex bytes of the SHA256 hash of the DenomTrace fields using the following formula:
//
// hash = sha256(tracePath + "/" + baseDenom)
func (dt DenomTrace) Hash() tmbytes.HexBytes {
  return tmhash.Sum(dt.Path + "/" + dt.BaseDenom)
}

// IBCDenom a coin denomination for an ICS20 fungible token in the format 'ibc/{hash(tracePath + baseDenom)}'. 
// If the trace is empty, it will return the base denomination.
func (dt DenomTrace) IBCDenom() string {
  if dt.Path != "" {
    return fmt.Sprintf("ibc/%s", dt.Hash())
  }
  return dt.BaseDenom
}

x/ibc-transfer Changes

In order to retrieve the trace information from an IBC denomination, a lookup table needs to be added to the ibc-transfer module. These values need to also be persisted between upgrades, meaning that a new []DenomTrace GenesisState field state needs to be added to the module:
// GetDenomTrace retrieves the full identifiers trace and base denomination from the store.
func (k Keeper) GetDenomTrace(ctx Context, denomTraceHash []byte) (DenomTrace, bool) {
  store := ctx.KVStore(k.storeKey)
  bz := store.Get(types.KeyDenomTrace(traceHash))
  if bz == nil {
    return &DenomTrace, false
  }

  var denomTrace DenomTrace
  k.cdc.MustUnmarshalBinaryBare(bz, &denomTrace)
  return denomTrace, true
}

// HasDenomTrace checks if a the key with the given trace hash exists on the store.
func (k Keeper) HasDenomTrace(ctx Context, denomTraceHash []byte)  bool {
  store := ctx.KVStore(k.storeKey)
  return store.Has(types.KeyTrace(denomTraceHash))
}

// SetDenomTrace sets a new {trace hash -> trace} pair to the store.
func (k Keeper) SetDenomTrace(ctx Context, denomTrace DenomTrace) {
  store := ctx.KVStore(k.storeKey)
  bz := k.cdc.MustMarshalBinaryBare(&denomTrace)
  store.Set(types.KeyTrace(denomTrace.Hash()), bz)
}
The MsgTransfer will validate that the Coin denomination from the Token field contains a valid hash, if the trace info is provided, or that the base denominations matches:
func (msg MsgTransfer) ValidateBasic() error {
  // ...
  return ValidateIBCDenom(msg.Token.Denom)
}
// ValidateIBCDenom validates that the given denomination is either:
//
//  - A valid base denomination (eg: 'uatom')
//  - A valid fungible token representation (i.e 'ibc/{hash}') per ADR 001 https://github.com/cosmos/ibc-go/blob/main/docs/architecture/adr-001-coin-source-tracing.md
func ValidateIBCDenom(denom string) error {
  denomSplit := strings.SplitN(denom, "/", 2)

  switch {
  case strings.TrimSpace(denom) == "",
    len(denomSplit) == 1 && denomSplit[0] == "ibc",
    len(denomSplit) == 2 && (denomSplit[0] != "ibc" || strings.TrimSpace(denomSplit[1]) == ""):
    return sdkerrors.Wrapf(ErrInvalidDenomForTransfer, "denomination should be prefixed with the format 'ibc/{hash(trace + \"/\" + %s)}'", denom)

  case denomSplit[0] == denom && strings.TrimSpace(denom) != "":
    return sdk.ValidateDenom(denom)
  }

  if _, err := ParseHexHash(denomSplit[1]); err != nil {
    return Wrapf(err, "invalid denom trace hash %s", denomSplit[1])
  }

  return nil
}
The denomination trace info only needs to be updated when token is received:
  • Receiver is source chain: The receiver created the token and must have the trace lookup already stored (if necessary ie native token case wouldn’t need a lookup).
  • Receiver is not source chain: Store the received info. For example, during step 1, when chain B receives transfer/channelToA/denom.
// SendTransfer
// ...

  fullDenomPath := token.Denom

// deconstruct the token denomination into the denomination trace info
// to determine if the sender is the source chain
if strings.HasPrefix(token.Denom, "ibc/") {
  fullDenomPath, err = k.DenomPathFromHash(ctx, token.Denom)
  if err != nil {
    return err
  }
}

if types.SenderChainIsSource(sourcePort, sourceChannel, fullDenomPath) {
//...
// DenomPathFromHash returns the full denomination path prefix from an ibc denom with a hash
// component.
func (k Keeper) DenomPathFromHash(ctx sdk.Context, denom string) (string, error) {
  hexHash := denom[4:]
  hash, err := ParseHexHash(hexHash)
  if err != nil {
    return "", Wrap(ErrInvalidDenomForTransfer, err.Error())
  }

  denomTrace, found := k.GetDenomTrace(ctx, hash)
  if !found {
    return "", Wrap(ErrTraceNotFound, hexHash)
  }

  fullDenomPath := denomTrace.GetFullDenomPath()
  return fullDenomPath, nil
}
// OnRecvPacket
// ...

// This is the prefix that would have been prefixed to the denomination
// on sender chain IF and only if the token originally came from the
// receiving chain.
//
// NOTE: We use SourcePort and SourceChannel here, because the counterparty
// chain would have prefixed with DestPort and DestChannel when originally
// receiving this coin as seen in the "sender chain is the source" condition.
if ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), data.Denom) {
  // sender chain is not the source, unescrow tokens

  // remove prefix added by sender chain
  voucherPrefix := types.GetDenomPrefix(packet.GetSourcePort(), packet.GetSourceChannel())
  unprefixedDenom := data.Denom[len(voucherPrefix):]
  token := sdk.NewCoin(unprefixedDenom, sdk.NewIntFromUint64(data.Amount))

  // unescrow tokens
  escrowAddress := types.GetEscrowAddress(packet.GetDestPort(), packet.GetDestChannel())
  return k.bankKeeper.SendCoins(ctx, escrowAddress, receiver, sdk.NewCoins(token))
}

// sender chain is the source, mint vouchers

// since SendPacket did not prefix the denomination, we must prefix denomination here
sourcePrefix := types.GetDenomPrefix(packet.GetDestPort(), packet.GetDestChannel())
// NOTE: sourcePrefix contains the trailing "/"
prefixedDenom := sourcePrefix + data.Denom

// construct the denomination trace from the full raw denomination
denomTrace := types.ParseDenomTrace(prefixedDenom)

// set the value to the lookup table if not stored already
traceHash := denomTrace.Hash()
if !k.HasDenomTrace(ctx, traceHash) {
  k.SetDenomTrace(ctx, traceHash, denomTrace)
}

voucherDenom := denomTrace.IBCDenom()
voucher := sdk.NewCoin(voucherDenom, sdk.NewIntFromUint64(data.Amount))

// mint new tokens if the source of the transfer is the same chain
if err := k.bankKeeper.MintCoins(
  ctx, types.ModuleName, sdk.NewCoins(voucher),
); err != nil {
  return err
}

// send to receiver
return k.bankKeeper.SendCoinsFromModuleToAccount(
  ctx, types.ModuleName, receiver, sdk.NewCoins(voucher),
)
func NewDenomTraceFromRawDenom(denom string) DenomTrace{
  denomSplit := strings.Split(denom, "/")
  trace := ""
  if len(denomSplit) > 1 {
    trace = strings.Join(denomSplit[:len(denomSplit)-1], "/")
  }
  return DenomTrace{
    BaseDenom: denomSplit[len(denomSplit)-1],
    Trace:     trace,
  }
}
One final remark is that the FungibleTokenPacketData will remain the same, i.e with the prefixed full denomination, since the receiving chain may not be an SDK-based chain.

Coin Changes

The coin denomination validation will need to be updated to reflect these changes. In particular, the denomination validation function will now:
  • Accept slash separators ("/") and uppercase characters (due to the HexBytes format)
  • Bump the maximum character length to 128, as the hex representation used by Tendermint’s HexBytes type contains 64 characters.
Additional validation logic, such as verifying the length of the hash, the may be added to the bank module in the future if the custom base denomination validation is integrated into the SDK.

Positive

  • Clearer separation of the source tracing behaviour of the token (transfer prefix) from the original Coin denomination
  • Consistent validation of Coin fields (i.e no special characters, fixed max length)
  • Cleaner Coin and standard denominations for IBC
  • No additional fields to SDK Coin

Negative

  • Store each set of tracing denomination identifiers on the ibc-transfer module store
  • Clients will have to fetch the base denomination every time they receive a new relayed fungible token over IBC. This can be mitigated using a map/cache for already seen hashes on the client side. Other forms of mitigation, would be opening a websocket connection subscribe to incoming events.

Neutral

  • Slight difference with the ICS20 spec
  • Additional validation logic for IBC coins on the ibc-transfer module
  • Additional genesis fields
  • Slightly increases the gas usage on cross-chain transfers due to access to the store. This should be inter-block cached if transfers are frequent.

References