Back to articles
Intermediate

IBC Channel Security: How Unordered Channels Enable Cross-Chain Replay Attacks on Cosmos

The Inter-Blockchain Communication protocol (IBC) is one of the most sophisticated pieces of engineering in the blockchain space.

@0xrafasecFebruary 18, 2026decentralized_systems_security

Available in Português

Share:

IBC Channel Security: How Unordered Channels Enable Cross-Chain Replay Attacks on Cosmos

Legal & Ethical Disclaimer

This content is provided for EDUCATIONAL and AUTHORIZED SECURITY TESTING purposes only.

DO
  • Use these techniques on systems you own or have explicit written permission to test
  • Practice in authorized lab environments (VulnHub, HackTheBox, DVWA, etc.)
  • Follow responsible disclosure practices when finding vulnerabilities
  • Use knowledge for defensive security and authorized penetration testing
DO NOT
  • Access systems without explicit authorization
  • Use these techniques for malicious purposes
  • Deploy exploits against production systems you don't own
  • Share working exploits for unpatched vulnerabilities

Legal warning

Unauthorized access to computer systems is illegal in most jurisdictions (e.g. CFAA in the US, Computer Misuse Act in the UK). Violators may face criminal prosecution and civil liability. The author and publisher assume no liability for misuse of this information. By continuing, you agree to use this knowledge ethically and legally.

Hook & Context

The Inter-Blockchain Communication protocol (IBC) is one of the most sophisticated pieces of engineering in the blockchain space. It enables sovereign chains to exchange arbitrary data with cryptographic guarantees — a genuinely hard problem. But cryptographic soundness at the transport layer does not automatically translate to security at the application layer, and nowhere is this tension more visible than in the distinction between ordered and unordered channels.

Most IBC applications default to unordered channels because they're operationally forgiving — packets can arrive out of order, relayers can retry without strict coordination, and the UX feels smoother. That operational convenience comes with a structural tradeoff: unordered channels do not enforce global sequence numbers at the channel level. In certain application designs, particularly those that track state on a per-packet basis without embedding sufficient uniqueness into the packet data itself, this creates a window for replay. A finalized packet — one whose acknowledgment has already been processed — can sometimes be resubmitted by a malicious relayer or counterparty to trigger a second execution of the receiving chain's business logic.

This article dissects that risk from first principles. We'll build the mental model for IBC's trust hierarchy, show exactly where the sequence enforcement gap lives in unordered channels, walk through a proof-of-concept replay scenario involving fund drainage, and then map out the mitigations that secure IBC application developers should be implementing today.


TL;DR

ConceptKey Point
Ordered channelsEnforce strict packet sequence; replay is structurally impossible
Unordered channelsNo global sequence enforcement; application layer must self-defend
Replay attack surfaceMalicious or compromised relayer resubmits an acknowledged packet
Root causeMissing nonce/sequence binding in application packet data
Primary mitigationApplication-level receipt tracking + packet commitment pruning
Risk levelHigh when combined with fund transfer or state mutation logic

Foundations & Theory

The IBC Trust Model

IBC is not a bridge in the traditional sense — it's a protocol for verified inter-chain communication. Its security derives from light client verification: chain A maintains a light client of chain B, and vice versa. Every packet transmitted includes a cryptographic commitment that the receiving chain can verify against a known, trusted block header. No trusted third party is required; trust flows from the consensus of each participating chain.

The channel abstraction sits on top of this transport layer. A channel is a named, versioned, ordered or unordered pipe between two port-bound modules on two chains. The ordering property is set at channel creation during the four-step handshake (ChanOpenInitChanOpenTryChanOpenAckChanOpenConfirm) and cannot be changed after the fact.

What "Ordered" Actually Enforces

In an ordered channel, the IBC core module tracks a nextSequenceSend on the sending side and a nextSequenceRecv on the receiving side. Packets must be delivered in strict ascending sequence. If packet 5 arrives before packet 4, the IBC module will reject it. This is enforced at the protocol level — not the application level — and it makes replay trivially impossible: a packet with sequence N can only ever be received once, because the receiver's expected sequence counter immediately advances to N+1.

