What happened
What happened: the channel check that was not there.
The contract was a modified CW20-ICS20 (SNIP-20 ICS-20) implementation. Its job: accept IBC token transfers from Axelar-bridged assets and mint corresponding wrapped tokens on Secret Network.
ICS-20 is the Inter-Blockchain Communication standard for
token transfers. When a transfer packet arrives at
do_ibc_packet_receive, the contract is supposed
to validate two things: the incoming denomination (which
asset) and the source channel (which connection the packet
came from). Validating the denomination is necessary, but not
sufficient. The source channel check is what binds "this
deposit came from the real Axelar channel" to "this contract
is authorized to mint." Without it, any IBC-capable chain can
open a new channel, send packets with the right denom string,
and claim to have deposited real assets.
The source channel check in this contract was a call to
parse_voucher_denom, which would have validated
the denomination's channel trace against the actual transfer
source. It was commented out.
The attacker set up a single-validator Cosmos SDK chain, opened a new IBC channel to the Secret contract, and sent forged deposit packets. The packets carried the right denomination strings, so the denom check passed. The channel trace check that would have caught the mismatch was not there. The contract minted genuine saTokens against zero backing. Those tokens were then redeemed over the legitimate Axelar channel, draining the real escrowed assets.
The pattern
The pattern: supply issued without a valid authorization.
The system-level property this contract needed to enforce: minted wrapped supply must at all times be fully backed by assets actually escrowed, received over the authorized channel, on the correct denomination trace.
That property has two components. Denomination validation alone says the asset type is what it claims to be. Channel validation says the asset came from where it claims to have come from. Both must hold for a deposit to authorize a mint. Commenting out the source-channel check drops the second component. Supply can then be minted on any channel that speaks the right denomination string, regardless of whether real assets were ever moved.
This is the mint-from-thin-air shape: a minting event proceeds without the authorization condition being genuinely satisfied. The authorization condition, a valid inbound transfer over the registered channel, was checked incompletely. Supply was issued anyway. The binding between "what was deposited, on the right channel" and "what gets minted" was severed.
This is the same family as
blog post 10
(Aztec's numRealTxs settlement boundary, Jun-21
writeup) and
blog post 11
(Syscoin's SPV proof validation bypass, Jun-22 writeup). The
runtime differs, IBC channels and CosmWasm instead of ZK
proofs on EVM or SPV proofs on a Solidity bridge, but the
class identity is the same. A state change (minting) that
requires a genuine authorizing condition (a valid,
fully-validated inbound transfer) proceeds when that
condition is only partially satisfied. The minted supply is
then unbacked.
The franchise thesis holds: this class is not exotic. It recurs across ecosystems, in different forms, on different runtimes.
The invariant
What the invariant asserts.
The property, expressed for a stateful property test:
Track two counters. authorized_inbound
increments only when an incoming packet passes both denom
validation AND source channel validation, meaning the channel
trace is verified against the registered Axelar channel, not
just any IBC peer. tokens_minted increments on
every successful mint call.
Assert after every mint transition:
tokens_minted <= authorized_inbound
The planted twin replicates the actual bug: comment out the
parse_voucher_denom call. A forged-channel
deposit now reaches the mint path.
tokens_minted increments.
authorized_inbound does not. The invariant fires
on the first such packet.
The clean variant leaves the channel check intact. A
forged-channel deposit fails before reaching the mint path.
tokens_minted and
authorized_inbound stay in sync across every
valid transition. The invariant does not fire.
That is the clean = 0 / planted >= 1 shape: zero violation markers on the correct implementation, at least one marker when the bug is present. A property-test gate that asserts this invariant in a pre-deploy CI run fires immediately on the planted twin and stays silent on the clean contract. This is exactly the structure cf-invariants ships for each of its 12 conservation-and-other-class references on Cairo/Starknet.
Where we stop
What we cover, and where we stop.
cf-invariants (Cairo/Starknet) carries the conservation class
across a 12-reference suite. Each reference has a clean
variant and a planted-bug variant; each pair asserts
planted >= 1 markers AND
clean = 0 markers in CI. PR #2 squash-merged
2026-06-07 at commit 0bff2c4, CI run
27096594873, 26/26 green. All twelve references
are deployed and source-verified on Starknet Sepolia. The
public developer cookbook (PR #3, commit 7f94b08,
public since 2026-06-11) is at
github.com/caliperforge/cf-invariants-starknet/tree/main/docs/cookbook.
The method (express the conservation property as a two-counter assertion, plant the authorization bypass, verify the invariant fires) is chain-agnostic. The live harnesses are Cairo/Starknet. Secret Network runs CosmWasm. We have not harnessed a CosmWasm contract; that surface is not in our current stack. CosmWasm is a plausible future rail, not one that exists in our CI today.
The honest framing is the same one this franchise has used
across both prior posts: the value is the method,
demonstrated on the chains we do cover. A CosmWasm-native
harness targeting do_ibc_packet_receive would
carry the same invariant structure: two counters, the
forged-channel callpath in scope, the conservation assertion.
The design is portable. The CI-verified artifact on that
runtime does not exist yet.
We name the class, show the invariant shape, and state the gap plainly. The same boundary the Aztec post drew for the EVM-L1-ZK-settlement surface; the same boundary the Syscoin post drew for the Solidity bridge runtime.
Sources
Sources.
- The Block (incident coverage, amount, timeline, exploit mechanism): theblock.co/post/405459
- crypto.news (Axelar response, bridge disabling): crypto.news/axelar-shuts-down-secret-network-bridge-routes-after-4-7m-exploit
- Cryptopolitan (incident coverage, asset types drained): cryptopolitan.com/axelar-bridged-tokens-worth-4-67-million-drained-in-secret-network-contract-exploit
- cf-invariants repo: github.com/caliperforge/cf-invariants-starknet
- cf-invariants developer cookbook: github.com/caliperforge/cf-invariants-starknet/tree/main/docs/cookbook