Secret Network CW20-ICS20 Exploit: What Exactly Happened

Reading time: 18 minutes

Axelar’s core protocol was not affected. The impact is confined to Secret-wrapped versions of Axelar assets on the Secret↔Axelar channel-69/channel-61 IBC connection; no other chains, assets, IBC channels, or escrow accounts were affected, and no action is needed.

Incident Summary

On June 10, 2026, an attacker exploited an ‘infinite mint’ bug in a vulnerable IBC-enabled smart contract on Secret Network (secret-4) to mint unbacked, Secret-wrapped versions of Axelar-wrapped assets (saTokens), then redeemed them back over the legitimate IBC channel to drain the real Axelar-wrapped assets held in escrow on Axelar. The smart contract, a modified CW20-ICS20 implementation deployed on Secret, did not verify the source channel of inbound IBC packets before minting, so deposits forged over an attacker-controlled channel minted genuine saTokens with no assets backing them. Approximately $4.67M worth of tokens were drained.

Background

The Inter-Blockchain Communication protocol (IBC) lets independent Cosmos chains exchange messages and tokens. Instead of a trusted intermediary, IBC secures each channel with light clients. For each counterparty chain, both chains run an on-chain light client of the other, tracking the counterparty’s validator set and block headers. This setup allows the light client to confirm whether some event happened on the other side, without needing to trust any external validators. To deliver an IBC packet, a relayer submits the packet together with a proof that this packet was committed on the source chain. The destination chain verifies that proof against its light client’s stored consensus state before accepting it. IBC relaying is therefore permissionless and trust-minimized, by only needing to trust the counterparty’s IBC implementation and chain validators.

Through IBC channels, any token can be transferred. When a token is sent to a chain from which it doesn’t originate, it is escrowed (locked) on the source chain and a wrapped voucher is minted on the destination chain. When that voucher is later sent back over the same channel, it is burnt on the destination side and the original token is unlocked on the source side. This accounting is per-channel: Each channel has its own escrow account, and a voucher carries a denomination path tied to the channel it came through, so it can only be redeemed over that same channel. This isolation is what keeps channels from interfering with each other. This isolation and correct handling of denomination paths must be enforced by all IBC implementations.

Opening IBC connections is permissionless. Anyone can stand up a new chain A, create a light client of a target chain B, open a connection and a channel, and begin transferring assets they hold. This is by design and harmless if IBC is implemented correctly: Tokens bridged in over a new channel receive a fresh, distinct denomination and have their own escrow, so they are not fungible with assets that arrive over other channels.

The Secret Network (chain id secret-4) connects to Axelar mainnet (chain id axelar-dojo-1) over the IBC channel axelar-dojo-1 transfer/channel-69secret-4 wasm.secret1yxjmepvyl2c25vnt53cr2dpn8amknwausxee83/channel-61, allowing both plain token transfers as well as token transfers carrying GMP messages to pass between these two networks. On the Secret Network side of this IBC connection, a smart contract handles receiving IBC messages and mints “Secret Axelar Wrapped Tokens” (saTokens such as saUSDC, saWBTC, and saDAI), wrapping the bridged Axelar assets in Secret’s privacy-preserving SNIP-20 token standard, so that the resulting balances are encrypted on-chain.

Visualization of the Token Flow

Token Flow Visualization

Detailed Root-Cause Analysis

The root cause was a modified CW20-ICS20 IBC-enabled smart contract (github.com/scrtlabs/ics20-for-axelar, with Code ID 2446, deployed at secret1yxjmepvyl2c25vnt53cr2dpn8amknwausxee83): It minted vouchers without verifying the source of the inbound transfer. The deployed contract is a fork of Secret’s SNIP-20 ICS-20 implementation (github.com/scrtlabs/snip20-ics20), modified for the Axelar↔Secret IBC connection (github.com/scrtlabs/ics20-for-axelar).

