Skip to content

Fee enabled fungible token transfers

Carlos Rodriguez edited this page Oct 20, 2022 · 17 revisions

This document outlines the setup of an e2e local environment and the sample usage of the fee middleware module applied to ICS20 fungible token transfers. The fee middleware module exposes two different ways to pay fees for relaying IBC packets:

  • using the MsgPayPacketFeeAsync message, which enables the payment of fees for a packet at a specified sequence;
  • and using the MsgPayPacketFee message, which enables the payment of fees for a packet at the next sequence send and should be combined with the message that will be paid for.

The use of both methods will be demonstrated in this guide.

The IBC packet flow has two different branches:

  • one that involves 2 transactions: one for the submission of the MsgRecvPacket message on the destination chain, and another one for the submission of the MsgAcknowledgement message on the source chain;
  • and another one that involves only the submission of the MsgTimeout message on the source chain.

All these three messages can be submitted by different relayer processes and ICS29 allows to specify separate fees that will be paid out to the relayer addresses for submitting each of them. Fees that are not paid out (either the fee for MsgTimeout or the fees for MsgRecvPacket and MsgAcknowledgement) will be reimbursed to the paying account.

Table of Contents

Prerequisites

  • Install the IBC simd binary:
git clone [email protected]:cosmos/ibc-go.git && cd ibc-go

git checkout main

make install
  • Install Hermes v0.15.0 (this is the latest version at the moment of writing) via cargo:
cargo install --version 0.15.0 ibc-relayer-cli --bin hermes --locked

Setup

Bootstrap two chains using the simd binary

Clone the following repository containing boilerplate scripts for creating the test environment. These scripts are intended for testing purposes and simplify the process of bootstrapping two chains, a relayer and creating an IBC connection between them.

git clone [email protected]:damiannolan/simd-scripts.git && cd simd-scripts

The repository contains a basic Makefile defining simple targets to assist in manual testing.

# bootstrap two chains and create connection 
make init

# start the relayer
make start-rly

For convenience, we are going to store a few account addresses as variables in the current shell environment. Execute the following commands to store the relayer addresses on chains test-1 (account rly1) and test-2 (account rly2), respectively:

export RLY_1=$(simd keys show rly1 -a \
--keyring-backend test \
--home ./data/test-1) && echo $RLY_1;
export RLY_2=$(simd keys show rly2 -a \
--keyring-backend test \
--home ./data/test-2) && echo $RLY_2;

And execute also the following commands to store the wallet account addresses on chains test-1 (accounts wallet1 and wallet2) and test-2 (account wallet3) that we will use throughout this guide:

export WALLET_1=$(simd keys show wallet1 -a \
--keyring-backend test \
--home ./data/test-1) && echo $WALLET_1;
export WALLET_2=$(simd keys show wallet2 -a \
--keyring-backend test \
--home ./data/test-1) && echo $WALLET_2;
export WALLET_3=$(simd keys show wallet3 -a \
--keyring-backend test \
--home ./data/test-2) && echo $WALLET_3;

Create a fee-enabled ICS20 fungible token transfer channel

We can now initiate a handshake for a new fee-enabled transfer channel using the existing connection:

hermes -c ./network/hermes/config.toml create channel test-1 connection-0 \
--port-a transfer \
--port-b transfer \
-v "{\"fee_version\":\"ics29-1\",\"app_version\":\"ics20-1\"}"

Once the handshake finishes, we can query the channels on both chains to ensure the handshake completed successfully:

# query the channels on chain test-1
> simd q ibc channel channels --home ./data/test-1 --node tcp://localhost:16657
channels:
- channel_id: channel-0
  connection_hops:
  - connection-0
  counterparty:
    channel_id: channel-0
    port_id: transfer
  ordering: ORDER_UNORDERED
  port_id: transfer
  state: STATE_OPEN
  version: '{"fee_version":"ics29-1","app_version":"ics20-1"}'
height:
  revision_height: "52"
  revision_number: "1"
pagination:
  next_key: null
  total: "0"
# query the channels on chain test-2
> simd q ibc channel channels --home ./data/test-2 --node tcp://localhost:26657
channels:
- channel_id: channel-0
  connection_hops:
  - connection-0
  counterparty:
    channel_id: channel-0
    port_id: transfer
  ordering: ORDER_UNORDERED
  port_id: transfer
  state: STATE_OPEN
  version: '{"fee_version":"ics29-1","app_version":"ics20-1"}'
