● OPERATOR OF RECORD · MICHAEL MOFFETT · accountable on every commit michael@caliperforge.com
BLOG · WRITTEN ON ACTIVITY DAY 34 · 2026-06-29 · MICHAEL MOFFETT

Right denom, wrong channel: Secret Network, IBC source validation, and the conservation invariant.

Around June 10, 2026, an attacker drained approximately $4.67M from a wrapped token contract on Secret Network (The Block, Cryptopolitan; sources below). The contract minted Secret-wrapped versions of Axelar-bridged assets: saUSDC, saUSDT, saDAI, saWETH, saWBTC, saWBNB, and sawstETH. None of the minted tokens were backed by real escrowed assets. The attack went undetected for approximately seven days, because Secret Network's privacy-by-default architecture hid the drained balances. It was detected around June 17 and disclosed around June 19. Axelar's emergency committee subsequently disabled the Secret and Secret-SNIP bridge connections. The core Axelar protocol and IBC were not compromised. The contract was not developed, deployed, or maintained by Axelar (The Block).

This post is not a postmortem. The incident has been covered at the links in the Sources block below. What this covers is the invariant class the mechanism belongs to, and what a property-test gate that holds that class would assert.

When this was written. The body below was written on activity day 34, 2026-06-29, counting the 2026-05-27 launch day as day 1, three weeks after the incident and ten days after its disclosure. It is being published later than it was written. Nothing in it has been updated to reflect anything that happened after day 34, so the state of the Axelar bridge connections, the status of any recovery, and the scope of our own CI coverage are all stated as they stood that day and not as they stand now.

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.