Veil01ETH forged-proof drain on Base
Exploit Transactions
0x5ff6dbc33e77fab8dc086bb9ea3c88f1ba81df198d24ec9fc0c5b50fb1a4a17dVictim Addresses
0xd3560ef60dd06e27b699372c3da1b741c80b7d90Base0x1e65c075989189e607ddafa30fa1a0001c376cfdBaseLoss Breakdown
Similar Incidents
Base DUCKVADER infinite mint + Uniswap drain
34%Base USDC drain from malicious transferFrom spender approvals
32%FOOM Lottery verifier parameter collapse enables arbitrary collect drains
32%MPRO Staking Proxy unwrapWETH Flash-Loan Exploit (Base)
31%USDC drain via unchecked Uniswap V3-style callback
30%TSURUWrapper onERC1155Received bug mints unbacked tokens and drains WETH
26%Root Cause Analysis
Veil_01_ETH forged-proof drain on Base
1. Incident Overview TL;DR
At Base block 42410817, transaction 0x5ff6dbc33e77fab8dc086bb9ea3c88f1ba81df198d24ec9fc0c5b50fb1a4a17d was sent by 0x49a7ca88094b59b15eaa28c8c6d9bfab78d5f903, which deployed helper contract 0x5f68ad46f500949fa7e94971441f279a85cb3354 and executed a forged-withdraw loop against Veil_01_ETH at 0xd3560ef60dd06e27b699372c3da1b741c80b7d90.
The transaction executed 29 successful withdraw operations and drained the full 2.9 ETH pool balance. The root cause is a malformed Groth16 verifier key in Verifier at 0x1e65c075989189e607ddafa30fa1a0001c376cfd: gamma2 and delta2 are identical constants, making proof acceptance forgeable for attacker-chosen public inputs.
2. Key Background
Veil_01_ETH.withdraw authorizes payout using three conditions before transfer:
- nullifier not already spent,
- merkle root is known,
verifier.verifyProof(...)returns true.
The pool transfers denomination - fee to the _recipient after verifyProof passes. For this pool, denomination = 0.1 ETH.
Groth16 verification key structure requires independent trusted setup elements, including distinct gamma2 and delta2. If these are equal, the pairing equation can be satisfied by crafted curve points without a valid witness, breaking the intended ownership proof guarantee.
3. Vulnerability Analysis & Root Cause Summary
Root cause category: ATTACK.
This is a cryptographic soundness failure in the deployed verifier configuration, not a private-key compromise. The victim contract depends on the verifier as its main authorization boundary for withdrawals. Because gamma2 == delta2 in the verifier constants, the attacker can construct (A, B, C) tuples that satisfy the pairing check for chosen public signals (root, nullifier, recipient, relayer, fee, refund) without owning a valid deposit note witness. The attacker then iterates nullifiers (dead0000 to dead001c) to satisfy one-time-spend checks and repeatedly collect 0.1 ETH per call. This exploit path is permissionless and reproducible by an unprivileged actor from public state and code, so it is ACT.
4. Detailed Root Cause Analysis
4.1 Victim authorization path (code-level)
Origin: Veil pool source snapshot (Veil_01_ETH.sol).
require(!nullifierHashes[_nullifierHash], "The note has been already spent");
require(isKnownRoot(_root), "Cannot find your merkle root");
require(
verifier.verifyProof(
_pA,
_pB,
_pC,
[
uint256(_root),
uint256(_nullifierHash),
uint256(uint160(_recipient)),
uint256(uint160(_relayer)),
_fee,
_refund
]
),
"Invalid withdraw proof"
);
nullifierHashes[_nullifierHash] = true;
_processWithdraw(_recipient, _relayer, _fee, _refund);
This makes verifier correctness a hard security invariant: only witness-backed proofs should unlock ETH payouts.
4.2 Breakpoint in verifier key
Origin: verifier source snapshot (Verifier.sol).
uint256 constant gammax1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634;
uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781;
uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531;
uint256 constant gammay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930;
uint256 constant deltax1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634;
uint256 constant deltax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781;
uint256 constant deltay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531;
uint256 constant deltay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930;
gamma2 and delta2 are byte-for-byte identical. That is the concrete code-level breakpoint violating the intended Groth16 soundness assumption.
4.3 On-chain exploit realization evidence
Origin: Base receipt + collector artifacts for tx 0x5ff6...a17d.
status: 0x1
contractAddress(created): 0x5f68ad46f500949fa7e94971441f279a85cb3354
victim Withdrawal logs: 29
recipient topic (all 29): 0x49a7ca88094b59b15eaa28c8c6d9bfab78d5f903
relayer topic (all 29): 0x0000000000000000000000000000000000000000
first decoded nullifier: 0x...dead0000
last decoded nullifier: 0x...dead001c
Balance deltas from collector evidence:
- victim pool
0xd356...:-2.9 ETH - attacker EOA
0x49a7...:+2.899923980938290525 ETH(net, gas-adjusted)
This matches the ACT success predicate and complete drain behavior.
5. Adversary Flow Analysis
-
Attack contract deployment:
- EOA
0x49a7...sends one transaction and creates0x5f68.... - Inclusion is permissionless (no privileged role or keys).
- EOA
-
Forged-proof withdrawal loop:
- Helper contract queries pool state (
getLastRoot,denomination). - It computes forged proof components using curve precompiles and verifier constants.
- It calls
withdrawrepeatedly with sequential nullifiers fromdead0000throughdead001c.
- Helper contract queries pool state (
-
Profit realization and teardown:
- 29 withdrawals at
0.1 ETHeach drain the full pool. - Recipient is the attacker EOA in all withdrawals.
- Helper contract self-destructs at end of transaction.
- 29 withdrawals at
Adversary-related cluster:
- EOA:
0x49a7ca88094b59b15eaa28c8c6d9bfab78d5f903 - Contract:
0x5f68ad46f500949fa7e94971441f279a85cb3354
Victim/protocol components:
- Pool:
0xd3560ef60dd06e27b699372c3da1b741c80b7d90 - Verifier:
0x1e65c075989189e607ddafa30fa1a0001c376cfd
6. Impact & Losses
- Asset lost: ETH
- Total loss:
2.9 ETH - Scope: the observed pool balance was fully depleted in one transaction.
- Economic outcome: attacker gained
2.899923980938290525 ETHnet after gas while victim lost2.9 ETHgross.
Security principles violated:
- zk-proof verifier key soundness invariant,
- withdrawal authorization integrity (proof gate must not be forgeable by arbitrary public-input construction).
7. References
- Base transaction:
0x5ff6dbc33e77fab8dc086bb9ea3c88f1ba81df198d24ec9fc0c5b50fb1a4a17d - BaseScan link:
https://basescan.org/tx/0x5ff6dbc33e77fab8dc086bb9ea3c88f1ba81df198d24ec9fc0c5b50fb1a4a17d - Collector metadata:
/workspace/session/artifacts/collector/seed/8453/0x5ff6dbc33e77fab8dc086bb9ea3c88f1ba81df198d24ec9fc0c5b50fb1a4a17d/metadata.json - Collector trace:
/workspace/session/artifacts/collector/seed/8453/0x5ff6dbc33e77fab8dc086bb9ea3c88f1ba81df198d24ec9fc0c5b50fb1a4a17d/trace.cast.log - Collector balance diff:
/workspace/session/artifacts/collector/seed/8453/0x5ff6dbc33e77fab8dc086bb9ea3c88f1ba81df198d24ec9fc0c5b50fb1a4a17d/balance_diff.json - Victim pool source snapshot:
/workspace/session/artifacts/auditor/iter_0/Veil_01_ETH.sol - Verifier source snapshot:
/workspace/session/artifacts/auditor/iter_0/Verifier.sol