All skills
Skillintermediate

IBC VULNERABILITY PATTERNS

> IBC-specific vulnerabilities for Cosmos chains integrating ibc-go. 16 vulnerability patterns covering middleware bugs, channel/denom handling, Interchain Accounts misuse, reentrancy, and general IBC integration checks. Referenced by [SKILL.md](../SKILL.md).

Claude Code Knowledge Pack7/10/2026

Overview

IBC Vulnerability Patterns & Audit Checks

IBC-specific vulnerabilities for Cosmos chains integrating ibc-go. 16 vulnerability patterns covering middleware bugs, channel/denom handling, Interchain Accounts misuse, reentrancy, and general IBC integration checks. Referenced by SKILL.md.

1 UNTRUSTED IBC COUNTERPARTY CHAIN

Description: An attacker can create a malicious chain and spoof IBC message fields. The counterparty chain controls the sender, memo, and other packet data fields. Any IBC middleware or hook that acts on these fields must validate against a channel/counterparty whitelist. With ICS-20 v2 forwarding (ibc-go v9+), a forwarded packet may originate from a chain several hops away with no direct trust relationship.

What to Check:

  • Chains that can connect are authorized via a channel/counterparty whitelist
  • IBC middleware and hooks that act on sender, memo, or other packet data fields validate the source channel
  • With ICS-20 v2 forwarding, multi-hop trust implications are considered (forwarded packets may originate from untrusted chains)

Grep Patterns (use Grep tool with glob: "*.go" in x/):

  • OnRecvPacket|OnAcknowledgementPacket — then check results for sender or memo usage

References:


2 UNSAFE LookupModuleByPort AND LookupModuleByChannel USAGE

Description: [ibc-go < v10 only] LookupModuleByPort and LookupModuleByChannel were problematic APIs in the IBC capability module. They were removed in v10 along with the capability module (#7239, #7252), replaced by a static port router (PortKeeper.GetRoute()). On older ibc-go versions, incorrect usage of these APIs can lead to wrong module routing.

What to Check:

  • If module has access to ibc keeper, LookupModuleByPort and LookupModuleByChannel usages are carefully reviewed
  • On ibc-go v10+, verify these APIs are no longer used and the static port router is used instead

Grep Patterns (use Grep tool with glob: "*.go" in x/):

  • LookupModuleByPort|LookupModuleByChannel

References:


3 MISSING IBC CHANNEL CLOSE VALIDATION

Description: If a user should NOT be able to close an IBC channel, the OnChanCloseInit callback must return an error. Failing to do so allows any user to close the channel, disrupting the IBC application. The transfer module explicitly rejects channel close as a reference pattern.

Detection Patterns:

// VULNERABLE: Missing or permissive channel close callback
func (im IBCModule) OnChanCloseInit(ctx sdk.Context, portID, channelID string) error {
    return nil  // Allows anyone to close the channel!
}

What to Check:

  • OnChanCloseInit returns an error if channels should not be closable by users
  • Channel close behavior is explicitly tested

Mitigation:

// SECURE: Reject channel close
func (im IBCModule) OnChanCloseInit(ctx sdk.Context, portID, channelID string) error {
    return errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "channel close not allowed")
}

4 INCORRECT IBC PACKET TIMEOUT HANDLING

Description: When an IBC packet times out or fails, the application must correctly refund tokens. On the source chain, escrowed tokens must be unescrowed; on the sink chain, burned vouchers must be minted back. See refundPacketToken in v8 (renamed to refundPacketTokens in v10 with InternalTransferRepresentation type). The core logic (source chain = unescrow, sink chain = mint back) is unchanged across versions.

What to Check:

  • Application handles packet timeout correctly — tokens are refunded on failed sends
  • Refund logic uses the same source/sink determination as the send path
  • OnTimeoutPacket is implemented and tested for all packet types
  • State changes from a failed send are fully rolled back

Grep Patterns (use Grep tool with glob: "*.go" in x/):

  • OnTimeoutPacket|refundPacketToken

5 INCORRECT IBC CHANNEL ORDERING TYPE

Description: ORDERED channels are closed on timeout — if a single packet times out, the entire channel is permanently closed. If message order doesn't matter, use UNORDERED to avoid this. Note: ORDERED_ALLOW_TIMEOUT was proposed but never merged into ibc-go.

What to Check:

  • Channel type (ORDERED vs UNORDERED) matches application invariants
  • If using ORDERED channels, the application handles channel closure on timeout gracefully
  • If message order doesn't matter, UNORDERED is used to avoid timeout-triggered channel closure

