From 9c5ce6d0d2527847c8a277129ce91c51536277cc Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Wed, 10 Nov 2021 19:43:37 +0800 Subject: [PATCH 01/18] initial draft of ics-021-nft-transfer --- spec/app/ics-021-nft-transfer/README.md | 360 ++++++++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 spec/app/ics-021-nft-transfer/README.md diff --git a/spec/app/ics-021-nft-transfer/README.md b/spec/app/ics-021-nft-transfer/README.md new file mode 100644 index 000000000..0e589eda4 --- /dev/null +++ b/spec/app/ics-021-nft-transfer/README.md @@ -0,0 +1,360 @@ +--- +ics: 21 +title: Non-Fungible Token Transfer +stage: draft +category: IBC/APP +requires: 25, 26 +kind: instantiation +author: Christopher Goes , Haifeng Xi +created: 2021-11-10 +modified: 2021-11-10 +--- + +## Synopsis + +This standard document specifies packet data structure, state machine handling logic, and encoding details for the transfer of non-fungible tokens over an IBC channel between two modules on separate chains. The state machine logic presented allows for safe multi-chain denomination handling with permissionless channel opening. This logic constitutes a "non-fungible token transfer bridge module", interfacing between the IBC routing module and an existing asset tracking module on the host state machine. + +### Motivation + +Users of a set of chains connected over the IBC protocol might wish to utilise an asset issued on one chain on another chain, perhaps to make use of additional features such as exchange or privacy protection, while retaining non-fungibility with the original asset on the issuing chain. This application-layer standard describes a protocol for transferring non-fungible tokens between chains connected with IBC which preserves asset non-fungibility, preserves asset ownership, limits the impact of Byzantine faults, and requires no additional permissioning. + +### Definitions + +The IBC handler interface & IBC routing module interface are as defined in [ICS 25](../../core/ics-025-handler-interface) and [ICS 26](../../core/ics-026-routing-module), respectively. + +### Desired Properties + +- Preservation of uniqueness (two-way peg). +- Preservation of total supply (maintained on a single source chain & module). +- Permissionless token transfers, no need to whitelist connections, modules, or denominations. +- Symmetric (all chains implement the same logic, no in-protocol differentiation of hubs & zones). +- Fault containment: prevents Byzantine-inflation of tokens originating on chain `A`, as a result of chain `B`'s Byzantine behaviour (though any users who sent tokens to chain `B` may be at risk). + +## Technical Specification + +### Data Structures + +Only one packet data type is required: `NonFungibleTokenPacketData`, which specifies the denomination, amount, sending account, and receiving account. + +```typescript +interface NonFungibleTokenPacketData { + classId: string + classUri: string + tokenId: string + tokenUri: string + sender: string + receiver: string +} +``` + +As tokens are sent across chains using the ICS 21 protocol, they begin to accrue a record of channels for which they have been transferred across. This information is encoded into the `classId` field. + +The ics21 token classes are represented in the form `{ics21Port}/{ics21Channel}/{classId}`, where `ics21Port` and `ics21Channel` are an ics21 port and channel on the current chain for which the token exists. The prefixed port and channel pair indicate which channel the token was previously sent through. If `{classId}` contains `/`, then it must also be in the ics21 form which indicates that this token has a multi-hop record. Note that this requires that the `/` (slash character) is prohibited in non-IBC token denomination names. + +A sending chain may be acting as a source or sink zone. When a chain is sending tokens across a port and channel which are not equal to the last prefixed port and channel pair, it is acting as a source zone. When tokens are sent from a source zone, the destination port and channel will be prefixed onto the `classId` (once the tokens are received) adding another hop to a tokens record. When a chain is sending tokens across a port and channel which are equal to the last prefixed port and channel pair, it is acting as a sink zone. When tokens are sent from a sink zone, the last prefixed port and channel pair on the `classId` is removed (once the tokens are received), undoing the last hop in the tokens record. A more complete explanation is present in the ibc-go implementation (TBD). + +The acknowledgement data type describes whether the transfer succeeded or failed, and the reason for failure (if any). + +```typescript +type NonFungibleTokenPacketAcknowledgement = NonFungibleTokenPacketSuccess | NonFungibleTokenPacketError; + +interface NonFungibleTokenPacketSuccess { + // This is binary 0x01 base64 encoded + success: "AQ==" +} + +interface NonFungibleTokenPacketError { + error: string +} +``` + +Note that both the `NonFungibleTokenPacketData` as well as `NonFungibleTokenPacketAcknowledgement` must be JSON-encoded (not Protobuf encoded) when they serialized into packet data. + +The non-fungible token transfer bridge module tracks escrow addresses associated with particular channels in state. Fields of the `ModuleState` are assumed to be in scope. + +```typescript +interface ModuleState { + channelEscrowAddresses: Map +} +``` + +### Sub-protocols + +The sub-protocols described herein should be implemented in a "non-fungible token transfer bridge" module with access to the IBC routing module. + +#### Port & channel setup + +The `setup` function must be called exactly once when the module is created (perhaps when the blockchain itself is initialised) to bind to the appropriate port and create an escrow address (owned by the module). + +```typescript +function setup() { + capability = routingModule.bindPort("nft", ModuleCallbacks{ + onChanOpenInit, + onChanOpenTry, + onChanOpenAck, + onChanOpenConfirm, + onChanCloseInit, + onChanCloseConfirm, + onRecvPacket, + onTimeoutPacket, + onAcknowledgePacket, + onTimeoutPacketClose + }) + claimCapability("port", capability) +} +``` + +Once the `setup` function has been called, channels can be created through the IBC routing module between instances of the non-fungible token transfer module on separate chains. + +An administrator (with the permissions to create connections & channels on the host state machine) is responsible for setting up connections to other state machines & creating channels +to other instances of this module (or another module supporting this interface) on other chains. This specification defines packet handling semantics only, and defines them in such a fashion +that the module itself doesn't need to worry about what connections or channels might or might not exist at any point in time. + +#### Routing module callbacks + +##### Channel lifecycle management + +Both machines `A` and `B` accept new channels from any module on another machine, if and only if: + +- The channel being created is unordered. +- The version string is `ics21-1`. + +```typescript +function onChanOpenInit( + order: ChannelOrder, + connectionHops: [Identifier], + portIdentifier: Identifier, + channelIdentifier: Identifier, + counterpartyPortIdentifier: Identifier, + counterpartyChannelIdentifier: Identifier, + version: string) { + // only unordered channels allowed + abortTransactionUnless(order === UNORDERED) + // assert that version is "ics21-1" + abortTransactionUnless(version === "ics21-1") + // allocate an escrow address + channelEscrowAddresses[channelIdentifier] = newAddress() +} +``` + +```typescript +function onChanOpenTry( + order: ChannelOrder, + connectionHops: [Identifier], + portIdentifier: Identifier, + channelIdentifier: Identifier, + counterpartyPortIdentifier: Identifier, + counterpartyChannelIdentifier: Identifier, + version: string, + counterpartyVersion: string) { + // only unordered channels allowed + abortTransactionUnless(order === UNORDERED) + // assert that version is "ics21-1" + abortTransactionUnless(version === "ics21-1") + abortTransactionUnless(counterpartyVersion === "ics21-1") + // allocate an escrow address + channelEscrowAddresses[channelIdentifier] = newAddress() +} +``` + +```typescript +function onChanOpenAck( + portIdentifier: Identifier, + channelIdentifier: Identifier, + version: string) { + // port has already been validated + // assert that version is "ics21-1" + abortTransactionUnless(version === "ics21-1") +} +``` + +```typescript +function onChanOpenConfirm( + portIdentifier: Identifier, + channelIdentifier: Identifier) { + // accept channel confirmations, port has already been validated, version has already been validated +} +``` + +```typescript +function onChanCloseInit( + portIdentifier: Identifier, + channelIdentifier: Identifier) { + // no action necessary +} +``` + +```typescript +function onChanCloseConfirm( + portIdentifier: Identifier, + channelIdentifier: Identifier) { + // no action necessary +} +``` + +##### Packet relay + +In plain English, between chains `A` and `B`: + +- When acting as the source zone, the bridge module escrows an existing local non-fungible token on the sending chain and mints a corresponding voucher on the receiving chain. +- When acting as the sink zone, the bridge module burns the local voucher on the sending chain and unescrows the local non-fungible token on the receiving chain. +- When a packet times-out, local non-fungible tokens are unescrowed back to the sender or vouchers minted back to the sender appropriately. +- Acknowledgement data is used to handle failures, such as invalid destination accounts. Returning + an acknowledgement of failure is preferable to aborting the transaction since it more easily enables the sending chain to take appropriate action based on the nature of the failure. + +`createOutgoingPacket` must be called by a transaction handler in the module which performs appropriate signature checks, specific to the account owner on the host state machine. + +```typescript +function createOutgoingPacket( + classId: string, + tokenId: string, + sender: string, + receiver: string, + source: boolean, + destPort: string, + destChannel: string, + sourcePort: string, + sourceChannel: string, + timeoutHeight: Height, + timeoutTimestamp: uint64) { + prefix = "{sourcePort}/{sourceChannel}/" + // we are the source if the denomination is not prefixed + source = classId.slice(0, len(prefix)) !== prefix + if source { + // determine escrow account + escrowAccount = channelEscrowAddresses[sourceChannel] + // escrow source token (assumed to fail if sender is not owner) + nft.Transfer(sender, escrowAccount, classId, tokenId) + } else { + // receiver is source chain, burn voucher (assumed to fail if sender is not owner) + bank.Burn(sender, classId, tokenId) + } + Class class = nft.getClass(classId) + NFT token = nft.getNFT(classId, tokenId) + NonFungibleTokenPacketData data = NonFungibleTokenPacketData{classId, class.getUri(), tokenId, token.getUri(), sender, receiver} + handler.sendPacket(Packet{timeoutHeight, timeoutTimestamp, destPort, destChannel, sourcePort, sourceChannel, data}, getCapability("port")) +} +``` + +`onRecvPacket` is called by the routing module when a packet addressed to this module has been received. + +```typescript +function onRecvPacket(packet: Packet) { + NonFungibleTokenPacketData data = packet.data + // construct default acknowledgement of success + NonFungibleTokenPacketAcknowledgement ack = NonFungibleTokenPacketAcknowledgement{true, null} + prefix = "{packet.sourcePort}/{packet.sourceChannel}/" + // we are the source if the packets were prefixed by the sending chain + source = data.classId.slice(0, len(prefix)) === prefix + if source { + // receiver is source chain: unescrow token + // determine escrow account + escrowAccount = channelEscrowAddresses[packet.destChannel] + // unescrow token to receiver + err = nft.Transfer(escrowAccount, data.receiver, data.classId.slice(len(prefix)), data.tokenId) + if (err !== nil) + ack = NonFungibleTokenPacketAcknowledgement{false, "transfer nft failed"} + } else { + prefix = "{packet.destPort}/{packet.destChannel}/" + prefixedClassId = prefix + data.classId + // sender was source, mint voucher to receiver + err = nft.Mint(data.receiver, prefixedClassId, data.tokenId) + if (err !== nil) + ack = NonFungibleTokenPacketAcknowledgement{false, "mint nft failed"} + } + return ack +} +``` + +`onAcknowledgePacket` is called by the routing module when a packet sent by this module has been acknowledged. + +```typescript +function onAcknowledgePacket( + packet: Packet, + acknowledgement: bytes) { + // if the transfer failed, refund the tokens + if (!ack.success) + refundToken(packet) +} +``` + +`onTimeoutPacket` is called by the routing module when a packet sent by this module has timed-out (such that it will not be received on the destination chain). + +```typescript +function onTimeoutPacket(packet: Packet) { + // the packet timed-out, so refund the tokens + refundToken(packet) +} +``` + +`refundToken` is called by both `onAcknowledgePacket`, on failure, and `onTimeoutPacket`, to refund escrowed token to the original sender. + +```typescript +function refundToken(packet: Packet) { + NonFungibleTokenPacketData data = packet.data + prefix = "{packet.sourcePort}/{packet.sourceChannel}/" + // we are the source if the classId is not prefixed + source = data.classId.slice(0, len(prefix)) !== prefix + if source { + // sender was source chain, unescrow tokens back to sender + escrowAccount = channelEscrowAddresses[packet.srcChannel] + nft.Transfer(escrowAccount, data.sender, data.classId, data.tokenId) + } else { + // receiver was source chain, mint voucher back to sender + bank.Mint(data.sender, data.classId, data.tokenId) + } +} +``` + +```typescript +function onTimeoutPacketClose(packet: Packet) { + // can't happen, only unordered channels allowed +} +``` + +#### Reasoning + +##### Correctness + +This implementation preserves both uniqueness & supply. + +Uniqueness: If tokens have been sent to the counterparty chain, they can be redeemed back in the same `classId` & `tokenId` on the source chain. + +Supply: Redefine supply as unlocked tokens. All send-recv pairs for any given token class sum to net zero. Source chain can change supply. + +##### Multi-chain notes + +This specification does not directly handle the "diamond problem", where a user sends a token originating on chain A to chain B, then to chain D, and wants to return it through D -> C -> A — since the supply is tracked as owned by chain B (and the `classId` will be "{portOnD}/{channelOnD}/{portOnB}/{channelOnB}/classId"), chain C cannot serve as the intermediary. It is not yet clear whether that case should be dealt with in-protocol or not — it may be fine to just require the original path of redemption (and if there is frequent liquidity and some surplus on both paths the diamond path will work most of the time). Complexities arising from long redemption paths may lead to the emergence of central chains in the network topology. + +In order to track all of the tokens moving around the network of chains in various paths, it may be helpful for a particular chain to implement a registry which will track the "global" source chain for each `classId`. End-user service providers (such as wallet authors) may want to integrate such a registry or keep their own mapping of canonical source chains and human-readable names in order to improve UX. + +#### Optional addenda + +- Each chain, locally, could elect to keep a lookup table to use short, user-friendly local `classId`s in state which are translated to and from the longer `classId`s when sending and receiving packets. +- Additional restrictions may be imposed on which other machines may be connected to & which channels may be established. + +## Backwards Compatibility + +Not applicable. + +## Forwards Compatibility + +This initial standard uses version "ics21-1" in the channel handshake. + +A future version of this standard could use a different version in the channel handshake, and safely alter the packet data format & packet handler semantics. + +## Example Implementation + +Coming soon. + +## Other Implementations + +Coming soon. + +## History + +Nov 10, 2021 - Initial draft adapted from ICS20 spec + +## Copyright + +All content herein is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). From 70e860c8b7a942ed2d6d1a04755f6359eff8a354 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Wed, 10 Nov 2021 20:00:20 +0800 Subject: [PATCH 02/18] add reference to the NFT asset tracking module --- spec/app/ics-021-nft-transfer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/app/ics-021-nft-transfer/README.md b/spec/app/ics-021-nft-transfer/README.md index 0e589eda4..68b9d4110 100644 --- a/spec/app/ics-021-nft-transfer/README.md +++ b/spec/app/ics-021-nft-transfer/README.md @@ -80,7 +80,7 @@ interface ModuleState { ### Sub-protocols -The sub-protocols described herein should be implemented in a "non-fungible token transfer bridge" module with access to the IBC routing module. +The sub-protocols described herein should be implemented in a "non-fungible token transfer bridge" module with access to the NFT asset tracking module and the IBC routing module. #### Port & channel setup From 17d2d3e4f4021553f1ec4ee12b5afa43ef4122a0 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Fri, 12 Nov 2021 18:01:51 +0800 Subject: [PATCH 03/18] add reference to ICS20 --- spec/app/ics-021-nft-transfer/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/app/ics-021-nft-transfer/README.md b/spec/app/ics-021-nft-transfer/README.md index 68b9d4110..f53a12507 100644 --- a/spec/app/ics-021-nft-transfer/README.md +++ b/spec/app/ics-021-nft-transfer/README.md @@ -6,10 +6,12 @@ category: IBC/APP requires: 25, 26 kind: instantiation author: Christopher Goes , Haifeng Xi -created: 2021-11-10 -modified: 2021-11-10 +created: 2021-11-10 +modified: 2021-11-12 --- +> This spec follows the same design principles of [ICS 20](../ics-020-fungible-token-transfer) and copies most of its content therefrom, while replacing `bank` module based asset tracking logic with that of the `nft` module. + ## Synopsis This standard document specifies packet data structure, state machine handling logic, and encoding details for the transfer of non-fungible tokens over an IBC channel between two modules on separate chains. The state machine logic presented allows for safe multi-chain denomination handling with permissionless channel opening. This logic constitutes a "non-fungible token transfer bridge module", interfacing between the IBC routing module and an existing asset tracking module on the host state machine. @@ -47,7 +49,7 @@ interface NonFungibleTokenPacketData { } ``` -As tokens are sent across chains using the ICS 21 protocol, they begin to accrue a record of channels for which they have been transferred across. This information is encoded into the `classId` field. +As tokens are sent across chains using the ICS 21 protocol, they begin to accrue a record of channels for which they have been transferred across. This information is encoded into the `classId` field. The ics21 token classes are represented in the form `{ics21Port}/{ics21Channel}/{classId}`, where `ics21Port` and `ics21Channel` are an ics21 port and channel on the current chain for which the token exists. The prefixed port and channel pair indicate which channel the token was previously sent through. If `{classId}` contains `/`, then it must also be in the ics21 form which indicates that this token has a multi-hop record. Note that this requires that the `/` (slash character) is prohibited in non-IBC token denomination names. @@ -330,7 +332,7 @@ In order to track all of the tokens moving around the network of chains in vario #### Optional addenda -- Each chain, locally, could elect to keep a lookup table to use short, user-friendly local `classId`s in state which are translated to and from the longer `classId`s when sending and receiving packets. +- Each chain, locally, could elect to keep a lookup table to use short, user-friendly local `classId`s in state which are translated to and from the longer `classId`s when sending and receiving packets. - Additional restrictions may be imposed on which other machines may be connected to & which channels may be established. ## Backwards Compatibility From 7598c6407e4b737dc92ed530d200d37ae0c731ab Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Fri, 12 Nov 2021 18:42:26 +0800 Subject: [PATCH 04/18] replace residual mentioning of denom; fix pseudo-code errors --- spec/app/ics-021-nft-transfer/README.md | 40 ++++++++++++++----------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/spec/app/ics-021-nft-transfer/README.md b/spec/app/ics-021-nft-transfer/README.md index f53a12507..e75ef9c69 100644 --- a/spec/app/ics-021-nft-transfer/README.md +++ b/spec/app/ics-021-nft-transfer/README.md @@ -14,11 +14,11 @@ modified: 2021-11-12 ## Synopsis -This standard document specifies packet data structure, state machine handling logic, and encoding details for the transfer of non-fungible tokens over an IBC channel between two modules on separate chains. The state machine logic presented allows for safe multi-chain denomination handling with permissionless channel opening. This logic constitutes a "non-fungible token transfer bridge module", interfacing between the IBC routing module and an existing asset tracking module on the host state machine. +This standard document specifies packet data structure, state machine handling logic, and encoding details for the transfer of non-fungible tokens over an IBC channel between two modules on separate chains. The state machine logic presented allows for safe multi-chain `classId` handling with permissionless channel opening. This logic constitutes a "non-fungible token transfer bridge module", interfacing between the IBC routing module and an existing asset tracking module on the host state machine. ### Motivation -Users of a set of chains connected over the IBC protocol might wish to utilise an asset issued on one chain on another chain, perhaps to make use of additional features such as exchange or privacy protection, while retaining non-fungibility with the original asset on the issuing chain. This application-layer standard describes a protocol for transferring non-fungible tokens between chains connected with IBC which preserves asset non-fungibility, preserves asset ownership, limits the impact of Byzantine faults, and requires no additional permissioning. +Users of a set of chains connected over the IBC protocol might wish to utilise an asset issued on one chain on another chain, perhaps to make use of additional features such as exchange or privacy protection, while retaining uniqueness with the original asset on the issuing chain. This application-layer standard describes a protocol for transferring non-fungible tokens between chains connected with IBC which preserves asset uniqueness, preserves asset ownership, limits the impact of Byzantine faults, and requires no additional permissioning. ### Definitions @@ -28,7 +28,7 @@ The IBC handler interface & IBC routing module interface are as defined in [ICS - Preservation of uniqueness (two-way peg). - Preservation of total supply (maintained on a single source chain & module). -- Permissionless token transfers, no need to whitelist connections, modules, or denominations. +- Permissionless token transfers, no need to whitelist connections, modules, or `classId`s. - Symmetric (all chains implement the same logic, no in-protocol differentiation of hubs & zones). - Fault containment: prevents Byzantine-inflation of tokens originating on chain `A`, as a result of chain `B`'s Byzantine behaviour (though any users who sent tokens to chain `B` may be at risk). @@ -36,7 +36,7 @@ The IBC handler interface & IBC routing module interface are as defined in [ICS ### Data Structures -Only one packet data type is required: `NonFungibleTokenPacketData`, which specifies the denomination, amount, sending account, and receiving account. +Only one packet data type is required: `NonFungibleTokenPacketData`, which specifies the class id, class uri, token id, token uri, sending account, and receiving account. ```typescript interface NonFungibleTokenPacketData { @@ -51,9 +51,9 @@ interface NonFungibleTokenPacketData { As tokens are sent across chains using the ICS 21 protocol, they begin to accrue a record of channels for which they have been transferred across. This information is encoded into the `classId` field. -The ics21 token classes are represented in the form `{ics21Port}/{ics21Channel}/{classId}`, where `ics21Port` and `ics21Channel` are an ics21 port and channel on the current chain for which the token exists. The prefixed port and channel pair indicate which channel the token was previously sent through. If `{classId}` contains `/`, then it must also be in the ics21 form which indicates that this token has a multi-hop record. Note that this requires that the `/` (slash character) is prohibited in non-IBC token denomination names. +The ics21 token classes are represented in the form `{ics21Port}/{ics21Channel}/{classId}`, where `ics21Port` and `ics21Channel` are an ics21 port and channel on the current chain for which the token exists. The prefixed port and channel pair indicate which channel the token was previously sent through. If `{classId}` contains `/`, then it must also be in the ics21 form which indicates that this token has a multi-hop record. Note that this requires that the `/` (slash character) is prohibited in non-IBC token `classId`s. -A sending chain may be acting as a source or sink zone. When a chain is sending tokens across a port and channel which are not equal to the last prefixed port and channel pair, it is acting as a source zone. When tokens are sent from a source zone, the destination port and channel will be prefixed onto the `classId` (once the tokens are received) adding another hop to a tokens record. When a chain is sending tokens across a port and channel which are equal to the last prefixed port and channel pair, it is acting as a sink zone. When tokens are sent from a sink zone, the last prefixed port and channel pair on the `classId` is removed (once the tokens are received), undoing the last hop in the tokens record. A more complete explanation is present in the ibc-go implementation (TBD). +A sending chain may be acting as a source or sink zone. When a chain is sending tokens across a port and channel which are not equal to the last prefixed port and channel pair, it is acting as a source zone. When tokens are sent from a source zone, the destination port and channel will be prefixed onto the `classId` (once the tokens are received) adding another hop to a tokens record. When a chain is sending tokens across a port and channel which are equal to the last prefixed port and channel pair, it is acting as a sink zone. When tokens are sent from a sink zone, the last prefixed port and channel pair on the `classId` is removed (once the tokens are received), undoing the last hop in the tokens record. A more complete explanation is present in the [ibc-go implementation]() (TBD). The acknowledgement data type describes whether the transfer succeeded or failed, and the reason for failure (if any). @@ -108,9 +108,7 @@ function setup() { Once the `setup` function has been called, channels can be created through the IBC routing module between instances of the non-fungible token transfer module on separate chains. -An administrator (with the permissions to create connections & channels on the host state machine) is responsible for setting up connections to other state machines & creating channels -to other instances of this module (or another module supporting this interface) on other chains. This specification defines packet handling semantics only, and defines them in such a fashion -that the module itself doesn't need to worry about what connections or channels might or might not exist at any point in time. +An administrator (with the permissions to create connections & channels on the host state machine) is responsible for setting up connections to other state machines & creating channels to other instances of this module (or another module supporting this interface) on other chains. This specification defines packet handling semantics only, and defines them in such a fashion that the module itself doesn't need to worry about what connections or channels might or might not exist at any point in time. #### Routing module callbacks @@ -219,17 +217,19 @@ function createOutgoingPacket( sourceChannel: string, timeoutHeight: Height, timeoutTimestamp: uint64) { + // assert that sender is token owner + abortTransactionUnless(sender === nft.getOwner(classId, tokenId)) prefix = "{sourcePort}/{sourceChannel}/" - // we are the source if the denomination is not prefixed + // we are the source if the classId is not prefixed source = classId.slice(0, len(prefix)) !== prefix if source { // determine escrow account escrowAccount = channelEscrowAddresses[sourceChannel] - // escrow source token (assumed to fail if sender is not owner) - nft.Transfer(sender, escrowAccount, classId, tokenId) + // escrow source token + nft.Transfer(classId, tokenId, escrowAccount) } else { - // receiver is source chain, burn voucher (assumed to fail if sender is not owner) - bank.Burn(sender, classId, tokenId) + // receiver is source chain, burn voucher + bank.Burn(classId, tokenId) } Class class = nft.getClass(classId) NFT token = nft.getNFT(classId, tokenId) @@ -252,15 +252,17 @@ function onRecvPacket(packet: Packet) { // receiver is source chain: unescrow token // determine escrow account escrowAccount = channelEscrowAddresses[packet.destChannel] + // assert that escrow account is token owner + abortTransactionUnless(escrowAccount === nft.getOwner(data.classId.slice(len(prefix)), data.tokenId)) // unescrow token to receiver - err = nft.Transfer(escrowAccount, data.receiver, data.classId.slice(len(prefix)), data.tokenId) + err = nft.Transfer(data.classId.slice(len(prefix)), data.tokenId, data.receiver) if (err !== nil) ack = NonFungibleTokenPacketAcknowledgement{false, "transfer nft failed"} } else { prefix = "{packet.destPort}/{packet.destChannel}/" prefixedClassId = prefix + data.classId // sender was source, mint voucher to receiver - err = nft.Mint(data.receiver, prefixedClassId, data.tokenId) + err = nft.Mint(prefixedClassId, data.tokenId, data.receiver) if (err !== nil) ack = NonFungibleTokenPacketAcknowledgement{false, "mint nft failed"} } @@ -300,10 +302,12 @@ function refundToken(packet: Packet) { if source { // sender was source chain, unescrow tokens back to sender escrowAccount = channelEscrowAddresses[packet.srcChannel] - nft.Transfer(escrowAccount, data.sender, data.classId, data.tokenId) + // assert that escrow account is token owner + abortTransactionUnless(escrowAccount === nft.getOwner(data.classId, data.tokenId)) + nft.Transfer(data.classId, data.tokenId, data.sender) } else { // receiver was source chain, mint voucher back to sender - bank.Mint(data.sender, data.classId, data.tokenId) + bank.Mint(data.classId, data.tokenId, data.sender) } } ``` From aba951b726951e02a0fdff43523dd5de881749d5 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Wed, 17 Nov 2021 14:43:52 +0800 Subject: [PATCH 05/18] clarify/generalize semantics of packet data fields --- spec/app/ics-021-nft-transfer/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/spec/app/ics-021-nft-transfer/README.md b/spec/app/ics-021-nft-transfer/README.md index e75ef9c69..3f50199b3 100644 --- a/spec/app/ics-021-nft-transfer/README.md +++ b/spec/app/ics-021-nft-transfer/README.md @@ -14,7 +14,7 @@ modified: 2021-11-12 ## Synopsis -This standard document specifies packet data structure, state machine handling logic, and encoding details for the transfer of non-fungible tokens over an IBC channel between two modules on separate chains. The state machine logic presented allows for safe multi-chain `classId` handling with permissionless channel opening. This logic constitutes a "non-fungible token transfer bridge module", interfacing between the IBC routing module and an existing asset tracking module on the host state machine. +This standard document specifies packet data structure, state machine handling logic, and encoding details for the transfer of non-fungible tokens over an IBC channel between two modules on separate chains. The state machine logic presented allows for safe multi-chain `classId` handling with permissionless channel opening. This logic constitutes a "non-fungible token transfer bridge module", interfacing between the IBC routing module and an existing asset tracking module on the host state machine, which could be either a Cosmos-style "native" module or a smart contract running in a virtual machine. ### Motivation @@ -48,6 +48,13 @@ interface NonFungibleTokenPacketData { receiver: string } ``` +`classId` uniquely identifies the class/collection to which this NFT belongs in the originating host environment. In the case of an ERC-1155 compliant smart contract, for example, this could be a string representation of the top 128 bits of the token ID. + +`classUri` is optional, but will be extremely beneficial for cross-chain interoperability with NFT marketplaces like OpenSea, where [class/collection metadata](https://docs.opensea.io/docs/contract-level-metadata) can be added for better user experience. + +`tokenId` uniquely identifies the NFT within the given class. In the case of an ERC-1155 compliant smart contract, for example, this could be a string representation of the bottom 128 bits of the token ID. + +`tokenUri` refers to an off-chain resource, typically an immutable JSON file containing the NFT's metadata. As tokens are sent across chains using the ICS 21 protocol, they begin to accrue a record of channels for which they have been transferred across. This information is encoded into the `classId` field. From 9a1c45cfc41315efd65a0237e0d0c92e26f6d21e Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Wed, 17 Nov 2021 15:04:02 +0800 Subject: [PATCH 06/18] add history entry --- spec/app/ics-021-nft-transfer/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/app/ics-021-nft-transfer/README.md b/spec/app/ics-021-nft-transfer/README.md index 3f50199b3..356aeef80 100644 --- a/spec/app/ics-021-nft-transfer/README.md +++ b/spec/app/ics-021-nft-transfer/README.md @@ -7,7 +7,7 @@ requires: 25, 26 kind: instantiation author: Christopher Goes , Haifeng Xi created: 2021-11-10 -modified: 2021-11-12 +modified: 2021-11-17 --- > This spec follows the same design principles of [ICS 20](../ics-020-fungible-token-transfer) and copies most of its content therefrom, while replacing `bank` module based asset tracking logic with that of the `nft` module. @@ -367,6 +367,7 @@ Coming soon. ## History Nov 10, 2021 - Initial draft adapted from ICS20 spec +Nov 17, 2021 - Revisions to better accommodate smart contracts ## Copyright From e662494cbb1eecfcc7db809462a292dc3d32d083 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Wed, 17 Nov 2021 21:51:15 +0800 Subject: [PATCH 07/18] rename from ics21 to ics721 --- .../README.md | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) rename spec/app/{ics-021-nft-transfer => ics-721-nft-transfer}/README.md (92%) diff --git a/spec/app/ics-021-nft-transfer/README.md b/spec/app/ics-721-nft-transfer/README.md similarity index 92% rename from spec/app/ics-021-nft-transfer/README.md rename to spec/app/ics-721-nft-transfer/README.md index 356aeef80..8fda63630 100644 --- a/spec/app/ics-021-nft-transfer/README.md +++ b/spec/app/ics-721-nft-transfer/README.md @@ -1,5 +1,5 @@ --- -ics: 21 +ics: 721 title: Non-Fungible Token Transfer stage: draft category: IBC/APP @@ -56,9 +56,9 @@ interface NonFungibleTokenPacketData { `tokenUri` refers to an off-chain resource, typically an immutable JSON file containing the NFT's metadata. -As tokens are sent across chains using the ICS 21 protocol, they begin to accrue a record of channels for which they have been transferred across. This information is encoded into the `classId` field. +As tokens are sent across chains using the ICS 721 protocol, they begin to accrue a record of channels for which they have been transferred across. This information is encoded into the `classId` field. -The ics21 token classes are represented in the form `{ics21Port}/{ics21Channel}/{classId}`, where `ics21Port` and `ics21Channel` are an ics21 port and channel on the current chain for which the token exists. The prefixed port and channel pair indicate which channel the token was previously sent through. If `{classId}` contains `/`, then it must also be in the ics21 form which indicates that this token has a multi-hop record. Note that this requires that the `/` (slash character) is prohibited in non-IBC token `classId`s. +The ics721 token classes are represented in the form `{ics721Port}/{ics721Channel}/{classId}`, where `ics721Port` and `ics721Channel` are an ics721 port and channel on the current chain for which the token exists. The prefixed port and channel pair indicate which channel the token was previously sent through. If `{classId}` contains `/`, then it must also be in the ics721 form which indicates that this token has a multi-hop record. Note that this requires that the `/` (slash character) is prohibited in non-IBC token `classId`s. A sending chain may be acting as a source or sink zone. When a chain is sending tokens across a port and channel which are not equal to the last prefixed port and channel pair, it is acting as a source zone. When tokens are sent from a source zone, the destination port and channel will be prefixed onto the `classId` (once the tokens are received) adding another hop to a tokens record. When a chain is sending tokens across a port and channel which are equal to the last prefixed port and channel pair, it is acting as a sink zone. When tokens are sent from a sink zone, the last prefixed port and channel pair on the `classId` is removed (once the tokens are received), undoing the last hop in the tokens record. A more complete explanation is present in the [ibc-go implementation]() (TBD). @@ -124,7 +124,7 @@ An administrator (with the permissions to create connections & channels on the h Both machines `A` and `B` accept new channels from any module on another machine, if and only if: - The channel being created is unordered. -- The version string is `ics21-1`. +- The version string is `ics721-1`. ```typescript function onChanOpenInit( @@ -137,8 +137,8 @@ function onChanOpenInit( version: string) { // only unordered channels allowed abortTransactionUnless(order === UNORDERED) - // assert that version is "ics21-1" - abortTransactionUnless(version === "ics21-1") + // assert that version is "ics721-1" + abortTransactionUnless(version === "ics721-1") // allocate an escrow address channelEscrowAddresses[channelIdentifier] = newAddress() } @@ -156,9 +156,9 @@ function onChanOpenTry( counterpartyVersion: string) { // only unordered channels allowed abortTransactionUnless(order === UNORDERED) - // assert that version is "ics21-1" - abortTransactionUnless(version === "ics21-1") - abortTransactionUnless(counterpartyVersion === "ics21-1") + // assert that version is "ics721-1" + abortTransactionUnless(version === "ics721-1") + abortTransactionUnless(counterpartyVersion === "ics721-1") // allocate an escrow address channelEscrowAddresses[channelIdentifier] = newAddress() } @@ -170,8 +170,8 @@ function onChanOpenAck( channelIdentifier: Identifier, version: string) { // port has already been validated - // assert that version is "ics21-1" - abortTransactionUnless(version === "ics21-1") + // assert that version is "ics721-1" + abortTransactionUnless(version === "ics721-1") } ``` @@ -352,7 +352,7 @@ Not applicable. ## Forwards Compatibility -This initial standard uses version "ics21-1" in the channel handshake. +This initial standard uses version "ics721-1" in the channel handshake. A future version of this standard could use a different version in the channel handshake, and safely alter the packet data format & packet handler semantics. @@ -366,8 +366,9 @@ Coming soon. ## History -Nov 10, 2021 - Initial draft adapted from ICS20 spec +Nov 10, 2021 - Initial draft adapted from ICS 20 spec Nov 17, 2021 - Revisions to better accommodate smart contracts +Nov 17, 2021 - Renamed from ICS 21 to ICS 721 ## Copyright From f3dbd22f630994c17d63eb7d9e0738f0ce9ae965 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Thu, 18 Nov 2021 12:17:59 +0800 Subject: [PATCH 08/18] allow multiple tokens to be transferred in one packet --- spec/app/ics-721-nft-transfer/README.md | 104 +++++++++++++----------- 1 file changed, 57 insertions(+), 47 deletions(-) diff --git a/spec/app/ics-721-nft-transfer/README.md b/spec/app/ics-721-nft-transfer/README.md index 8fda63630..68ee30505 100644 --- a/spec/app/ics-721-nft-transfer/README.md +++ b/spec/app/ics-721-nft-transfer/README.md @@ -36,14 +36,14 @@ The IBC handler interface & IBC routing module interface are as defined in [ICS ### Data Structures -Only one packet data type is required: `NonFungibleTokenPacketData`, which specifies the class id, class uri, token id, token uri, sending account, and receiving account. +Only one packet data type is required: `NonFungibleTokenPacketData`, which specifies the class id, class uri, token id's, token uri's, sending account, and receiving account. ```typescript interface NonFungibleTokenPacketData { classId: string classUri: string - tokenId: string - tokenUri: string + tokenIds: []string + tokenUris: []string sender: string receiver: string } @@ -52,9 +52,9 @@ interface NonFungibleTokenPacketData { `classUri` is optional, but will be extremely beneficial for cross-chain interoperability with NFT marketplaces like OpenSea, where [class/collection metadata](https://docs.opensea.io/docs/contract-level-metadata) can be added for better user experience. -`tokenId` uniquely identifies the NFT within the given class. In the case of an ERC-1155 compliant smart contract, for example, this could be a string representation of the bottom 128 bits of the token ID. +`tokenIds` uniquely identifies some NFTs within the given class that are being transferred. In the case of an ERC-1155 compliant smart contract, for example, a `tokenId` could be a string representation of the bottom 128 bits of the token ID. -`tokenUri` refers to an off-chain resource, typically an immutable JSON file containing the NFT's metadata. +Each `tokenId` has a corresponding entry in `tokenUris`, which refers to an off-chain resource that is typically an immutable JSON file containing the NFT's metadata. As tokens are sent across chains using the ICS 721 protocol, they begin to accrue a record of channels for which they have been transferred across. This information is encoded into the `classId` field. @@ -214,7 +214,7 @@ In plain English, between chains `A` and `B`: ```typescript function createOutgoingPacket( classId: string, - tokenId: string, + tokenIds: []string, sender: string, receiver: string, source: boolean, @@ -224,23 +224,25 @@ function createOutgoingPacket( sourceChannel: string, timeoutHeight: Height, timeoutTimestamp: uint64) { - // assert that sender is token owner - abortTransactionUnless(sender === nft.getOwner(classId, tokenId)) prefix = "{sourcePort}/{sourceChannel}/" // we are the source if the classId is not prefixed source = classId.slice(0, len(prefix)) !== prefix - if source { - // determine escrow account - escrowAccount = channelEscrowAddresses[sourceChannel] - // escrow source token - nft.Transfer(classId, tokenId, escrowAccount) - } else { - // receiver is source chain, burn voucher - bank.Burn(classId, tokenId) + tokenUris = [] + for (let tokenId in tokenIds) { + // assert that sender is token owner + abortTransactionUnless(sender === nft.getOwner(classId, tokenId)) + if source { + // determine escrow account + escrowAccount = channelEscrowAddresses[sourceChannel] + // escrow source token + nft.Transfer(classId, tokenId, escrowAccount) + } else { + // receiver is source chain, burn voucher + bank.Burn(classId, tokenId) + } + tokenUris.push(nft.getNFT(classId, tokenId).getUri()) } - Class class = nft.getClass(classId) - NFT token = nft.getNFT(classId, tokenId) - NonFungibleTokenPacketData data = NonFungibleTokenPacketData{classId, class.getUri(), tokenId, token.getUri(), sender, receiver} + NonFungibleTokenPacketData data = NonFungibleTokenPacketData{classId, nft.getClass(classId).getUri(), tokenIds, tokenUris, sender, receiver} handler.sendPacket(Packet{timeoutHeight, timeoutTimestamp, destPort, destChannel, sourcePort, sourceChannel, data}, getCapability("port")) } ``` @@ -255,23 +257,29 @@ function onRecvPacket(packet: Packet) { prefix = "{packet.sourcePort}/{packet.sourceChannel}/" // we are the source if the packets were prefixed by the sending chain source = data.classId.slice(0, len(prefix)) === prefix - if source { - // receiver is source chain: unescrow token - // determine escrow account - escrowAccount = channelEscrowAddresses[packet.destChannel] - // assert that escrow account is token owner - abortTransactionUnless(escrowAccount === nft.getOwner(data.classId.slice(len(prefix)), data.tokenId)) - // unescrow token to receiver - err = nft.Transfer(data.classId.slice(len(prefix)), data.tokenId, data.receiver) - if (err !== nil) - ack = NonFungibleTokenPacketAcknowledgement{false, "transfer nft failed"} - } else { - prefix = "{packet.destPort}/{packet.destChannel}/" - prefixedClassId = prefix + data.classId - // sender was source, mint voucher to receiver - err = nft.Mint(prefixedClassId, data.tokenId, data.receiver) - if (err !== nil) - ack = NonFungibleTokenPacketAcknowledgement{false, "mint nft failed"} + for (var i in data.tokenIds) { + if source { + // receiver is source chain: unescrow token + // determine escrow account + escrowAccount = channelEscrowAddresses[packet.destChannel] + // assert that escrow account is token owner + abortTransactionUnless(escrowAccount === nft.getOwner(data.classId.slice(len(prefix)), data.tokenIds[i])) + // unescrow token to receiver + err = nft.Transfer(data.classId.slice(len(prefix)), data.tokenIds[i], data.receiver) + if (err !== nil) { + ack = NonFungibleTokenPacketAcknowledgement{false, "transfer nft(" + data.classId + ", " + data.tokenIds[i] + ") failed"} + break + } + } else { + prefix = "{packet.destPort}/{packet.destChannel}/" + prefixedClassId = prefix + data.classId + // sender was source, mint voucher to receiver + err = nft.Mint(prefixedClassId, data.classUri, data.tokenIds[i], data.tokenUris[i], data.receiver) + if (err !== nil) { + ack = NonFungibleTokenPacketAcknowledgement{false, "mint nft(" + data.classId + ", " + data.tokenIds[i] + ") failed"} + break + } + } } return ack } @@ -304,17 +312,19 @@ function onTimeoutPacket(packet: Packet) { function refundToken(packet: Packet) { NonFungibleTokenPacketData data = packet.data prefix = "{packet.sourcePort}/{packet.sourceChannel}/" - // we are the source if the classId is not prefixed - source = data.classId.slice(0, len(prefix)) !== prefix - if source { - // sender was source chain, unescrow tokens back to sender - escrowAccount = channelEscrowAddresses[packet.srcChannel] - // assert that escrow account is token owner - abortTransactionUnless(escrowAccount === nft.getOwner(data.classId, data.tokenId)) - nft.Transfer(data.classId, data.tokenId, data.sender) - } else { - // receiver was source chain, mint voucher back to sender - bank.Mint(data.classId, data.tokenId, data.sender) + for (let tokenId in data.tokenIds) { + // we are the source if the classId is not prefixed + source = data.classId.slice(0, len(prefix)) !== prefix + if source { + // sender was source chain, unescrow tokens back to sender + escrowAccount = channelEscrowAddresses[packet.srcChannel] + // assert that escrow account is token owner + abortTransactionUnless(escrowAccount === nft.getOwner(data.classId, tokenId)) + nft.Transfer(data.classId, tokenId, data.sender) + } else { + // receiver was source chain, mint voucher back to sender + bank.Mint(data.classId, tokenId, data.sender) + } } } ``` From bbe12222728789fece5d9e19a4d99d952a38e6ee Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Thu, 18 Nov 2021 15:16:41 +0800 Subject: [PATCH 09/18] add history entry --- spec/app/ics-721-nft-transfer/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/app/ics-721-nft-transfer/README.md b/spec/app/ics-721-nft-transfer/README.md index 68ee30505..ecd549444 100644 --- a/spec/app/ics-721-nft-transfer/README.md +++ b/spec/app/ics-721-nft-transfer/README.md @@ -7,7 +7,7 @@ requires: 25, 26 kind: instantiation author: Christopher Goes , Haifeng Xi created: 2021-11-10 -modified: 2021-11-17 +modified: 2021-11-18 --- > This spec follows the same design principles of [ICS 20](../ics-020-fungible-token-transfer) and copies most of its content therefrom, while replacing `bank` module based asset tracking logic with that of the `nft` module. @@ -379,6 +379,7 @@ Coming soon. Nov 10, 2021 - Initial draft adapted from ICS 20 spec Nov 17, 2021 - Revisions to better accommodate smart contracts Nov 17, 2021 - Renamed from ICS 21 to ICS 721 +Nov 18, 2021 - Revisions to allow for multiple tokens in one packet ## Copyright From bf28e9868a404bb60d61bd213a50504f8bafc897 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Thu, 10 Feb 2022 19:16:07 +0800 Subject: [PATCH 10/18] made updates according to AdityaSripal's comments and suggestions --- spec/app/ics-721-nft-transfer/README.md | 43 ++++++++++++------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/spec/app/ics-721-nft-transfer/README.md b/spec/app/ics-721-nft-transfer/README.md index ecd549444..757cdbe93 100644 --- a/spec/app/ics-721-nft-transfer/README.md +++ b/spec/app/ics-721-nft-transfer/README.md @@ -58,7 +58,7 @@ Each `tokenId` has a corresponding entry in `tokenUris`, which refers to an off- As tokens are sent across chains using the ICS 721 protocol, they begin to accrue a record of channels for which they have been transferred across. This information is encoded into the `classId` field. -The ics721 token classes are represented in the form `{ics721Port}/{ics721Channel}/{classId}`, where `ics721Port` and `ics721Channel` are an ics721 port and channel on the current chain for which the token exists. The prefixed port and channel pair indicate which channel the token was previously sent through. If `{classId}` contains `/`, then it must also be in the ics721 form which indicates that this token has a multi-hop record. Note that this requires that the `/` (slash character) is prohibited in non-IBC token `classId`s. +The ics721 token classes are represented in the form `{ics721Port}/{ics721Channel}/{classId}`, where `ics721Port` and `ics721Channel` identifiy the channel on the current chain from which the token arrived. The prefixed port and channel pair indicate which channel the token was previously sent through. If `{classId}` contains `/`, then it must also be in the ics721 form which indicates that this token has a multi-hop record. Note that this requires that the `/` (slash character) is prohibited in non-IBC token `classId`s. A sending chain may be acting as a source or sink zone. When a chain is sending tokens across a port and channel which are not equal to the last prefixed port and channel pair, it is acting as a source zone. When tokens are sent from a source zone, the destination port and channel will be prefixed onto the `classId` (once the tokens are received) adding another hop to a tokens record. When a chain is sending tokens across a port and channel which are equal to the last prefixed port and channel pair, it is acting as a sink zone. When tokens are sent from a sink zone, the last prefixed port and channel pair on the `classId` is removed (once the tokens are received), undoing the last hop in the tokens record. A more complete explanation is present in the [ibc-go implementation]() (TBD). @@ -77,9 +77,9 @@ interface NonFungibleTokenPacketError { } ``` -Note that both the `NonFungibleTokenPacketData` as well as `NonFungibleTokenPacketAcknowledgement` must be JSON-encoded (not Protobuf encoded) when they serialized into packet data. +Note that both the `NonFungibleTokenPacketData` as well as `NonFungibleTokenPacketAcknowledgement` must be JSON-encoded (not Protobuf encoded) when serialized into packet data. -The non-fungible token transfer bridge module tracks escrow addresses associated with particular channels in state. Fields of the `ModuleState` are assumed to be in scope. +The non-fungible token transfer bridge module maintains a separate escrow address for each NFT channel. ```typescript interface ModuleState { @@ -93,7 +93,7 @@ The sub-protocols described herein should be implemented in a "non-fungible toke #### Port & channel setup -The `setup` function must be called exactly once when the module is created (perhaps when the blockchain itself is initialised) to bind to the appropriate port and create an escrow address (owned by the module). +The `setup` function must be called exactly once when the module is created (perhaps when the blockchain itself is initialised) to bind to the appropriate port (owned by the module). ```typescript function setup() { @@ -115,7 +115,7 @@ function setup() { Once the `setup` function has been called, channels can be created through the IBC routing module between instances of the non-fungible token transfer module on separate chains. -An administrator (with the permissions to create connections & channels on the host state machine) is responsible for setting up connections to other state machines & creating channels to other instances of this module (or another module supporting this interface) on other chains. This specification defines packet handling semantics only, and defines them in such a fashion that the module itself doesn't need to worry about what connections or channels might or might not exist at any point in time. +This specification defines packet handling semantics only, and defines them in such a fashion that the module itself doesn't need to worry about what connections or channels might or might not exist at any point in time. #### Routing module callbacks @@ -139,8 +139,6 @@ function onChanOpenInit( abortTransactionUnless(order === UNORDERED) // assert that version is "ics721-1" abortTransactionUnless(version === "ics721-1") - // allocate an escrow address - channelEscrowAddresses[channelIdentifier] = newAddress() } ``` @@ -159,8 +157,6 @@ function onChanOpenTry( // assert that version is "ics721-1" abortTransactionUnless(version === "ics721-1") abortTransactionUnless(counterpartyVersion === "ics721-1") - // allocate an escrow address - channelEscrowAddresses[channelIdentifier] = newAddress() } ``` @@ -172,6 +168,8 @@ function onChanOpenAck( // port has already been validated // assert that version is "ics721-1" abortTransactionUnless(version === "ics721-1") + // allocate an escrow address + channelEscrowAddresses[channelIdentifier] = newAddress() } ``` @@ -180,6 +178,8 @@ function onChanOpenConfirm( portIdentifier: Identifier, channelIdentifier: Identifier) { // accept channel confirmations, port has already been validated, version has already been validated + // allocate an escrow address + channelEscrowAddresses[channelIdentifier] = newAddress() } ``` @@ -201,13 +201,12 @@ function onChanCloseConfirm( ##### Packet relay -In plain English, between chains `A` and `B`: +In plain English: -- When acting as the source zone, the bridge module escrows an existing local non-fungible token on the sending chain and mints a corresponding voucher on the receiving chain. -- When acting as the sink zone, the bridge module burns the local voucher on the sending chain and unescrows the local non-fungible token on the receiving chain. -- When a packet times-out, local non-fungible tokens are unescrowed back to the sender or vouchers minted back to the sender appropriately. -- Acknowledgement data is used to handle failures, such as invalid destination accounts. Returning - an acknowledgement of failure is preferable to aborting the transaction since it more easily enables the sending chain to take appropriate action based on the nature of the failure. +- When a non-fungible token is sent away from its source, the bridge module escrows the token on the sending chain and mints a corresponding voucher on the receiving chain. +- When a non-fungible token is sent back toward its source, the bridge module burns the token on the sending chain and unescrows the corresponding locked token on the receiving chain. +- When a packet times out, tokens represented in the packet are either unescrowed or minted back to the sender appropriately -- depending on whether the tokens are being moved away from or back to their source. +- Acknowledgement data is used to handle failures, such as invalid destination accounts. Returning an acknowledgement of failure is preferable to aborting the transaction since it more easily enables the sending chain to take appropriate action based on the nature of the failure. `createOutgoingPacket` must be called by a transaction handler in the module which performs appropriate signature checks, specific to the account owner on the host state machine. @@ -225,7 +224,7 @@ function createOutgoingPacket( timeoutHeight: Height, timeoutTimestamp: uint64) { prefix = "{sourcePort}/{sourceChannel}/" - // we are the source if the classId is not prefixed + // we are the source if the classId is not prefixed with the sourcePort and sourceChannel source = classId.slice(0, len(prefix)) !== prefix tokenUris = [] for (let tokenId in tokenIds) { @@ -238,7 +237,7 @@ function createOutgoingPacket( nft.Transfer(classId, tokenId, escrowAccount) } else { // receiver is source chain, burn voucher - bank.Burn(classId, tokenId) + nft.Burn(classId, tokenId) } tokenUris.push(nft.getNFT(classId, tokenId).getUri()) } @@ -255,15 +254,13 @@ function onRecvPacket(packet: Packet) { // construct default acknowledgement of success NonFungibleTokenPacketAcknowledgement ack = NonFungibleTokenPacketAcknowledgement{true, null} prefix = "{packet.sourcePort}/{packet.sourceChannel}/" - // we are the source if the packets were prefixed by the sending chain + // we are the source if the classId is prefixed with the packet's sourcePort and sourceChannel source = data.classId.slice(0, len(prefix)) === prefix for (var i in data.tokenIds) { if source { // receiver is source chain: unescrow token // determine escrow account escrowAccount = channelEscrowAddresses[packet.destChannel] - // assert that escrow account is token owner - abortTransactionUnless(escrowAccount === nft.getOwner(data.classId.slice(len(prefix)), data.tokenIds[i])) // unescrow token to receiver err = nft.Transfer(data.classId.slice(len(prefix)), data.tokenIds[i], data.receiver) if (err !== nil) { @@ -297,7 +294,7 @@ function onAcknowledgePacket( } ``` -`onTimeoutPacket` is called by the routing module when a packet sent by this module has timed-out (such that it will not be received on the destination chain). +`onTimeoutPacket` is called by the routing module when a packet sent by this module has timed out (such that it will not be received on the destination chain). ```typescript function onTimeoutPacket(packet: Packet) { @@ -313,7 +310,7 @@ function refundToken(packet: Packet) { NonFungibleTokenPacketData data = packet.data prefix = "{packet.sourcePort}/{packet.sourceChannel}/" for (let tokenId in data.tokenIds) { - // we are the source if the classId is not prefixed + // we are the source if the classId is not prefixed with the packet's sourcePort and sourceChannel source = data.classId.slice(0, len(prefix)) !== prefix if source { // sender was source chain, unescrow tokens back to sender @@ -323,7 +320,7 @@ function refundToken(packet: Packet) { nft.Transfer(data.classId, tokenId, data.sender) } else { // receiver was source chain, mint voucher back to sender - bank.Mint(data.classId, tokenId, data.sender) + nft.Mint(data.classId, tokenId, data.sender) } } } From 72141bb7bcbb3e409e7891b66e37757b1a361953 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Thu, 10 Feb 2022 22:04:08 +0800 Subject: [PATCH 11/18] made changes according to feedbacks from Chris --- spec/app/ics-721-nft-transfer/README.md | 34 +++++++++++++------------ 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/spec/app/ics-721-nft-transfer/README.md b/spec/app/ics-721-nft-transfer/README.md index 757cdbe93..31ce821bb 100644 --- a/spec/app/ics-721-nft-transfer/README.md +++ b/spec/app/ics-721-nft-transfer/README.md @@ -5,20 +5,20 @@ stage: draft category: IBC/APP requires: 25, 26 kind: instantiation -author: Christopher Goes , Haifeng Xi +author: Haifeng Xi created: 2021-11-10 -modified: 2021-11-18 +modified: 2022-02-10 --- -> This spec follows the same design principles of [ICS 20](../ics-020-fungible-token-transfer) and copies most of its content therefrom, while replacing `bank` module based asset tracking logic with that of the `nft` module. +> This standard document follows the same design principles of [ICS 20](../ics-020-fungible-token-transfer) and inherits most of its content therefrom, while replacing `bank` module based asset tracking logic with that of the `nft` module. ## Synopsis -This standard document specifies packet data structure, state machine handling logic, and encoding details for the transfer of non-fungible tokens over an IBC channel between two modules on separate chains. The state machine logic presented allows for safe multi-chain `classId` handling with permissionless channel opening. This logic constitutes a "non-fungible token transfer bridge module", interfacing between the IBC routing module and an existing asset tracking module on the host state machine, which could be either a Cosmos-style "native" module or a smart contract running in a virtual machine. +This standard document specifies packet data structure, state machine handling logic, and encoding details for the transfer of non-fungible tokens over an IBC channel between two modules on separate chains. The state machine logic presented allows for safe multi-chain `classId` handling with permissionless channel opening. This logic constitutes a _non-fungible token transfer bridge module_, interfacing between the IBC routing module and an existing asset tracking module on the host state machine, which could be either a Cosmos-style native module or a smart contract running in a virtual machine. ### Motivation -Users of a set of chains connected over the IBC protocol might wish to utilise an asset issued on one chain on another chain, perhaps to make use of additional features such as exchange or privacy protection, while retaining uniqueness with the original asset on the issuing chain. This application-layer standard describes a protocol for transferring non-fungible tokens between chains connected with IBC which preserves asset uniqueness, preserves asset ownership, limits the impact of Byzantine faults, and requires no additional permissioning. +Users of a set of chains connected over the IBC protocol might wish to utilize a non-fungible token on a chain other than the chain where the token was originally issued -- perhaps to make use of additional features such as exchange, royalty payment or privacy protection. This application-layer standard describes a protocol for transferring non-fungible tokens between chains connected with IBC which preserves asset non-fungibility, preserves asset ownership, limits the impact of Byzantine faults, and requires no additional permissioning. ### Definitions @@ -26,11 +26,10 @@ The IBC handler interface & IBC routing module interface are as defined in [ICS ### Desired Properties -- Preservation of uniqueness (two-way peg). -- Preservation of total supply (maintained on a single source chain & module). +- Preservation of non-fungibility and uniqueness (i.e., only one instance of the token is *live* across all the IBC-connected blockchains). - Permissionless token transfers, no need to whitelist connections, modules, or `classId`s. - Symmetric (all chains implement the same logic, no in-protocol differentiation of hubs & zones). -- Fault containment: prevents Byzantine-inflation of tokens originating on chain `A`, as a result of chain `B`'s Byzantine behaviour (though any users who sent tokens to chain `B` may be at risk). +- Fault containment: prevents Byzantine-creation of tokens originating on chain `A`, as a result of chain `B`'s Byzantine behavior. ## Technical Specification @@ -336,12 +335,10 @@ function onTimeoutPacketClose(packet: Packet) { ##### Correctness -This implementation preserves both uniqueness & supply. +This implementation preserves token uniqueness. Uniqueness: If tokens have been sent to the counterparty chain, they can be redeemed back in the same `classId` & `tokenId` on the source chain. -Supply: Redefine supply as unlocked tokens. All send-recv pairs for any given token class sum to net zero. Source chain can change supply. - ##### Multi-chain notes This specification does not directly handle the "diamond problem", where a user sends a token originating on chain A to chain B, then to chain D, and wants to return it through D -> C -> A — since the supply is tracked as owned by chain B (and the `classId` will be "{portOnD}/{channelOnD}/{portOnB}/{channelOnB}/classId"), chain C cannot serve as the intermediary. It is not yet clear whether that case should be dealt with in-protocol or not — it may be fine to just require the original path of redemption (and if there is frequent liquidity and some surplus on both paths the diamond path will work most of the time). Complexities arising from long redemption paths may lead to the emergence of central chains in the network topology. @@ -353,6 +350,9 @@ In order to track all of the tokens moving around the network of chains in vario - Each chain, locally, could elect to keep a lookup table to use short, user-friendly local `classId`s in state which are translated to and from the longer `classId`s when sending and receiving packets. - Additional restrictions may be imposed on which other machines may be connected to & which channels may be established. +## Further Discussion +Extended and complex use cases such as royalties, marketplaces or permissioned transfers can be supported on top of this specification. Solutions could be modules, hooks, [IBC middleware](../ics-030-middleware) and so on. Designing a guideline for this is out of the scope. + ## Backwards Compatibility Not applicable. @@ -372,11 +372,13 @@ Coming soon. Coming soon. ## History - -Nov 10, 2021 - Initial draft adapted from ICS 20 spec -Nov 17, 2021 - Revisions to better accommodate smart contracts -Nov 17, 2021 - Renamed from ICS 21 to ICS 721 -Nov 18, 2021 - Revisions to allow for multiple tokens in one packet +| Date | Description | +| ------------- | ---------------------------------------------------- | +| Nov 10, 2021 | Initial draft adapted from ICS 20 spec | +| Nov 17, 2021 | Revisions to better accommodate smart contracts | +| Nov 17, 2021 | Renamed from ICS 21 to ICS 721 | +| Nov 18, 2021 | Revisions to allow for multiple tokens in one packet | +| Feb 10, 2022 | Revisions to incorporate feedbacks from IBC team | ## Copyright From d6ef093969b37403299c163f4d48faeb7655d351 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Mon, 14 Feb 2022 11:24:21 +0800 Subject: [PATCH 12/18] resolve comments from Aditya --- spec/app/ics-721-nft-transfer/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/app/ics-721-nft-transfer/README.md b/spec/app/ics-721-nft-transfer/README.md index 31ce821bb..8990c0cd9 100644 --- a/spec/app/ics-721-nft-transfer/README.md +++ b/spec/app/ics-721-nft-transfer/README.md @@ -7,7 +7,7 @@ requires: 25, 26 kind: instantiation author: Haifeng Xi created: 2021-11-10 -modified: 2022-02-10 +modified: 2022-02-14 --- > This standard document follows the same design principles of [ICS 20](../ics-020-fungible-token-transfer) and inherits most of its content therefrom, while replacing `bank` module based asset tracking logic with that of the `nft` module. @@ -35,7 +35,7 @@ The IBC handler interface & IBC routing module interface are as defined in [ICS ### Data Structures -Only one packet data type is required: `NonFungibleTokenPacketData`, which specifies the class id, class uri, token id's, token uri's, sending account, and receiving account. +Only one packet data type is required: `NonFungibleTokenPacketData`, which specifies the class id, class uri, token id's, token uri's, sender address, and receiver address. ```typescript interface NonFungibleTokenPacketData { @@ -57,7 +57,7 @@ Each `tokenId` has a corresponding entry in `tokenUris`, which refers to an off- As tokens are sent across chains using the ICS 721 protocol, they begin to accrue a record of channels for which they have been transferred across. This information is encoded into the `classId` field. -The ics721 token classes are represented in the form `{ics721Port}/{ics721Channel}/{classId}`, where `ics721Port` and `ics721Channel` identifiy the channel on the current chain from which the token arrived. The prefixed port and channel pair indicate which channel the token was previously sent through. If `{classId}` contains `/`, then it must also be in the ics721 form which indicates that this token has a multi-hop record. Note that this requires that the `/` (slash character) is prohibited in non-IBC token `classId`s. +The ics721 token classes are represented in the form `{ics721Port}/{ics721Channel}/{classId}`, where `ics721Port` and `ics721Channel` identify the channel on the current chain from which the token arrived. The prefixed port and channel pair indicate which channel the token was previously sent through. If `{classId}` contains `/`, then it must also be in the ics721 form which indicates that this token has a multi-hop record. Note that this requires that the `/` (slash character) is prohibited in non-IBC token `classId`s. A sending chain may be acting as a source or sink zone. When a chain is sending tokens across a port and channel which are not equal to the last prefixed port and channel pair, it is acting as a source zone. When tokens are sent from a source zone, the destination port and channel will be prefixed onto the `classId` (once the tokens are received) adding another hop to a tokens record. When a chain is sending tokens across a port and channel which are equal to the last prefixed port and channel pair, it is acting as a sink zone. When tokens are sent from a sink zone, the last prefixed port and channel pair on the `classId` is removed (once the tokens are received), undoing the last hop in the tokens record. A more complete explanation is present in the [ibc-go implementation]() (TBD). @@ -186,7 +186,8 @@ function onChanOpenConfirm( function onChanCloseInit( portIdentifier: Identifier, channelIdentifier: Identifier) { - // no action necessary + // abort and return error to prevent channel closing by user + abortTransaction } ``` @@ -200,11 +201,9 @@ function onChanCloseConfirm( ##### Packet relay -In plain English: - - When a non-fungible token is sent away from its source, the bridge module escrows the token on the sending chain and mints a corresponding voucher on the receiving chain. - When a non-fungible token is sent back toward its source, the bridge module burns the token on the sending chain and unescrows the corresponding locked token on the receiving chain. -- When a packet times out, tokens represented in the packet are either unescrowed or minted back to the sender appropriately -- depending on whether the tokens are being moved away from or back to their source. +- When a packet times out, tokens represented in the packet are either unescrowed or minted back to the sender appropriately -- depending on whether the tokens are being moved away from or back toward their source. - Acknowledgement data is used to handle failures, such as invalid destination accounts. Returning an acknowledgement of failure is preferable to aborting the transaction since it more easily enables the sending chain to take appropriate action based on the nature of the failure. `createOutgoingPacket` must be called by a transaction handler in the module which performs appropriate signature checks, specific to the account owner on the host state machine. @@ -379,6 +378,7 @@ Coming soon. | Nov 17, 2021 | Renamed from ICS 21 to ICS 721 | | Nov 18, 2021 | Revisions to allow for multiple tokens in one packet | | Feb 10, 2022 | Revisions to incorporate feedbacks from IBC team | +| Feb 14, 2022 | Revisions to resolve comments from IBC team | ## Copyright From d9bc3c83a5139b7f632564886dbc0d7300908310 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Thu, 3 Mar 2022 19:22:10 +0800 Subject: [PATCH 13/18] updated TRY callback to be consistent with PR#629 --- spec/app/ics-721-nft-transfer/README.md | 33 ++++++++++--------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/spec/app/ics-721-nft-transfer/README.md b/spec/app/ics-721-nft-transfer/README.md index 8990c0cd9..209a0ddbf 100644 --- a/spec/app/ics-721-nft-transfer/README.md +++ b/spec/app/ics-721-nft-transfer/README.md @@ -7,7 +7,7 @@ requires: 25, 26 kind: instantiation author: Haifeng Xi created: 2021-11-10 -modified: 2022-02-14 +modified: 2022-02-17 --- > This standard document follows the same design principles of [ICS 20](../ics-020-fungible-token-transfer) and inherits most of its content therefrom, while replacing `bank` module based asset tracking logic with that of the `nft` module. @@ -47,19 +47,19 @@ interface NonFungibleTokenPacketData { receiver: string } ``` -`classId` uniquely identifies the class/collection to which this NFT belongs in the originating host environment. In the case of an ERC-1155 compliant smart contract, for example, this could be a string representation of the top 128 bits of the token ID. +`classId` uniquely identifies the class/collection which the tokens being transferred belong to in the sending chain. In the case of an ERC-1155 compliant smart contract, for example, this could be a string representation of the top 128 bits of the token ID. `classUri` is optional, but will be extremely beneficial for cross-chain interoperability with NFT marketplaces like OpenSea, where [class/collection metadata](https://docs.opensea.io/docs/contract-level-metadata) can be added for better user experience. -`tokenIds` uniquely identifies some NFTs within the given class that are being transferred. In the case of an ERC-1155 compliant smart contract, for example, a `tokenId` could be a string representation of the bottom 128 bits of the token ID. +`tokenIds` uniquely identifies some tokens of the given class that are being transferred. In the case of an ERC-1155 compliant smart contract, for example, a `tokenId` could be a string representation of the bottom 128 bits of the token ID. -Each `tokenId` has a corresponding entry in `tokenUris`, which refers to an off-chain resource that is typically an immutable JSON file containing the NFT's metadata. +Each `tokenId` has a corresponding entry in `tokenUris`, which refers to an off-chain resource that is typically an immutable JSON file containing the token's metadata. -As tokens are sent across chains using the ICS 721 protocol, they begin to accrue a record of channels for which they have been transferred across. This information is encoded into the `classId` field. +As tokens are sent across chains using the ICS-721 protocol, they begin to accrue a record of channels across which they have been transferred. This record information is encoded into the `classId` field. -The ics721 token classes are represented in the form `{ics721Port}/{ics721Channel}/{classId}`, where `ics721Port` and `ics721Channel` identify the channel on the current chain from which the token arrived. The prefixed port and channel pair indicate which channel the token was previously sent through. If `{classId}` contains `/`, then it must also be in the ics721 form which indicates that this token has a multi-hop record. Note that this requires that the `/` (slash character) is prohibited in non-IBC token `classId`s. +An ICS-721 token class is represented in the form `{ics721Port}/{ics721Channel}/{classId}`, where `ics721Port` and `ics721Channel` identify the channel on the current chain from which the tokens arrived. The prefixed port and channel pair indicate which channel the tokens were previously sent through. If `{classId}` contains `/`, then it must also be in the ICS-721 form which indicates that the tokens have a multi-hop record. Note that this requires that the `/` (slash character) is prohibited in non-IBC token `classId`s. -A sending chain may be acting as a source or sink zone. When a chain is sending tokens across a port and channel which are not equal to the last prefixed port and channel pair, it is acting as a source zone. When tokens are sent from a source zone, the destination port and channel will be prefixed onto the `classId` (once the tokens are received) adding another hop to a tokens record. When a chain is sending tokens across a port and channel which are equal to the last prefixed port and channel pair, it is acting as a sink zone. When tokens are sent from a sink zone, the last prefixed port and channel pair on the `classId` is removed (once the tokens are received), undoing the last hop in the tokens record. A more complete explanation is present in the [ibc-go implementation]() (TBD). +A sending chain may be acting as a source or sink zone. When a chain is sending tokens across a port and channel which are not equal to the last prefixed port and channel pair, it is acting as a source zone. When tokens are sent from a source zone, the destination port and channel will be prefixed onto the `classId` (once the tokens are received) adding another hop to the tokens record. When a chain is sending tokens across a port and channel which are equal to the last prefixed port and channel pair, it is acting as a sink zone. When tokens are sent from a sink zone, the last prefixed port and channel pair on the `classId` is removed (once the tokens are received), undoing the last hop in the tokens record. The acknowledgement data type describes whether the transfer succeeded or failed, and the reason for failure (if any). @@ -130,9 +130,7 @@ function onChanOpenInit( order: ChannelOrder, connectionHops: [Identifier], portIdentifier: Identifier, - channelIdentifier: Identifier, counterpartyPortIdentifier: Identifier, - counterpartyChannelIdentifier: Identifier, version: string) { // only unordered channels allowed abortTransactionUnless(order === UNORDERED) @@ -163,10 +161,11 @@ function onChanOpenTry( function onChanOpenAck( portIdentifier: Identifier, channelIdentifier: Identifier, - version: string) { + counterpartyVersion: string, + counterpartyChannelIdentifier: string) { // port has already been validated // assert that version is "ics721-1" - abortTransactionUnless(version === "ics721-1") + abortTransactionUnless(counterpartyVersion === "ics721-1") // allocate an escrow address channelEscrowAddresses[channelIdentifier] = newAddress() } @@ -187,7 +186,7 @@ function onChanCloseInit( portIdentifier: Identifier, channelIdentifier: Identifier) { // abort and return error to prevent channel closing by user - abortTransaction + abortTransactionUnless(FALSE) } ``` @@ -257,12 +256,9 @@ function onRecvPacket(packet: Packet) { for (var i in data.tokenIds) { if source { // receiver is source chain: unescrow token - // determine escrow account - escrowAccount = channelEscrowAddresses[packet.destChannel] - // unescrow token to receiver err = nft.Transfer(data.classId.slice(len(prefix)), data.tokenIds[i], data.receiver) if (err !== nil) { - ack = NonFungibleTokenPacketAcknowledgement{false, "transfer nft(" + data.classId + ", " + data.tokenIds[i] + ") failed"} + ack = NonFungibleTokenPacketAcknowledgement{false, err.Error()} break } } else { @@ -271,7 +267,7 @@ function onRecvPacket(packet: Packet) { // sender was source, mint voucher to receiver err = nft.Mint(prefixedClassId, data.classUri, data.tokenIds[i], data.tokenUris[i], data.receiver) if (err !== nil) { - ack = NonFungibleTokenPacketAcknowledgement{false, "mint nft(" + data.classId + ", " + data.tokenIds[i] + ") failed"} + ack = NonFungibleTokenPacketAcknowledgement{false, err.Error()} break } } @@ -312,9 +308,6 @@ function refundToken(packet: Packet) { source = data.classId.slice(0, len(prefix)) !== prefix if source { // sender was source chain, unescrow tokens back to sender - escrowAccount = channelEscrowAddresses[packet.srcChannel] - // assert that escrow account is token owner - abortTransactionUnless(escrowAccount === nft.getOwner(data.classId, tokenId)) nft.Transfer(data.classId, tokenId, data.sender) } else { // receiver was source chain, mint voucher back to sender From 194c63b05c84f5eef91e9357944d98048d829abc Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Sat, 5 Mar 2022 20:29:38 +0800 Subject: [PATCH 14/18] improved history section --- spec/app/ics-721-nft-transfer/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/app/ics-721-nft-transfer/README.md b/spec/app/ics-721-nft-transfer/README.md index 209a0ddbf..e6200a59b 100644 --- a/spec/app/ics-721-nft-transfer/README.md +++ b/spec/app/ics-721-nft-transfer/README.md @@ -7,7 +7,7 @@ requires: 25, 26 kind: instantiation author: Haifeng Xi created: 2021-11-10 -modified: 2022-02-17 +modified: 2022-03-03 --- > This standard document follows the same design principles of [ICS 20](../ics-020-fungible-token-transfer) and inherits most of its content therefrom, while replacing `bank` module based asset tracking logic with that of the `nft` module. @@ -366,12 +366,12 @@ Coming soon. ## History | Date | Description | | ------------- | ---------------------------------------------------- | -| Nov 10, 2021 | Initial draft adapted from ICS 20 spec | -| Nov 17, 2021 | Revisions to better accommodate smart contracts | +| Nov 10, 2021 | Initial draft - adapted from ICS 20 spec | +| Nov 17, 2021 | Revised to better accommodate smart contracts | | Nov 17, 2021 | Renamed from ICS 21 to ICS 721 | -| Nov 18, 2021 | Revisions to allow for multiple tokens in one packet | -| Feb 10, 2022 | Revisions to incorporate feedbacks from IBC team | -| Feb 14, 2022 | Revisions to resolve comments from IBC team | +| Nov 18, 2021 | Revised to allow for multiple tokens in one packet | +| Feb 10, 2022 | Revised to incorporate feedbacks from IBC team | +| Mar 03, 2022 | Revised to make TRY callback consistent with PR#629 | ## Copyright From a62c65a543fd78959cfbc0c32780ce03ed4baf37 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Fri, 11 Mar 2022 14:59:57 +0800 Subject: [PATCH 15/18] added example to illustrate the prefix concept --- spec/app/ics-721-nft-transfer/README.md | 39 ++++++++++++++++++------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/spec/app/ics-721-nft-transfer/README.md b/spec/app/ics-721-nft-transfer/README.md index e6200a59b..447e03dce 100644 --- a/spec/app/ics-721-nft-transfer/README.md +++ b/spec/app/ics-721-nft-transfer/README.md @@ -7,7 +7,7 @@ requires: 25, 26 kind: instantiation author: Haifeng Xi created: 2021-11-10 -modified: 2022-03-03 +modified: 2022-03-11 --- > This standard document follows the same design principles of [ICS 20](../ics-020-fungible-token-transfer) and inherits most of its content therefrom, while replacing `bank` module based asset tracking logic with that of the `nft` module. @@ -26,7 +26,7 @@ The IBC handler interface & IBC routing module interface are as defined in [ICS ### Desired Properties -- Preservation of non-fungibility and uniqueness (i.e., only one instance of the token is *live* across all the IBC-connected blockchains). +- Preservation of non-fungibility (i.e., only one instance of any token is *live* across all the IBC-connected blockchains). - Permissionless token transfers, no need to whitelist connections, modules, or `classId`s. - Symmetric (all chains implement the same logic, no in-protocol differentiation of hubs & zones). - Fault containment: prevents Byzantine-creation of tokens originating on chain `A`, as a result of chain `B`'s Byzantine behavior. @@ -61,6 +61,19 @@ An ICS-721 token class is represented in the form `{ics721Port}/{ics721Channel}/ A sending chain may be acting as a source or sink zone. When a chain is sending tokens across a port and channel which are not equal to the last prefixed port and channel pair, it is acting as a source zone. When tokens are sent from a source zone, the destination port and channel will be prefixed onto the `classId` (once the tokens are received) adding another hop to the tokens record. When a chain is sending tokens across a port and channel which are equal to the last prefixed port and channel pair, it is acting as a sink zone. When tokens are sent from a sink zone, the last prefixed port and channel pair on the `classId` is removed (once the tokens are received), undoing the last hop in the tokens record. +Each send to any chain other than the one from which the token was previously received is a movement forward 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 `classId`. In these instances the sender chain is acting as the *source zone*. When the token is sent back to the chain it was previously received from, the prefix is removed. This is a backward movement in the token's timeline and the sender chain is acting as the *sink zone*. + +For example, assume these steps of transfer occur: + +A(p1,c1) -> B(p2,c2) -> C(p3,c3) -> A(p4,c4) -> C(p3,c3) -> B(p2,c2) -> A(p1,c1) + +1. A -> B : A is source zone. `classId` in B: 'p2/c2/nftClass' +2. B -> C : B is source zone. `classId` in C: 'p3/c3/p2/c2/nftClass' +3. C -> A : C is source zone. `classId` in A: 'p4/c4/p3/c3/p2/c2/nftClass' +4. A -> C : A is sink zone. `classId` in C: 'p3/c3/p2/c2/nftClass' +5. C -> B : C is sink zone. `classId` in B: 'p2/c2/nftClass' +6. B -> A : B is sink zone. `classId` in A: 'nftClass' + The acknowledgement data type describes whether the transfer succeeded or failed, and the reason for failure (if any). ```typescript @@ -90,6 +103,8 @@ interface ModuleState { The sub-protocols described herein should be implemented in a "non-fungible token transfer bridge" module with access to the NFT asset tracking module and the IBC routing module. +The `x/nft` module specified in [ADR-043](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-043-nft-module.md) is one such example of the NFT asset tracking module, where the ownership of each NFT should be properly tracked. If an NFT is transferred to a recipient by its current owner, the module is expected to record the ownership change by updating the token's new owner to the recipient. + #### Port & channel setup The `setup` function must be called exactly once when the module is created (perhaps when the blockchain itself is initialised) to bind to the appropriate port (owned by the module). @@ -221,7 +236,7 @@ function createOutgoingPacket( timeoutHeight: Height, timeoutTimestamp: uint64) { prefix = "{sourcePort}/{sourceChannel}/" - // we are the source if the classId is not prefixed with the sourcePort and sourceChannel + // we are source chain if classId is not prefixed with sourcePort and sourceChannel source = classId.slice(0, len(prefix)) !== prefix tokenUris = [] for (let tokenId in tokenIds) { @@ -230,10 +245,10 @@ function createOutgoingPacket( if source { // determine escrow account escrowAccount = channelEscrowAddresses[sourceChannel] - // escrow source token + // escrow token (escrow account becomes new owner) nft.Transfer(classId, tokenId, escrowAccount) } else { - // receiver is source chain, burn voucher + // we are sink chain, burn voucher nft.Burn(classId, tokenId) } tokenUris.push(nft.getNFT(classId, tokenId).getUri()) @@ -251,11 +266,11 @@ function onRecvPacket(packet: Packet) { // construct default acknowledgement of success NonFungibleTokenPacketAcknowledgement ack = NonFungibleTokenPacketAcknowledgement{true, null} prefix = "{packet.sourcePort}/{packet.sourceChannel}/" - // we are the source if the classId is prefixed with the packet's sourcePort and sourceChannel + // we are source chain if classId is prefixed with packet's sourcePort and sourceChannel source = data.classId.slice(0, len(prefix)) === prefix for (var i in data.tokenIds) { if source { - // receiver is source chain: unescrow token + // unescrow token (receiver becomes new owner) err = nft.Transfer(data.classId.slice(len(prefix)), data.tokenIds[i], data.receiver) if (err !== nil) { ack = NonFungibleTokenPacketAcknowledgement{false, err.Error()} @@ -264,7 +279,7 @@ function onRecvPacket(packet: Packet) { } else { prefix = "{packet.destPort}/{packet.destChannel}/" prefixedClassId = prefix + data.classId - // sender was source, mint voucher to receiver + // we are sink chain, mint voucher to receiver (owner) err = nft.Mint(prefixedClassId, data.classUri, data.tokenIds[i], data.tokenUris[i], data.receiver) if (err !== nil) { ack = NonFungibleTokenPacketAcknowledgement{false, err.Error()} @@ -327,13 +342,14 @@ function onTimeoutPacketClose(packet: Packet) { ##### Correctness -This implementation preserves token uniqueness. +This implementation preserves token non-fungibility and redeemability. -Uniqueness: If tokens have been sent to the counterparty chain, they can be redeemed back in the same `classId` & `tokenId` on the source chain. +* Non-fungibility: Only one instance of any token is *live* across all the IBC-connected blockchains. +* Redeemability: If tokens have been sent to the counterparty chain, they can be redeemed back in the same `classId` & `tokenId` on the source chain. ##### Multi-chain notes -This specification does not directly handle the "diamond problem", where a user sends a token originating on chain A to chain B, then to chain D, and wants to return it through D -> C -> A — since the supply is tracked as owned by chain B (and the `classId` will be "{portOnD}/{channelOnD}/{portOnB}/{channelOnB}/classId"), chain C cannot serve as the intermediary. It is not yet clear whether that case should be dealt with in-protocol or not — it may be fine to just require the original path of redemption (and if there is frequent liquidity and some surplus on both paths the diamond path will work most of the time). Complexities arising from long redemption paths may lead to the emergence of central chains in the network topology. +This specification does not directly handle the "diamond problem", where a user sends a token originating on chain A to chain B, then to chain D, and wants to return it through D -> C -> A — since the token is tracked as owned by chain B (and the `classId` will be "{portOnD}/{channelOnD}/{portOnB}/{channelOnB}/classId"), chain C cannot serve as the intermediary. The original path, in reverse order, is required to redeem the token at its source chain. Complexities arising from long redemption paths may lead to the emergence of central chains in the network topology. In order to track all of the tokens moving around the network of chains in various paths, it may be helpful for a particular chain to implement a registry which will track the "global" source chain for each `classId`. End-user service providers (such as wallet authors) may want to integrate such a registry or keep their own mapping of canonical source chains and human-readable names in order to improve UX. @@ -372,6 +388,7 @@ Coming soon. | Nov 18, 2021 | Revised to allow for multiple tokens in one packet | | Feb 10, 2022 | Revised to incorporate feedbacks from IBC team | | Mar 03, 2022 | Revised to make TRY callback consistent with PR#629 | +| Mar 11, 2022 | Added example to illustrate the prefix concept | ## Copyright From 526c1e3f5b78e78da9a76cb17ecfd55ce2eb9ea7 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Thu, 31 Mar 2022 00:21:07 +0800 Subject: [PATCH 16/18] added NFT module definition and fixed pseudo-code errors --- spec/app/ics-721-nft-transfer/README.md | 157 +++++++++++++++++------- 1 file changed, 112 insertions(+), 45 deletions(-) diff --git a/spec/app/ics-721-nft-transfer/README.md b/spec/app/ics-721-nft-transfer/README.md index 447e03dce..21390a2d5 100644 --- a/spec/app/ics-721-nft-transfer/README.md +++ b/spec/app/ics-721-nft-transfer/README.md @@ -7,7 +7,7 @@ requires: 25, 26 kind: instantiation author: Haifeng Xi created: 2021-11-10 -modified: 2022-03-11 +modified: 2022-03-30 --- > This standard document follows the same design principles of [ICS 20](../ics-020-fungible-token-transfer) and inherits most of its content therefrom, while replacing `bank` module based asset tracking logic with that of the `nft` module. @@ -57,7 +57,7 @@ Each `tokenId` has a corresponding entry in `tokenUris`, which refers to an off- As tokens are sent across chains using the ICS-721 protocol, they begin to accrue a record of channels across which they have been transferred. This record information is encoded into the `classId` field. -An ICS-721 token class is represented in the form `{ics721Port}/{ics721Channel}/{classId}`, where `ics721Port` and `ics721Channel` identify the channel on the current chain from which the tokens arrived. The prefixed port and channel pair indicate which channel the tokens were previously sent through. If `{classId}` contains `/`, then it must also be in the ICS-721 form which indicates that the tokens have a multi-hop record. Note that this requires that the `/` (slash character) is prohibited in non-IBC token `classId`s. +An ICS-721 token class is represented in the form `{ics721Port}/{ics721Channel}/{classId}`, where `ics721Port` and `ics721Channel` identify the channel on the current chain from which the tokens arrived. If `{classId}` contains `/`, then it must also be in the ICS-721 form which indicates that the tokens have a multi-hop record. Note that this requires that the `/` (slash character) is prohibited in non-IBC token `classId`s. A sending chain may be acting as a source or sink zone. When a chain is sending tokens across a port and channel which are not equal to the last prefixed port and channel pair, it is acting as a source zone. When tokens are sent from a source zone, the destination port and channel will be prefixed onto the `classId` (once the tokens are received) adding another hop to the tokens record. When a chain is sending tokens across a port and channel which are equal to the last prefixed port and channel pair, it is acting as a sink zone. When tokens are sent from a sink zone, the last prefixed port and channel pair on the `classId` is removed (once the tokens are received), undoing the last hop in the tokens record. @@ -65,14 +65,14 @@ Each send to any chain other than the one from which the token was previously re For example, assume these steps of transfer occur: -A(p1,c1) -> B(p2,c2) -> C(p3,c3) -> A(p4,c4) -> C(p3,c3) -> B(p2,c2) -> A(p1,c1) +A -> B -> C -> A -> C -> B -> A -1. A -> B : A is source zone. `classId` in B: 'p2/c2/nftClass' -2. B -> C : B is source zone. `classId` in C: 'p3/c3/p2/c2/nftClass' -3. C -> A : C is source zone. `classId` in A: 'p4/c4/p3/c3/p2/c2/nftClass' -4. A -> C : A is sink zone. `classId` in C: 'p3/c3/p2/c2/nftClass' -5. C -> B : C is sink zone. `classId` in B: 'p2/c2/nftClass' -6. B -> A : B is sink zone. `classId` in A: 'nftClass' +1. A(p1,c1) -> (p2,c2)B : A is source zone. `classId` in B: 'p2/c2/nftClass' +2. B(p3,c3) -> (p4,c4)C : B is source zone. `classId` in C: 'p4/c4/p2/c2/nftClass' +3. C(p5,c5) -> (p6,c6)A : C is source zone. `classId` in A: 'p6/c6/p4/c4/p2/c2/nftClass' +4. A(p6,c6) -> (p5,c5)C : A is sink zone. `classId` in C: 'p4/c4/p2/c2/nftClass' +5. C(p4,c4) -> (p3,c3)B : C is sink zone. `classId` in B: 'p2/c2/nftClass' +6. B(p2,c2) -> (p1,c1)A : B is sink zone. `classId` in A: 'nftClass' The acknowledgement data type describes whether the transfer succeeded or failed, and the reason for failure (if any). @@ -103,7 +103,73 @@ interface ModuleState { The sub-protocols described herein should be implemented in a "non-fungible token transfer bridge" module with access to the NFT asset tracking module and the IBC routing module. -The `x/nft` module specified in [ADR-043](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-043-nft-module.md) is one such example of the NFT asset tracking module, where the ownership of each NFT should be properly tracked. If an NFT is transferred to a recipient by its current owner, the module is expected to record the ownership change by updating the token's new owner to the recipient. +The NFT asset tracking module should implement the following functions: + +```typescript +function SaveClass( + classId: string, + classUri: string) { + // creates a new NFT Class identified by classId +} +``` + +```typescript +function Mint( + classId: string, + tokenId: string, + tokenUri: string, + sender: string) { + // creates a new NFT identified by + // sender becomes owner of the newly minted NFT +} +``` + +```typescript +function Transfer( + classId: string, + tokenId: string, + receiver: string) { + // transfers the NFT identified by to receiver + // receiver becomes new owner of the NFT +} +``` + +```typescript +function Burn( + classId: string, + tokenId: string) { + // destroys the NFT identified by +} +``` + +```typescript +function GetOwner( + classId: string, + tokenId: string) { + // returns current owner of the NFT identified by +} +``` + +```typescript +function GetNFT( + classId: string, + tokenId: string) { + // returns NFT identified by +} +``` + +```typescript +function HasClass(classId: string) { + // returns true if NFT Class identified by classId already exists; + // returns false otherwise +} +``` + +```typescript +function GetClass(classId: string) { + // returns NFT Class identified by classId +} +``` #### Port & channel setup @@ -145,7 +211,9 @@ function onChanOpenInit( order: ChannelOrder, connectionHops: [Identifier], portIdentifier: Identifier, + channelIdentifier: Identifier, counterpartyPortIdentifier: Identifier, + counterpartyChannelIdentifier: Identifier, version: string) { // only unordered channels allowed abortTransactionUnless(order === UNORDERED) @@ -162,12 +230,10 @@ function onChanOpenTry( channelIdentifier: Identifier, counterpartyPortIdentifier: Identifier, counterpartyChannelIdentifier: Identifier, - version: string, counterpartyVersion: string) { // only unordered channels allowed abortTransactionUnless(order === UNORDERED) // assert that version is "ics721-1" - abortTransactionUnless(version === "ics721-1") abortTransactionUnless(counterpartyVersion === "ics721-1") } ``` @@ -176,8 +242,8 @@ function onChanOpenTry( function onChanOpenAck( portIdentifier: Identifier, channelIdentifier: Identifier, - counterpartyVersion: string, - counterpartyChannelIdentifier: string) { + counterpartyChannelIdentifier: Identifier, + counterpartyVersion: string) { // port has already been validated // assert that version is "ics721-1" abortTransactionUnless(counterpartyVersion === "ics721-1") @@ -240,21 +306,19 @@ function createOutgoingPacket( source = classId.slice(0, len(prefix)) !== prefix tokenUris = [] for (let tokenId in tokenIds) { - // assert that sender is token owner - abortTransactionUnless(sender === nft.getOwner(classId, tokenId)) + // ensure that sender is token owner + abortTransactionUnless(sender === nft.GetOwner(classId, tokenId)) if source { - // determine escrow account - escrowAccount = channelEscrowAddresses[sourceChannel] - // escrow token (escrow account becomes new owner) - nft.Transfer(classId, tokenId, escrowAccount) + // escrow token + nft.Transfer(classId, tokenId, channelEscrowAddresses[sourceChannel]) } else { // we are sink chain, burn voucher nft.Burn(classId, tokenId) } - tokenUris.push(nft.getNFT(classId, tokenId).getUri()) + tokenUris.push(nft.GetNFT(classId, tokenId).GetUri()) } - NonFungibleTokenPacketData data = NonFungibleTokenPacketData{classId, nft.getClass(classId).getUri(), tokenIds, tokenUris, sender, receiver} - handler.sendPacket(Packet{timeoutHeight, timeoutTimestamp, destPort, destChannel, sourcePort, sourceChannel, data}, getCapability("port")) + NonFungibleTokenPacketData data = NonFungibleTokenPacketData{classId, nft.GetClass(classId).GetUri(), tokenIds, tokenUris, sender, receiver} + ics4Handler.sendPacket(Packet{timeoutHeight, timeoutTimestamp, destPort, destChannel, sourcePort, sourceChannel, data}, getCapability("port")) } ``` @@ -265,29 +329,31 @@ function onRecvPacket(packet: Packet) { NonFungibleTokenPacketData data = packet.data // construct default acknowledgement of success NonFungibleTokenPacketAcknowledgement ack = NonFungibleTokenPacketAcknowledgement{true, null} + err = ProcessReceivedPacketData(data) + if (err !== null) { + ack = NonFungibleTokenPacketAcknowledgement{false, err.Error()} + } + return ack +} + +function ProcessReceivedPacketData(data: NonFungibleTokenPacketData) { prefix = "{packet.sourcePort}/{packet.sourceChannel}/" // we are source chain if classId is prefixed with packet's sourcePort and sourceChannel source = data.classId.slice(0, len(prefix)) === prefix for (var i in data.tokenIds) { if source { - // unescrow token (receiver becomes new owner) - err = nft.Transfer(data.classId.slice(len(prefix)), data.tokenIds[i], data.receiver) - if (err !== nil) { - ack = NonFungibleTokenPacketAcknowledgement{false, err.Error()} - break - } - } else { - prefix = "{packet.destPort}/{packet.destChannel}/" - prefixedClassId = prefix + data.classId - // we are sink chain, mint voucher to receiver (owner) - err = nft.Mint(prefixedClassId, data.classUri, data.tokenIds[i], data.tokenUris[i], data.receiver) - if (err !== nil) { - ack = NonFungibleTokenPacketAcknowledgement{false, err.Error()} - break + // unescrow token to receiver + nft.Transfer(data.classId.slice(len(prefix)), data.tokenIds[i], data.receiver) + } else { // we are sink chain + prefixedClassId = "{packet.destPort}/{packet.destChannel}/" + data.classId + // create NFT class if it doesn't exist already + if (nft.HasClass(prefixedClassId) === false) { + nft.SaveClass(data.classId, data.classUri) } + // mint voucher to receiver + nft.Mint(prefixedClassId, data.tokenIds[i], data.tokenUris[i], data.receiver) } } - return ack } ``` @@ -318,15 +384,15 @@ function onTimeoutPacket(packet: Packet) { function refundToken(packet: Packet) { NonFungibleTokenPacketData data = packet.data prefix = "{packet.sourcePort}/{packet.sourceChannel}/" - for (let tokenId in data.tokenIds) { - // we are the source if the classId is not prefixed with the packet's sourcePort and sourceChannel - source = data.classId.slice(0, len(prefix)) !== prefix + // we are the source if the classId is not prefixed with the packet's sourcePort and sourceChannel + source = data.classId.slice(0, len(prefix)) !== prefix + for (var i in data.tokenIds) { { if source { - // sender was source chain, unescrow tokens back to sender - nft.Transfer(data.classId, tokenId, data.sender) + // unescrow token back to sender + nft.Transfer(data.classId, data.tokenIds[i], data.sender) } else { - // receiver was source chain, mint voucher back to sender - nft.Mint(data.classId, tokenId, data.sender) + // we are sink chain, mint voucher back to sender + nft.Mint(data.classId, data.tokenIds[i], data.tokenUris[i], data.sender) } } } @@ -389,6 +455,7 @@ Coming soon. | Feb 10, 2022 | Revised to incorporate feedbacks from IBC team | | Mar 03, 2022 | Revised to make TRY callback consistent with PR#629 | | Mar 11, 2022 | Added example to illustrate the prefix concept | +| Mar 30, 2022 | Added NFT module definition and fixed pseudo-code errors | ## Copyright From fce583417633a96790474c61142b3440c50108f5 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Thu, 31 Mar 2022 01:11:23 +0800 Subject: [PATCH 17/18] removed duplicate paragraph about source/sink zone --- spec/app/ics-721-nft-transfer/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/app/ics-721-nft-transfer/README.md b/spec/app/ics-721-nft-transfer/README.md index 21390a2d5..cb4217884 100644 --- a/spec/app/ics-721-nft-transfer/README.md +++ b/spec/app/ics-721-nft-transfer/README.md @@ -61,8 +61,6 @@ An ICS-721 token class is represented in the form `{ics721Port}/{ics721Channel}/ A sending chain may be acting as a source or sink zone. When a chain is sending tokens across a port and channel which are not equal to the last prefixed port and channel pair, it is acting as a source zone. When tokens are sent from a source zone, the destination port and channel will be prefixed onto the `classId` (once the tokens are received) adding another hop to the tokens record. When a chain is sending tokens across a port and channel which are equal to the last prefixed port and channel pair, it is acting as a sink zone. When tokens are sent from a sink zone, the last prefixed port and channel pair on the `classId` is removed (once the tokens are received), undoing the last hop in the tokens record. -Each send to any chain other than the one from which the token was previously received is a movement forward 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 `classId`. In these instances the sender chain is acting as the *source zone*. When the token is sent back to the chain it was previously received from, the prefix is removed. This is a backward movement in the token's timeline and the sender chain is acting as the *sink zone*. - For example, assume these steps of transfer occur: A -> B -> C -> A -> C -> B -> A From 7f982fe9a3eabd51e1ad28eb1b45e3d5fcfdce68 Mon Sep 17 00:00:00 2001 From: Haifeng Xi Date: Wed, 18 May 2022 23:24:08 +0800 Subject: [PATCH 18/18] added paragraph about NFT metadata mutability --- spec/app/ics-721-nft-transfer/README.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/spec/app/ics-721-nft-transfer/README.md b/spec/app/ics-721-nft-transfer/README.md index cb4217884..d5a9424a6 100644 --- a/spec/app/ics-721-nft-transfer/README.md +++ b/spec/app/ics-721-nft-transfer/README.md @@ -7,7 +7,7 @@ requires: 25, 26 kind: instantiation author: Haifeng Xi created: 2021-11-10 -modified: 2022-03-30 +modified: 2022-05-18 --- > This standard document follows the same design principles of [ICS 20](../ics-020-fungible-token-transfer) and inherits most of its content therefrom, while replacing `bank` module based asset tracking logic with that of the `nft` module. @@ -116,9 +116,9 @@ function Mint( classId: string, tokenId: string, tokenUri: string, - sender: string) { + receiver: string) { // creates a new NFT identified by - // sender becomes owner of the newly minted NFT + // receiver becomes owner of the newly minted NFT } ``` @@ -411,12 +411,6 @@ This implementation preserves token non-fungibility and redeemability. * Non-fungibility: Only one instance of any token is *live* across all the IBC-connected blockchains. * Redeemability: If tokens have been sent to the counterparty chain, they can be redeemed back in the same `classId` & `tokenId` on the source chain. -##### Multi-chain notes - -This specification does not directly handle the "diamond problem", where a user sends a token originating on chain A to chain B, then to chain D, and wants to return it through D -> C -> A — since the token is tracked as owned by chain B (and the `classId` will be "{portOnD}/{channelOnD}/{portOnB}/{channelOnB}/classId"), chain C cannot serve as the intermediary. The original path, in reverse order, is required to redeem the token at its source chain. Complexities arising from long redemption paths may lead to the emergence of central chains in the network topology. - -In order to track all of the tokens moving around the network of chains in various paths, it may be helpful for a particular chain to implement a registry which will track the "global" source chain for each `classId`. End-user service providers (such as wallet authors) may want to integrate such a registry or keep their own mapping of canonical source chains and human-readable names in order to improve UX. - #### Optional addenda - Each chain, locally, could elect to keep a lookup table to use short, user-friendly local `classId`s in state which are translated to and from the longer `classId`s when sending and receiving packets. @@ -425,6 +419,8 @@ In order to track all of the tokens moving around the network of chains in vario ## Further Discussion Extended and complex use cases such as royalties, marketplaces or permissioned transfers can be supported on top of this specification. Solutions could be modules, hooks, [IBC middleware](../ics-030-middleware) and so on. Designing a guideline for this is out of the scope. +It is assumed that application logic in host state machines will be responsible for metadata immutability of IBC tokens minted according to this specification. For any IBC token, NFT applications are strongly advised to check upstream blockchains (all the way back to the source) to ensure its metadata has not been modified along the way. If it is decided, sometime in the future, to accommodate NFT metadata mutability over IBC, we will update this specification or create an entirely new specification -- by using advanced DID features perhaps. + ## Backwards Compatibility Not applicable. @@ -454,6 +450,7 @@ Coming soon. | Mar 03, 2022 | Revised to make TRY callback consistent with PR#629 | | Mar 11, 2022 | Added example to illustrate the prefix concept | | Mar 30, 2022 | Added NFT module definition and fixed pseudo-code errors | +| May 18, 2002 | Added paragraph about NFT metadata mutability | ## Copyright