height:
  revision_height: "64"
  revision_number: "2"
pagination:
  next_key: null
  total: "0"

There is only one channel between these two chains, and we can see that the version string in both ends of the channel is {"fee_version":"ics29-1","app_version":"ics20-1"}, which is what we expected.

As part of the fee middleware feature we have added some new queries, availabe both as CLI commands and as gRPC endpoints. In this guide we will make use of the CLI commands. We will show usage of all these queries along this guide.

We can query which channels on both chains have fee enabled:

# query the fee-enabled channels on chain test-1
> simd q ibc-fee channels --node tcp://localhost:16657
fee_enabled_channels:
- channel_id: channel-0
  port_id: transfer
# query the fee-enabled channels on chain test-2
> simd q ibc-fee channels --node tcp://localhost:26657
fee_enabled_channels:
- channel_id: channel-0
  port_id: transfer

We can also query for the fee-enabled status of a particular channel (i.e. by channel ID):

# query the fee-enabled status of channel with ID channel-0 on chain test-1
> simd q ibc-fee channel transfer channel-0 --chain-id test-1 --node tcp://localhost:16657
fee_enabled: true
# query the fee-enabled status of channel with ID channel-0 on chain test-2
> simd q ibc-fee channel transfer channel-0 --chain-id test-2 --node tcp://localhost:26657
fee_enabled: true

Unsurprisingly, both ends of the channel have fee enabled.

Register the counterparty payee

All incentivization fees are paid to accounts on the chain from where the IBC packets originate. To ensure that the relayer that delivers the MsgRecvPacket on the destination chain is correctly compensated, the counterparty payee address (i.e. the account address of the relayer on the source chain) needs to be registered on the destination chain. Throughout this guide the source chain is test-1 and the destination chain is test-2, therefore we need to register the account address of the relayer on chain test-1 (RLY_1) on chain test-2 (where the relayer has the account address RLY_2):

simd tx ibc-fee register-counterparty-payee transfer channel-0 $RLY_2 $RLY_1 \
--from $RLY_2 \
--home ./data/test-2 \
--node tcp://localhost:26657 \
--keyring-backend test \
--chain-id test-2

Once the above command succeeds, then we can verify which counterparty payee is registered on chain test-2 for account RLY_2:

> simd q ibc-fee counterparty-payee channel-0 $RLY_2 --chain-id test-2 --node tcp://localhost:26657
counterparty_address: cosmos1mjk79fjjgpplak5wq838w0yd982gzkyfrk07am

We see that the counterparty payee address matches what we expected (i.e. the RLY_1 address). In this guide we are going to send only packets from chain test-1 to chain test-2, so we only need to register the counterparty payee on chain test-2. In real life circumstances relayers relay packets on both directions (i.e. from chain test-1 to test-2 and also viceversa), and thus relayers should register as well on chain test-1 the counterparty payee address to be compensated for delivering the MsgRecvPacket on chain test-1.

Asynchronous incentivization of a fungible token transfer

In this section we are going to pay fees using the asynchronous method, i.e. using MsgPayPacketFeeAsync to pay for a packet at a specified sequence. The asynchronous incentivization method is meant to incentivize packets that have already been submitted on the source chain and are still waiting to be relayed. Please note that this method is NOT meant to incentivize packets that will be submitted in the future (since it is not reliable to try to predict the packet sequence). In fact, ibc-go will not allow this and will reject the transaction.

Fees for a packet can be paid paid by one or more parties. We will see an example of both in the coming sections.

With single MsgPayPacketFeeAsync message - success scenario

We first inspect the balance of the account WALLET_1 on chain test-1:

# query the balance of WALLET_1 on chain test-1 
> simd q bank balances $WALLET_1 --node tcp://localhost:16657
balances:
- amount: "100000000000"
  denom: stake
pagination:
  next_key: null
  total: "0"

We are going to send 1000stake from WALLET_1 on chain test-1 to the account WALLET_3 on chain test-2. We will use the account WALLET_1 to incentivize the packet as well. In the next section we will see that it is also possible that multiple accounts incentive the same packet.

