Atharva Khewle

ExploitNov 9, 2026 · Core chain · 14 min read · every number traced on-chain

Zero: the b14g heist, minute by minute

TL;DR On Oct 31, 2026, a wallet asked 91 b14g staking vaults to withdraw 0 CORE. On Core, 0 means "everything". 97,045,563 CORE (~$1.47M) left in one transaction. Over the next 106 minutes the attacker spread it across 25 wallets, raced b14g's emergency pause and lost by 42 seconds, then parked most of it. Two days later Core shipped a hard fork that set three of those wallets to zero and moved 68.15M CORE into a recovery contract.

97.05MCORE drained
1transaction
25wallets the loot touched
42 sbetween pause and escape
68.15Mclawed back by hard fork
Timeline: Oct 31 funding, drain, laundering, pause; Oct 31 fork code; Nov 2 fork; Nov 7 recovery moves 09:00gas 09:29:51DRAIN 09:37–11:18launder 10:35:51pause Oct 31 15:22fork coded Nov 2 04:00FORK Nov 7funds move Oct 31 → Nov 7, 2026 · UTC · not to scale

FADE IN:

Act I: The zero

Int. a wallet with history · Oct 31, 01:04 UTC · 8 hours before

Our first character is not a burner. 0x63c619d6…5f9ee1d8 has sent over 650 transactions on Core. On Oct 20 it moved 46,263 CORE to a deposit address. At 01:04 and 01:31 on Oct 31 it sends two more chunks out: 10,301 and 9,678 CORE. Then it waits with 36,749 CORE in its pocket.

Call it the Fixer. It shows up three more times in this story.

Ext. Core mainnet · block 40,041,542 · 09:00:42 UTC

The Fixer sends 615.62 CORE to a wallet that has never existed before: 0xc556ed20…eaea579f1. Gas money. Call this one Wallet A.

tx 0x698ad5a4…3e181ee

For 29 minutes, nothing happens.

Ext. b14g vault manager · block 40,042,125 · 09:29:51 UTC

Wallet A sends its first transaction ever (nonce 0) to b14g's manager 0x04ea61c4…213f8967. The calldata is a list of 99 requests. Each one names a vault, a validator, and an amount.

// selector 0x94923c60, 99 entries (vault: 0x4b904a98…, validator: 0x741095f5…, amount: 0) (vault: 0x61645ce4…, validator: …, amount: 0) (vault: 0x6705db1f…, validator: …, amount: 0) … 96 more. Every single amount: 0

Here's what each vault does with that request:

  1. b14g checks: "is the requested amount more than allowed?" Zero is never more than anything. Pass.
  2. b14g never checks whether the caller owns the vault.
  3. The vault calls Core's staking contract: undelegateCoin(validator, 0).
  4. Core's CoreAgent sees amount == 0 and replaces it with the delegator's entire stake. The delegator here is the vault.
  5. Core pays the vault. The vault forwards everything to the caller.
Anakin and Padme meme: I'll withdraw 0 CORE. So you're taking nothing, right? ...right?

One block. Ninety-nine payouts. The biggest fifteen are 2,040,000 CORE each. The smallest is 788. Wallet A's balance goes from 615 to 97,046,178 CORE.

block 40,042,125 · internal transfers to Wallet A 0x4b904a98… → A 2,040,000 0x61645ce4… → A 2,040,000 0x6705db1f… → A 2,040,000 … 12 more vaults at 2,040,000 0x6be1adad… → A 1,972,000 0x0cc016d6… → A 1,904,000 … 81 more, down to … 0x6a7ba8b8… → A 788 ──────────────────────────────── total 97,045,563 CORE (91 vaults, 23 validators)
Drake meme: rejecting checking who owns the vault, approving checking if 0 is too much
Wallet A calls the b14g manager with 99 zero requests; vaults call Core's CoreAgent; full stakes flow back to Wallet A Wallet A0xc556…79f1 b14g manager0x04ea…8967 vault ×91vaultvault Core CoreAgent0x…1011 99 × 0 undelegate(…, 0) full stake ×99 → 97,045,563 CORE to the caller