6 IBC TRANSFERS BREAKING INTERNAL BOOKKEEPING

Description: IBC transfers always mint/burn coins via x/bank. If a module maintains internal accounting (e.g., total deposited, collateral tracking), IBC transfers can silently break those invariants by moving tokens without going through the module's accounting path. Use blocklist, SendEnabled, or SendRestriction to control. In newer Cosmos SDK versions, SendRestriction is checked before deducting coins (SDK #20517), making it a more reliable defense. With ICS-20 v2 forwarding, tokens can arrive via unexpected multi-hop paths, amplifying bookkeeping risks.

What to Check:

  • IBC transfers cannot bypass internal bookkeeping for tokens managed by the module
  • Module uses blocklist, SendEnabled, or SendRestriction to prevent unauthorized IBC movement of managed tokens
  • With ICS-20 v2 forwarding, multi-hop token arrival paths are considered

Grep Patterns (use Grep tool with glob: "*.go" in x/):

  • SendRestriction|BlockedAddr|SendEnabled

References:


7 NON-DETERMINISTIC JSON IN IBC ACKNOWLEDGEMENTS

Description: Custom IBC middleware that constructs or deserializes acknowledgement bytes using Go's encoding/json can produce non-deterministic results across different validators. Go's JSON unmarshaller does not guarantee consistent output for map key ordering, numeric precision for large numbers, unknown field handling, and Unicode normalization. If any middleware in the chain's IBC application stack introduces a custom ack format or manipulates ack bytes using standard JSON, different validators may compute different state transitions, halting the chain. Any user that can open an IBC channel can introduce this state — this is trivially exploitable.

Known incidents: ISA-2025-001 (GHSA-4wf3-5qj9-368v), ASA-2025-004 (GHSA-jg6f-48ff-5xrw) Affected versions: ibc-go >= v7 (earlier versions may also be affected) Patched in: v7.10.0, v8.7.0

Detection Patterns:

// VULNERABLE: Using encoding/json for IBC acknowledgements
func (m MyMiddleware) OnAcknowledgementPacket(ctx sdk.Context, packet channeltypes.Packet, ack []byte) error {
    var result map[string]interface{}
    json.Unmarshal(ack, &result)  // NON-DETERMINISTIC for maps!
    newAck, _ := json.Marshal(result)  // Different output across validators
    // ...
}

What to Check:

  • Custom IBC middleware does NOT produce acknowledgement bytes using json.Marshal on types containing maps or floating-point numbers
  • Acknowledgement serialization uses protobuf (not JSON), or a deterministic JSON marshaller (e.g., cosmossdk.io/x/tx/signing/aminojson)
  • All middleware in the IBC stack uses deterministic serialization
  • Tests do not assume determinism from running on a single machine

Mitigation:

// SECURE: Use protobuf for acknowledgement serialization
func (m MyMiddleware) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet) ibcexported.Acknowledgement {
    // Use the channeltypes ack types which serialize via protobuf
    return channeltypes.NewResultAcknowledgement([]byte{byte(1)})
}

Grep Patterns (use Grep tool with glob: "*.go" in x/):

  • json\\.(Marshal|Unmarshal) — then check results for ack or acknowledgement context

References:


8 PFM INVERTED ESCROW/MINT LOGIC

Description: In packet-forward-middleware, the moveFundsToUserRecoverableAccount() function (for nonrefundable forwarded packets) has the if/else branches for escrow vs. mint inverted relative to what SendTransfer() actually does. When a nonrefundable forward fails, the buggy code MINTS new tokens instead of UNESCROWING (or vice versa), inflating supply. An attacker sends tokens from Chain A to Chain B with a PFM forward to Chain C (marked nonrefundable). When the forward fails, PFM mints new tokens instead of unescrowing. The attacker sends the minted vouchers back to Chain A, draining A's escrow of real tokens. The attack is repeatable: each round inflates supply by the transfer amount.

Detection Patterns:

In relay.go SendTransfer():
  if HasPrefix(sourcePort, sourceChannel) -> BURN
  else (!HasPrefix)                       -> ESCROW

In WriteAcknowledgementForForwardedPacket() [CORRECT, non-nonrefundable path]:
  if !HasPrefix -> tokens were ESCROWED   -> code UNESCROWS from escrow  [correct]
  else HasPrefix -> tokens were BURNED     -> code MINTS back            [correct]