Please make sure that the relayer is not running before submitting the IBC transfer transaction. Otherwise the packet would be immediately relayed and we would not have time to submit the MsgPayPacketFeeAsync to incentivize it.

We send the IBC transfer from WALLET_1 to WALLET_3:

simd tx ibc-transfer transfer transfer channel-0 $WALLET_3 1000stake \
--from $WALLET_1 \
--home ./data/test-1 \
--keyring-backend test \
--chain-id test-1 \
--node tcp://localhost:16657

And we verify that the amount of 1000stake has been escrowed (i.e. the balance of WALLET_1 has decreased by 1000stake):

# query the balance of WALLET_1 on chain test-1 
> simd q bank balances $WALLET_1 --node tcp://localhost:16657
balances:
- amount: "99999999000"
  denom: stake
pagination:
  next_key: null
  total: "0"

Since we have started chains test-1 and test-2 from a clean state and we have created a new channel, we know that this is the first IBC packet that is transfered between chains test-1 and test-2 on this channel, and thus we know that we are going to incentivise sequence number 1. Nevertheless, the sequence number of a packet can be obtained by querying the transaction of the IBC transfer and inspecting the events for the send_packet attribute and checking the value for the key packet_sequence. The following is an extract of the relevant output for the IBC transfer transaction above:

logs:
- events:
  - attributes:
    - key: packet_data
      value: '{"amount":"1000","denom":"stake","receiver":"cosmos14zs2x38lmkw4eqvl3lpml5l8crzaxn6m7wuznx","sender":"cosmos1m9l358xunhhwds0568za49mzhvuxx9uxre5tud"}'
    - key: packet_data_hex
      value: 7b22616d6f756e74223a2231303030222c2264656e6f6d223a227374616b65222c227265636569766572223a22636f736d6f7331347a73327833386c6d6b77346571766c336c706d6c356c3863727a61786e366d3777757a6e78222c2273656e646572223a22636f736d6f73316d396c33353878756e6868776473303536387a6134396d7a6876757878397578726535747564227d
    - key: packet_timeout_height
      value: 2-1255
    - key: packet_timeout_timestamp
      value: "1651667579045137000"
    - key: packet_sequence
      value: "1"
    - key: packet_src_port
      value: transfer
    - key: packet_src_channel
      value: channel-0
    - key: packet_dst_port
      value: transfer
    - key: packet_dst_channel
      value: channel-0
    - key: packet_channel_ordering
      value: ORDER_UNORDERED
    - key: packet_connection
      value: connection-0
    type: send_packet

The relayer should be stopped and thus the packet should not be relayed yet. We are now going to asynchronously incentivize the packet with sequence 1 on port transfer and channel channel-0, signed by account WALLET_1 on chain test-1:

# incentivize packet with 50stake for MsgRecvPacket, 
# 25stake for MsgAcknowledgement and 10stake for MsgTimeout
simd tx ibc-fee pay-packet-fee transfer channel-0 1 \
--recv-fee 50stake \
--ack-fee 25stake \
--timeout-fee 10stake \
--from $WALLET_1 \
--home ./data/test-1 \
--keyring-backend test \
--chain-id test-1 \
--node tcp://localhost:16657

In the case of a successful relay of the packet, the receive and acknowledgement fees will be paid out to the relayer address which has submitted the MsgRecvPacket and MsgAcknowledgement messages, and the timeout fee will be reimbursed to the paying account (WALLET_1).

We check again the balance of the account WALLET_1 on chain test-1 and verify that the fees have been successfully escrowed and a total of 85stake is deducted from account balance (balance was 99999999000stake after the token transfer of 1000stake).

> simd q bank balances $WALLET_1 --node tcp://localhost:16657
balances:
- amount: "99999998915"
  denom: stake
pagination:
  next_key: null
  total: "0"

We can query chain test-1 to retrieve all unrelayed incentivized packets in all channels:

> simd q ibc-fee packets --node tcp://localhost:16657
incentivized_packets:
- packet_fees:
  - fee:
      ack_fee:
      - amount: "25"
        denom: stake
      recv_fee:
      - amount: "50"
        denom: stake
      timeout_fee:
      - amount: "10"
        denom: stake
    refund_address: cosmos1m9l358xunhhwds0568za49mzhvuxx9uxre5tud
    relayers: []
  packet_id:
    channel_id: channel-0
    port_id: transfer
    sequence: "1"