According to Bitquery's analysis, this logic had been b14g's live implementation for about 17 months. Anyone could have sent this transaction on any day of those 17 months. I replayed it from a made-up address against the chain state one block before the attack: asking for 0 paid out 2,040,000 CORE, asking for 1 was rejected.

Act II: The scatter

Montage · 09:37 → 10:10 UTC · eleven wallets, one use each

Seven minutes after the drain, Wallet A starts dealing cards. Eleven brand-new wallets, one transfer each, between 1M and 2.9M CORE. Every one of them forwards its full balance, usually within a minute, and never moves again. One of them waits over an hour.

TimeRelayGotForwarded to
09:370xef7490a3…1.0MWallet X (09:37:51)
09:410x86eea108…2.5MWallet X (09:41:51)
09:450x5fa2bbbb…2.5MWallet X (09:45:51)
09:470x1dec7b5f…2.0M0x18435b4e… (10:54:15)
09:480xea7b3e63…2.5MBybit hot wallet (09:54:54)
09:490xa9b32822…2.5MWallet X (09:49:51)
09:560x5e89076e…2.0MWallet X (09:57:21)
09:570x4831ef8e…2.0MBybit hot wallet (10:04:54)
09:580x7b4083a5…2.9MWallet X (09:58:51)
10:050xd86d3df1…2.0Mthe Fixer (10:15:15)
10:100x10d9a10e…1.5Mthe Fixer (10:20:45)

Three destinations stand out:

Int. the Fixer · 10:09 → 11:18 UTC

The Fixer has a routine. Everything it receives goes to one address, 0x82513217…756753ec. That address forwards every coin, within minutes, to 0xe07f73dd…e3be, a wallet with 463,000+ transactions and 30M CORE. That is the classic shape of an exchange deposit address sweeping into an exchange hot wallet.

the Fixer → 0x8251… (deposit) → 0xe07f… (hot wallet) 10:09:39 36,248 ← its own pre-hack balance goes first 10:17:33 2,000,000 10:24:45 1,500,000 10:33:06 +57,657 ← paid IN by 0xe07f, sent straight back 10:41:06 +105,710 ← same again 10:56:51 160,000 11:03:39 7,341,464 11:15:36 104,246

The detail that matters: the same hot wallet paid the Fixer twice during the hack. A wallet with 650+ transactions that withdraws from and deposits to the same exchange is the kind of trail an exchange compliance team can follow to an account. b14g later said exchange partners froze about 9M CORE, without naming them.

Distracted boyfriend meme: the Fixer looks at an exchange deposit address while staying anonymous looks on
This is fine meme: 97M CORE leaves in one block. b14g, minute 30 of 66

Act III: The 42 seconds

Int. b14g Safe multisig · block 40,043,445 · 10:35:51 UTC

Sixty-six minutes after the drain, an owner of b14g's 2-of-N Safe, 0xff2a4d8f…7f94233, executes one call on the manager: pause().

tx 0x3126656e…e52ae02e · sent by 0x15246372…62e0678a

Ext. Wallet A · block 40,043,459 · 10:36:33 UTC

Forty-two seconds later, Wallet A empties itself: 73,646,178 CORE in one transfer to another new wallet, 0x27758c30…6ce92c. Call it Wallet B.

tx 0x714f5812…4b6b58c

The pause stopped new withdrawals from b14g. It could not touch coins that had already left. Wallet B starts the same dance:

Wallet B, 10:41 → 10:54 10:41:51 2.5M → 0x41110474… → 0xa2cc1744… (10:54:45, then never moves) 10:43:27 5.0M → 0x1353c2e9… → the Fixer (10:53:24) 10:44:27 2.5M → 0x9a47e939… → the Fixer (10:54:21) 10:47:03 5.0M → 0x911c3bf1… → back to Wallet B (10:50:12, minus 1 CORE)
Two buttons meme: keep 5M in the fresh wallet, or send it straight back. Wallet B, 10:47 UTC

Then Wallet B goes quiet, sitting on 63,646,177 CORE. That turns out to be a mistake.

