|
| 1 | +## Introduction |
| 2 | +This document outlines the ICS-20 workflow that supports IRC2 Tokens instead of a consolidated Bank Module. This will help us to represent transferred tokens in proper IRC2 wrapped assets. |
| 3 | +This is an extension of [ICS20](https://github.com/cosmos/ibc/blob/main/spec/app/ics-020-fungible-token-transfer/README.md) specs to support IBC on ICON. |
| 4 | + |
| 5 | +### Logical Components |
| 6 | + |
| 7 | +#### Token Contract: |
| 8 | +This will be mintabe burnable IRC2 token contract that will be deployed for each local asset or foreign wrapped asset. We will be using the audited [IRC2 Tradeable Contract](https://github.com/icon-project/icon-bridge/tree/main/javascore/irc2Tradeable) from the ICON Bridge repo |
| 9 | + |
| 10 | +#### ICS20 Contract: |
| 11 | +This contract will be entrypoint for sending and receiving tokens from foreign chains. It will also maintain a registry of tokens that are allowed to be transferred to and fro. Official ICS-20 specs can be found [here](https://github.com/cosmos/ibc/blob/main/spec/app/ics-020-fungible-token-transfer/README.md). |
| 12 | + |
| 13 | +Any arbitrary tokens from the COSMOS chains will not be allowed to transfer to ICON. Only registered tokens will be allowed to do so. The ICS20 Contract will have an admin, which can register tokens of COSMOS chains on ICON. Once registered, those tokens can be minted on ICON by the ICS20 contract. The name of the token MUST be the corresponding denom from centauri chain. |
| 14 | + |
| 15 | +**Cross chain tokens to be supported** |
| 16 | + |
| 17 | +If `ARCH` token is to be bridged from Archway to ICON, it has to go through Archway -> Centauri -> Icon. |
| 18 | + |
| 19 | +Denom of ARCH on centauri: `transfer/channel-X/ARCH` |
| 20 | + |
| 21 | +Denom of ARCH on icon: `transfer/channel-ICON/transfer/channel-X/ARCH`. |
| 22 | + |
| 23 | +The name of the token to register MUST be `transfer/channel-ICON/transfer/channel-X/ARCH`. |
| 24 | + |
| 25 | +If ARCH token was transfered to another cosmos chain, neutron, and if that needs to be sent to ICON, it MUST go back to Archway chain, then be sent to ICON via centauri. Only the denoms from their native chain will be supported. |
| 26 | + |
| 27 | +#### Token register |
| 28 | +```js |
| 29 | +function registerCosmosToken(name: String, symbol: String, decimals: int) { |
| 30 | + onlyAdmin() |
| 31 | + tokenAddress = deployIRC2Tradeable(name, symbol, decimals) |
| 32 | + tokenContracts[name] = tokenAddress |
| 33 | +} |
| 34 | +``` |
| 35 | + |
| 36 | +The following function will be used to register tokens on ICON to the ICS 20 App. |
| 37 | + |
| 38 | +```js |
| 39 | +function registerIconToken(tokenAddress: Address) { |
| 40 | + onlyAdmin() |
| 41 | + tokenContracts[tokenAddress.toString()] = tokenAddress |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +#### Helper methods |
| 46 | +```js |
| 47 | +function isNativeAsset(denom:String){ |
| 48 | + return denom=="icx" |
| 49 | +} |
| 50 | + |
| 51 | +function getTokenContractAddress(denom:String):String { |
| 52 | + assert(tokenContracts[denom]!=null) |
| 53 | + return tokenContracts[denom] |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +#### Sending Tokens |
| 58 | +- To send ICX, send using the `sendICX` function. |
| 59 | + |
| 60 | +- To send tokens other then ICX, it should go through `tokenFallback` function. The `data` bytes, should be parsed into the following structure. |
| 61 | + ```json |
| 62 | + { |
| 63 | + "method": "sendFungibleTokens", |
| 64 | + "params": { |
| 65 | + "denomination": "string", |
| 66 | + "amount": "uint64", |
| 67 | + "sender": "string", |
| 68 | + "receiver": "string", |
| 69 | + "sourcePort": "string", |
| 70 | + "sourceChannel": "string", |
| 71 | + "timeoutHeight": { |
| 72 | + "latestHeight": "uint64", |
| 73 | + "revisionNumber": "uint64", |
| 74 | + }, |
| 75 | + "timeoutTimestamp": "uint64", |
| 76 | + "memo":"string" |
| 77 | + } |
| 78 | + } |
| 79 | + ``` |
| 80 | +- Implementation of `tokenFallback` and `sendICX` function |
| 81 | +```js |
| 82 | +// to send tokens other than icx |
| 83 | +function tokenFallback(from: Address, value: uint64, data: bytes) { |
| 84 | + data = parseStructure(data) |
| 85 | + if data.method == "sendFungibleTokens" { |
| 86 | + sendFungibleToken = parseFungibleToken(data.params) |
| 87 | + assert(sendFungibleToken.amount == value) |
| 88 | + assert(sendFungibleToken.sender == from) |
| 89 | + sendFungibleTokens(...) |
| 90 | + } else { |
| 91 | + revert("wrong data") |
| 92 | + } |
| 93 | +} |
| 94 | + |
| 95 | +@payable |
| 96 | +function sendICX( |
| 97 | + receiver: string, |
| 98 | + sourcePort: string, |
| 99 | + sourceChannel: string, |
| 100 | + timeoutHeight: Height, |
| 101 | + timeoutTimestamp: string, |
| 102 | + @Optional memo: string |
| 103 | +) { |
| 104 | + sendFungibleTokens("icx", Context.getValue(), Context.getCaller().toString(), ...) |
| 105 | +} |
| 106 | + |
| 107 | + |
| 108 | +internal function sendFungibleTokens( |
| 109 | + denomination: string, |
| 110 | + amount: uint256, |
| 111 | + sender: string, |
| 112 | + receiver: string, |
| 113 | + sourcePort: string, |
| 114 | + sourceChannel: string, |
| 115 | + timeoutHeight: Height, |
| 116 | + timeoutTimestamp: uint64, // in unix nanoseconds |
| 117 | + @Optional memo: string |
| 118 | +): uint64 { |
| 119 | + prefix = "{sourcePort}/{sourceChannel}/" |
| 120 | + // we are the source if the denomination is not prefixed |
| 121 | + source = denomination.slice(0, len(prefix)) !== prefix |
| 122 | + tokenContract=getTokenContracts(denomination) |
| 123 | + if source { |
| 124 | + if isNativeAsset(denomination) { |
| 125 | + assert amount == Context.getValue() |
| 126 | + } |
| 127 | + } |
| 128 | + if !source { |
| 129 | + tokenContract.burn(amount); |
| 130 | + } |
| 131 | + |
| 132 | + // create FungibleTokenPacket data |
| 133 | + data = FungibleTokenPacketData{denomination, amount, sender, receiver, memo} |
| 134 | + |
| 135 | + // send packet using the interface defined in ICS4 |
| 136 | + sequence = handler.sendPacket( |
| 137 | + getCapability("port"), |
| 138 | + sourcePort, |
| 139 | + sourceChannel, |
| 140 | + timeoutHeight, |
| 141 | + timeoutTimestamp, |
| 142 | + json.marshal(data) // json-marshalled bytes of packet data |
| 143 | + ) |
| 144 | + |
| 145 | + return sequence |
| 146 | +} |
| 147 | + |
| 148 | +``` |
| 149 | + |
| 150 | +#### Receiving tokens |
| 151 | + |
| 152 | +```js |
| 153 | + |
| 154 | +function onRecvPacket(packet: Packet) { |
| 155 | + FungibleTokenPacketData data = packet.data |
| 156 | + assert(data.denom !== "") |
| 157 | + assert(data.amount > 0) |
| 158 | + assert(data.sender !== "") |
| 159 | + assert(data.receiver !== "") |
| 160 | + |
| 161 | + // construct default acknowledgement of success |
| 162 | + FungibleTokenPacketAcknowledgement ack = FungibleTokenPacketAcknowledgement{true, null} |
| 163 | + prefix = "{packet.sourcePort}/{packet.sourceChannel}/" |
| 164 | + // we are the source if the packets were prefixed by the sending chain |
| 165 | + source = data.denom.slice(0, len(prefix)) === prefix |
| 166 | + assert data.receiver is Address |
| 167 | + if source { |
| 168 | + // receiver is source chain: unescrow tokens |
| 169 | + // determine escrow account |
| 170 | + denomOnly=data.denom.slice(len(prefix),len(data.denom.prefix)); |
| 171 | + if isNativeAsset(denomOnly){ |
| 172 | + Context.transfer(data.receiver, data.amount) |
| 173 | + } |
| 174 | + tokenContract=getTokenContract(denomOnly) |
| 175 | + // unescrow tokens to receiver (assumed to fail if balance insufficient) |
| 176 | + try { |
| 177 | + tokenContract.transfer(data.receiver,data.amount) |
| 178 | + } catch (Exception e) { |
| 179 | + ack = FungibleTokenPacketAcknowledgement{false, "transfer coins failed"} |
| 180 | + } |
| 181 | + } else { |
| 182 | + prefix = "{packet.destPort}/{packet.destChannel}/" |
| 183 | + prefixedDenomination = prefix + data.denom |
| 184 | + tokenContract=getTokenContract(prefixedDenomination) |
| 185 | + try { |
| 186 | + // sender was source, mint vouchers to receiver (assumed to fail if balance insufficient) |
| 187 | + tokenContract.mint(data.receiver, data.amount) |
| 188 | + } catch (Exception e) { |
| 189 | + ack = FungibleTokenPacketAcknowledgement{false, "mint coins failed"} |
| 190 | + } |
| 191 | + } |
| 192 | + return ack |
| 193 | +} |
| 194 | +``` |
| 195 | + |
| 196 | +#### Acknowledge Packet |
| 197 | +```js |
| 198 | +function onAcknowledgePacket( |
| 199 | + packet: Packet, |
| 200 | + acknowledgement: bytes) { |
| 201 | + // if the transfer failed on dst chain, refund the tokens |
| 202 | + if (!acknowledgement.success) |
| 203 | + refundTokens(packet) |
| 204 | +} |
| 205 | +``` |
| 206 | +#### Timeout Packet |
| 207 | +```js |
| 208 | +function onTimeoutPacket(packet: Packet) { |
| 209 | + // the packet timed-out, so refund the tokens |
| 210 | + refundTokens(packet) |
| 211 | +} |
| 212 | +``` |
| 213 | +#### Refund logic |
| 214 | +```js |
| 215 | + |
| 216 | +function refundTokens(packet: Packet) { |
| 217 | + FungibleTokenPacketData data = packet.data |
| 218 | + prefix = "{packet.sourcePort}/{packet.sourceChannel}/" |
| 219 | + // we are the source if the denomination is not prefixed |
| 220 | + tokenContract=getTokenContracts(data.denom) |
| 221 | + source = data.denom.slice(0, len(prefix)) !== prefix |
| 222 | + if source { |
| 223 | + // sender was source chain, unescrow tokens back to sender |
| 224 | + if isNativeAsset { |
| 225 | + Context.transfer(data.sender, data.amount) |
| 226 | + return |
| 227 | + } |
| 228 | + tokenContract.transfer(data.sender, data.amount) |
| 229 | + } else { |
| 230 | + // receiver was source chain, mint vouchers back to sender |
| 231 | + tokenContract.mint(data.sender,data.amount) |
| 232 | + } |
| 233 | +} |
| 234 | + |
| 235 | +``` |
| 236 | + |
| 237 | +### Hopchain |
| 238 | +Hop is done based on the memo field during sendFungibleTokens. The structure of memo should follow the following [spec](https://github.com/cosmos/ibc-apps/tree/main/middleware/packet-forward-middleware) |
| 239 | + |
0 commit comments