We can also query chain test-1 to retrieve all unrelayed incentivized packets on port transfer and channel channel-0:

> simd q ibc-fee packets-for-channel transfer channel-0 --node tcp://localhost:16657
incentivized_packets:
- packet_fees:
  - fee:
      ack_fee:
      - amount: "25"
        denom: stake
      recv_fee:
      - amount: "50"
        denom: stake
      timeout_fee:
      - amount: "10"
        denom: stake
    refund_address: cosmos1m9l358xunhhwds0568za49mzhvuxx9uxre5tud
    relayers: []
  packet_id:
    channel_id: channel-0
    port_id: transfer
    sequence: "1"

And we can even query chain test-1 to retrieve the fees for the unrelayed packet with sequence number 1 on port transfer and channel channel-0:

> simd q ibc-fee packet transfer channel-0 1 --node tcp://localhost:16657
incentivized_packet:
  packet_fees:
  - fee:
      ack_fee:
      - amount: "25"
        denom: stake
      recv_fee:
      - amount: "50"
        denom: stake
      timeout_fee:
      - amount: "10"
        denom: stake
    refund_address: cosmos1m9l358xunhhwds0568za49mzhvuxx9uxre5tud
    relayers: []
  packet_id:
    channel_id: channel-0
    port_id: transfer
    sequence: "1"

Notice that in all three sample responses above the refund_address (cosmos1m9l358xunhhwds0568za49mzhvuxx9uxre5tud) matches the address of WALLET_1, which is the account we have used to pay fees for the packet.

We can now start the relayer:

# start relaying if not previously started
make start-rly

# wait for the packet to be relayed and then stop the relayer again.

We can query the balance of account WALLET_3 on chain test-2 and see that it has indeed received an equivalent amount of vouchers for the 1000stake sent by WALLET_1:

# query the balance of WALLET_3 on chain test-2
> simd q bank balances $WALLET_3 --node tcp://localhost:26657
balances:
- amount: "1000"
  denom: ibc/C053D637CCA2A2BA030E2C5EE1B28A16F71CCB0E45E8BE52766DC1B241B77878
- amount: "100000000000"
  denom: stake
pagination:
  next_key: null
  total: "0"

We can now query as well the balance of the account WALLET_1 on chain test-1:

# query the balance of WALLET_1 on chain test-1 
> simd q bank balances $WALLET_1 --node tcp://localhost:16657
balances:
- amount: "99999998925"
  denom: stake
pagination:
  next_key: null
  total: "0"

And we can see that the balance of the account has been reduced by 1075stake (1000stake transfered to WALLET_3 + 50stake paid for the receive fee + 25stake paid for the acknowledgment fee). Also, the timeout fee of 10stake has been refunded and the relayer address should have therefore gained an additional 75stake for the receive and acknowledgement fees.

With multiple MsgPayPacketFeeAsync messages - timeout scenario

We check first the balances of the two accounts (WALLET_1 and WALLET_2) that we are going to use to incentivize the packet:

# query the balance of WALLET_1 on chain test-1 
> simd q bank balances $WALLET_1 --node tcp://localhost:16657
balances:
- amount: "99999998925"
  denom: stake
pagination:
  next_key: null
  total: "0"
# query the balance of WALLET_2 on chain test-1 
> simd q bank balances $WALLET_2 --node tcp://localhost:16657
balances:
- amount: "100000000000"
  denom: stake
pagination:
  next_key: null
  total: "0"

We are going to send again 1000stake from account WALLET_1 to account WALLET_3 on chain test-2. The balance of WALLET_3 has the equivalent amount of vouchers for the 1000stake that was sent in the previous section.

# query the balance of WALLET_3 on chain test-2 
> simd q bank balances $WALLET_3 --node tcp://localhost:26657
balances:
- amount: "1000"
  denom: ibc/C053D637CCA2A2BA030E2C5EE1B28A16F71CCB0E45E8BE52766DC1B241B77878
- amount: "100000000000"
  denom: stake
pagination:
  next_key: null
  total: "0"

Please make sure that the relayer is not running before submitting the IBC transfer transaction. Otherwise the packet would be immediately relayed and we would not have time to use the MsgPayPacketFeeAsync to incentivize it.

We send the IBC transfer from WALLET_1 to WALLET_3:

