3/2/2026
Verifying state information across blockchains requires rigorous proof validation. In 2024, Nathan Kirkland and I identified a vulnerability in Neutron’s newly launched Interchain Queries (ICQ) module that compromised its proof verification logic without any fancy cryptography. By exploiting this weakness, an attacker could inject forged transaction proofs into the ICQ pipeline. This would have enabled the spoofing of arbitrary transactions for Neutron applications, leading to a variety of impacts depending on the application.
Neutron is a Cosmos SDK blockchain with support for CosmWasm smart contracts. I enjoy the Cosmos ecosystem and have done previous research into it, such as an RCE and an Infinite Mint. The following post breaks down how IBC ICQ works, the mechanics of the proof bypass, and the steps that an attacker could have taken to exploit it.

IBC (Inter-Blockchain Communication) is a light-client-based protocol for exchanging information between sovereign blockchains. While native to the Cosmos ecosystem, it is an agnostic standard. Chain interactions are permissionless, allowing applications to communicate and route arbitrary data through dedicated channels. Under the hood, this transport layer relies on a client-connection combination: the light client cryptographically verifies the state of a specific counterparty chain, while the connection establishes the secure, negotiated link between them. To see this relationship, review Figure 1.
In IBC, the client object holds the consensus state of a counterparty chain. This includes block heights, state roots, the current validator set, and other data required to mathematically verify Merkle proofs via light clients.
A connection is built on top of this client. Rather than just tracking state, a connection represents a stateful, negotiated agreement between two chains. It establishes the specific IBC version and features they will use to communicate, and is strictly tied to a specific client to verify those interactions. In practice, a connection is a slight abstraction of a client, but they are mostly the same thing.
The full details of how IBC works are out of scope for this article, but this should be sufficient context to understand the vulnerability. To learn more about IBC, read about it here.
ICQ is a mechanism for retrieving data from remote chains via Merkle inclusion and non-inclusion proofs. Neutron supports two types of proofs for this: Transaction and Key Value (KV). These function exactly as the IBC Merkle proof verification stack in Cosmos; the Neutron code mostly reuses IBC's underlying primitives for proof verification.
The standard IBC Apps implementation of Interchain Queries (ICQ) operates over IBC channels and requires both the querying and the queried blockchains to have the ICQ module installed. Because this creates friction and a poor developer experience, Neutron engineered a custom ICQ implementation that removes this requirement for the remote chain. Instead, Neutron applications can verify whether a transaction occurred or if a specific item exists in a remote chain's storage entirely locally. All the code and cryptographic proof verification for this process runs directly within Neutron, leveraging its existing IBC light clients.
At a high level, ICQ works as follows. This is also shown in Figure 2:

To understand this vulnerability, we must first look at how trust is established in IBC. It boils down to two identifiers:
ClientId: Represents the light client tracking the consensus state of a specific remote chain. ConnectionId: Represents the actual, verified connection established between the two blockchains, built on top of that client.When a smart contract initiates an Interchain Query (ICQ), it specifies a ConnectionId to retrieve information about a specific chain (e.g., "Did Alice send funds on Chain A?"). When the Relayer submits the result, it calls SubmitQueryResult. This function requires the Relayer to provide a proof and a ClientId to verify that proof against.
The vulnerability stems from a crucial missing link between these two components. The code never verified that the ClientId provided by the Relayer matches the ConnectionId requested by the smart contract. Because the system didn't enforce this link, the verification logic blindly trusted any ClientId the Relayer passed in. This allowed an attacker to perform a ChainSwap for the proof, an exploit similar to providing your own public key to verify a forged JWT.
Instead of proving that a transaction occurred on the real target chain (Chain A), the attacker can prove that a transaction occurred on a malicious chain they control (Chain E) and instruct the Neutron contract to verify the proof against Chain E's client. This is what is shown in Figure 3 above. This resulted in a complete bypass of Transaction Proofs on Neutron.
As with developer features, the impact depends on how the developers use it. If a developer implements a cross-chain DEX that requires users to send tokens to a specific address on the remote chain, this becomes a problem. An attacker could fake a proof for a large transfer to trick the dex into crediting the account for funds it does not have. We will use this scenario for the exploit steps below.
For the proof-of-concept sent to Neutron, we modified their ICQ relayer to submit proofs for the wrong ClientId. In the following steps, we assume a benign chain A and a malicious chain E, controlled by the attacker.
Client must be created for the chain. SubmitQueryResult with the attacker-controlled ClientId. This transaction is now considered valid after verification. Because Neutron's ICQ feature and their bug bounty program were both brand new at the time of our report, no user funds were ever at risk. The vulnerability was resolved very quickly after reporting. Nonetheless, the chain-swapping mechanics involved make this a highly unique and interesting vulnerability.
IBC is permissionless and powerful. In this post, we demonstrated how an integration can fail due to a minor misunderstanding of the IBC protocol and a lack of input validation. The fix was to not use clientID as an input; it is instead calculated from the stored connectionID associated with the query. A good takeaway from this is to always validate/sanitize your inputs. Thanks to the Neutron team for taking this vulnerability seriously, applying a quick patch, offering a nice bounty, and allowing me to discuss this bug publicly.
Thanks for reading the post! I hope to have provided some value to you on bug hunting or Cosmos SDK knowledge. Feel free to contact me (contact information is in the footer) if you have any questions or comments about this article or anything else. Cheers from Maxwell "ꓘ" Dulin.