Zilliqa X-Bridge drain: on-chain trace and link analysis (2026-10-08)
On 2026-10-08 03:29:23 UTC (Ethereum block 26,144,997) the Ethereum side of Zilliqa's X-Bridge was emptied by four MessageBus.dispatch calls. Each carried one signature from the bridge's only validator key, 0x29cd9c758239259a15792591a9aa64667a409572. Zilliqa says that key was compromised. This report traces where the funds went and asks one question: were the wallets involved funded by, or linked to, Zilliqa?
Everything below was checked on-chain (Ethereum archive state, Blockscout for Ethereum and Zilliqa, mempool.space for Bitcoin). Snapshot taken 2026-11-07, updated 2026-11-12. Each claim is labelled as either an on-chain fact or an inference.
Answer in short
The drain key was a Zilliqa ops key, funded and authorised by Zilliqa wallets. It was added as the sole validator by the bridge owner
0xb9366e40…. It was gas-funded only by the previous validator0x054973fb…and by Zilliqa's ops/treasury wallet0xaaf33a7e…, which sent it 0.1 ETH on 2026-09-22, 16 days before the drain. This is true under either theory (stolen key or insider).The drain recipient and its laundering wallets were never funded by any Zilliqa wallet. The recipient
0x72852ca9…was new. Its only funding was 0.086 ETH that the drain key itself sent it in the drain block. After that it paid its own gas from the stolen funds, plus 0.001 ETH top-ups from two public gas-refuel wallets.A second Zilliqa key, the X-Bridge deployer
0xd89421cd…, was used on drain day. At 06:07 UTC a Binance withdrawal sent it 0.58 ETH. Within 4.5 minutes the deployer forwarded that to a new wallet. On 2026-11-06 that money was laundered the same way as the bridge proceeds, through the same gas-refuel wallet, with the same Bitcoin wallet fingerprint, five minutes before the bridge proceeds started moving. Inference: one operator controlled both the drain key and the deployer key. This is the strongest link in the data. Binance can identify the account behind withdrawal tx0xd16c3efa…, and that would settle it.THORChain did not hide anything. Every swap memo names its Bitcoin destination in plain text. 9.70 BTC arrived at five known addresses. 3.06 BTC has since gone into Wasabi-style CoinJoins. 6.68 BTC is still unmixed at five known addresses (table below), which should be flagged to exchanges now.
Nothing in the bridge could slow the drain. Withdrawals cannot be paused by design, and there are no caps or delays. Only the single-key owner could stop outflows, and the setup is unchanged today (what would have prevented it). Separately, Zilliqa's own relayer kept using the "compromised" key for 18 days. On Ethereum the key was rotated 28 hours after the drain. On Zilliqa it stayed the only validator until 2026-10-26 10:20 UTC. On Oct 22 and Oct 26 Zilliqa's relayer used it to deliver deposits. The Ethereum
TokenManagerwas never paused (paused() == falsetoday).The drain was run with Foundry
forge script, very likely from Zilliqa's own public relay script. The gas limits of all five drain txs match Foundry's gas-search formula exactly. So do Zilliqa's own manual admin txs, including the Oct 9 key rotation and earlier manual transfers out of the same validator key. Only 1 in 171,911 sampled Ethereum transfers carries that fingerprint. The drain used the raw private key, not a hardware wallet. The script is public, so this cannot tell an insider from an outsider who had the key (details).The ops wallet pays into a Binance account that is visible on-chain on both chains.
0xd6c86912…has Etherscan's public tag "Binance Deposit". The ops wallet sent it a 100 USDT test and then 749,900 USDT on Ethereum (2026-09-11), and 207,010,000 ZIL on Zilliqa (2026-10-06 07:54, tx0x412458f3…). The chain cannot say whose account it is. Binance can, and can say whether it is the account behind the 0.58 ETH withdrawal to the deployer on drain day (lead 1). The wider treasury around the ops wallet is traced in treasury report: ≈ $1.49M of ZIL went to exchanges in the 30 days before the drain (almost all on Oct 6) and ≈ $355k in the 30 days after, and nothing from it went back into the bridge beyond the ops wallet's ≈ $17k on Oct 22.
No direct path was found from any other Zilliqa wallet (owner, earlier validators, ops wallet, the 3-of-5 Safe) into the attacker's wallets. No link was found to the Oct 22 ZRC2 exploit wallets.
What Zilliqa says, and what the chain shows
| Zilliqa's incident page | On-chain | |
|---|---|---|
| Cause | "The X-Bridge was drained using a compromised key." | One signature from the bridge's only validator key, 0x29cd9c75…, released 100% of its USDT, USDC, WBTC and XCAD. The same operator also used the deployer key that day (below) |
| Amount | Not stated | 343,801 USDT, 79,371 USDC, 5.16 WBTC and 10.6M XCAD, ≈ $855k |
| Response | "The bridge is paused and its interface is suspended. The compromised key no longer has any role on Zilliqa, and the incident has been reported to the authorities." | The key was removed on Ethereum 28 h after the drain, but stayed the only validator on Zilliqa for 18 days, and Zilliqa's relayer used it on Oct 22 and 26. The TokenManager was never paused |
| Compensation | Not mentioned | The TokenManager has received ≈ $17k since the drain, all from the ops wallet on Oct 22. Two backlogged withdrawals failed for lack of USDT |
Timeline (UTC)
| When | What |
|---|---|
| 2024-12-18 | Deployer 0xd89421cd… first funded by withdrawals from 0x21a31ee1… and 0x28c6c062…1d60 (Etherscan labels Binance 15 / Binance 14; Blockscout has no tags for them) |
| 2025-01-07 | 3-of-5 Safe 0x69cfa6d0… sends the deployer the bridge's starting liquidity (1.20M USDT, 10.0M XCAD, 10.9 WBTC, 121.8 ETH, TRAXX, BRKL, OPUL); deployer deposits it into the TokenManager |
| 2025-01-11 | Bridge deployed by the deployer. Validator set = [0x250572ed…], one key |
| 2025-04-17 | Validator → 0x054973fb…; ownership of all bridge contracts → 0xb9366e40… |
| 2026-01-30 / 02-25 | Validator → 0x29cd9c75… (two validators for 24 minutes on Jan 30, otherwise always one) |
| 2026-09-11 08:40, 19:13 | Ops wallet sends 100 USDT, then 749,900 USDT, to Binance deposit address 0xd6c86912… |
| 2026-09-22 10:24 | Ops wallet 0xaaf33a7e… sends the validator 0.1 ETH |
| 2026-10-06 07:13–07:14 | Last regular Zilliqa→Ethereum batch (nonces 301–312), signed and sent by 0x29cd… |
| 2026-10-06 07:54, 08:50 | On Zilliqa the ops wallet deposits 207,010,000 ZIL to Binance via 0xd6c86912… and 203,878,000 ZIL to another exchange via 0x682be515…, ≈ $1.49M together, the treasury's largest exchange day |
| 2026-10-07 11:49–15:38 | New wallet 0x86a877d8… bridges 1 USDT in twice and 1 zUSDT out twice. Who owns it is unresolved |
| 2026-10-07 15:14 | Last Ethereum→Zilliqa delivery before a 15-day gap |
| 2026-10-08 03:28:48–51 | The four forged messages use nonces 1,791,430,128–131, which are Unix timestamps of these seconds; legitimate nonces are sequential (…311, 312) |
| 2026-10-08 03:29:23 | Drain, block 26,144,997: 343,801.12 USDT, 79,371.00 USDC, 5.1634 WBTC, 10,614,127.58 XCAD to 0x72852ca9…. The validator key also sends its own 0.086 ETH there |
| 03:31–03:39 | Recipient sells all USDT, USDC and WBTC through UniswapX for 331.09 ETH. XCAD is never sold |
| 06:07:11 | Binance 14 (0x28c6c062…1d60) withdraws 0.57985 ETH to the deployer |
| 06:11:47 | Deployer sends it all to new wallet 0x7426360c… |
| 07:10:35 | 0x7426360c… also gets 0.64 ETH via an Across fill from BNB Chain (depositor 0x144c5fb3…) |
| 2026-10-09 07:40:59 | Owner removes 0x29cd… and adds 0x092a47c0… on Ethereum only |
| 2026-10-13 17:16 | Treasury sends 101,566,299 ZIL to Binance through two pass-through wallets |
| 2026-10-15 15:31 | Treasury's main holder 0xd46b12a5… unstakes 250,000,000 ZIL from the LTIN validator (claimed Oct 23 14:19, unmoved since) |
| 2026-10-22 12:36, 17:48 | 0x29cd… key still relays deposits on Zilliqa, including the ops wallet's own 5,000 USDT and ~12,046 USDC |
| 2026-10-22 16:49–16:55 | Those deposits: the ops wallet withdraws 47,375.5 USDT from Binance 15 (0x21a31ee1…) and within 6 minutes bridges 5,000 USDT and 12,046 USDC into the TokenManager. The only money put back into the drained TokenManager |
| 2026-10-25 | First public notice from Zilliqa ("8:19 PM", as shown on X) |
| 2026-10-26 08:52 | Ops wallet 0xaaf33a7e… gas-funds the new validator |
| 2026-10-26 09:14–09:23 | New key pays 21 backlogged withdrawals on Ethereum (nonces 317–339; 313–316, 332 and 337 skipped; 321 and 334 fail for lack of USDT). In the same minutes the old key 0x29cd… delivers the backlog on Zilliqa |
| 2026-10-26 10:20:02 | Zilliqa-side validator finally rotated 0x29cd… → 0x092a… (block 38,963,027) |
| 2026-10-28 | Zilliqa: X-Bridge suspended, rotating all operational keys |
| 2026-11-06 01:22–01:37 | Deployer-path ETH: two hops, Uniswap → USDC, MAYAChain → BTC bc1qeh0j…, then peeled to a new taproot address |
| 2026-11-06 01:32 → 11-07 10:43 | Drain ETH: test sends, two hops each, then THORChain ×4 and MAYAChain ×1 → BTC → peeled to taproot → Wasabi-style CoinJoins |
How the drain worked (fact)
ChainGatewayUpgradeable (verified source, out/contracts) lets anyone call dispatch. It only checks that the signatures over toEthSignedMessageHash(abi.encode(sourceChainId, block.chainid, target, call, gasLimit, nonce)) form a supermajority of ValidatorManager.getValidators(), which means count * 3 > validators * 2. With one validator, one signature is enough.
The four drain messages call TokenManager.accept(...) as if they came from Zilliqa's token managers (0x4fa6148c… for USDT/WBTC/XCAD, 0xaaf7f44c… for USDC). No such messages were ever emitted on Zilliqa. Their nonces are wall-clock timestamps, not the bridge's sequential counter. Each signature recovers to 0x29cd…, which was the only validator at block 26,144,996. The amounts equal the TokenManager's full balances of each token at the block before, so the drain took 100% of USDT, USDC, WBTC and XCAD.
Loss, at the prices the attacker got on UniswapX (≈ $2,570/ETH): 343.8k USDT + 79.4k USDC + 5.16 WBTC (≈ $428k) + 10.6M XCAD (≈ $4k at $0.0004, unsold) ≈ $855k.
Who controlled the drain key: the validator lineage (fact)
flowchart TD
B[("Binance withdrawals")] --> D["Deployer 0xd89421cd"]
D -- "0.2 ETH" --> V1["Validator #1 0x250572ed"]
V1 -- "0.10 ETH" --> V2["Validator #2 0x054973fb"]
V2 -- "0.053 ETH" --> V3["Validator #3 0x29cd9c75
signed the drain"]
D -- "ownership (Apr 2025)" --> O["Owner 0xb9366e40"]
O -- "add/removeValidator" --> V3
OPS["Ops wallet 0xaaf33a7e"] -- "0.1 ETH (Sep 22)" --> V3
OPS -- "0.1 ETH (Oct 26)" --> V4["Validator #4 0x092a47c0"]
V3 -- "0.086 ETH (drain block)" --> R["Recipient 0x72852ca9"]
0xaaf33a7e…is a large operating wallet. Since 2022 it has moved at least 14.4k ETH and 3.9M USDT on Ethereum. It holds 42.8M ZIL on Zilliqa. It funded the deployer (15.1 ETH), the drained validator and the replacement validator, and it bridged funds through X-Bridge. Bearby calls it "a bridge address". It is an EOA, not a bridge contract.- The bridge was seeded from a 3-of-5 Safe (owners
0x69498F71…,0x26bDC7C9…,0x4C0E2e00…,0xc1261Dc5…,0xDEec30C4…, none of them ops-cluster wallets). Once deployed, the bridge was secured by a single hot key.
Where the money went (fact)
flowchart TD R["Recipient 0x72852ca9"] -- "UniswapX, Oct 8" --> E["331.09 ETH
idle 29 days"] E -- "10 ETH" --> L1["0x2bacf2a8 → 0xcdc8a768"] --> M1["MAYAChain"] --> B1["bc1qa2y8… → bc1prfdk…
CoinJoin"] E -- "50 ETH" --> L2["0xef453faa → 0x8b94b5b9
Uniswap → USDT"] --> T2["THORChain"] --> B2["bc1qc3q9… → bc1pzcg9…
CoinJoin"] E -- "50 ETH" --> L3["0x18813037 → 0x4f9ad58b"] --> T3["THORChain"] --> B3["bc1qh9s5… → bc1pk92q…
CoinJoin, 0.18 left"] E -- "100 ETH" --> L4["0x60ef8938 → 0xb7863acb"] --> T4["THORChain"] --> B4["bc1qcyrm… → bc1pv965…
unspent"] E -- "121.7 ETH" --> L5["0xe0426048 → 0xb763b3cd"] --> T5["THORChain"] --> B5["bc1qtd8p… → bc1pzjhw…
CoinJoin, 2.31 left"]
Each hop wallet forwards its whole balance within 7–9 minutes. Two 0.1 ETH "test" wallets sent their 0.1 ETH back. Each router deposit's memo is =:BTC.BTC:<address>:<limit>/<interval>/0, which names the Bitcoin address outright. On Bitcoin, each destination peels to one new taproot address in 1–3 txs, and those addresses then join ~200-input CoinJoins. The CoinJoins are version 1, locktime 0, at 0.15–1.1 sat/vB, with WabiSabi standard denominations (2ⁿ, 2·3ⁿ/3ⁿ, 1-2-5 sats).
BTC not yet mixed (2026-11-07):
| Address | BTC | Origin |
|---|---|---|
bc1pv965tsrz00qwazje92hsvctsgedeavsn4dt05juhucsaqnp0pzwsjzdjyq |
2.92488457 | 100 ETH leg |
bc1pzjhw0kdf3mr5favyx2lhcvl9c6dxmjfnpcu20tmxgufktv9tkv9q8v4jjv |
2.31183980 | 121.7 ETH leg |
bc1qtd8pl6wuhx2wae738cdtrzj22s0svqjf5x9d69 |
1.22582564 | 121.7 ETH leg |
bc1pk92qwml7yhujryd6jphyalxc9vcm54h8hsug7c9skd0vltya7vdsz6dpgm |
0.18453159 | 50 ETH leg |
bc1pr7mun2q9e983tnfd7nl63a0xzrg8ujlsp09knsq3j9ckupptn37svurqd4 |
0.03548818 | deployer path |
| Total | 6.68256978 |
The 10.6M XCAD is still in 0x72852ca9….
The deployer-key link
Facts:
- The deployer
0xd89421cd…has had no role in the bridge since it handed over ownership on 2025-04-17. Before drain day its last tx was on 2026-02-27. - 2h38m after the drain, Binance 14 (
0x28c6c06298d514db089934071355e5743bf21d60, nonce 18.6M) withdrew 0.57985 ETH to it. Within 4m36s the deployer key signed a transfer of the full amount to new wallet0x7426360c…. The deployer has done nothing since. 0x7426360c…sat untouched until 2026-11-06 01:22:11. Then it went0x77da00ea…(01:26:47) →0x198ceabf…, which ran UniswapexactInputSingleETH→USDC (01:27:11), got a 0.000677 ETH gas top-up from0xa568569568…(01:27:23), and called MAYAChaindepositWithExpiry(01:27:47). That produced BTC atbc1qeh0jkemzrx6kpfh8yr0j59mwgqdhe7hzuj6lc3(01:32:30), which was peeled to new taprootbc1pr7mun…(01:37:37).- The bridge proceeds made their first move at 01:32:35, five minutes after that Maya deposit.
- The bridge proceeds' USDT leg (
0x8b94b5b9…) used the same steps: UniswapexactInputSingle→ gas top-up from the same0xa568569568…→ approve → router deposit, all within a minute. - Every Bitcoin peel tx on both paths has the same fingerprint: version 1, locktime 0, nSequence
0xffffffff(no RBF), P2WPKH input → P2TR output, mostly 3.01–3.02 sat/vB.
Inference: the person laundering the bridge proceeds also controlled the deployer key on 2026-10-08. The Oct 8 withdrawal could have been Zilliqa topping up its own deployer, swept by someone watching the key, or the key holder funding it themselves. Either way, the attacker controlled two Zilliqa ops keys, not one relayer key. That fits Zilliqa's Oct 28 "rotating all operational keys". Weak points: 0xa568… is a gas-refuel distributor that serves ~190 addresses, and the BTC fingerprint is common to some wallet software. Neither alone proves anything. Together with the deployer-key sweep and the five-minute timing, the link is strong.
What would settle it: Binance KYC for the account behind withdrawal 0xd16c3efa…. Also the Across depositor 0x144c5fb3… on BNB Chain (deposit to 0x7426360c…, fill tx 0x19a4922d…). It is a busy EOA (nonce 3,908) and probably a service.
How the keys were held: tx fingerprints
A signature carries no trace of the device that made it, so the chain cannot show whether a key was on a Ledger. The software that built each tx does leave traces: tx type, gas limit and fee policy.
- The validator keys were hot software keys, not hardware wallets (fact). Each validator (#1–#4, including the post-rotation key) sent dozens of
dispatchtxs, up to 12 in about a minute, always EIP-1559 with a fixed 3 gwei priority fee. That is automated relayer software signing without a human pressing a button. - The drain was not sent by the relayer software (fact). All five drain txs in block 26,144,997 use a 0.005 gwei priority fee and identical fee caps. The four messages were built one second apart (their nonces are the timestamps). Inference: a separate script had direct use of the private key, either the raw key or access to whatever signed for the relayer.
- The drain was sent with Foundry
forge script, the tool Zilliqa used for its own manual operations (fact). See the Foundry section below. - The deployer's Oct 8 sweep was built by different software (fact). It used type 2, a 21,000 gas limit and a 2 gwei priority fee. Every earlier deployer transfer used type 0 and a 29,005 gas limit.
- The two laundering paths share tool settings (fact). On both the drain path and the deployer path, contract calls use a legacy fixed 1 gwei gas price and plain forwards use a 75,000 gas limit. This adds to the deployer-key link above.
- The new validator
0x092a…is again a single hot key run by the same relayer software. The setup that was drained has not changed.
The drain was run as a Foundry script
The drain's 0.086 ETH sweep used a gas limit of 29,005. A plain ETH transfer needs exactly 21,000, and every RPC tested (dRPC, PublicNode, Zilliqa) estimates 21,000 for it. So the number comes from the sending software.
Where 29,005 comes from (fact). Foundry forge script --broadcast simulates each tx and runs GasSearch (crates/script/src/runner.rs). That is a binary search from gas_used up to 3 × gas_used, which stops when two successive successful limits differ by less than 10% in integer maths. It then sets gas_limit = result × 130 / 100 (crates/script/src/transaction.rs, default --gas-estimate-multiplier 130). For a transfer the search goes 42,000 → 31,500 → 26,250 → 23,625 → 22,312 and stops; × 1.3 = 29,005. Re-implementing this predicts the exact gas limit of each tx from its gas used and minimum gas:
| Key | Txs whose gas limit matches forge script exactly |
|---|---|
| Validator #3 (drain key) | The 5 drain txs (4 × dispatch + the ETH sweep) and its 3 manual ETH transfers to validator #2 (2026-01-30, 2026-02-04). None of its relayer batches match |
| Deployer | Ownership transfers, addValidator, pause, registerToken, its ETH transfers to the owner |
| Owner | addValidator calls, including the 2026-10-09 rotation, and registerToken |
| Validators #1, #2 | Their manual ETH transfers only |
| Validator #4, relayer batches, attacker laundering wallets | None |
The same keys' manual transfers on Zilliqa also used 29,005.
How rare it is (fact). In 1,500 evenly spaced Ethereum blocks from Oct 2025 to Nov 2026, 1 of 171,911 plain ETH transfers used a 29,005 gas limit, sent by an unrelated MEV-style address. Wallets use 21,000 or round multiples of it (31,500, 23,100, 25,200, 27,300). Foundry is a common developer tool, but almost nobody moves ETH with it.
The template is Zilliqa's own, and it is public (fact). Zilliqa/xbridge (public, archived 2026-05-07) contains smart-contracts/script/bsc/validators/dispatchRelay.s.sol and script/zq/validator/dispatchRelay.s.sol. These Foundry scripts read PRIVATE_KEY_VALIDATOR from the environment, build abi.encode(sourceChainId, block.chainid, target, call, gasLimit, nonce).toEthSignedMessageHash(), sign it with vm.sign, and broadcast dispatch from the same validator key. The drain is that flow with two edits: message gas limit 2,000,000 instead of the script's fixed 1,000,000 (which every legitimate relay used), and nonces set to the current Unix time. It also added a sweep of the key's own ETH. (oscar01428/Universal-Cross-Chain-Bridge, created 2026-07-08, is a copy of Zilliqa/xbridge with recent commit authors rewritten; its script is byte-identical.)
What this means (inference).
- Whoever drained the bridge held the validator's raw private key in a form
vm.signcan use, and ran Foundry against the bridge. The best fit is Zilliqa's public relay script, slightly modified. - Zilliqa's own staff handled that same key the same way: in Jan–Feb 2026 they moved ETH out of it with
forge script. So the key existed outside the relayer, on machines where people ran Foundry. A copy taken from such a machine, a CI secret, or an.envfile fits the evidence. - The fingerprint shows a developer workflow identical to Zilliqa's. It cannot tell an insider from an outsider who had the key, because the script is public.
How widely the key was exposed
How Zilliqa stored and passed keys (fact; full history of the public repo Zilliqa/xbridge):
- Production validator key: 1Password item
op://prj-p-blockchain-infra/xbridge_validator0/PRIVATE_KEY(since Feb 2024). At deploy time it is written into a plain KubernetesSecret, then into an environment variable, and the container runsbash -c "./bridge-validator $PRIVATE_KEY …". The raw key is on the process command line. - Foundry scripts: read raw keys from environment variables:
PRIVATE_KEY_OWNER(156 mentions in history),PRIVATE_KEY_ZILBRIDGE,PRIVATE_KEY_VALIDATOR,PRIVATE_KEY. Foundry also loads them automatically from a local.envfile. - No bridge key was ever committed. Of 1,477 distinct 32-byte hex strings in 215 commits, none is the private key of a validator, the deployer, either owner or the ops wallet. The committed secret files are templates, 1Password references, or encrypted (
.env.enc). - The relayer moved off the public deployment. The public validator deployment was set to 0 replicas on 2025-08-14, yet validators #2 and #3 kept relaying until Oct 2026, with a changed gas profile from Apr 2026. The relayer ran from infrastructure not in the public repo: at least one more copy of the key.
- Turnover: 16 distinct commit authors from Nov 2023 to May 2026; 13 of them touched scripts, CI or validator infrastructure. 13 were active in 2024, 4 in 2025, 2 in 2026. The repo was archived on 2026-05-07.
Human use of the keys (fact). Every rotation was done by hand with Foundry, and each session used several keys within minutes:
| Session | Keys used |
|---|---|
| 2025-01-11/13 (launch) | Deployer deploys and sets validator #1, then funds it. Validator #1's first two relays were sent by hand, not by the relayer |
| 2025-04-17/25 | Deployer rotates to validator #2, hands ownership to the owner, and sends it ETH and ZIL. Validator #1 sends its ETH and ZIL to validator #2 |
| 2026-01-30 → 02-04 | Owner swaps validator #2 → #3 and back. ETH moves #2 → #3 → #2 (twice), and again on Feb 4 |
| 2026-02-25 → 27 | Owner swaps #2 → #3 (Feb 25 11:19). Validator #2 sends ETH (11:21) and later ZIL to #3. #3 starts relaying (Feb 26 10:09). Deployer sweeps ETH and ZIL to the owner (Feb 27 15:08). Owner calls registerToken (15:12) |
By Feb 2026 the owner, deployer, validator #2 and validator #3 keys were all used from one operator environment. The attacker controlled validator #3 and the deployer. There is no sign they had the owner key: they swept the 0.086 ETH on validator #3 and the 0.58 ETH that reached the deployer, but the owner's 0.055 ETH and 93 ZIL are untouched (a weak signal).
Who could have had the drain key (inference):
- anyone with access to the 1Password vault
prj-p-blockchain-infra; - anyone who could read Secrets, exec into pods, or reach the nodes in the production cluster, plus the deploy pipeline;
- whoever ran the relayer after Aug 2025;
- every machine used in the manual sessions above.
.envfiles and shell history persist after a session ends.
The drain key first appears on 2026-01-30. Anyone whose access ended before then could not have copied it, unless they kept access to a vault, machine, .env or backup where it was later stored. Whoever drained the bridge also held the deployer key, which sat in the same operator environment.
Fits / does not fit:
| Explanation | Fit with the evidence |
|---|---|
Malware or phishing on an operator machine holding a .env with the operational keys |
Good: explains validator #3 and the deployer together, and the raw-key Foundry use |
| Former staff with a copy | Only if they left after 2026-01-30, or kept access to the vault, a machine or a backup after leaving |
| Current insider | Fits as well; on-chain data cannot separate it from the two above |
| Leak through the public repo | Ruled out for these keys: none was ever committed |
| Full 1Password vault compromise | Weaker: the owner key would probably have been taken too, and it shows no use |
What would settle it: 1Password access logs for xbridge_validator0, the deployer and owner items since Jan 2026; cluster audit logs (secret reads, pod exec) in production; offboarding dates compared with 2026-01-30; forensics on the machines used in the Jan 30 – Feb 27, 2026 sessions; and the Binance account behind the Oct 8 withdrawal to the deployer.
Incident response facts that need explaining
- Key in use after the drain. On Zilliqa,
0x29cd…sentdispatchtxs on 2026-10-22 (×4) and 2026-10-26 (×3). It remained the sole Zilliqa-side validator until 2026-10-26 10:20:02, so for 18 days whoever held it could have minted unbacked bridged assets on Zilliqa. It did not: every delivery on Zilliqa from Oct 6 to Oct 26 matches a real Ethereum deposit (nonces 1294–1304, none missing and none extra). On Oct 26, Zilliqa's relayer sent with the new key on Ethereum and the old key on Zilliqa in the same minutes. Zilliqa therefore held a working copy of the key throughout. - Rotation on Ethereum 28 h after the drain, so the owner knew by 2026-10-09 07:40. The first public notice came on Oct 25.
- TokenManager never paused. Users and the ops wallet kept depositing into a bridge with no USDT/USDC left. Two of the Oct 26 withdrawals (570 USDT, 6,000 USDT) failed for that reason.
pause()only blocks deposits, so pausing would have protected depositors but could not have stopped the drain (see next section). - The bridge was never refilled. Since the drain the TokenManager's only stablecoin inflows are the ops wallet's 5,000 USDT and 12,046 USDC on Oct 22 and ≈ $126 of user deposits; the Oct 26 backlog payouts used them up. Meanwhile the treasury held the means: it sent ≈ $1.49M of ZIL to exchanges on Oct 6 and has held 288M ZIL (≈ $520k) unmoved since claiming its LTIN unstake on Oct 23 (treasury report).
What would have prevented it
What the verified contracts allow (fact):
- Withdrawals cannot be paused.
TokenManager.accept, which releases funds, has nowhenNotPaused. The source says: "No pausing here because we want incoming txns to go through that have already initiated".pause()only stopstransfer, i.e. deposits.ChainGatewayUpgradeablehas no pause at all. - No caps, rate limits or delays anywhere on the withdrawal path. One signed message can release a token's entire balance, as the drain did.
- The only brake is the owner. Stopping outflows needs
removeValidator,setValidatorManageror a UUPS upgrade, allonlyOwner, and the owner0xb9366e40…is a single EOA. Used 28 h after the drain. - Validators must be plain keys. Signatures are checked with
ECDSA.recover; there is no ERC-1271 path, so a Safe cannot be a validator. Zodiac Roles limits what a Safe executes, not what a key signs, so it cannot constrain a validator either. - None of this has changed: the TokenManager implementation dates from Feb 2025, and the post-drain validator
0x092a…is again one hot key.
Controls, and whether each would have stopped this drain:
| Control | Applies to | Stops this drain? | Change needed |
|---|---|---|---|
| Several independent validators, e.g. 4-of-5 on separate infrastructure and operators | Validator set | Yes. One stolen key is useless | Config only: the contract already enforces count * 3 > validators * 2 |
| Validator keys in a non-exportable HSM / cloud KMS with signing policy and audit logs | Validator keys | Very likely. The drain used the raw key in a Foundry script; a non-exportable key cannot be copied into one | Ops |
| Per-token outflow limit per period, plus a delay queue for large withdrawals that a guardian can cancel | TokenManager | Yes, loss capped and time to react. Must fit real traffic: the largest legitimate withdrawal seen was 300,000 USDT to the ops wallet (2026-03-18), so large amounts need a delay, not a hard block | Contract upgrade |
Guardian-pausable accept/dispatch, plus a watcher checking each dispatch against a Relayed event on Zilliqa |
TokenManager / gateway | Only with a delay. All four drain txs were in one block | Contract upgrade + monitoring bot |
Owner as a Safe multisig, with Zodiac Delay on upgrades, setValidatorManager and validator changes, and Zodiac Roles giving an on-call operator only brake powers (pause, remove a validator) |
Owner | Not this drain. It removes a single point of failure just as large: today one EOA can upgrade the TokenManager or replace the validators and take everything. A brake role also allows reacting in minutes, not 28 h | Ops: transferOwnership to the Safe |
| Retire the deployer key | Deployer | No; but it was still live and used on drain day | Ops |
| Ops/treasury wallet as a Safe with Zodiac Roles allowances (operator may spend up to X per day/week) | 0xaaf33a7e… |
No; it protects 42.8M ZIL and the stablecoin flows from the same single-key exposure | Ops |
| Rotate both chains' validator sets together | Process | Would have closed the 18-day window on Zilliqa | Process |
A rate limit for the bridge itself has to live in the contracts. The money sits in the TokenManager and moves on signed messages, not on Safe transactions, so a Safe or Zodiac cannot cap it. Safe and Zodiac belong on the admin keys and the treasury.
What did not link
- No Zilliqa wallet funded the recipient or any hop wallet. The only funding of attacker-side wallets came from the drain key, the deployer (above), UniswapX/solver settlements, an Across fill, and two gas-refuel distributors (
0xae06669d…,0xa568569568…) that pay hundreds of unrelated addresses. - Shared counterparties between attacker-side and ops-side wallets are only the bridge contracts, the validator lineage, and the deployer. Spam tokens, address-poisoning lookalikes and fake "ETH" tokens that spoof real transfers were filtered out.
- Attacker wallets on Zilliqa: the recipient, hops,
0x7426…path and gas funders have no Zilliqa activity. - PlunderSwap (Oct 22) exploit wallets: never active on Ethereum, and no X-Bridge address appears in their Zilliqa histories.
- Zilliqa treasury web on Zilliqa: the ~100 wallets around the ops wallet (distribution wallets, validator vaults, main holder, the 2026-07-20 sweep) never transacted with the X-Bridge deployer, owner, validators #1–#4 or the drain chain on Zilliqa, directly or through a shared non-exchange counterparty (treasury report). The ops wallet's ETH gas sends on Ethereum remain the only link.
- Oct 7 test wallet
0x86a877d8…: funded through a MetaMask swap from0x368b1c51…, which was funded by a high-volume hot wallet0x963737c5…. Owner unresolved. Its two withdrawals (nonces 313–314) were skipped in the Oct 26 catch-up.
Corrections to the Bearby post
- One validator since launch on 2025-01-11, not April 2025. The ValidatorManager was created by the deployer that day via the CREATE2 factory; the owner only accepted ownership on 2025-04-17.
- XCAD drained: 10,614,127.58, not 10,614.13 (worth ≈ $4k, unsold).
- The 0.58 ETH that "Binance-2 tops up the deployer" with came from Binance 14 (
0x28c6c062…1d60). The post misses that the deployer forwarded it within 4.5 minutes and that the money was laundered alongside the drain proceeds. 0x225a38bc…is a UniswapX filler. The proceeds were 331.09 ETH, not "WETH → burn".0xAAF33a7e…is an ops/treasury EOA, not a bridge contract.- "Nothing was hacked" / "exit scam" describes intent, and the chain cannot show intent. The chain does show that the same operator held at least two Zilliqa ops keys and that Zilliqa kept using the drained key for 18 days.
Leads, most useful first
- Binance: account behind withdrawal
0xd16c3efac8021cac2afa6905848efcf015aa9bbdd6d03c7cebfc4aff676ad1cb(0.57985 ETH to the deployer, 2026-10-08 06:07:11). Also the Dec 2024 withdrawals that first funded the deployer, to check whether they came from the same account. And whether either is the account behind deposit address0xd6c869121021de9a8dea0771e1f79ae8099e4cd9, which the ops wallet pays into (749,900 USDT on 2026-09-11, 207M ZIL on 2026-10-06). - Flag the 6.68 BTC above with exchanges and analytics firms before it is mixed.
- CoinJoin outputs from txs
334dc0bb…,5bdd9bb2…,a837ea24…,c2b79f8f…: watch for post-mix consolidation (timing and amounts often survive mixing). - Across: deposit behind fill
0x19a4922d…(BNB Chain depositor0x144c5fb3…). - Zilliqa: ask how the relayer still held and used
0x29cd…on Oct 22–26, who has access to the deployer key, and why the Zilliqa-side validator and the Ethereum TokenManager were left live.