simd tx ibc-transfer transfer transfer channel-0 $WALLET_3 1000stake \
--from $WALLET_1 \
--home ./data/test-1 \
--keyring-backend test \
--chain-id test-1 \
--node tcp://localhost:16657

And we verify that the amount of 1000stake has been escrowed (i.e. the balance of WALLET_1 has decreased by 1000stake):

# query the balance of WALLET_1 on chain test-1 
> simd q bank balances $WALLET_1 --node tcp://localhost:16657
balances:
- amount: "99999997925"
  denom: stake
pagination:
  next_key: null
  total: "0"

We are going now to incentivize the packet (which has sequence number 2, since this is the second packet we send over channel-0) using two different accounts:

# pay fees with account WALLET_1
> simd tx ibc-fee pay-packet-fee transfer channel-0 2 \
--recv-fee 50stake \
--ack-fee 25stake \
--timeout-fee 10stake \
--from $WALLET_1 \
--home ./data/test-1 \
--keyring-backend test \
--chain-id test-1 \
--node tcp://localhost:16657
# pay fees with account WALLET_2
> simd tx ibc-fee pay-packet-fee transfer channel-0 2 \
--recv-fee 50stake \
--ack-fee 25stake \
--timeout-fee 10stake \
--from $WALLET_2 \
--home ./data/test-1 \
--keyring-backend test \
--chain-id test-1 \
--node tcp://localhost:16657

We can now check the balances of both accounts to see that the incentivization funds (85stake in total) have been escrowed successfully:

# query the balance of WALLET_1 on chain test-1
> simd q bank balances $WALLET_1 --node tcp://localhost:16657
balances:
- amount: "99999997840"
  denom: stake
pagination:
  next_key: null
  total: "0"
# query the balance of WALLET_2 on chain test-1
> simd q bank balances $WALLET_2 --node tcp://localhost:16657
balances:
- amount: "99999999915"
  denom: stake
pagination:
  next_key: null
  total: "0"

We can query chain test-1 to retrieve all unrelayed incentivized packets on port transfer and channel channel-0:

> simd q ibc-fee packets-for-channel transfer channel-0 --node tcp://localhost:16657
incentivized_packets:
packet_fees:
  - fee:
      ack_fee:
      - amount: "25"
        denom: stake
      recv_fee:
      - amount: "50"
        denom: stake
      timeout_fee:
      - amount: "10"
        denom: stake
    refund_address: cosmos10h9stc5v6ntgeygf5xf945njqq5h32r53uquvw
    relayers: []
  - fee:
      ack_fee:
      - amount: "25"
        denom: stake
      recv_fee:
      - amount: "50"
        denom: stake
      timeout_fee:
      - amount: "10"
        denom: stake
    refund_address: cosmos1m9l358xunhhwds0568za49mzhvuxx9uxre5tud
    relayers: []
  packet_id:
    channel_id: channel-0
    port_id: transfer
    sequence: "2"

We see that packet with sequence number 2 has two different fees attached to it, and the refund addresses are the addresses for WALLET_1 and WALLET_2. We can also query chain test-1 to check if the specific packet with sequence number 2 on port transfer and channel channel-0 has any fees:

> simd q ibc-fee packet transfer channel-0 2 --node tcp://localhost:16657
incentivized_packet:
  packet_fees:
  - fee:
      ack_fee:
      - amount: "25"
        denom: stake
      recv_fee:
      - amount: "50"
        denom: stake
      timeout_fee:
      - amount: "10"
        denom: stake
    refund_address: cosmos10h9stc5v6ntgeygf5xf945njqq5h32r53uquvw
    relayers: []
  - fee:
      ack_fee:
      - amount: "25"
        denom: stake
      recv_fee:
      - amount: "50"
        denom: stake
      timeout_fee:
      - amount: "10"
        denom: stake
    refund_address: cosmos1m9l358xunhhwds0568za49mzhvuxx9uxre5tud
    relayers: []
  packet_id:
    channel_id: channel-0
    port_id: transfer
    sequence: "2"

We can also use different queries to retrieve the total amount escrowed for the different messages. For MsgRecvPacket:

> simd q ibc-fee total-recv-fees transfer channel-0 2 --node http://localhost:16657
recv_fees:
- amount: "100"
  denom: stake

For MsgAcknowledgement:

