Crypto exploits

ZkLinkNova exploit analysis

An attacker reused a cross-chain claim authorisation on zkLink Nova's Ethereum bridge and installed a batch root of their own choosing, then withdrew against it. DeFiHackLabs records about $783,000 taken from the escrow and the token bridge across two transactions, the first on 28 October 2026.

David

Security ·

Reported loss
$783K
Occurred
Logged

Logged as “2026-10-28: Zklink Novas L1 Arbitrator Authenticates A Cross Chain Claim With A Transient Finalizemessagehash That Claimmessage Never Clears And Never Re Checks Per Call And The Syncl2Txhashrelayer That Feeds It Is Public With No Role So An Attacker Populates A Valid Transient Hash With One Relay Call Then Reuses It In A Second Call That Points The Claim Target At Ethereumgatewaysendmessage Which Runs Zklinksyncbatchroot Under The Gateway Identity And Installs An Attacker Chosen L2Logsroothash For A Batch After Which Withdrawals Finalize Against Forged Proofs That The Real On Chain Verifier Accepts on ZkLinkNova”.

Analysis3 min read

zkLink Nova's bridge on Ethereum lost about $783,000 in two transactions by DeFiHackLabs' count, the first at 22:21 UTC on 28 October 2026. Nothing cryptographic was broken and no key was taken. The attacker reused an authorisation the protocol had already issued, used it to install a batch root of their choosing, and the bridge's own withdrawal verifier accepted the proof that went with it.

Where the authorisation came from

The L1 Arbitrator at 0x1Ee09A2cAa0813A5183f90F5a6d0E4871f4C6002 authenticates cross-chain claims with two transient storage values. claimMessage sets claiming to 1 with tstore at the start of the call, and receiveMessage writes the message hash into finalizeMessageHash with another tstore. Both are declared as ordinary state variables, commented as transient values, and the contract only touches them with tload and tstore. Neither is ever reset (verified source).

That matters because of the check that follows. Once the inner call returns, claimMessage loads finalizeMessageHash back and requires it to equal a hash of its own arguments. A fresh callback writes a fresh hash, and the check passes. A second claimMessage in the same transaction finds the first call's hash still sitting in the slot, and if the arguments match, the same authorisation is accepted twice.

The entry point is a separate contract, SyncL2TxHashRelayer at 0x0FDC28e64772a70109d10fd068f859cFa7Dea2c6. Its claimPrimaryChainSyncL2TxHashMessage function is external payable with no role check, no whitelist and no signature, and it passes the caller's own _sourceChainCanonicalMessageService and _sourceChainClaimCallData straight into ARBITRATOR.claimMessage, whose onlyRelayer check the relayer contract passes. claimMessage then calls that address with that calldata itself, through Address.functionCall. The relayer always builds the same _receiveCallData from the two L2 transaction hashes it is given, so two calls with the same hashes present the same arguments to the hash check (verified source).

The first transaction

The attack transaction is 0x4f3c23e75b9134e8cae5a462550daad42fbbc19242567fcc34c0a035e297462c, sent from 0xaCA0655a11200A80d33D0E48786317304cDE5718 at 22:21:35 UTC on 28 October, in block 26294188 (transaction). Its destination is the attacker's own contract at 0xD5f162B68a0281686E8Dc76dB8d67d2f3E6Aff07, and the whole mechanism fits inside it.

The relayer is called twice. The first call populates the transient hash with a legitimate syncL2TxHash relay. The second call keeps the same L2 transaction hashes but names EthereumGateway at 0x83Bc7394738A7A084081aF22EEC0051908c0055c as the message service, with a sendMessage call as the claim data. The Arbitrator makes that call itself, so it passes the gateway's onlyArbitrator check, and the gateway forwards the enclosed syncBatchRoot call to ZkLink under the gateway's own identity (verified source). No callback runs, finalizeMessageHash still holds the first call's hash, and the check passes. On 0x5fD9F73286b7E8683Bab45019C94553b93e015Cf that function is onlyGateway and writes the supplied root straight into l2LogsRootHashes[_batchNumber], with no check that the batch existed. Batch 116409 received a root the attacker chose (verified source).

With that root in place, finalizeEthWithdrawal on zkLink and finalizeWithdrawal on the L1ERC20Bridge at 0xAd16eDCF7DEB7e90096A259c81269d811544B6B6 accepted the attacker's withdrawal messages. The bridge's own verifier, proveL2MessageInclusion, accepted the proofs, because the root it checked against was the one the attacker had installed through the legitimate path. The DeFiHackLabs proof of concept includes a negative control that skips the root-install step, and the first withdrawal then reverts (proof of concept).

What was taken

In the first transaction the escrow lost 22.431294525563808989 ETH, and the token bridge lost 11 ERC-20 balances, including 18,113.448053 USDC, 3,360.898152 USDT and 0.17360197 WBTC. The largest token amount was 245.74 pufETH.

The totals differ by a little between sources. DeFiHackLabs records about $783,000 across the two exploit transactions, and its proof of concept reconstructs only the first (README entry). DeFiLlama records $783,100 (DeFiLlama). Blockaid put the figure at about $786,000 while the attack was still running (Phemex), and a later report valued a single transaction at about $779,000 (Phemex).

The response

Blockaid flagged the attack while it was still running, and said the attacker had used a public relayer to inject a forged batch root and finalize withdrawals to an attacker-controlled address (Phemex). zkLink paused block production on zkLink Nova, closed withdrawals and said it had patched the affected Arbitrator contracts on Ethereum and Optimism (Phemex). The ZkLink proxy at 0x5fD9F73286b7E8683Bab45019C94553b93e015Cf was upgraded at 10:26:59 UTC on 29 October, in block 26297792, to an implementation named ZkLinkLegacyPaused (upgrade transaction, verified source). zkLink later offered the attacker a 20% bounty to return the funds, and up to 20% of recovered funds to anyone whose information helped recover them (Phemex).

Sources read (19)

Other exploits

Query every incident yourself — free, no signup

Updated 11 Nov 2026. Figures are as reported by the sources listed above and are not independently verified.