What "Unordered" Actually Guarantees (and Doesn't)

Unordered channels replace the global sequence counter with a per-packet receipt store. When a packet is successfully received, the IBC module writes a receipt to packetReceipt/{channelId}/{sequence}. Subsequent delivery attempts for the same packet are rejected if the receipt exists.

Here's the critical nuance: packet receipt stores are pruned after acknowledgment processing. Once the sending chain writes the acknowledgment commitment and the relayer delivers it back, the original packet commitment on the sending side is deleted. Depending on the implementation path and whether the application properly handles the receipt, a window can open where:

  1. The receipt has been deleted or never written (due to a bug or non-standard path).
  2. The packet commitment has been cleared on the sending chain.
  3. The receiving chain's application state has been mutated once already.

A resubmission in this window would pass the IBC module's receipt check and re-enter the application's OnRecvPacket handler.


Where It Fits in the Workflow

Loading diagram…

Key Concepts in Depth

1. Packet Lifecycle and Where Receipts Live

Understanding the exact lifecycle is essential for pinpointing the attack surface.

Loading diagram…

The receipt on Chain B is the gate. If it's written correctly and never pruned before acknowledgment is fully processed, replay is blocked at the IBC module level. The vulnerability emerges in edge cases:

  • Application modules that implement custom receive logic and forget to let the IBC module write the receipt first.
  • Chains running non-standard IBC versions where pruning logic is aggressive.
  • Test environments using the Go IBC testing framework where RecvPacket is called without going through the full commit/proof path, which can silently skip receipt writes.

2. The Replay Attack Scenario: Step by Step

Consider a simplified cross-chain token transfer application running over an unordered channel where the application developer made a subtle mistake: they store received amounts keyed only by (sender, denom) rather than by (sender, denom, sequence).

Setup:

  • Chain A sends packet with sequence 42: "transfer 1000 ATOM to address X on Chain B"
  • Relayer delivers the packet. Chain B credits 1000 ATOM to address X. Receipt for seq 42 is written.
  • Relayer delivers the acknowledgment to Chain A. Chain A deletes its commitment.

Attack: A malicious relayer now constructs a MsgRecvPacket containing the original packet data for sequence 42, paired with a valid Merkle proof from a historical block on Chain A where the commitment still existed. If Chain B's receipt for sequence 42 has been deleted (due to aggressive pruning or a bug), the IBC module will not find an existing receipt and will call OnRecvPacket again. Chain B credits another 1000 ATOM to address X. The attacker has effectively doubled their funds.

Loading diagram…

3. Why the Go IBC Testing Framework Can Mask This

The ibctesting package in cosmos/ibc-go provides fast-path helpers like path.RelayPacket() that skip actual Tendermint consensus and proof generation. In a testing environment, this means:

go
// This does NOT simulate the full receipt-write path
suite.coordinator.RelayAndAckPendingPackets(path)

Developers who only test with the framework may never discover that their OnRecvPacket handler is non-idempotent, because the test infrastructure silently handles deduplication that the real network does not guarantee under all conditions. Always supplement framework tests with integration tests using real gaiad nodes and hermes.

4. Constructing the PoC with hermes and gaiad

The PoC workflow does not require exploiting a protocol bug — it's about exploiting application logic gaps through legitimate protocol paths.

bash
# Start two local chains with ignite CLI
ignite chain serve --config chain_a.yml &
ignite chain serve --config chain_b.yml &

# Configure hermes to connect both chains
hermes config validate

# Create an UNORDERED channel between the two chains
hermes create channel \
  --a-chain chain-a \
  --b-chain chain-b \
  --a-port transfer \
  --b-port transfer \
  --channel-version ics20-1 \
  --order UNORDERED

# Send a packet and capture the tx hash + block height
gaiad tx ibc-transfer transfer \
  channel-0 cosmos1<recipient> 1000uatom \
  --from attacker --chain-id chain-a

# Record the block height H where the commitment exists
# Attempt relay with hermes (normal path)
hermes start

# After acknowledgment, manually construct MsgRecvPacket
# with proof anchored at block H (pre-acknowledgment)
# Submit via gaiad tx broadcast

The key step is capturing the CommitmentProof at block height H before the commitment is deleted. This is a standard Merkle proof query:

bash
gaiad query ibc channel packet-commitment transfer channel-0 42 \
  --height <H> \
  --prove true \
  --chain-id chain-a

If the receiving chain's application layer does not independently verify that this packet has already been processed, the resubmission succeeds.

5. Application-Level Defenses

The IBC module's receipt mechanism is the first line of defense, but application developers must add a second line:

a) Embed sequence numbers in packet data. Every packet processed by OnRecvPacket should include the channel sequence in the application-level payload. The handler should verify this matches and record it in a processed-sequence map.

b) Implement application-level receipt tracking. Maintain a store keyed by (portId, channelId, sequence) → bool. Write to it at the start of OnRecvPacket, before any state mutations. Revert if the key already exists.

go
func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketData) error {
    // Application-level idempotency guard
    if k.HasPacketProcessed(ctx, packet.DestinationPort, packet.DestinationChannel, packet.Sequence) {
        return sdkerrors.Wrap(types.ErrDuplicatePacket, "packet already processed")
    }
    k.SetPacketProcessed(ctx, packet.DestinationPort, packet.DestinationChannel, packet.Sequence)

    // ... rest of business logic
}

c) Use ordered channels when delivery order matters to application semantics. If your application cannot tolerate replays and requires strict sequencing, the cost of ordered channels (blocking on missing packets) is worth the structural guarantee.


Alternatives & Comparison

ApproachReplay ProtectionThroughputComplexityBest For
Ordered channels✅ Protocol-enforcedLower (head-of-line blocking)Low (protocol handles it)High-value transfers, stateful protocols
Unordered + app receipts✅ Application-enforcedHighMediumGeneral messaging, fungible transfers
Unordered + nonce in payload⚠️ Partial (requires correct verification)HighMedium-HighCustom protocols with unique packet data
Unordered, no defense❌ No protectionHighLow (unsafe)⚠️ Never in production
Middleware interception✅ Defense-in-depthDepends on middlewareHighStacks requiring cross-cutting guarantees

Ordered channels are the structurally simplest solution but impose liveness constraints — if a packet is lost, no subsequent packets can be delivered until it's resolved. For high-throughput applications like DEX order books or oracle feeds, this is unacceptable.

Unordered channels with application-level receipts are the standard production pattern for ICS-20 fungible token transfers. The reference implementation in ibc-go does this correctly; the risk emerges in custom application modules that roll their own OnRecvPacket without auditing for idempotency.

IBC middleware (introduced in ibc-go v3+) allows replay protection logic to be factored into a reusable layer that wraps application modules, which is architecturally elegant for protocol designers building on top of IBC.


Takeaways & Further Reading

Further Reading & References

Found this article interesting? Follow me on X and LinkedIn.