> simd q ibc-fee total-ack-fees transfer channel-0 2 --node http://localhost:16657
ack_fees:
- amount: "50"
  denom: stake

For MsgTimeout:

> simd q ibc-fee total-timeout-fees transfer channel-0 2 --node http://localhost:16657
timeout_fees:
- amount: "20"
  denom: stake

The default timeout of the IBC packet is 10 minutes, so we can either wait that amount of time before starting the relayer:

# start relaying if not previously started
make start-rly

#wait for the packet to be timed out and then stop the relayer again.

or using the --packet-timeout-height flag set a height which is guaranteed to timeout in the original transfer tx:

simd tx ibc-transfer transfer transfer channel-0 $WALLET_3 1000stake \
--from $WALLET_1 \
--home ./data/test-1 \
--keyring-backend test \
--chain-id test-1 \
--node tcp://localhost:16657 \
--packet-timeout-height 0-3

Once the packet has timed out, we query the balances of accounts WALLET_1 and WALLET_2:

# query the balance of WALLET_1 on chain test-1
> simd q bank balances $WALLET_1 --node tcp://localhost:16657
balances:
- amount: "99999998915"
  denom: stake
pagination:
  next_key: null
  total: "0"
# query the balance of WALLET_1 on chain test-1
> simd q bank balances $WALLET_2 --node tcp://localhost:16657
balances:
- amount: "99999999990"
  denom: stake
pagination:
  next_key: null
  total: "0"

And we can verify that only an amout of 10stake has been deducted from the balances of WALLET_1 and WALLET_2 comparing to the balances that they had before the IBC packet was submitted and the fees escrowed. And at the same time, the relayer account RLY_1 should have gained 20stake from submitting MsgTimeout on chain test-1.

Synchronous incentivization of a fungible token transfer

In this section we are going to pay fees using the synchronous method, i.e. using MsgPayPacketFee to incentivize the IBC packet contained in the same transaction.

Similarly as with the asynchronous method, fees for a packet can be paid by one or more parties. We will see an example of both in the coming sections.

Multi-message transaction with single MsgPayPacketFee message - success scenario

We begin by inspecting the balance of the account WALLET_1, which is again the account we are going to use to incentivize tha packet:

# query the balance of WALLET_1 on chain test-1 
> simd q bank balances $WALLET_1 --node tcp://localhost:16657
balances:
- amount: "99999998915"
  denom: stake
pagination:
  next_key: null
  total: "0"

Since we are going to broadcast a multi-message transaction containing the messages both for the incentivization of the packet and the IBC packet itself, there would be no need to stop the relayer to prevent it from relaying the IBC packet before we are able to incentivizate it, as in the asynchronous method. However, we stop the relayer now so that we are able to query the channel for the fee information for unrelayed incentivized packets.

We first generate (not execute) an IBC transfer transaction (again 1000stake from WALLET_1 to WALLET_3):

simd tx ibc-transfer transfer transfer channel-0 $WALLET_3 1000stake \
--from $WALLET_1 \
--home ./data/test-1 \
--keyring-backend test \
--chain-id test-1 \
--node tcp://localhost:16657 \
--generate-only > transfer.json

Then we prepend a MsgPayPacketFee, sign the transaction and broadcast it. Please note that jq is used to manipulate the transaction JSON file, making it a multi-message transaction. In practice, this multi-message transaction would be built using a gRPC or web client, for example, a web-based wallet application could fulfill this role. Note also that the signer field uses the address of WALLET_1.

jq '.body.messages |= [{"@type":"/ibc.applications.fee.v1.MsgPayPacketFee","fee": {"recv_fee": [{"denom": "stake", "amount": "50"}], "ack_fee": [{"denom": "stake", "amount": "25"}], "timeout_fee": [{"denom": "stake", "amount": "10"}]}, "source_port_id": "transfer", "source_channel_id": "channel-0", "signer": "cosmos1m9l358xunhhwds0568za49mzhvuxx9uxre5tud" }] + .' transfer.json > incentivized_transfer.json

simd tx sign incentivized_transfer.json \
--from $WALLET_1 \
--keyring-backend test \
--chain-id test-1 \
--home ./data/test-1 \
--node tcp://localhost:16657 > signed.json
simd tx broadcast signed.json \
--home ./data/test-1 \
--chain-id test-1 \
--node tcp://localhost:16657