The defect lived in do_ibc_packet_receive, where the two checks that bind a voucher to the channel it arrived on were commented out:

  • parse_voucher_denom(&msg.denom, &packet.src) (ibc.rs#L231), which would have validated the denom’s <port>/<channel> trace against the packet’s actual source.
  • reduce_channel_balance(...) (ibc.rs#L234), which would have capped any release at the amount that channel had genuinely escrowed.

What remained rejected denoms containing a / and minted any bare denom present on the allow-list. Because that allow-list was keyed by denom name rather than by channel, a uusdt arriving on the attacker’s channel-227 was indistinguishable from one arriving on Axelar’s legitimate channel-69: Both minted the same saUSDT, but only the latter had anything escrowed behind it. Because opening an IBC channel is permissionless, the attacker was free to connect a chain under their own control, running a single validator, and self-relay forged deposits into it.

The attack followed directly from this: The attacker spun up a fake chain, opened an IBC channel to Secret, and sent over bare denoms (carrying no source-channel path prefix) whose names matched the allow-list. The contract minted genuine saTokens with nothing backing them. Each forged deposit was sized to the saToken’s entire circulating supply, so redeeming the minted balances back over the Axelar channel released exactly the backing held in the IBC escrow on the Axelar side.

The vulnerability was not introduced by a recent change. It was present in the codebase from the repo’s initial commit on January 15, 2023, and deployed on Secret mainnet on March 30, 2023 (with Code ID 872), uploaded by secret1mryld3gd05c7gtm36hfq64emdv54djz7rcmfva, with codehash 2976a2577999168b89021ecb2e09c121737696f71c4342f9a922ce8654e98662, and instantiated the same day with contract address secret1yxjmepvyl2c25vnt53cr2dpn8amknwausxee83.

On March 5, 2026 (Secret block 24186627, MsgMigrateContract A049548A...ACADB8B5), the migration admin secret1lrnpnp6ltfxwuhjeaz97htnajh096q7y72rp5d migrated the contract to Code ID 2446, uploaded by secret1f2jrcqsx7glyta39c6tum2lhk5kh2a0ty6r9ms, codehash ba26d9bcba2901300a53343b7aa9e71095afa149ae18ee1772a64d1a7e5add2f. This migration changed the deployed bytecode (the add-migration-message branch added a memo field and the migrate message itself) but carried the same missing source verification forward. The June 10 exploit struck this migrated code, Code ID 2446 (HEAD b206182, “fix reproducible build”, January 27, 2026).

Impact

Approximately $4.67M worth of tokens were minted without backing, across saUSDT, saUSDC, saDAI, saWETH, saWBTC, saWBNB, and sawstETH, and then redeemed back over the legitimate channel to Axelar. The drained balance represented the real assets that users had bridged into Secret over this connection. With the Axelar IBC escrow account for channel-69 now effectively empty, those assets can no longer be bridged out of Secret. The impact is confined to the seven saTokens on this connection. No other Axelar chains, assets, IBC channels, or escrow accounts were affected.

Incident Response & Remediation

We detected the incident through our firewalling functionality, which prevented contamination to other chains. The Axelar Emergency Committee disabled the Secret↔Axelar connection. Squid disabled Secret on its frontend. The Secret team was notified to halt and migrate the affected bridge contract.

Exploit Timeline

All times are UTC.

June 10, 2026

Fund Flow of Stolen Assets

Once the saTokens had been redeemed, the attacker unescrowed the real tokens from the IBC escrow account on Axelar, moved everything to Ethereum, converted it all to ETH, and split it across multiple wallets, eventually depositing the funds into exchanges. It happened in six steps, all on June 10, 2026:

  1. Withdraw the stolen assets from Secret to Axelar via IBC.
  2. Bridge the tokens off Axelar and over to Ethereum.
  3. Consolidate tokens in a single Ethereum wallet.
  4. Sell every token for ETH.
  5. Break the ETH into ≈30 smaller transfers to fresh wallets.
  6. Deposit to exchanges.

The attacker’s destination wallet on Ethereum is 0x6c2eAB82bA2897A6E99FB6Af018020dA15123976. The attacker’s Axelar address is axelar1hzra9z4zn8q0w8f3dj2wnw0xgetu8dfdhl6ad8.

Step 1: Withdraw the stolen assets from Secret to Axelar

Timeline: 19:33-19:36

The attacker withdraws the newly minted tokens over channel-61 to Axelar axelar1hzra9z4zn8q0w8f3dj2wnw0xgetu8dfdhl6ad8, and Axelar’s channel-69 escrow account unlocks the equivalent amount of locked tokens. This is step 2 in the visualization.

Step 2: Off Axelar, on the way to Ethereum

Timeline: 19:38-19:56

The attacker moved the tokens off Axelar to Osmosis through 18 IBC transfers, sent in 3 batches between 19:38 and 19:56 UTC. Each transfer carried a packet-forwarding memo (the Skip/PFM convention), instructing each chain along the route to immediately forward the tokens to the next hop rather than hold them, so the whole multi-hop path executed automatically without manual relaying. Batches 1 and 2 each contained one transfer for each of the 7 stolen assets; batch 3 contained 4 retries (WETH, USDT, DAI, wstETH). WBNB, WBTC, and USDC were not retried in batch 3, leaving the residual amounts that appear in the leftover paragraph below.

# Time (UTC) Asset Amount Axelar Transaction Hash
1 19:38:42 WETH 223.367 C6F4F590…4F9EB0FB
2 19:43:07 USDT 280,317 AB74CDDE…487EF291
3 19:43:24 WBTC 4.134 D0EF0311…2FF13AFF
4 19:43:42 USDC 159,549 BE8F0F13…89E7BF36
5 19:44:00 DAI 43,634 B0DF7FE2…37DE588C
6 19:44:17 WBNB 42.691 7EA8439F…9D9AE008
7 19:44:36 wstETH 4.414 8B384A98…49D18BED
8 19:49:43 WETH 335.051 4D92E886…1CD0E49C
9 19:49:59 USDT 420,476 86767670…A54EC5CA
10 19:50:16 WBTC 6.201 D34C6714…613BC80E
11 19:50:36 USDC 239,324 7E56A115…853EC383
12 19:50:53 DAI 65,451 8FB1F586…1A17C41C
13 19:51:10 WBNB 64.037 53382B68…26868229
14 19:51:26 wstETH 6.621 998EEDA7…68FB80FC
15 19:55:31 WETH 335.051 5F3B02D6…4C7370C0
16 19:55:46 USDT 420,476 56CA93E0…001188E3
17 19:56:02 DAI 65,451 E65C67D6…D224D8BC
18 19:56:19 wstETH 6.621 55BA01EF…A4B7C6E7

From Osmosis, the funds reached Ethereum via two routes:

  • Non-USDC tokens: Axelar → Osmosis → back to Axelar → Ethereum. The round-trip through Osmosis is consistent with an automated cross-chain router (the transfers carry Skip-style forwarding memos): Osmosis provides the swap liquidity and Axelar is used to transfer the tokens to Ethereum.
  • USDC: Swapped on Osmosis, sent over IBC to Noble, and bridged to Ethereum through Circle’s native USDC bridge (CCTP).

Step 3: Funds land in one Ethereum wallet

Timeline: ≈20:00

Every bridged transfer converged on a single Ethereum wallet, 0x6c2eAb82Ba2897A6e99fB6aF018020da15123976. The tokens routed through Axelar were bridged via Axelar’s GMP Gateway, while new USDC was minted via CCTP. The amounts that arrived are listed below:

Asset Amount Arrived Bridge Transaction Hash
WETH 893.47 (223.367 + 335.050 + 335.050) Axelar GMP 0x3513509c...98ca8e05, 0xc5cff113...004d0d5e, 0xe40f58d2...2cc7133e
USDT 1,121,267 (280,316.74 + 420,475.34 + 420,475.32) Axelar GMP 0x5abb7ce7...1e899025, 0xae231983...1b0f6225, 0x20d9cfdc...1cd298bb
DAI 174,533.8 (43,633.36 + 65,450.19 + 65,450.22) Axelar GMP 0xde42dd55...59d73343, 0x2cbd4a70...f34ae2e6, 0x78028bf1...98d0396d
WBTC 10.335 (4.134 + 6.201) Axelar GMP 0xe49353d3...efdf8967, 0x5b111208...9606bf0b
wstETH 17.654 (4.414 + 6.620 + 6.620) Axelar GMP 0xbc3bde34...e05661da, 0xf05277c5...179c7593, 0x08db0a3c...97a2ee3f
USDC 398,873 (239,323.54 via ITS + 159,549.57 via CCTP) Axelar GMP + CCTP Axelar GMP: 0xd0fc7d56...ee8b2317; CCTP mint: 0x7039eba2...cd582cff

Not all of the assets were bridged out of Axelar. Roughly 6.2 WBTC, 239k USDC, 64 WBNB, and 248 AXL were left in the attacker’s Axelar wallet. These stranded amounts match the leftover funds we identified in the escrow during the investigation.

Step 4: Swap funds to ETH

Timeline: 20:04-20:15

From the destination wallet the attacker swapped each token for WETH on CoW Protocol, then unwrapped the WETH into native ETH:

Token Sold Amount Transaction Hash
USDT 1,121,267.40 0x388b57dc...f3512d5d
USDC 398,873.11 0xdf2b389b...51e46f22
DAI 174,533.78 0x16b309fb...486de1d1
WBTC 10.335 0xfdc4a8a2...b82a8c37
wstETH 17.654 0xd3ac3c2e...418ccdfd

The swaps returned about 1,456 WETH (689.22 + 392.04 + 245.23 + 107.24 + 21.83). Added to the 893 WETH that had bridged in as WETH already, and unwrapped to native ETH (for example 0x3984f61c...b801d84b and 0xa3b05a11...72c236ff), this left roughly 2,350 ETH (≈$4M) sitting in this Ethereum wallet.

Step 5: Split ETH across many wallets

Timeline: 20:51 the same day to 09:19 the next

The attacker then split the ≈2,350 ETH into about 30 transfers of 50-139 ETH each, sending them to fresh wallets. A few of the transfers:

Time (UTC) Amount To Transaction Hash
2026-06-11 09:19 139 ETH 0x737cF06B...B364EBC8 0xc311d3f1...b685ec84
2026-06-11 09:10 100 ETH 0xD3ef2D45...b0F5d807 0xcdb8c7f0...724058ad
2026-06-10 22:33 110 ETH 0xf039D57e...C0967882 0x4cf74260...72528a05
2026-06-10 22:32 100 ETH 0xd73a2182...7d201290 0x8803e97a...17e217e4
2026-06-10 20:51 50 ETH 0xf4F50ed6...2e1b5897 0xa206fa2d...cb702183

About 25 more followed in the 50-100 ETH range over the same window.

Step 6: Deposit ETH to exchanges

The attacker moved ETH into KuCoin (1,199 ETH), ChangeNow (1,050 ETH), and HitBTC (100 ETH):

Attacker depositing ETH on Ethereum to centralized exchanges

From those 30 wallets the funds moved on to three exchanges, usually in a single hop, sometimes through one or two intermediate wallets first.

KuCoin (1,199 ETH across 15 deposits to 0x45300136...d992b785):

Amount From Deposit Transaction Hash
50 ETH 0x1eafd8c5...533f19c8 0xd0fb3e4a...92963ae5
50 ETH 0xf32a06aa...66f1b886 0x011b764c...cf108ae7
70 ETH 0xc3e2f603...47fa848b 0xb7caefae...85b4f4fb
60 ETH 0x517e0c32...86804c28 0x482c1d2e...d771c64e
50 ETH 0xd49b8443...edebad94 0xcc396950...6932cf8e
80 ETH 0xe5db1200...dfa3abdd 0xa1990282...99420e93
100 ETH 0xf7bbb4fb...d43bc3b9 0x5013bc93...7dcd7a45
90 ETH 0xb6156c50...8de95a70 0xf195881d...e99afc73
100 ETH 0x0187f9b4...b060f725 0xa813149e...1e5576e8
100 ETH 0xd73a2182...7d201290 0x12f54f02...0e4acc3d
110 ETH 0xf039d57e...c0967882 0x9255c0c9...c010afee
50 ETH 0xe5130ade...a6d37b5e 0xdc35dad7...f08cf62a
50 ETH 0xb04331ae...e4eb7c11 0xdc35dad7...f08cf62a
100 ETH 0xd3ef2d45...b0f5d807 0xadda1323...713f6519
139 ETH 0x737cf06b...b364ebc8 0xc66dd132...4c07961a

ChangeNow (1,050 ETH across 13 deposits to 0xEbA88149...DE94cB1):

Amount From Deposit Transaction Hash
50 ETH 0xf4f50ed6...2e1b5897 0x3e75cf1e...735a249d
100 ETH 0x67e25c71...5be0b46c 0xccaa3656...5759319a
80 ETH 0xdacf61aa...3687d36c 0xd9fbc4fa...67df9519
60 ETH 0xc12fd863...7ddc5b0e 0xc51a6a46...6cacfbb1
80 ETH 0x4de57c31...f80cb13b 0x550c558c...0fcd7400
90 ETH 0x40b42df7...b29b53cb 0xe2134546...440b4c40
60 ETH 0xd8df7fd5...5fc0bc52 0xebd687f7...daecc0bb
80 ETH 0x2c08e99e...54a4c56c 0x962ece0f...8bb82f46
90 ETH 0x67f0eb70...b2889ce0 0x93542413...a3025df4
90 ETH 0x8364b1bf...51825aa3 0x3615606d...4dad1098
90 ETH 0x5d88861e...2a2f2c7a 0x86dbce29...ec261df6
90 ETH 0x74a4a5c0...ab4a4219 0x920690b1...861802ee
90 ETH 0x528beb3c...0c868a88 0x20f37c2a...1d8c303e

HitBTC (100 ETH across 2 deposits to 0x80787af1...d7Ee1A):

Amount From Deposit Transaction Hash
50 ETH 0xc466504f...feaccfa5 0x4a2319a8...246db50d
50 ETH 0x8ec5569b...1861f056 0x0be415c8...b4a3ff8c

Discovery Timeline

June 17, 2026

  • 15:39: Team identified a failed cross-chain transaction on Axelar: 6B6A8951…CD75CF39. This IBC transfer failed on Axelar with error: unable to unescrow tokens, this may be caused by a malicious counterparty module or a bug: please open an issue on counterparty module: spendable balance 803200wbtc-satoshi is smaller than 1000000wbtc-satoshi: insufficient funds, indicating that more tokens are trying to be bridged out of Secret than have been bridged into Secret.
  • 16:00: Team discovered that the IBC escrow account on Axelar for the secret-snip connection had been drained.
  • 16:48: Team discovered that nearly all funds had been withdrawn from Secret to Axelar in 7 single IBC transactions on June 10, 2026, indicating an attack. Further analysis traced the funds, showing that most had been moved out of Axelar to Ethereum via Osmosis, where they had in part been deposited in KuCoin.
  • 18:28: We first notified the Secret team about the incident in a shared Telegram channel.
  • 18:30: The Secret team responded.
  • 18:40: We notified Squid to disable Secret from their frontend.
  • 18:52: Analysis uncovered the outline of the attack via the attacker-controlled, connected chain, which was passed on to the Secret team.
  • 19:21: The Secret team began investigating on their end as well.
  • 20:00: Squid confirmed Secret was removed from their frontend.
  • 20:45: Axelar Emergency Committee paused the Secret↔Axelar connections.

Root Cause and Contributing Factors

Root cause: The bridge minted saTokens against a denom’s name without verifying the source channel of the inbound packet. The allow-list decided which denoms were mintable but never checked whether a transfer actually arrived over the channel holding the backing, so a forged deposit over an attacker-controlled channel minted genuine, unbacked saTokens. A mint must be authorized by the packet’s source channel, not the denom name alone.

Contributing factors:

  • An unaudited fork. The deployed contract was a fork of github.com/scrtlabs/snip20-ics20 with its core security checks (parse_voucher_denom, reduce_channel_balance) commented out. The fork changed the contract’s trust model, so the original audit no longer applied, yet it shipped to mainnet without re-audit. Forks that alter a contract’s trust model must be re-audited against the new invariants.
  • Permissionless channel opening. Opening an IBC channel into the minting contract required no permission, letting the attacker connect a chain under their own control and self-relay forged packets. A minting contract in this position cannot trust its counterparties and must enforce every invariant itself, or restrict who may open a channel. This was a necessary enabling condition for the attack.

Appendix A: Addresses and Identifiers

Appendix B: Reproducing the On-Chain Contracts (Code IDs 872 & 2446)

Both contracts were compiled on amd64 (x86_64) and optimized with wasm-opt -Oz. The Secret Network codehash is the sha256 of the uncompressed optimized .wasm.

Toolchain Summary

Code 872 (2023 – Mar 2026) Code 2446 (Mar 2026 – Present)
Repo scrtlabs/ics20-for-axelar scrtlabs/ics20-for-axelar
Branch / commit master / 0546744 add-migration-message / b206182
Rust 1.64.0 (x86_64) 1.86.0 (x86_64)
wasm-opt -Oz (binaryen) ~v91 (v90–97 all match) v116
Build local make build Docker make build-reproducible (optimizer 1.0.13)

Expected codehashes:

  • Code 872: 2976a2577999168b89021ecb2e09c121737696f71c4342f9a922ce8654e98662
  • Code 2446: ba26d9bcba2901300a53343b7aa9e71095afa149ae18ee1772a64d1a7e5add2f

Building Code 2446

git clone https://github.com/scrtlabs/ics20-for-axelar
cd ics20-for-axelar && git checkout add-migration-message   # commit b206182
cd contracts/cw20-ics20
make build-reproducible

Building Code 872

Code 872 was a local build, so its absolute paths are baked into the binary. For a byte-exact hash you must replicate them (/mnt/d/scrtlabs/ics20-for-axelar and CARGO_HOME=/home/tovi/.cargo).

rustup toolchain install 1.64.0-x86_64-unknown-linux-gnu --force-non-host
rustup target add --toolchain 1.64.0-x86_64-unknown-linux-gnu wasm32-unknown-unknown

# 1. lay out the source at the exact path the 2023 build used
sudo mkdir -p /mnt/d/scrtlabs/ics20-for-axelar && sudo chown -R "$(id -u)" /mnt/d/scrtlabs/ics20-for-axelar
git -C /path/to/ics20-for-axelar archive 0546744 | tar -x -C /mnt/d/scrtlabs/ics20-for-axelar
cd /mnt/d/scrtlabs/ics20-for-axelar

# 2. nest the cw-plus packages UNDER the contract, fix the contract's path deps
cp -a packages contracts/cw20-ics20/packages
sed -i 's#path = "\.\./\.\./packages/#path = "packages/#g' contracts/cw20-ics20/Cargo.toml

# 3. build standalone with x86_64 Rust 1.64.0 and the dev's CARGO_HOME
cd contracts/cw20-ics20
export CARGO_HOME=/home/tovi/.cargo
sudo mkdir -p /home/tovi/.cargo && sudo chown -R "$(id -u)" /home/tovi/.cargo
RUSTFLAGS='-C link-arg=-s' cargo +1.64.0-x86_64-unknown-linux-gnu build \
    --release --target wasm32-unknown-unknown --locked

# 4. optimize with an old binaryen
wasm-opt -Oz target/wasm32-unknown-unknown/release/cw20_ics20.wasm -o out_872.wasm   # binaryen v91 (v90-97 work)
sha256sum out_872.wasm
# => 2976a2577999168b89021ecb2e09c121737696f71c4342f9a922ce8654e98662