TL;DR
On October 31, 2026, at 09:29:51 UTC, one transaction took 97,045,563 CORE, about $1.47M, from 91 staking orders on b14g, a Bitcoin dual-staking protocol on Core. Bitquery's trace shows the wallet asked for 0 CORE in all 99 requests, and Core's staking code reads 0 as the whole stake. Bitquery did not read b14g's code. By our reading of the deployed bytecode, b14g compared the amount against the caller's recorded stake, and a wallet with no stake asking for zero passes that. Two days later a Core hard fork moved 68,146,177.22 CORE out of three attacker wallets, about 70% of the loss. Users have not been repaid, and we found no b14g post-mortem as of November 6.
What Happened to b14g on October 31?
b14g matches two sides of Core's dual staking. Bitcoin holders time-lock BTC on Bitcoin, CORE holders fill those positions, and both share the boosted reward. Each position is an order: a small contract that holds the CORE delegation with a validator.
The drain transaction came from a wallet about half an hour old. It called one batch withdrawal on b14g's marketplace with 99 entries, each a tuple of order, validator and amount. We decoded the calldata from Core's public RPC: 91 distinct orders, 23 validators, and an amount of zero in every entry. Eight orders appear twice, once per validator, and all 99 pairs paid out, so the attacker's list had no misses. The receipt logs 99 WithdrawProxy events reporting zero from b14g and 99 events from Core's staking contract reporting the real amounts, which sum to 97,045,563.31 CORE. The largest order lost 2,040,000 CORE and the smallest 788.
There are no ERC-20 transfers in that receipt. CORE is the native asset, so the money moved as value on internal calls.
One early post on X read the coin count as dollars, which overstated the loss about 66 times. We use Bitquery's $1.47M, which prices 97.05M CORE at $0.0151, the quote just before the drain.
Why Does Zero Mean Everything on Core?
A sentinel value is an ordinary input that a function reads as a command. Passing type(uint256).max to mean "repay all" is the common one in lending. Core's version is zero.
CoreAgent.sol contains the substitution: if amount == 0, the amount becomes the delegator's realtimeAmount with that validator. The doc comment on the parameter says only that it is the amount of CORE to unstake. The convention looks like a legacy of an older call shape: PledgeAgent.sol keeps a deprecated one-argument undelegateCoin(agent) that forwards undelegateCoin(agent, 0).
Core does have a zero guard. _undelegateCoin carries require(amount != 0, 'Undelegate zero coin'). It runs after the substitution, so it only fires when the stake is already empty.
A delegator calling this for its own stake loses nothing. The sentinel turns dangerous when a contract holds other people's stake and forwards an amount it did not choose.
What Did b14g's Contract Actually Check?
Early write-ups, including PandaSecurity's, describe a missing ownership check. The bytecode says something narrower.
b14g's source is not public, so what follows is our reading of the deployed order implementation, read at the current state. The 91 orders point at two implementation versions, 84 at this one and 7 at an older build, and both carry the same check and the same call to Core. The function selector and event hash match withdrawCore(address,uint256,address) and WithdrawCore, the names in Halborn's report. The sketch is a reconstruction with reward bookkeeping omitted. Variable names are ours, except totalCoreStake, which appears in a revert string.
// Reconstructed from bytecode. Not b14g's source.
function withdrawCore(address candidate, uint256 amount, address to)
external nonReentrant onlyMarketplace
{
require(amount <= stakes[to][candidate], "InSufficient Amount");
stakes[to][candidate] -= amount; // subtracts 0
totalCoreStake -= amount; // subtracts 0
uint256 before = address(this).balance;
CORE_AGENT.undelegateCoin(candidate, amount); // 0 means everything
Address.sendValue(payable(to), address(this).balance - before);
emit WithdrawCore(to, candidate, amount);
}By our reading, the check reads the caller's recorded stake, which for a stranger is 0, and 0 <= 0 passes. The ledger deducts the requested amount. The payout sends whatever arrived, measured by balance delta. Nothing requires those two numbers to match.
We tested the first line with an eth_call on one order against the current state, impersonating the marketplace and naming a burn address with no stake. Asking for 1 wei reverts with InSufficient Amount, from b14g. Asking for 0 reverts with Undelegate zero coin, from Core. The zero got past b14g and reached the staking contract, which only refused because the order is now empty. That is a simulation of today's state, since the public node no longer serves the state from before the drain, so read it as evidence for the mechanism and not as a replay of the attack.
So the check, as we read it, covers the case everyone pictures: someone asking for more than they own. amount <= stake says nothing about ownership when the amount is zero.
The Ledger Still Says the Money Is There
Because every deduction was zero, b14g's books never registered the drain. We read totalCoreStake from storage on all 91 orders. The values sum to 97,045,563.31 CORE, and each one matches, exactly, the amount Core logged leaving that order. The same 91 contracts hold about 27,079 CORE in native balance.
That is the cleanest available proof of the mechanism, and it has a practical side. The per-staker records were not touched either, so the contract state appears to preserve who was owed what at the moment of the drain. b14g says it is still verifying balances and has no claims process, and it has not said what data its plan will use.
The Attack, Step by Step
| Time (UTC) | Event | Amount (CORE) |
|---|---|---|
| Oct 31, 09:00:42 | Wallet A receives its only funding, from an unlabeled service Bitquery calls Y | 615.62 |
| 09:29:51 | 99 zero-amount requests, 91 orders paid in full | 97,045,563 |
| 09:37:30 | b14g's multisig runs routine upkeep on 7 contracts, withdrawals stay open | |
| 09:37 to 10:10 | Wallet A sends coins through 11 new addresses | 23.4M |
| 09:59:12 | An ordinary user withdraws through the same function | 80 |
| 10:15 to 11:15 | Service Y takes stolen coins and passes them to a MEXC deposit address | 11.16M |
| 10:35:51 | b14g's multisig pauses the contract | |
| 10:36:33 | Wallet A moves the rest to wallet B, 42 seconds after the pause | 73,646,178 |
| 10:41 to 10:47 | Wallet B sends coins to four new addresses | 15M |
| 16:38 | b14g posts its security update | |
| 17:24 | Core DAO says its network is secure | |
| Nov 2, 04:00 | Core's Fermi fork moves the balances of three wallets | 68,146,177.22 |
The times through 17:24 come from Bitquery's reconstruction. The attacker's only visible outlay was the 615.62 CORE of fee money, about $9 at the pre-drain price. The order and validator pairs it needed are public: the marketplace's StakeCoreProxy event emits all three fields on every stake.
How Did Core Move the Coins?
It rewrote three balances. The Fermi code names wallet B and two satellite wallets, sets each balance to zero, and credits the total to one recovery address, once, at the first block at or after 04:00 UTC on November 2. Nothing was rolled back, and supply did not change. The holders signed nothing, which is why The Defiant's report calls it a balance rewrite.
The timing has not been widely reported. GitHub dates the Fermi commit to 15:22:45 UTC on October 31, under six hours after the drain, and the v1.0.27 release to 16:35:12 UTC. That is about three minutes before b14g's first public statement. The commit message calls this the "established CoreRewardFix pattern", the same route Core used after August's reward replay.
A published release named the attacker's wallets about 35 hours before activation. The coins stayed put anyway. Read on November 6, wallet B's nonce is 4, the four sends Bitquery logged, and both satellite wallets have a nonce of 0. The 68.15M Bitquery counted on the evening of October 31 is what the fork collected. We found nothing public that explains why the attacker left it there.
The code comment states the limit of this power: "Shared or custodial accounts must never be listed here". So the fork could only reach coins still sitting in single-owner wallets. The 23.4M that had gone out through new addresses to exchange and service wallets in the first hour was outside it.
A chain has a reason to do this for $1.47M when the victim is most of its DeFi. DefiLlama figures reported by BSC News put b14g at $2.81M of Core's $3.28M. Polygon is weighing the same tool: a proposal to move 70.49M POL tied to the Binance and Polymarket incidents, after block producers restricted five addresses. A fork needs a chain willing to act and loot that has not moved, and an operator controls neither.
What Came Back, and What Did Not?
| Bucket | CORE | Status |
|---|---|---|
| Moved by the fork | 68,146,177.22 | In the recovery address, not distributed |
| Reported frozen by exchanges | about 9M | b14g's figure, not visible on-chain, not returned |
| Unaccounted | about 19.9M | The Defiant's provisional estimate |
At $0.0151, the fork bucket is about $1.03M and the remaining 28.9M about $436K. Our read of the recovery address shows 68,146,177.22 CORE, which matches b14g's figure.
A message signed "b14g Team" offers 10% of any further CORE returned, with a deadline of 23:59 UTC on November 6. b14g says BTC staked through it stayed time-locked on Bitcoin, a claim Bitquery did not check. Core DAO says direct stakers were unaffected.
Would an Audit Have Caught It?
Yes, if the review traced what Core does with the amount. That was outside the engagement on paper.
b14g's audit page lists Halborn's report on the Merge Marketplace and Order contract. CoreDAO commissioned it: one engineer, 3.5 days, January 28 to February 5, 2025. The report quotes withdrawCore in a Low finding about event parameter names, with the body elided. Its scope line reads "Out-of-Scope: Third party dependencies and economic attacks", and Core's staking contract is a dependency. Halborn's Low "Missing Input Validation" finding concerns initializers and setters, so it does not describe this bug.
We cannot say whether the audited code matches what ran on October 31. The repository is private, the report covers commit 5638d2e from early 2025, and Bitquery dates the running logic to about 17 months before the drain. Either way, the bug sits between two codebases, and Core's side was out of scope.
This is the review we do. Our DeFi security audits read the external contracts your funds pass through as part of the system, and test every value they treat as a command against your own checks.
What Could Monitoring Have Changed?
It could not have stopped the drain. All 91 orders emptied in one transaction, and the attacker's first call to b14g was the drain itself, so no alert comes ahead of it. Bitquery found no stolen key and no new code. A pause existed, but the stake in those orders was gone before any rule could fire.
What monitoring could change was when the response started. A rule watching b14g's proxy needs nothing beyond the drain receipt:
- Reported and real amounts disagree. The receipt holds both sets of events. b14g's 99
WithdrawProxyvalues sum to 0, and Core's 99 sum to 97.05M. - One caller touches 91 orders in one block. Bitquery found no prior b14g record for the wallet, and it held 615.62 CORE of fee money.
| From an alert at 09:29:51 | Time available |
|---|---|
| First hop out of wallet A, about 09:37 | about 7 minutes |
| First coins from service Y toward MEXC, about 10:15 | about 45 minutes |
| b14g's pause, 10:35:51 | 66 minutes |
b14g says it paused as soon as it discovered the problem, and the chain cannot show when that was. Bitquery also records the multisig executing routine upkeep at 09:37:30 with withdrawals still open.
An earlier pause would not have touched wallet A, because the coins had already left the contract. What an alert buys is a head start on naming the wallets to exchanges. Part of the 23.4M was moving within those first minutes, and we cannot say what a freeze would have saved.
What Should Protocols Built on Another Contract Do Now?
- List the sentinels in every interface you call. Zero as all, max as all, the zero address as default. Decide for each whether you reject it or authorize it. In CoreAgent.sol, the legacy
proxyTransferpath carries the same zero substitution asundelegate. - Reject zero at the boundary.
require(amount > 0)inwithdrawCorecloses this bug. This class is CWE-1284, improper validation of a specified quantity. - Assert that paid equals requested. Paying out the balance delta hid the mismatch here.
require(received == amount)reverts the whole transaction. - Make authorization independent of the value. Require a nonzero recorded stake, or a position owner, before reading the amount. A check that any value can satisfy is CWE-863 in practice.
- Fuzz against the real dependency. A fork test with amount 0 and an unfunded caller finds this in one run. Mocks of Core's contract would have copied the author's assumption.
- Cap what a batch can move. One call emptied 91 positions. A per-call or per-block outflow limit turns a total loss into a partial one.
- Rehearse the response. Bitquery's trail ends at Bybit, Gate.io and MEXC deposits. Have those contacts ready, and measure the time from alert to exchange notice.
Frequently Asked Questions
Was Core DAO hacked?
No. Core DAO says its consensus, validators and staking contracts operated normally, and Bitquery agrees: the staking contract paid what b14g's orders asked for. The flaw sat in how b14g passed the request on.
How much was stolen from b14g?
97,045,563 CORE, about $1.47M at $0.0151. A hard fork later moved 68,146,177.22 CORE to a recovery address, and b14g reports about 9M frozen at exchanges.
Have b14g users been repaid?
No. As of b14g's latest update the recovered CORE has not been distributed and there is no official claims process. Treat any site offering one as suspect, and do not share keys with a third party.
Did b14g have an ownership check?
Partly. By our reading of the bytecode, b14g compared the amount with the caller's recorded stake. That works as an ownership check for every value above zero, and it passes for anyone at zero. b14g has not published its own explanation.
Could monitoring have prevented the b14g exploit?
No. The drain was one transaction, so no alert comes ahead of it. Same-block signals could have given responders about 45 minutes before the first coins headed to MEXC.
Sources / References
- b14g Hack: 97M CORE Taken by Asking to Withdraw Zero, Bitquery
- Drain transaction on Core Scan
- b14g order implementation on Core Scan
- CoreAgent.sol, Core genesis contracts
- PledgeAgent.sol, Core genesis contracts
- fermi.go at v1.0.27, core-chain
- Fermi commit, GitHub API
- v1.0.27 release, GitHub API
- Ecosystem - Merge Marketplace b14g, Halborn assessment (PDF)
- Audits, b14g documentation
- marketplace.json ABI, b14g subgraph
- Core Fork Recovers 68.15M CORE After b14g Hack, The Defiant via KuCoin
- b14g Recovers 68.15 Million CORE in Core Network Hard Fork, TokenPost
- Message Signed "b14g Team" Offers 10% Reward for More Core Funds, TokenPost
- b14g: Core Network Recovers ~68.15 Million Stolen CORE via Hard Fork, Not Yet Distributed to Users, PANews
- Zero-token withdrawals let a hacker drain 97 million CORE from Bitcoin staking platform b14g, CryptoSlate
- b14g Probes Exploit After 97M CORE Withdrawn, GNcrypto
- b14g Exploit: Withdrawing Zero CORE Drained 97M CORE, PandaSecurity
- b14g Accounts for $2.81 Million of Core Blockchain's DEFI TVL After 97 Million CORE Exploit, BSC News via Cryptonews
- Polygon considers hard fork to return $6.5M stolen in Binance, Polymarket hacks, crypto.news
- CWE-1284: Improper Validation of Specified Quantity in Input, MITRE
- CWE-863: Incorrect Authorization, MITRE



