
The Payy Exploit: A $1.92M Forged ZK Proof Attack
How forged zero-knowledge proofs passed Payy’s verifier and took 1.92M USDC, 73 days after Aztec released a fix for the Barretenberg bug Payy blames.
Author:
Lilian CariouOn September 24, 2026, an attacker took 1,918,793 USDC (about $1.92 million) from Payy, a privacy-focused payment network built as a rollup on Ethereum. The money was users' deposits, held in Payy's bridge contract. Payy has ruled out a stolen private key. The attacker asked to withdraw 1,828,589 USDC without spending any funds, and attached a forged zero-knowledge proof, the cryptographic receipt that is supposed to show a transaction followed the rules. Payy's verifier accepted the proof anyway, and two smaller forged withdrawals followed.
Payy's postmortem attributes the acceptance to a soundness bug in Barretenberg, the proving backend that Aztec develops for the Noir language. A soundness bug is one that lets a verifier accept a proof of a false statement. One sub-protocol of the verifier checks a running sum and never forced that sum to start at zero. Aztec committed a fix on May 14, 2026 and released it on July 13. Payy was still running a Barretenberg build from October 30, 2025.
The missing rule fits in one line. In a small Lean model of the check, the old version accepts any total a prover claims, and the fixed version accepts only the true total. This post is a reconstruction from public sources, chiefly Payy's postmortem and the incident report attached to it, Aztec's repository and blog, Payy's public code and the transactions on Ethereum.
The loss is 1,918,793.198148 USDC, the sum of three unauthorized burns: 1,828,589.378132 USDC, 1 USDC and 90,202.820016 USDC. A burn is Payy’s term for a withdrawal to Ethereum. The three burns were paid in two settlements, the Ethereum transactions that finalize a batch of Payy transactions and pay out its withdrawals. Early reports of about $1.83 million reflected only the first settlement.
A burn substitutor is a service that pays a withdrawal early and is repaid when the withdrawal settles. It paid the attacker’s 1 USDC burn 24 seconds after the first payout, and the bridge repaid it about five hours later. Only one of those two transfers is a loss.
October 30, 2025
Payy locked its services to a Barretenberg build labeled 3.0.0-manual.20251030, with Noir 1.0.0-beta.14. The incident report attached to Payy's postmortem, which Payy attributes to Spearbit, dates the build to that day. One line of Payy's Cargo.toml and six of its Dockerfiles point at the build.
May 14, 2026
Aztec committed the fix. The commit, 58bf73a, is titled "fix!: small subgroup ipa bug (#44)" and dated 19:15 UTC. It changed 34 files, among them the native verifier, the recursive verifier and the Solidity verifier templates. The fix is included in the v5.0.0 release published on July 13, 60 days after the commit’s recorded date.
May 15, 2026
Payy deployed a new on-chain verifier to fix a separate critical bug in one of its own circuits. The contract was created at 16:04 UTC, 20 hours and 49 minutes after the time on Aztec's commit. The incident report says the contract was generated by the October 2025 build.
July 13, 2026 Aztec published v5.0.0, a stable release that contains the fix. The two v5 release candidates published in June did not contain it, and neither did v4.4.0, published on June 27. The commit entered Aztec's public development branch the next day, in a pull request titled "chore(merge): Private next to public".
September 24, 2026, 03:25:41 UTC
The first forged burn was recorded on Payy's own chain at height 33,195,439.
04:21:23 UTC
Payy's prover settled the batch in Ethereum block 26,044,909. The bridge transferred 1,828,589.378132 USDC to 0xAa4985dBDaBfACa344237D40F7E06C4a0BB57E70. Within eight minutes the funds were in a second wallet and sold for about 683 ETH.
04:21:47 UTC
The burn substitutor paid the attacker's second burn, 1 USDC.
09:30:35 UTC
A second settlement, in block 26,046,439, paid 90,202.820016 USDC to the same address and repaid the substitutor.
14:04 UTC
Payy announced the exploit and said all network transactions were paused.
September 25 to 29, 2026
On September 25 Payy ruled out a compromised key, social engineering and an exploit of its off-chain infrastructure. On September 26 DC Posch of Daimo traced the payout to the verifier and linked Aztec's commit. Payy published its postmortem on September 29.
Balances on Payy are private notes. A transaction spends up to two input notes and creates up to two output notes, and it publishes only their commitments, the fingerprints that stand for the hidden notes. A burn also publishes the amount, the Ethereum recipient and a burn hash, which the circuit sets to the first input commitment.
The first forged transaction published four zero commitments, a zero burn hash, an amount of 1,828,589,378,132 base units (USDC has six decimals) and the attacker's address. A zero commitment is how Payy encodes a padding note, an empty placeholder with zero value. The note tree's root (a summary of every note on the network) was the same before and after the transaction.
Payy’s transaction rules require a withdrawal to be backed by the notes it spends. In the published reconstruction, four empty padding notes provide no funds, so running the transaction circuit directly fails its balance check. Payy does not rerun the circuit for each withdrawal. It relies on a cryptographic proof that the rules were followed. According to Payy’s postmortem, a flaw in the verifier allowed the attacker’s forged proof to pass, and the withdrawal reached settlement on Ethereum. The burn branch of Payy’s transaction circuit, at commit 2f95947f, has four assertions, shown here without their comments:
With four padding notes, input_value and output_value are both zero, so a positive msg_value fails the third assertion. The fourth assertion would also fail for those notes: a padding note has kind 0, while the message names the USDC note kind.
Payy published a reproduction package containing the proof, its public inputs and the verification key. According to the postmortem, the 16,256-byte proof passes verification under the build Payy was running. A local replay on October 11 reproduced that acceptance. Changing the public burn hash from 0 to 1 caused the same proof to fail verification. This confirms that the verifier rejected that modified input, but does not establish how the attacker constructed the original proof. The package contains Payy’s captured transaction data. Its provenance note states that the raw response from Payy’s node was not retained.
Payy’s postmortem and the incident report point to Barretenberg’s small-subgroup IPA check, a piece of bookkeeping inside the verifier.The prover claims the total of a list of numbers, and the verifier has to confirm the total without seeing the numbers.
The method is a running sum, like the balance column of a bank statement. The prover commits to a column of values, which seals them so they cannot be changed later. The column should start at zero. Each value should be the previous one plus the next number in the list. The last value should be the claimed total. If all three rules hold, the total is right.
Barretenberg's documentation, in the last release before the fix, lists all three rules. The check did not enforce the first one on its own. The rule that the sum starts at zero and the rule for the first step were added together into a single equation, and one equation cannot pin down two unknowns. A prover could start the running sum at the wrong value and make the first step wrong by the amount that cancels it. Every later step is then honest, every later value is off by the same constant, and so is the total. The prover chooses the constant, and with it the total.
Aztec's description of the fix says the old check left "one degree of freedom" for a malicious prover. The fix adds the missing rule as a separate check: the running sum must be zero at its first position. The Barretenberg build that Payy ran checked four claims about values committed by the prover in this step. The fixed code checks five, adding an explicit requirement that the running sum starts at zero.
The total matters because of where it goes next. In Barretenberg’s zero-knowledge mode, the prover’s data is hidden behind a random mask, and this total is the mask’s contribution to the verifier’s final equation. The missing rule lets a prover misstate that contribution in the modeled check. Payy’s postmortem attributes the accepted forged withdrawals to this defect. The exact method used to construct the attacker’s proof has not been established by this reconstruction.
Later stages checked proofs again, but did not independently repeat the calculation that the withdrawal was backed by sufficient note value. According to the incident report, the forged proof was accepted first by the verifier inside Payy's node. Payy's first aggregation circuit verifies each transaction proof again with the same Barretenberg verifier, compiled into the circuit, and the layers above it verify only the layer below. For privacy none of them sees note values, so none can redo the balance check. Payy's own prover submitted the batch and Payy's validator signed it, as the security firm ExVul pointed out the same day. The rollup contract then paid the burn. It has no rule that a burn hash be nonzero. Such a rule would have stopped these three messages and left the cause in place.
The check needs one property to hold. If it accepts, the claimed total is the true total of the numbers.
That sentence is about arithmetic, so it can be written and proved in Lean 4. Lean is a proof assistant: a statement and its proof are written as code, and a small trusted program checks every step. The model below uses five names. t holds the numbers to add, a is the running sum the prover submits, s is the total the prover claims, n is the number of positions in the sum, and g is the constant the protocol uses to step from one position to the next. A sum with n positions adds n - 1 numbers. K is any field, the kind of number system the protocol computes in. The two blocks show the definitions and the theorem statements. The proofs are in the linked file.
OldCheck models what the verifier enforced before the fix. Its first line is the bug: a single equation that mentions both the start, a 0, and the first step. FixedCheck is the same check with one more condition, a 0 = 0. Two theorems carry the result.
The first theorem states the bug. With at least two positions and g ≠ 0, the forged running sum passes the old check for any list of numbers and any claimed total s. For example, to claim the true total plus one, it starts at 1 − g instead of zero and adds one to every later honest partial sum. The second theorem states the correction: with at least two positions and g ≠ 1, requiring the sum to start at zero forces an accepted total to equal the true total. These calculations use field arithmetic.
Lean 4.34.1, with Mathlib, Lean's mathematics library, checked both theorems. The file has no sorry, Lean's marker for an unfinished proof, and the theorems depend on no axioms beyond Lean's three standard ones. The theorems and their proofs fit in 57 lines, which open in the Lean web editor. They confirm that the old check, as modeled, can accept an incorrect total. With the missing starting condition restored, an accepted total must equal the actual sum.
Applying the theorems to the deployed verifier takes two more things as given. Neither is proved in the Lean file:
A1. The cryptographic commitments bind the prover to one running sum, and every spot check on it is answered honestly. A2. The verifier tests one combined equation at a random point. Passing that test means, except with negligible probability, that the three lines of OldCheck hold at every position.
The two theorems are about a model of one check. In that model the old check accepts wrong totals and the fixed check does not. A1 and A2 are what connect the model to the deployed verifier.
They do not show that Barretenberg is sound. They say nothing about its C++ or Solidity code, about the rest of the proof system, or about Payy's circuits and contracts. In the deployed protocol the new rule is itself a spot check on a commitment, so the fix depends on A1 as well.
Lean did not find this bug. What the model adds is the property itself. A soundness proof for this step has to establish the one-sentence property. For the old check the property is false: the first theorem constructs a running sum that passes for an incorrect claimed total. A proof of that property for this exact model cannot succeed without changing the check or the property being claimed. The flaw was in the protocol as written. The code matched it, and the fix changed both.
Aztec committed the fix on May 14 and released it on July 13, 60 days later. That gap matches a policy Aztec had stated for its Alpha network. Its security post of March 10 says fixes are packaged and distributed with the v5 release. On March 27 Aztec announced a critical vulnerability in its proving system, found on March 17, and said the bug and its patch would not be disclosed until v5. Nothing public confirms that this is the small-subgroup bug, so we cannot say that it is.
For a critical bug that needs new verification keys, the public parameters that proofs are checked against, the March 10 post says Aztec will first alert the operators of portals, the bridges into its network, and then post a notice on its forum. Payy is not on that network. It uses Noir and Barretenberg as a library for its own rollup, a choice that Aztec's blog profiled in September 2025. By July 13 the fix was public, and it described the bug: the commit added a section to the protocol's documentation that spells out the forgery. On October 8 Aztec published two security advisories for a different Barretenberg verifier bug, with a patch release that it recommends to "direct barretenberg users".
Payy's last public repository sync, on June 24, still points at the October 2025 build, and the verifier Payy deployed on May 15 was still the one in use on September 24. For a rollup built like Payy’s, upgrading Barretenberg means a new major version and a coordinated redeployment. The stable release containing the fix was v5.0.0, a new major version. The verifier is compiled into each of Payy's aggregation circuits, so the circuits, their verification keys and the on-chain verifier contract have to be regenerated and redeployed together.
Payy paused its network on September 24, and nothing public by October 11 says it has restarted. The pause limits exposure: both payout routes used on September 24, the settlement and the substitutor’s advance, begin with Payy’s node accepting a burn.
Payy's postmortem traces 682.3 ETH from the first payout into Tornado Cash in 19 deposits on September 26. The last 90,202.82 USDC was still in the receiving wallet on October 11.
On September 29 Payy said a remediation plan for users would follow in the coming days. By October 11 the postmortem contained no such plan and none was visible on Payy's X account, and neither says whether Payy's circuits and verifier have been rebuilt on a release that contains the fix. The default branch of Payy’s public repository has had no commit since June 24.
When run directly, Payy's burn circuit rejects a withdrawal like the one in the reproduction. Payy's postmortem attributes the loss to the Barretenberg verifier accepting a forged proof despite that circuit's checks. That explanation needs to be distinguished from Payy's separately disclosed agg_agg vulnerability, which also allows unauthorized burn messages.
The fix went unreleased for 60 days and had been in a public release for 73, and no advisory in Aztec’s repository named it. A library with users outside its vendor's own network needs a channel that reaches them. A team that locks its software to a proving stack needs someone reading the verifier changes in every upstream release, and a rebuild of circuits, keys and verifier contract that it can run in days. One team that builds on Aztec's stack opened an issue on September 30 to automate that watch, citing Payy as the precedent.
The first settlement paid the attacker 1,828,589 USDC in a single transfer, about 56 minutes after the forged burn was recorded on Payy's chain. A cap per withdrawal bounds what one forged proof can take, and a delay on large withdrawals leaves time to react. Aztec's March 10 post gives the same advice to builders of portals on its network.
-
CERTORA
Try Certora Prover, the free formal verification tool.
See what AutoProver automates, AI-assisted specification generation built on top of Prover.
Get a Certora smart contract audit, manual review combined with formal methods.
Explore Certora's formal verification capabilities.
Provable is a Certora production.