Exploit
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.
FADE IN:
Act I: The zero
Int. a wallet with history · Oct 31, 01:04 UTC · 8 hours beforeOur 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 UTCThe Fixer sends 615.62 CORE to a wallet that has never existed before: 0xc556ed20…eaea579f1. Gas money. Call this one Wallet A.
For 29 minutes, nothing happens.
Ext. b14g vault manager · block 40,042,125 · 09:29:51 UTCWallet 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.
Here's what each vault does with that request:
- b14g checks: "is the requested amount more than allowed?" Zero is never more than anything. Pass.
- b14g never checks whether the caller owns the vault.
- The vault calls Core's staking contract:
undelegateCoin(validator, 0). - Core's
CoreAgentseesamount == 0and replaces it with the delegator's entire stake. The delegator here is the vault. - Core pays the vault. The vault forwards everything to the caller.

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.

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 eachSeven 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.
| Time | Relay | Got | Forwarded to |
|---|---|---|---|
| 09:37 | 0xef7490a3… | 1.0M | Wallet X |
| 09:41 | 0x86eea108… | 2.5M | Wallet X |
| 09:45 | 0x5fa2bbbb… | 2.5M | Wallet X |
| 09:47 | 0x1dec7b5f… | 2.0M | 0x18435b4e… |
| 09:48 | 0xea7b3e63… | 2.5M | Bybit hot wallet |
| 09:49 | 0xa9b32822… | 2.5M | Wallet X |
| 09:56 | 0x5e89076e… | 2.0M | Wallet X |
| 09:57 | 0x4831ef8e… | 2.0M | Bybit hot wallet |
| 09:58 | 0x7b4083a5… | 2.9M | Wallet X |
| 10:05 | 0xd86d3df1… | 2.0M | the Fixer |
| 10:10 | 0x10d9a10e… | 1.5M | the Fixer |
Three destinations stand out:
- Wallet X, 0xa78a018a…379d814e, takes 13.4M. It already had 7,379 transactions and 2M CORE before the hack, and it behaves like a service's hot wallet. In its first 35 minutes after the hack it pays 2.59M out to 0x5cea74f8…, which forwards straight into Bybit.
- Bybit's hot wallet, 0xf89d7b9c…35eaa40 (labelled Bybit by DefiLlama and Forta, 625,000+ transactions), receives 4.5M directly from two relays.
- The Fixer gets 3.5M back. The old wallet that paid for the gas is now handling the loot.
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 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.


Act III: The 42 seconds
Int. b14g Safe multisig · block 40,043,445 · 10:35:51 UTCSixty-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 UTCForty-two seconds later, Wallet A empties itself: 73,646,178 CORE in one transfer to another new wallet, 0x27758c30…6ce92c. Call it Wallet B.
The pause stopped new withdrawals from b14g. It could not touch coins that had already left. Wallet B starts the same dance:

Then Wallet B goes quiet, sitting on 63,646,177 CORE. That turns out to be a mistake.
Act IV: The fork
Int. coredao-org/core-chain · Oct 31, 15:22:45 UTC · 6 hours afterA 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.
The first block after Fermi time. It holds just two ordinary transactions. Now compare the balances one block apart:
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.

Act V: Where it is now
Int. recovery contract · Nov 7, 10:30 → 11:13 UTCThe 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
| Bucket | CORE | Status |
|---|---|---|
| Hard fork (3 wallets) | 68,146,177 | in b14g's hands since Nov 7 |
| Wallet X 0xa78a…814e | 13,400,000 | unlabelled service wallet; 2.59M forwarded to Bybit |
| Fixer → 0x8251… → 0xe07f… | 11,000,000 | exchange-style deposit and hot wallet, unlabelled |
| Bybit hot wallet (direct) | 4,500,000 | labelled Bybit |
| Off-fork total | 28,900,000 | b14g 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
- Reject 0 at your boundary when a dependency gives 0 (or
type(uint256).max) a special meaning. - Authorize by owner, not by amount. A limit check is not an access check.
- Pause is not a recovery plan. It took 66 minutes; the loot needed 7 to start moving.
- Watch your own TVL in real time. 97M CORE leaving in one block is the loudest possible alert.
Every address in this story
Show all 25 wallets, 3 b14g contracts and the recovery contract
| Role | Address |
|---|---|
| the Fixer (gas + laundering) | 0x63c619d68c0f6daacf09f0a11ad03cea5f9ee1d8 |
| Wallet A (drain) | 0xc556ed208285406d71705927f92d4f4eaea579f1 |
| Wallet B (escape) | 0x27758c30f8e424fbd8ca605bf31e9496c26ce92c |
| Relays from A | 0xef7490a34241a7413902b0583740399b97787949 0x86eea10846e15e27a4a2438a20ec10b17becff90 0x5fa2bbbb63dddf4eb59c826142e3d528a34e199e 0x1dec7b5fc08b9b3ce01bdcd36333c675e3deb9ee 0xea7b3e63a438c5e887b0fd7dc934ca120c21129d 0xa9b32822b1cccdd54f7eb2e3e35679b0dbe2418d 0x5e89076eee623b7a4c16f3d6f1dcbf8ed2609f5e 0x4831ef8edea79a79ebd1ee149328e287f747998c 0x7b4083a5d7bdce296c9dffa877b1fdb967302127 0xd86d3df117967d9d565be082342e9a465bf021ee 0x10d9a10e511c972bfd4d6a7443eb4eb2033b0fe1 |
| Relays from B | 0x41110474bd9f255549c185aa30ee504a31f54752 0x1353c2e98431f496f38571e1a21d997861552d91 0x9a47e939f79fc0541f9fece49643e96e1b905779 0x911c3bf11dfc5497a53a7a04ff7980532516f176 |
| Parked, zeroed by fork | 0x18435b4e54a5771c77170689a86ae9b1df5fad8e 0xa2cc1744a33fe73522b43d70ae2a7ff025dccae5 |
| Wallet X (service wallet) | 0xa78a018a7df5bcb6bd5517bdb1b0cd0a379d814e |
| X → Bybit forwarder | 0x5cea74f8ca48c3239731ea9b2de59a6938b0277a |
| Bybit hot wallet | 0xf89d7b9c864f589bbf53a82105107622b35eaa40 |
| Exchange deposit / hot wallet | 0x8251321719179b7dc5680c5722741347756753ec 0xe07f73dd2a52beceb25f30eb9faa3ee43fb2a3be |
| b14g manager (proxy) | 0x04ea61c431f7934d51fed2acb2c5f942213f8967 |
| b14g Safe multisig | 0xff2a4d8f696991d5350b28fd6fbcf3c7b7f94233 |
| Fork recovery contract | 0xb85095fe01c1d95da3b1df0998ff5eddcc9ac064 |
| 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
- Who operates Wallet X and the 0x8251 → 0xe07f pair, and which exchanges froze the ~9M.
- What the 23.07M moved on Nov 7 is for, and how b14g will distribute the recovered CORE.
- b14g's own post-mortem, which it has promised.
Sources
- On-chain data, Core mainnet, blocks 39,500,000 to 40,282,723, traced Nov 8–9, 2026
- coredao-org/core-chain v1.0.27, "network upgrade: Fermi"
- b14g exploit write-up citing Bitquery, b14g and The Defiant (dev.to)
- Core fork recovers 68.15M CORE (KuCoin News)
- b14g probes exploit (GNcrypto)
- Core DAO says the network was not compromised (TokenPost)