Balancer V2, 2025
More than $120 million across Balancer and its forks, as OpenZeppelin reports; The Block reports estimates of over $128 million. OpenZeppelin, The Block, Certora
A swap for an exact amount out rounded that amount down before pricing it, so 17 wei of osETH, worth 17.988 of the pool's units, was priced as 17.
Certora writes that its 2022 verified properties of these pools “did not constrain the relationship between individual swaps or rounding behavior”. The locked requirements here are about exactly that.
The attack, with the state after each step
- The attacker sells BPT for the pool's tokens, 22 swaps in one batch. BPT swaps, joins and exits are not modeled.67,000 wei of WETH and 67,000 of osETH
- It takes osETH out until 18 wei are left.
- It takes 17 wei of osETH out. At the cached osETH rate that is 17.988 of the pool's units; the pool rounds it down and prices 17.
- It takes WETH out, paid with osETH, and repeats the three swaps, 30 rounds in all.889 wei of WETH and 1,472 of osETH
- It buys BPT back at the collapsed price. This step is not modeled; the specification states the property it relied on.
REFUTEDDeployed swap path of the osETH/WETH pool
The kernel checks that four rounds of these swaps lower the pool's value from 4,749,898,000 to 297,229,191, and that the port, run with a port of StableMath, charges exactly the logged amount of all 90 swaps of the attack.
Kernel-checked counterexample and Passport d65bd405a13b
VERIFIEDMuse Spark 1.3 repair, planned by Muse Wonder 1.0
The repair rounds the amount out up before pricing it and refuses the swap unless that amount is below the pool's balance. It proves all 8 requirements, among them that for any pool math meeting the stated contract, no batch of swaps of any length lowers the pool's value. Attempt 1, 2,344 s, $2.51 at list prices.
Passport 48930efdd4d6
Against the real Solidity code
On a fork of Ethereum taken just before the attack transaction, the attack's first 22 swaps brought the deployed pool to 67,000 wei of each token, as on chain. Then 501 batches (the attack's 90 swaps and 500 generated, 10,385 swaps in all) ran through the deployed Vault, the Lean port and the Muse repair.
Deployed Vault and osETH/WETH poolthe pool's invariant falls in 56 batches, and its BPT price with it
56 of 501
56 of 501 batches break a requirementMuse repairthe proved component on the same 501 batches
0 of 501
0 of 501 batches break a requirement
The real code's output equals its Lean port's on every one of the 501 batches. Recorded violations restated as Lean goals and checked in the kernel: 6 of 6. Report
Euler Finance, 2023
About $197 million in net profit to the attacker, as Cyfrin reports; CoinDesk later reported that all recoverable funds were returned. Cyfrin, Zellic, CoinDesk
donateToReserves moved an account's collateral to the reserves without the liquidity check that withdraw, mint and transfer perform.
The attack, with the state after each step
- A lender deposits 8,904,507.
- The violator deposits 20,000,000, then mints 200,000,000, repays 10,000,000 and mints 200,000,000.collateral 420,000,000, debt 390,000,000: healthy
- The violator donates 100,000,000 eTokens to the reserves. No liquidity check runs.collateral 320,000,000, debt 390,000,000: unhealthy
- A second attacker account liquidates 256,000,000 of the debt at the 20% maximum discount.violator: collateral 0, debt 134,000,000
- The liquidator withdraws the whole pool, 38,904,507: the lender's entire deposit.
VERIFIEDMuse Spark 1.3 repair
The repair adds the health check to the donation guard. It proves all 6 requirements, among them that no sequence of operations leaves any account with more debt than its risk-adjusted collateral. Attempt 1, 1,682 s, $0.83 at list prices.
Passport 6de7bb94c38c
Against the real Solidity code
The unmodified contracts at b1ee326 were deployed in a local Foundry test and run on the attack, Euler's three donate test scenarios and 1,000 generated sequences, then compared after every operation with the requirements, with Euler's own liquidity computation and with the Muse repair.
Euler v1 at b1ee326, unmodifiedEuler's own liquidity check also reports collateral below liability in 57
57 of 1,004
57 of 1,004 sequences break a requirementThe same with checkLiquidity(account) addeda local copy, not an upstream release
0 of 1,004
0 of 1,004 sequences break a requirement
Recorded violations restated as Lean goals and checked in the kernel: 2 of 2. Report
Knight Capital, 2012
$460 million realized loss, as the SEC order states. SEC order 34-70694, archived copy
New code was not copied to one of eight servers. There, orders carrying a repurposed flag ran dead Power Peg code, which kept sending child orders without counting the executions.
The SEC order reports millions of child orders for 212 parent orders: 4 million executions in 154 stocks, more than 397 million shares, in about 45 minutes.
The attack, with the state after each step
- Seven of eight servers run the new code; the eighth still runs the old code (paragraph 15).
- A parent order for 100 shares arrives with the repurposed flag, which on the eighth server activates Power Peg (paragraphs 13 and 16).
- A routing opportunity for 100 shares arrives. The router sends a 100-share child order, and it is filled.100 of 100 shares executed
- Power Peg's count never sees the fill, so the next opportunity sends another 100 shares, and so on for every opportunity.after n rounds: n child orders, 100 × n shares executed
REFUTEDReconstructed Power Peg router
The kernel proves by induction that the reconstructed router sends n child orders for one 100-share order, for every n, while the new code sends exactly one on the same events. It also refutes the obvious patch: counting fills again still sends 200 shares for a 100-share order when two opportunities arrive before a fill.
Kernel-checked counterexample and Passport ca3e7312fe7a
VERIFIEDMuse Spark 1.3 repair, planned by Muse Wonder 1.0
The repair removes the Power Peg path, so every build routes by the shares not yet sent and every fill advances the executed count. It proves all 6 requirements, among them that on every server and code version, after any sequence of events, the shares sent for an order never exceed its quantity. Attempt 2, 4,079 s, $4.96 at list prices.
Passport 98cfed64ae1e
UNRESOLVEDEarlier attempts: tests passed, no Passport
Three earlier Muse attempts produced code whose rebuilt executable passed all 2,001 property cases, but none proved every requirement, so ProofForge issued no VERIFIED Passport: A1, attempt 1 proved 1 of 6 before its wall-clock cap; A1, attempt 2 proved 1 of 6 before its dollar cap; and A2, attempt 1 proved 5 of 6 before its dollar cap.
A1, attempt 1 4fddc1683956, A1, attempt 2 27f4c83221ed, A2, attempt 1 edc4ec8cd40a
Hundred Finance, 2023
About $7 million in reported losses (rekt.news gives $7.4M; this figure was not checked here). rekt.news, BlockSec
redeemUnderlying rounded the shares burned down, so a withdrawal worth 1.99999999992 share units burned 1.
The attack, with the state after each step
- The market is emptied: no shares outstanding, 1 satoshi of cash.
- The attacker deposits 400,000,000 satoshi (4 WBTC).20,000,000,000 share units
- It redeems 19,999,999,998 share units.2 share units backed by 1 satoshi
- It sends 50,030,063,816 satoshi straight to the market, so each unit is worth about 250 WBTC, and borrows ETH against its 2 units. Borrowing is not modeled.2 share units backed by 50,030,063,817 satoshi
- It withdraws 50,030,063,815 satoshi, worth 1.99999999992 share units. The contract burns 1.1 share unit backed by 2 satoshi
REFUTEDHundred hToken share accounting
The kernel checks that the port breaks REQ-SHARE-VALUE (a share falls from 50,030,063,817 / 2 satoshi to 2) and REQ-WITHDRAW-ROUNDS-UP. The Muse repair burns both share units.
Kernel-checked counterexample
VERIFIEDMuse Spark 1.3 repair
The repair prices shares from the exact ratio of assets to supply and rounds the shares burned up. It proves all 10 requirements, among them that no sequence of operations, donations included, lowers the value of a share while shares are outstanding. Attempt 1, 1,200 s, $0.65 at list prices.
Passport 08fe86f17f27
Against the real Solidity code
On a fork of Optimism at block 90761917, the hWBTC market was emptied as in the attack, and 501 sequences (the attack and 500 generated) ran on the deployed CErc20Delegate bytecode, on the Lean port and on the Muse repair.
Deployed hWBTC contractidentical to the Lean port after every operation of all 501
292 of 501
292 of 501 sequences break a requirementMuse repairthe proved component, on the same 501 sequences
0 of 501
0 of 501 sequences break a requirement
The real code's output equals its Lean port's on every one of the 501 sequences. Recorded violations restated as Lean goals and checked in the kernel: 5 of 5. Report