What has to happen to IBC tokens before a Cosmos chain halts for good

Two threads here this month made me look at the same problem from both ends. In one (the UX Chain thread, t/17483), a holder has ATOM on UX Chain, which halted in July, and RoboMcGobo explained why the Hub can’t just release it: the escrow only opens when the other chain burns the voucher, and that chain makes no blocks. In the other, Akash’s proposal 346 (voting until 2 November) starts a migration that ends with akashnet-2 halting 90 days after a later cutover.

So I went through ibc-go (v10.7.0, the version Gaia v29.0.1 runs; akashnet-2 runs v10.5.0, and the transfer code I cite is the same there) to list what has to happen before the last block, because after it nobody can fix anything from the other side. This is not about whether any chain should leave. It’s only about the tokens.

Why tokens get stuck

  • When a token leaves its home chain, it goes into that chain’s escrow for the channel and the other side mints a voucher. The escrow releases it only when a packet shows the voucher was burned, and that packet has to come from a live chain.
  • Transfer channels can’t be closed. The transfer module rejects ChanCloseInit, so there is no close-and-settle step.
  • Every packet, ack and timeout is checked through the counterparty’s light client, and the client has to be Active. A Tendermint client expires one trusting period after its latest header: 14 days for the Hub’s client of akashnet-2.
  • Recovering an expired client also needs the chain alive. MsgRecoverClient only takes a substitute client that is Active and ahead of the old one, and that means new blocks.

You can see the end state on the Hub today. Its client for umee-1 was last updated with a header from 8 July and has been Expired since its 13 day trusting period ran out. The transfer channel still shows OPEN.

It’s not only tokens on the halting chain

Each hop has its own escrow, so tokens that only passed through a chain depend on it too. Today the Hub’s escrow for the Akash channel holds about 544 ATOM, matching the ATOM voucher supply on akashnet-2. Of those vouchers, 162.53 ATOM are locked in Akash’s own escrow for its Osmosis channel, and the two hop voucher on Osmosis has exactly that supply, to the uatom.

So whoever holds that voucher on Osmosis holds real ATOM that is sitting in the Hub’s escrow, but the only way home is back through Akash, hop by hop, before the halt. Sending it from Osmosis straight to the Hub doesn’t unwind anything. It just adds a third hop.

Akash’s plan (AEP-79 in the akash-network/AEP repo) already covers a lot of this: a return window for AKT held on other chains, a per-channel escrow ledger, and an open question on foreign vouchers still on akashnet-2 at the halt. Tokens that only passed through are the part I’d add to any plan like it.

Before the last block

  1. Count both directions, per channel, including pass-through paths. Your token on other chains is backed by your escrows. Other tokens on your chain, and anything that went through you to a third chain, need their route back. Publish the numbers and the route for each.
  2. Refuse late transfers with an error ack, don’t just drop them. With receive_enabled off, the transfer module returns an error, an error acknowledgement is written, and the sender gets a refund on their own chain once the ack is relayed. If you filter MsgRecvPacket in an ante handler instead, the packet is never received and the only refund is a timeout, which needs your chain’s clock to pass the packet’s timeout. A packet whose timeout is after your last block can never time out.
  3. Remember that switch is global. receive_enabled is checked before the module decides between minting a voucher and releasing escrow (relay.go L126-L128 in v10.7.0), so turning it off also blocks your own token coming home. If you still need returns, refuse only the packets that would mint a new voucher, with a small middleware that writes the error ack.
  4. Relay to the very end. Keep relayers running after the last transfer, update every counterparty’s client to the final height, and clear every pending packet and ack before those clients expire. In the UX case the Hub’s client never got a header after 8 July, while the thread gives 10 July as the halt date.
  5. Halt at a planned height and keep the export. A software upgrade plan with no handler in the binary stops every node at the same height, and the export at that height is the record any later claim or restart works from.

If you hold ATOM, or anything else, on or through a chain that announces a wind-down: send it back the way it came while transfers are still open. After the halt the Hub can’t help, as RoboMcGobo explained.

For teams that have already wound down a chain: what did you do with foreign tokens still on it at the halt? And has anyone here used receive_enabled for this in practice?