Where 97M CORE went: 13.4M to Wallet X, 4.5M to Bybit, 11M via the Fixer to an exchange, 68.15M frozen by the hard fork Wallet A97.05M 11 relays23.4M Wallet B73.65M Wallet X13.4M Bybit hot wallet4.5M direct the Fixer11.0M in exchange deposit0x8251 → 0xe07f HARD FORK · 68.15M Wallet B 63.65M 0x18435b 2.0M · 0xa2cc 2.5M 2.59M on to Bybit amounts in CORE · arrows simplified · see tables for every hop

Act IV: The fork

Int. coredao-org/core-chain · Oct 31, 15:22:45 UTC · 6 hours after

A commit lands in Core's official node repository: network upgrade: Fermi. It adds one new file, consensus/misc/fermi.go. Release v1.0.27 ships at 16:35. The interesting part is the list:

var fermiAccounts = []common.Address{
    common.HexToAddress("0x27758c30f8e424fbd8ca605bf31e9496c26ce92c"), // Wallet B
    common.HexToAddress("0x18435b4e54a5771c77170689a86ae9b1df5fad8e"),
    common.HexToAddress("0xa2cc1744a33fe73522b43d70ae2a7ff025dccae5"),
}
var fermiRecipient = common.HexToAddress("0xb85095fE01C1d95da3B1df0998ff5eDDcC9ac064")

func ApplyFermiAdjustment(statedb vm.StateDB) {
    ...
    statedb.SetBalance(addr, uint256.NewInt(0), ...)   // for each account
    statedb.SetBalance(fermiRecipient, cur + total, ...)
}

Activation: FermiTime = 1793592000, which is Nov 2, 04:00:00 UTC. Validators have 35 hours to upgrade. The attacker can read the same public code. The three listed wallets never move again.

Ext. Core mainnet · block 40,093,117 · Nov 2, 04:00:01 UTC

The first block after Fermi time. It holds just two ordinary transactions. Now compare the balances one block apart:

block 40,093,116 block 40,093,117 0x27758c30… (Wallet B) 63,646,177.23 → 0 0x18435b4e… 1,999,999.99 → 0 0xa2cc1744… 2,499,999.99 → 0 0xb85095fe… (recovery) 0 → 68,146,177.22

No transaction moved this money. The node software edited three balances as part of the block itself. Total supply is unchanged: what was debited was credited to 0xb85095fe…cC9ac064, a contract. b14g called it a recovery "without reversing transactions, minting new tokens, or changing the total CORE supply". All three parts are technically true. It is still a chain deciding, after the fact, who owns what.

Expanding brain meme: ask the hacker nicely, offer a 10% bounty, get exchanges to freeze 9M, ship a hard fork that sets their balance to 0

Act V: Where it is now

Int. recovery contract · Nov 7, 10:30 → 11:13 UTC
10:30:09 b14g manager upgraded to a new implementation (block 40,244,460) 10:42:15 recovery → b14g Safe 787.88 ← test transfer 11:03:45 recovery → b14g Safe 45,013,740.53 11:11:54 recovery → 0xcd6d74b6… 23,065,746.41 11:13:21 b14g Safe executes a call that moves those 23.07M on to 0xee21ab61…

The recovery contract now holds 65,902 CORE. Where the 23.07M ended up and what the Safe does with the 45M is b14g's next chapter. As of b14g's Recovery Update #1, no user distribution or claims process has been announced.

The final ledger

68.15M recovered by fork, 13.4M to Wallet X, 11.0M via the Fixer to an exchange, 4.5M to Bybit fork 68.15M (70.2%) X 13.4M Fixer 11.0M Bybit 4.5M
BucketCOREStatus
Hard fork (3 wallets)68,146,177in b14g's hands since Nov 7
Wallet X 0xa78a…814e13,400,000unlabelled service wallet; 2.59M forwarded to Bybit
Fixer → 0x8251… → 0xe07f…11,000,000exchange-style deposit and hot wallet, unlabelled
Bybit hot wallet (direct)4,500,000labelled Bybit
Off-fork total28,900,000b14g says ~9M of it is frozen at exchanges

28.9M left the reach of the fork. Take away the ~9M reportedly frozen and roughly 19.9M CORE is unaccounted for, which matches The Defiant's estimate. My trace adds where that money entered: two unlabelled high-volume wallets and Bybit.