We now query chain test-1 to retrieve all unrelayed incentivized packets on port transfer and channel channel-0:

> simd q ibc-fee packets-for-channel transfer channel-0 --node tcp://localhost:16657
incentivized_packets:
- packet_fees:
  - fee:
      ack_fee:
      - amount: "25"
        denom: stake
      recv_fee:
      - amount: "50"
        denom: stake
      timeout_fee:
      - amount: "10"
        denom: stake
    refund_address: cosmos1m9l358xunhhwds0568za49mzhvuxx9uxre5tud
    relayers: []
  packet_id:
    channel_id: channel-0
    port_id: transfer
    sequence: "3"

The information matches what we expect (i.e. fees paid by WALLET_1 for the next IBC packet, which has sequence number 3). We can now start the relayer:

# start relaying if not previously started
make start-rly

# wait for the packet to be relayed and then stop the relayer again.

We can query the balance of account WALLET_3 on chain test-2 and see that it has indeed received an equivalent amount of vouchers for the 1000stake sent by WALLET_1:

# query the balance of WALLET_3 on chain test-2
> simd q bank balances $WALLET_3 --node tcp://localhost:26657
balances:
- amount: "2000"
  denom: ibc/C053D637CCA2A2BA030E2C5EE1B28A16F71CCB0E45E8BE52766DC1B241B77878
- amount: "100000000000"
  denom: stake
pagination:
  next_key: null
  total: "0"

We check as well the balance for account WALLET_1 on chain test-1:

# query the balance of WALLET_1 on chain test-1 
> simd q bank balances $WALLET_1 --node tcp://localhost:16657
balances:
- amount: "99999997840"
  denom: stake
pagination:
  next_key: null
  total: "0"

An amount of 1075stake has been deducted, which is what we expected: 1000stake have been transfered to WALLET_3 and 75stake have been paid othe receive and acknowledgment fees. The timeout fee has been refunded to WALLET_1 and the relayer address RLY_1 should have gained 75stake for submitting the MsgRecvPacket and the MsgAcknowledgement messages.

Multi-message transaction with multiple MsgPayPacketFee messages - timeout scenario

We check first the balances of the two accounts (WALLET_1 and WALLET_2) that we are going to use to incentivize the packet:

# query the balance of WALLET_1 on chain test-1 
> simd q bank balances $WALLET_1 --node tcp://localhost:16657
balances:
- amount: "99999997840"
  denom: stake
pagination:
  next_key: null
  total: "0"
# query the balance of WALLET_2 on chain test-1 
> simd q bank balances $WALLET_2 --node tcp://localhost:16657
balances:
- amount: "99999999990"
  denom: stake
pagination:
  next_key: null
  total: "0"

Please stop the relayer now so that we are able to query the channel for the fee information for unrelayed incentivized packets.

Similarly as in the previous section we first generate (not execute) an IBC transfer transaction (again 1000stake from WALLET_1 to WALLET_3):

simd tx ibc-transfer transfer transfer channel-0 $WALLET_3 1000stake \
--from $WALLET_1 \
--home ./data/test-1 \
--keyring-backend test \
--chain-id test-1 \
--node tcp://localhost:16657 \
--generate-only > transfer.json

Using again jq we are going to prepend this time two MsgPayPacketFee messages to the transaction JSON file, making it a multi-message transaction:

jq '.body.messages |= [{"@type":"/ibc.applications.fee.v1.MsgPayPacketFee","fee": {"recv_fee": [{"denom": "stake", "amount": "50"}], "ack_fee": [{"denom": "stake", "amount": "25"}], "timeout_fee": [{"denom": "stake", "amount": "10"}]}, "source_port_id": "transfer", "source_channel_id": "channel-0", "signer": "cosmos1m9l358xunhhwds0568za49mzhvuxx9uxre5tud" },{"@type":"/ibc.applications.fee.v1.MsgPayPacketFee","fee": {"recv_fee": [{"denom": "stake", "amount": "50"}], "ack_fee": [{"denom": "stake", "amount": "25"}], "timeout_fee": [{"denom": "stake", "amount": "10"}]}, "source_port_id": "transfer", "source_channel_id": "channel-0", "signer": "cosmos10h9stc5v6ntgeygf5xf945njqq5h32r53uquvw" }] + .' transfer.json > incentivized_transfer.json