In moveFundsToUserRecoverableAccount() [BUGGY, nonrefundable path]:
  if !HasPrefix -> tokens were ESCROWED   -> code MINTS (WRONG!)
  else HasPrefix -> tokens were BURNED     -> code UNESCROWS (WRONG!)

What to Check:

  • Every code path that reverses a transfer uses the same source/sink logic (HasPrefix) as the original send
  • Nonrefundable forwarding paths have explicit tests comparing escrow/mint direction against the refundable path
  • PFM is fuzz-tested with various denom trace configurations

Mitigation:

// SECURE: Verify source/sink logic is consistent across all refund paths
// The escrow/mint decision in error-handling paths must mirror SendTransfer():
//   !HasPrefix (escrowed on send) -> UNESCROW on refund
//   HasPrefix  (burned on send)   -> MINT on refund

Grep Patterns (use Grep tool with glob: "*.go" in x/):

  • HasPrefix|SenderChainIsSource|ReceiverChainIsSource — source/sink logic
  • nonrefundable|Nonrefundable — nonrefundable forwarding

9 ICS-20 V2 MULTI-TOKEN PARTIAL RECEIVE

Description: ICS-20 v2 allows sending multiple tokens in a single packet. The onRecvPacket function processes them sequentially in a for loop. If token N fails (e.g., insufficient escrow for unescrow), the loop breaks and returns an error acknowledgement. However, tokens 1..N-1 have already been minted/unescrowed on the receiver chain and are NOT rolled back. The sender chain then receives the error ack and calls refundTokens, which refunds ALL tokens — including the ones that were successfully delivered to the receiver. Tokens end up existing on both chains simultaneously, created from nothing. The ICS-20 v2 spec pseudocode itself contains this bug — implementers who faithfully translate the spec inherit it.

Detection Patterns:

// VULNERABLE: Non-atomic multi-token processing
func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet) error {
    var data types.FungibleTokenPacketDataV2
    // ...
    for _, token := range data.Tokens {
        // Each token modifies state immediately!
        if err := k.processToken(ctx, token); err != nil {
            return err  // Tokens 1..N-1 already committed, no rollback
        }
    }
    return nil
}

What to Check:

  • Multi-token onRecvPacket is wrapped in CacheContext to make all operations atomic
  • State is only committed if ALL tokens process successfully
  • Tests cover packets where the Nth token fails for all values of N (not just happy path or first-token-fails)
  • The ICS-20 v2 spec pseudocode itself contains this bug — verify the implementation does NOT faithfully copy it

Mitigation:

// SECURE: Wrap multi-token processing in CacheContext
func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet) error {
    var data types.FungibleTokenPacketDataV2
    // ...
    cacheCtx, writeFn := ctx.CacheContext()
    for _, token := range data.Tokens {
        if err := k.processToken(cacheCtx, token); err != nil {
            return err  // Discard all cached writes
        }
    }
    writeFn()  // Commit only if ALL tokens succeeded
    return nil
}

10 UNPERMISSIONED IBC CHANNEL OPENING

Description: By default, anyone can open an IBC channel to any chain. If a chain's IBC application doesn't properly validate channel parameters (version, ordering, counterparty), a malicious actor can open channels with unexpected configurations. This was the precondition for the non-deterministic JSON ack vulnerability (§7) — "any user that can open an IBC channel can introduce this state."

Detection Patterns:

// VULNERABLE: No validation in channel open callbacks
func (im IBCModule) OnChanOpenInit(ctx sdk.Context, order channeltypes.Order,
    connectionHops []string, portID string, channelID string,
    chanCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty,
    version string) (string, error) {
    return version, nil  // Accepts ANY version, ordering, counterparty!
}

What to Check:

  • OnChanOpenInit / OnChanOpenTry implement strict version and ordering checks
  • Channel opening is permissioned (governance-gated) for sensitive applications
  • IBC application version strings are validated against an allowlist
  • Data from the counterparty chain is never trusted without validation
  • Unexpected port bindings are rejected

Mitigation:

// SECURE: Strict validation in channel open
func (im IBCModule) OnChanOpenInit(ctx sdk.Context, order channeltypes.Order,
    connectionHops []string, portID string, channelID string,
    chanCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty,
    version string) (string, error) {
    if order != channeltypes.UNORDERED {
        return "", errorsmod.Wrapf(channeltypes.ErrInvalidChannelOrdering,