Introduction
We express our gratitude to the Zilliqa team for the collaborative engagement that enabled the execution of this Smart Contract Security Assessment.
Zilliqa 1.0 was a legacy L1 that combined PoW consensus with pBFT finality and EC-Schnorr transaction signing over secp256k1. It was shut down over a year ago in favor of Zilliqa 2.0, an EVM-compatible, PoS-based chain that continues to support legacy Zilliqa 1.0 accounts and transaction formats. Following a July 2026 incident which exposed the leaf keys of a section of users, affected holders now need a way to migrate their legacy accounts to new EVM-compatible accounts without exposing the underlying key material — the leaked leaf keys must not be sufficient to migrate funds.
Migration requires proving ownership of a legacy account without exposing the underlying key material — the leaked leaf keys must not be sufficient to migrate funds. The audited system addresses this with a Groth16 zero-knowledge circuit: a holder proves in zero knowledge that they control a BIP-44 HD wallet address, by supplying the parent node one level up the derivation tree (m/44'/313'/n'/0' or m/44'/313'/n'/0) as a private witness and re-deriving the final child key in-circuit. Because the incident only exposed leaf keys, an attacker holding a stolen leaf key cannot reconstruct the parent and therefore cannot produce a valid migration proof — only the rightful seed holder can. The proof binds the source address, migration destination, and a replay domain as public signals, and is submitted to an on-chain escrow contract that verifies the proof and releases the corresponding balance to the new address.
Review Scope | |
|---|---|
| Repository | https://github.com/Zilliqa/zkp_recovery_app→ |
| Commit | d52b9210fa3c53439bd217ca04dbad6b0d6fa154 |
Review Scope
- Commit
- d52b9210fa3c53439bd217ca04dbad6b0d6fa154
Audit Summary
The system users should acknowledge all the risks summed up in the risks section of the report
Documentation quality
The circuit files themselves carry dense, precise inline comments explaining the BIP-32 CKD logic, security fixes and constraints.
Code quality
The circom circuit (circuit.circom →) is tight, well-commented, and shows real security hardening. Flutter app (~1,677 lines across lib/) is a small, single-purpose onboarding-stepper UI wired to a ProofService/DownloadService pair; reasonably organized by widget/service/model.
Test coverage
securecoretest.js → is a real end-to-end self-test (proves both Ledger-hardened and BIP-44 paths against a known test vector, and asserts tampered public signals fail verification) — good for what it covers, but it's a single manual script, not a suite (no assertions library, no CI-style runner, no edge-case/negative fuzzing beyond the two tamper checks). Flutter-side tests (circom_test.dart →) are thin and self-admittedly limited to pre-dependency validation (can't exercise the native/zkey path in a test env), plus a near-empty default widget smoke test.
System Overview
Circuits (circuit.circom, bip32lib.circom)
The circuit proves ownership of a legacy Zilliqa address by re-deriving it from a private parent key one level up the BIP-32 tree, rather than from the leaf key itself — the core design choice that keeps the tool safe even though leaf keys were exposed in the incident. It supports both wallet conventions in use today: Ledger's fully-hardened path (m/44'/313'/n'/0'/0') and the standard hot-wallet path (m/44'/313'/n'/0/i), selected by a public isHardened flag. The circuit derives the child private key from the parent via one HMAC-SHA512-based BIP-32 step (with the standard IL < n / childPriv ≠ 0 validity checks), turns it into a public key, and hashes that down to a 160-bit address. Three values are bound into the proof as public signals: the address being claimed (expectedAddr, must match what the circuit derives), the migration destination (newAddr, range-checked as a valid 160-bit address), and a replay-protection domain value.
Contract (src/contracts/escrow/escrow_v1.sol)
An upgradeable escrow contract with two entry points: lodge(), which anyone can call to deposit funds credited against their own address, and claim(), which accepts a proof and its public signals, checks the domain matches the current chain, verifies the proof against the generated Groth16 verifier, and — if valid — pays out the depositor's balance to the migration destination named in the proof. Balances are cleared before the payout is sent, which is the correct ordering to avoid reentrancy. Because Zilliqa's legacy (Scilla-side) transactions can't directly call EVM contracts, node software special-cases transfers to this contract's fixed address and routes them into lodge().
Privileged roles
lodge() and claim() are fully permissionless — there is no admin who can block, redirect, or approve a claim; a valid proof is sufficient on its own. The one privileged action is upgrading the contract's implementation, which is gated to calls originating from the zero address. In practice that means only the protocol's own infrastructure — not a deployer, multisig, or any ordinary key — can trigger an upgrade, though exactly how and when that happens is outside this contract and can't be verified from the code alone.
Potential Risks
Native-token-only scope; no coverage for staked ZIL, other tokens, or NFTs. escrow_v1.sol's balances mapping tracks only native ZIL lodged via lodge(). Any ZIL staked at the time of the incident, ZRC-2 fungible tokens, or ZRC-6/721 NFTs held by an affected legacy address have no migration path through this contract — a holder proving ownership recovers their liquid native balance only, while staked or tokenized assets remain stranded with no analog remediation flow defined anywhere in scope.
No migration path for multisig or custom-derivation wallets. The entire scheme assumes a single-signer BIP-44 HD wallet. Legacy multisig holders (different derivation scheme entirely) have no way to prove ownership under this circuit — a scope gap, not a bug, but a real class of funds this tool cannot rescue.
Unaudited, partly unmaintained wallet-side dependencies. Flutter app's bech32 package is unmaintained since Feb 2023 with no audit, and bip32_keys/bip39_mnemonic are actively maintained but single-maintainer and unaudited. A single unaudited/unmaintained dependency in the mnemonic→key path is a supply-chain risk to the tool.
No mnemonic/parent-key export path for some legacy wallets (e.g. ZHIP/Zesame). Zesame only exports a keystore or raw leaf private key — never the BIP-39 mnemonic or any BIP-32 extended key. Since the circuit requires the parent node (one level above the leaf) as a witness, a holder whose only wallet was ZHIP-class has no way to produce that witness at all: they cannot migrate through this tool regardless of any fix, not because their leaf key is compromised, but because the wallet software never gave them the derivation material the proof needs.
Trusted-setup ceremony execution risk. A properly executed trusted ceremony includes 5–10 independent, ideally air-gapped contributors, each contribution hash published, a public verifiable beacon, and a re-runnable snarkjs zkey verify transcript. A ceremony that runs but falls short of these (too few contributors, no air-gapping, no public transcript) is a soft failure the current finding wouldn't catch.
Device/environment assumptions for local proving. The tool requires proving on a non-shared, single-user machine so no other local user can read the process via /proc while key material is resident. This is an environmental assumption the tool cannot enforce and isn't captured by the existing findings.
RAM/swap exposure during proving. Groth16 witness generation for this circuit size is memory-heavy; on constrained devices this can force paging to swap, writing key-derived material to disk unencrypted outside the app's control — a distinct risk from any in-memory zeroization gaps.
Findings
Code ― | Title | Status | Severity | |
|---|---|---|---|---|
| F-2026-1902 | Hardcoded hardened-only derivation path locks out all standard BIP-44 wallets | fixed | High | |
| F-2026-1900 | .trim() on the BIP-39 passphrase silently derives a different master key | fixed | High | |
| F-2026-1900 | Missing EIP-55 check and destination confirmation send the balance to a mistyped address | fixed | High | |
| F-2026-1902 | Unconstrained newAddr and domain desynchronize the proof from the on-chain address | fixed | Medium | |
| F-2026-1902 | Missing IL < n and childPriv != 0 checks break BIP-32 conformance in CKDHardened | fixed | Medium | |
| F-2026-1900 | Unnormalized U+3000 separator rejects Japanese phrases typed with a regular space | fixed | Medium | |
| F-2026-1900 | ASCII-only mnemonic validator rejects six of the ten wordlists | fixed | Medium | |
| F-2026-1900 | Mnemonic checksum validation is optional and English-only, silently skipping on non-English mnemonics or a missing dependency | fixed | Medium | |
| F-2026-1900 | Missing length validation on decoded addresses in ProofService | fixed | Medium | |
| F-2026-1900 | stdin is not checked for TTY before prompting, so the "hidden" mnemonic prompt can silently echo in cleartext | fixed | Medium |
Appendix 1. Definitions
Severities
When auditing smart contracts, Hacken is using a risk-based approach that considers Likelihood, Impact, Exploitability and Complexity metrics to evaluate findings and score severities.
Reference on how risk scoring is done is available through the repository in our Github organization:
Severity | Description |
|---|---|
Critical | Critical vulnerabilities are usually straightforward to exploit and can lead to the loss of user funds or contract state manipulation. |
High | High vulnerabilities are usually harder to exploit, requiring specific conditions, or have a more limited scope, but can still lead to the loss of user funds or contract state manipulation. |
Medium | Medium vulnerabilities are usually limited to state manipulations and, in most cases, cannot lead to asset loss. Contradictions and requirements violations. Major deviations from best practices are also in this category. |
Low | Major deviations from best practices or major Gas inefficiency. These issues will not have a significant impact on code execution. |
Severity
- Critical
Description
- Critical vulnerabilities are usually straightforward to exploit and can lead to the loss of user funds or contract state manipulation.
Severity
- High
Description
- High vulnerabilities are usually harder to exploit, requiring specific conditions, or have a more limited scope, but can still lead to the loss of user funds or contract state manipulation.
Severity
- Medium
Description
- Medium vulnerabilities are usually limited to state manipulations and, in most cases, cannot lead to asset loss. Contradictions and requirements violations. Major deviations from best practices are also in this category.
Severity
- Low
Description
- Major deviations from best practices or major Gas inefficiency. These issues will not have a significant impact on code execution.
Potential Risks
The "Potential Risks" section identifies issues that are not direct security vulnerabilities but could still affect the project’s performance, reliability, or user trust. These risks arise from design choices, architectural decisions, or operational practices that, while not immediately exploitable, may lead to problems under certain conditions. Additionally, potential risks can impact the quality of the audit itself, as they may involve external factors or components beyond the scope of the audit, leading to incomplete assessments or oversight of key areas. This section aims to provide a broader perspective on factors that could affect the project's long-term security, functionality, and the comprehensiveness of the audit findings.
Appendix 2. Scope
The scope of the project includes the following smart contracts from the provided repository:
Scope Details | |
|---|---|
| Repository | https://github.com/Zilliqa/zkp_recovery_app→ |
| Commit | d52b921→ |
| Whitepaper | https://hackenio.cc/hacken-methodologies→ |
| Requirements | |
| Technical Requirements |
Scope Details
- Commit
- d52b921→
- Requirements
- Technical Requirements
Assets in Scope
Appendix 3. Additional Valuables
Verification of System Invariants
The following invariants were independently checked and confirmed sound:
Circuit arithmetic: the secp256k1 field prime, group order, and 8160-entry precomputed generator table (
ecdsa_func.circom) were checked entry-by-entry against reference values;CheckInRangeSecp256k1,CheckCarryToZero, andBigMultNoCarry's polynomial identity were verified sound with overflow margins recomputed by hand.SHA-512/HMAC stack: padding is correct for every instantiated bit-length, and the in-circuit chain code was verified byte-for-byte against real
@scure/bip32derivations — the load-bearing check that the whole CKD path is faithful to BIP-32.Contract invariants: the ERC-7201 storage slot was recomputed and matches; CEI ordering in
claim()is correct (no reentrancy);_disableInitializers()correctly locks the implementation; public signals[old, new, domain, isHardened]are read from the bound proof output rather than a separate caller-supplied argument, which closes proof-redirection;chainidbinding prevents cross-chain replay.Client/circuit consistency: the Dart proof-input encoding, calldata ABI layout (selector, G2 coordinate swap,
uint256[2],uint256[2][2],uint256[2],uint256[4]head layout), and address derivation (sha256(pubkey)[-20:]) all match the circuit's constraints exactly, with reproduction scripts pinning the correspondence.
Recommendations
The Escrow.sol contract in scope could benefit from the introduction of automatic emergency actions for critical activities, such as unauthorized operations like proxy upgrades, as well as unexpected fund manipulations, including large or anomalous claim() payouts. Adding such mechanisms would enable the protocol to react automatically to unusual activity, ensuring that the contract remains secure and functions as intended even if a circuit soundness gap or a compromised proving artifact is exploited before it is caught.
To improve functionality, these emergency actions could be designed to trigger under specific conditions, such as:
Detecting proxy upgrade attempts, given that
_authorizeUpgradecurrently gates onmsg.sender address(0)with no on-chain visibility into what triggers that call.Monitoring large or unexpected
claim()payouts relative to typical lodged balances, and flagging repeated claims against the samesrcAddressin a short window.Pausing
lodge()andclaim()when irregularities are identified, so deposits and payouts halt while the anomaly is investigated rather than continuing to process against a potentially compromised verifier orzkey.
These enhancements would provide an added layer of security, making the contract more robust and better equipped to handle unexpected situations — such as a forged proof slipping past an unaudited trusted-setup contribution, or a circuit defect surfacing post-deployment — while maintaining smooth operations for legitimate migration claims.
Frameworks and Methodologies
This security assessment was conducted in alignment with recognised penetration testing standards, methodologies and guidelines, including the NIST SP 800-115 – Technical Guide to Information Security Testing and Assessment →, and the Penetration Testing Execution Standard (PTES) →, These assets provide a structured foundation for planning, executing, and documenting technical evaluations such as vulnerability assessments, exploitation activities, and security code reviews. Hacken’s internal penetration testing methodology extends these principles to Web2 and Web3 environments to ensure consistency, repeatability, and verifiable outcomes.
This was supplemented by methodologies specific to the audited system:
Spec cross-referencing: Findings are anchored to primary sources — BIP-32/39/44, BIP-173 (bech32), SEC 1 v2 §2.3.3 (point compression), RFC 4231/2104 (HMAC), EIP-55, SLIP-44.
Incident-driven threat modeling: The July 2026 Zilliqa Ledger nonce-bias postmortem directly shaped the security property under test — that a leaked leaf key must be insufficient to forge a migration proof, since only the parent node (never exposed by the incident) is accepted as a witness.
Conventional unit/integration testing:
circom_tester+ Mocha across circuit templates (BitAdd,ModAddN,Hmac512,CompressedPub,PrivBitsToAddr,CKDFinalStep,SeedOwnershipMin) and full end-to-end proof/CLI flows.