CUT TO BLACK.

Why it worked, in one paragraph

b14g wrapped Core's staking contract and asked one question before paying out: "is this amount too much?" It never asked "is this yours?". Core's staking contract, which b14g called on its own behalf, treats 0 as "my whole position". Each contract made sense alone. Together, the most harmless number in computing became a master key.

// Core CoreAgent (genesis contract 0x…1011)
function undelegateCoin(address candidate, uint256 amount) public {
  amount = undelegate(candidate, msg.sender, amount);   // msg.sender = the b14g vault
  Address.sendValue(payable(msg.sender), amount);
}
function undelegate(address candidate, address delegator, uint256 amount) internal returns (uint256) {
  ...
  if (amount == 0) {
    amount = candidateMap[candidate].cDelegatorMap[delegator].realtimeAmount;
  }

For builders

Every address in this story

Show all 25 wallets, 3 b14g contracts and the recovery contract
RoleAddress
the Fixer (gas + laundering)0x63c619d68c0f6daacf09f0a11ad03cea5f9ee1d8
Wallet A (drain)0xc556ed208285406d71705927f92d4f4eaea579f1
Wallet B (escape)0x27758c30f8e424fbd8ca605bf31e9496c26ce92c
Relays from A0xef7490a34241a7413902b0583740399b97787949
0x86eea10846e15e27a4a2438a20ec10b17becff90
0x5fa2bbbb63dddf4eb59c826142e3d528a34e199e
0x1dec7b5fc08b9b3ce01bdcd36333c675e3deb9ee
0xea7b3e63a438c5e887b0fd7dc934ca120c21129d
0xa9b32822b1cccdd54f7eb2e3e35679b0dbe2418d
0x5e89076eee623b7a4c16f3d6f1dcbf8ed2609f5e
0x4831ef8edea79a79ebd1ee149328e287f747998c
0x7b4083a5d7bdce296c9dffa877b1fdb967302127
0xd86d3df117967d9d565be082342e9a465bf021ee
0x10d9a10e511c972bfd4d6a7443eb4eb2033b0fe1
Relays from B0x41110474bd9f255549c185aa30ee504a31f54752
0x1353c2e98431f496f38571e1a21d997861552d91
0x9a47e939f79fc0541f9fece49643e96e1b905779
0x911c3bf11dfc5497a53a7a04ff7980532516f176
Parked, zeroed by fork0x18435b4e54a5771c77170689a86ae9b1df5fad8e
0xa2cc1744a33fe73522b43d70ae2a7ff025dccae5
Wallet X (service wallet)0xa78a018a7df5bcb6bd5517bdb1b0cd0a379d814e
X → Bybit forwarder0x5cea74f8ca48c3239731ea9b2de59a6938b0277a
Bybit hot wallet0xf89d7b9c864f589bbf53a82105107622b35eaa40
Exchange deposit / hot wallet0x8251321719179b7dc5680c5722741347756753ec
0xe07f73dd2a52beceb25f30eb9faa3ee43fb2a3be
b14g manager (proxy)0x04ea61c431f7934d51fed2acb2c5f942213f8967
b14g Safe multisig0xff2a4d8f696991d5350b28fd6fbcf3c7b7f94233
Fork recovery contract0xb85095fe01c1d95da3b1df0998ff5eddcc9ac064
Recovery → (Nov 7)0xcd6d74b6852fbeeb1187ec0e231ab91e700ec3ba
0xee21ab613d30330823d35cf91a84ce964808b83f

Method

Core has no public explorer API without a key, so everything here comes from raw RPC against an archive node (core.drpc.org). For each wallet I binary-searched the blocks where its balance or nonce changed, then traced those blocks with debug_traceTransaction / debug_traceBlockByNumber (callTracer) to catch internal transfers. Exchange labels come from public lists (DefiLlama, Forta); everything I call "exchange-style" without a label is based only on behaviour. The replay script is repro_b14g.py. It signs nothing.

Still unknown

Sources

Found an error? Email me. I'll fix it and credit you.

← All field notes