The signer of the first MsgPayPacketFee message is the address of WALLET_1 and the signer of the second MsgPayPacketFee message is the address of WALLET_2.

Now we need to sign the transaction, and since we need to sign it with both WALLET_1 and WALLET_2 accounts, we need to use the flag --sign-mode amino-json. The first signer (WALLET_1) will sign the unsigned transaction file (incentivized_transfer.json), but the second signer (WALLET_2) will sign the file signed by previous signer (signed_wallet1.json):

# sign transaction with `WALLET_1` account
simd tx sign incentivized_transfer.json \
--from $WALLET_1 \
--sign-mode amino-json \
--keyring-backend test \
--chain-id test-1 \
--home ./data/test-1 \
--node tcp://localhost:16657 > signed_wallet1.json
# sign transaction with `WALLET_2` account
simd tx sign signed_wallet1.json \
--from $WALLET_2 \
--sign-mode amino-json \
--keyring-backend test \
--chain-id test-1 \
--home ./data/test-1 \
--node tcp://localhost:16657 > signed_wallet1_and_wallet2.json

And finally we broadcast the signed transaction:

simd tx broadcast signed_wallet1_and_wallet2.json \
--home ./data/test-1 \
--chain-id test-1 \
--node tcp://localhost:16657

We now query chain test-1 to retrieve all unrelayed incentivized packets on port transfer and channel channel-0:

> simd q ibc-fee packets-for-channel transfer channel-0 --node tcp://localhost:16657
incentivized_packets:
- packet_fees:
  - fee:
      ack_fee:
      - amount: "25"
        denom: stake
      recv_fee:
      - amount: "50"
        denom: stake
      timeout_fee:
      - amount: "10"
        denom: stake
    refund_address: cosmos10h9stc5v6ntgeygf5xf945njqq5h32r53uquvw
    relayers: []
  - fee:
      ack_fee:
      - amount: "25"
        denom: stake
      recv_fee:
      - amount: "50"
        denom: stake
      timeout_fee:
      - amount: "10"
        denom: stake
    refund_address: cosmos1m9l358xunhhwds0568za49mzhvuxx9uxre5tud
    relayers: []
  packet_id:
    channel_id: channel-0
    port_id: transfer
    sequence: "4"

This is the information we expected, with two fees attached for the next packet (sequence number 4). We query the balances of the accounts used for paying fees:

# query the balance of WALLET_1 on chain test-1 
> simd q bank balances $WALLET_1 --node tcp://localhost:16657
balances:
- amount: "99999996755"
  denom: stake
pagination:
  next_key: null
  total: "0"
# query the balance of WALLET_2 on chain test-1 
> simd q bank balances $WALLET_2 --node tcp://localhost:16657
balances:
- amount: "99999999905"
  denom: stake
pagination:
  next_key: null
  total: "0"

We see that an amount of 1085stake has been deducted from the balance of WALLET_1 (1000stake that shoud be transfered to WALLET_3 + 85stake for all fees) and an amount of 85stake has been deducted from the balance of WALLET_3 also for the fees. These amounts are in escrow at the moment, since the relayer is stopped and the packet has not been relayed yet.

We wait enought time to make sure that the IBC packet will time out before starting the relayer:

# start relaying if not previously started
make start-rly

#wait for the packet to be timed out and then stop the relayer again.

Once the packet has timed out, we query again the balances of accounts WALLET_1 and WALLET_2:

# query the balance of WALLET_1 on chain test-1
> simd q bank balances $WALLET_1 --node tcp://localhost:16657
balances:
- amount: "99999997830"
  denom: stake
pagination:
  next_key: null
  total: "0"
# query the balance of WALLET_1 on chain test-1
> simd q bank balances $WALLET_2 --node tcp://localhost:16657
balances:
- amount: "99999999980"
  denom: stake
pagination:
  next_key: null
  total: "0"

And we can verify that only an amout of 10stake has been deducted from the balances of WALLET_1 and WALLET_2 comparing to the balances that they had before the IBC packet was submitted and the fees escrowed. The other fees have been refunded, as well as the 1000stake that WALLET_1 intended to transfer to WALLET_3. And at the same time, the relayer account RLY_1 should have gained 20stake for submitting the MsgTimeout on chain test-1.