Most crypto isn't lost to "hacks."
It's lost to approvals people sign themselves.
Here's how wallet drainers actually work β and how not to be next π§΅
Confirmed your 2.94 WETH independently β net flow across the exploit contract is +2.941350352900037140 WETH and exactly 0 wei of ENS. Nothing to correct.
One figure that sits beside yours rather than against it: the vault's LPs are down more than the attacker took. Measured at one fixed tick so the ENS move is out of it, NAV went 28.4579 β 24.6586 WETH β β13.35%, β$9,219. The 0.86 WETH gap went to the 1% pool as fees. Share supply never changed, so all of it is per-share, and 92.67% of the shares belong to a single wallet that deposited in Feb 2022 and never came back.
Seventeen hours on, the proxy implementation slot is still the pre-attack one.
Full working here:
https://t.co/HbsSTVs9qf
π΄ Arrakis V1 Β· G-UNI ENS/WETH
The attacker cleared 2.94 WETH ($7,137). The vault's LPs lost $9,219 β 29% more than that. 93% of it fell on one wallet that deposited in February 2022 and has never touched the position since.
Every number in circulation is the attacker's take. The take is not the damage.
Priced at the same tick before and after, so the market move is out of it, the vault's NAV went 28.4579 β 24.6586 WETH. That is β3.80 WETH, β13.35%, β$9,219 at $2,426.45/ETH. The 0.86 WETH between the loss and the take went to the Uniswap pool as fees β the ENS/WETH pool this vault sits on is the 1% tier.
Share supply never changed: 324.7566 before, 324.7566 after. So the entire drop is per-share, and it lands on whoever was already inside. One wallet holds 300.9671 of those shares β 92.67%. It entered through a single ZapIn on 5 Feb 2022 and has not come back to the vault since. Its share of the loss is 3.52 WETH, β$8,544.
Seventeen hours later, nothing has moved:
Β· proxy implementation slot β same as the block before the attack
Β· position liquidity, idle balances, share supply β identical to the second the attack ended
Β· no rebalance, no withdrawal, no patch
One observation, offered as an observation. 0.05 ETH left the exploit contract to 0xdadB0d80β¦3711 β the builder of block 25817966, extraData "BuilderNet". The attack arrived as a private bundle and was never in the public mempool. The Arrakis risks page lists "real-time mempool exploit-monitoring solutions with emergency hack preventions" among the protocol's defences. Whether legacy V1 was ever inside that perimeter, we cannot tell from the chain.
Root cause is ExVul's and DeFiHackLabs', and it checks out against the deployed source: mint() and burn() value the position off pool.slot0() with no TWAP. A _checkSlippage() does exist in the contract β reachable only from rebalance(), which only the manager can call. The same shape sits in all four implementations behind the 105 live Arrakis V1 vaults, $1.29M between them.
https://t.co/KC8NVrSICU
Method, for anyone re-running it.
NAV is measured at one fixed price for both snapshots β position liquidity from pool.positions() plus idle balances, valued at tick 60333 β so the β13.35% is the hole, not the ENS price. At the end-of-block tick 59789 it reads β12.48%; the loss is 3.71β3.80 WETH either way.
The take closes exactly. Net token flow across the exploit contract: +2.941350352900037140 WETH and 0 wei of ENS. No leg missing. Gas for the whole operation, including a deploy: ~0.0058 ETH, about $14.
The vault's position was out of range when it was hit β tick 60333 against an upper bound of 60000, so it sat 100% in ENS earning nothing. The attacker minted 4,486.6191 shares against 324.7566 outstanding: 13.8x the entire vault in one transaction.
Three blocks earlier the same wallet deployed a byte-identical copy of the exploit contract, 0x5e3eaf56β¦b24c, and abandoned it.
Vault 0x7c687f775a3b73bbab0e15832f24caab5d53bdde
Attacker 0xa3B096e4df1247794599a37Af8F5b8CB05D5EB44
Exploit contract 0x028d9C17B1a097e7e115A6400203df86339BAf4a
Factory (105 V1 vaults) 0xEA1aFf9dbFfD1580F6b81A3ad3589E66652dB7D9
Credit: ExVul and DeFiHackLabs on the mechanism, SlowMist on the incident, DeFiLlama's registry for the $7.1K row that has no source link on it.
π΄ Arrakis V1 Β· G-UNI ENS/WETH
The attacker cleared 2.94 WETH ($7,137). The vault's LPs lost $9,219 β 29% more than that. 93% of it fell on one wallet that deposited in February 2022 and has never touched the position since.
Every number in circulation is the attacker's take. The take is not the damage.
Priced at the same tick before and after, so the market move is out of it, the vault's NAV went 28.4579 β 24.6586 WETH. That is β3.80 WETH, β13.35%, β$9,219 at $2,426.45/ETH. The 0.86 WETH between the loss and the take went to the Uniswap pool as fees β the ENS/WETH pool this vault sits on is the 1% tier.
Share supply never changed: 324.7566 before, 324.7566 after. So the entire drop is per-share, and it lands on whoever was already inside. One wallet holds 300.9671 of those shares β 92.67%. It entered through a single ZapIn on 5 Feb 2022 and has not come back to the vault since. Its share of the loss is 3.52 WETH, β$8,544.
Seventeen hours later, nothing has moved:
Β· proxy implementation slot β same as the block before the attack
Β· position liquidity, idle balances, share supply β identical to the second the attack ended
Β· no rebalance, no withdrawal, no patch
One observation, offered as an observation. 0.05 ETH left the exploit contract to 0xdadB0d80β¦3711 β the builder of block 25817966, extraData "BuilderNet". The attack arrived as a private bundle and was never in the public mempool. The Arrakis risks page lists "real-time mempool exploit-monitoring solutions with emergency hack preventions" among the protocol's defences. Whether legacy V1 was ever inside that perimeter, we cannot tell from the chain.
Root cause is ExVul's and DeFiHackLabs', and it checks out against the deployed source: mint() and burn() value the position off pool.slot0() with no TWAP. A _checkSlippage() does exist in the contract β reachable only from rebalance(), which only the manager can call. The same shape sits in all four implementations behind the 105 live Arrakis V1 vaults, $1.29M between them.
https://t.co/KC8NVrSICU
Good question β the proposal itself is the https://t.co/V4Dt04oQSP wasn't a proposal to approve anything. It was a proposal to veto. In Term's design a curator queues parameter changes and holders get a window to block them; if the window closes without a successful veto, the queued transaction becomes executable. So the attacker queued his own changes and opened the veto vote against himself. He didn't need a single YES. He needed nobody to reach one β and he owned 90.66% of the votes that could.The queued payload was 17 calls. The ones that matter:enableModule(0x0ae12af3β¦) on the vault's Safe 0x35c99cf4β¦ β installs a module with unrestricted execution rights over the vault. Verified: isModuleEnabled returns true at the drain block, false today.
Through that module, execTransactionFromModule β update_debt(strategy, 0, 10000) against the real strategies, incl. Shorewoods ETH 0x330732β¦ β target debt zero, recalling every deployed position back into the vault as idle WETH.
That idle WETH then goes into a "strategy" the attacker deployed at his own nonce 0 at 05:19:11 on Aug 17 β six minutes before he bought his shares. Its name, on-chain: "Fixed Recipient WETH Exit Strategy", ticker frWETH-EXIT.
Step 3 is why the theft is still invisible in the vault's numbers. A vault books a strategy at the value the strategy reports. His contract reports 2,841.7435 back, so totalAssets() reads 2,926.2159 ETH before and after the drain block, and the share price is still 1.0308. The WETH balance is 0.Nothing in it was clever. It was permitted.Proposal: https://t.co/fTJRIF8tkP
The $8.5M was not stolen past the vault's defences. The defence was bought for $951.
Term's vaults let share-holders veto a curator's parameter changes. But votes only count if you stake your shares, and on the block before the drain the entire staked supply was 0.5352 out of 2,838.95 β 0.019% of the vault.
The attacker bought 0.4852 of it for 0.5 ETH on Aug 17. That was 90.66% of every vote in existence. He opened the veto proposal himself, nobody voted, and it executed 12 seconds after the window closed.
The vault's own accounting still hasn't noticed: totalAssets() reads 2,926.2159 ETH before and after the drain block, share price still 1.0308. Its WETH balance is 0.
It cost $951.
0.5 ETH bought 0.4852 staked gtmvETH β 90.66% of every vote in the vault, because only 0.019% of shares had ever been staked.
Nobody voted. It executed on expiry.
It was not a vote, it was a veto. Term's Meta Vaults let a curator queue parameter changes and give LP holders a window to veto them. The attacker queued the change himself, then opened the veto proposal using Term's own boilerplate β "Vote YES to VETO the curator's proposed vault parameter changes." Voting power comes only from tmvETH staked into the governance vault, and pre-attack that was 0.5352 of 2,838.95 shares. The 99.94% holder held plain unstaked shares, so held zero votes. They staked 2,837.28 tmvETH at 07:59:23 β 94 minutes after the money was gone.
The marketed safeguard was the attack surface.
And the hole is still invisible in the vault's own books. ETH Meta Vault, 0x26fcb50eec367ddab060ccf5e7394cecd95f7db2, read at 10:31:15 UTC β four hours after the drain:
totalAssets() 2,926.215884 ETH
convertToAssets(1e18) 1.030751 ETH per share
actual WETH balance 0.000000
actual ETH balance 0.000000
What it holds instead is 2,841.743536 units of a token the attacker deployed at his own nonce 0 and named "Fixed Recipient WETH Exit Strategy". The strategy swap took the WETH and left the vault an IOU written by the person taking it, so totalAssets() never moved across the drain block. Same pattern in all five USDC vaults: every one of them reports the same or a higher totalAssets() after the drain than before, still accruing yield on assets that are gone.
Across the six victim vaults the books say $11.41M. $8.53M of it does not exist.
https://t.co/5QgGqAQy5J
π¨ Term Finance β $8.53M governance attack
The veto that was meant to stop it cost $951 to buy.
Voting power comes only from staked vault shares. 0.5352 of 2,838.95 were staked β 0.019%. The attacker held 90.66% of it.
Term's Strategy Vaults were sold on exactly this safeguard: "LP token holders retain veto power over risk parameter adjustments." The attacker did not beat the veto. He bought it.
How it worked
Voting rights on the ETH Meta Vault don't come from holding tmvETH. They come from staking it into the Governance ETH Meta Vault. On the block before the drain, the entire staked supply was 0.5352 gtmvETH against 2,838.95 tmvETH outstanding β 0.019% of the vault.
Aug 17, 05:19:11 UTC: the attacker deploys a contract at his own nonce 0. It is named "Fixed Recipient WETH Exit Strategy", ticker frWETH-EXIT.
05:21:47: he buys 0.4852 tmvETH for 0.5 ETH (~$951 at that hour's price) and stakes it. That is 90.66% of every vote in existence.
05:25:35: he opens the veto proposal β carrying Term's own boilerplate: "Vote YES to VETO the curator's proposed vault parameter changes. Otherwise, the transaction will become executable when this proposal expires."
Nobody voted. It expired Aug 23, 06:25:35. It executed at 06:25:47 β 12 seconds, one block, after the window closed.
The holder who could have stopped it
One EOA held 2,837.28 tmvETH β 99.94% of the vault. Unstaked, so zero votes for all 145 hours.
They staked it at 07:59:23 today. 94 minutes after the money was gone.
The books still show the money
The vault handed over 2,841.7435 WETH and received 2,841.7435 units of the attacker's own contract, which its accounting values 1:1.
totalAssets() before the drain block: 2,926.2159 ETH
totalAssets() after the drain block: 2,926.2159 ETH
WETH balance: 0
Share price: still 1.0308 ETH
maxDeposit: 47,073 ETH β deposits open (checked 09:45 UTC)
maxWithdraw: 0
The USDC leg is five more vaults
One transaction, 06:47:47, took 1,679,639.29 USDC:
Parity High Yield USDC v2 β 848,410.95
RockawayX Tori USDC β 454,046.26
Parity High Yield USDC β 348,877.20
Parity Core USDC β 14,172.39
Parity Prime USDC β 14,132.49
Converted to 1,679,642.45 DAI. Every one of the five reports the same or a higher totalAssets after the drain than before β they are still accruing yield on assets that left.
Across all six victim vaults the books publish $11.41M. $8.53M of it does not exist.
Two more captures are loaded and unfired
Eight takeovers were queued. Six went off. Two never did:
Parity Core ETH β veto window expired today 06:30:47
Parity Prime ETH β veto window expired today 06:36:47
Those two vaults hold 8.99 + 75.63 = 84.62 ETH (~$204K) β which is precisely the residual the emptied ETH Meta Vault still claims to own. The last of it sits behind two locks that were already picked. Windows don't reopen.
Funding
Tornado Cash 1 ETH pool, twice: 0.9945 ETH on Aug 17 05:01:11 and 0.9944 ETH on Aug 18 03:59:59. Both execution transactions carried a priority fee of exactly 0 β nothing ever hit the public mempool.
ETH drain: https://t.co/G7i6OKDrks
The $8.5M figure and the addresses were first published by CertiK Insight and @osint_based. The cost of the veto, the empty books and the two live windows are ours.
Addresses
Attacker 1: 0xa908b3472d76e7744baB0A5911768a4a6300612B
Attacker 2: 0x686457a7468B9B31c5dbA43b1b16077B48520691
Funds: 0xD5183d8BfC65a50863C62aF2538198A8288FFc13
Fake strategy: 0x184f2e57b4ce135181fa2a2166ac394339016338
ETH Meta Vault: 0x26fcb50eec367ddab060ccf5e7394cecd95f7db2
Governance ETH Meta Vault: 0x5b96c5bbdcb361e1e9944baa071b237e27829be0
Both, and each is one eth_call.
Canonical Ethereum totalSupply: 3,000,000,000 β unchanged through the entire incident.
Base OFT totalSupply right now: 327.57 trillion.
The headline was unbacked bridge balance.
Read at 2026-08-23 10:02:53 UTC, Base block 50,345,013. Do not take my count β call totalSupply() on 0xac531eb26ca1d21b85126de8fb87e80e09002dcf on Base and on 0x3845badade8e6dff049820680d1f14bd3903a5d0 on Ethereum. The two answers sit 109,000x apart and neither needs an indexer.
That gap is the whole answer to the second question. Nothing on Base can move canonical supply β the Ethereum side is an OFT Adapter, not a mint. It backs the satellites by holding real locked SAND, so the theft ceiling was never the minted figure. It was that balance:
14,769,723.07 SAND on Aug 21 07:05 β 0.005560 now
14,753,431.67 out in 15 events, 00:32:11β00:32:35 UTC Aug 22
~$675K notional, of which ~79.74 ETH was actually realized
https://t.co/tD0DFioa5q
So the $706M was price Γ a balance nothing stood behind. So is 327.57 trillion. The only number that ever moved real value was a 14.7M SAND lockbox that emptied in 24 seconds.
Status at time of reading: no mint in 29 hours, peers for eid 30101 and 30102 still zero, and the LayerZero delegate is still 0xa467cd7bβ¦7952, which is not an address the project controls. Containment is the peers, not a fix.
Two more takeovers from the same attacker are queued and were never executed.
Parity Core ETH β veto window expired today 06:30:47 UTC
Parity Prime ETH β expired 06:36:47 UTC
Those two hold 84.62 ETH, which is the entire residual the emptied ETH Meta Vault still claims to own. The strategy contract has a fixed recipient, so whoever fires it, it pays the same address as the first drain.
Also worth flagging: the ETH Meta Vault's totalAssets() reads 2,926.2159 ETH before and after the drain block. WETH balance is 0. It took 2,841.74 units of the attacker's own contract in exchange and books them 1:1.
π¨ Term Finance β $8.53M governance attack
The veto that was meant to stop it cost $951 to buy.
Voting power comes only from staked vault shares. 0.5352 of 2,838.95 were staked β 0.019%. The attacker held 90.66% of it.
Term's Strategy Vaults were sold on exactly this safeguard: "LP token holders retain veto power over risk parameter adjustments." The attacker did not beat the veto. He bought it.
How it worked
Voting rights on the ETH Meta Vault don't come from holding tmvETH. They come from staking it into the Governance ETH Meta Vault. On the block before the drain, the entire staked supply was 0.5352 gtmvETH against 2,838.95 tmvETH outstanding β 0.019% of the vault.
Aug 17, 05:19:11 UTC: the attacker deploys a contract at his own nonce 0. It is named "Fixed Recipient WETH Exit Strategy", ticker frWETH-EXIT.
05:21:47: he buys 0.4852 tmvETH for 0.5 ETH (~$951 at that hour's price) and stakes it. That is 90.66% of every vote in existence.
05:25:35: he opens the veto proposal β carrying Term's own boilerplate: "Vote YES to VETO the curator's proposed vault parameter changes. Otherwise, the transaction will become executable when this proposal expires."
Nobody voted. It expired Aug 23, 06:25:35. It executed at 06:25:47 β 12 seconds, one block, after the window closed.
The holder who could have stopped it
One EOA held 2,837.28 tmvETH β 99.94% of the vault. Unstaked, so zero votes for all 145 hours.
They staked it at 07:59:23 today. 94 minutes after the money was gone.
The books still show the money
The vault handed over 2,841.7435 WETH and received 2,841.7435 units of the attacker's own contract, which its accounting values 1:1.
totalAssets() before the drain block: 2,926.2159 ETH
totalAssets() after the drain block: 2,926.2159 ETH
WETH balance: 0
Share price: still 1.0308 ETH
maxDeposit: 47,073 ETH β deposits open (checked 09:45 UTC)
maxWithdraw: 0
The USDC leg is five more vaults
One transaction, 06:47:47, took 1,679,639.29 USDC:
Parity High Yield USDC v2 β 848,410.95
RockawayX Tori USDC β 454,046.26
Parity High Yield USDC β 348,877.20
Parity Core USDC β 14,172.39
Parity Prime USDC β 14,132.49
Converted to 1,679,642.45 DAI. Every one of the five reports the same or a higher totalAssets after the drain than before β they are still accruing yield on assets that left.
Across all six victim vaults the books publish $11.41M. $8.53M of it does not exist.
Two more captures are loaded and unfired
Eight takeovers were queued. Six went off. Two never did:
Parity Core ETH β veto window expired today 06:30:47
Parity Prime ETH β veto window expired today 06:36:47
Those two vaults hold 8.99 + 75.63 = 84.62 ETH (~$204K) β which is precisely the residual the emptied ETH Meta Vault still claims to own. The last of it sits behind two locks that were already picked. Windows don't reopen.
Funding
Tornado Cash 1 ETH pool, twice: 0.9945 ETH on Aug 17 05:01:11 and 0.9944 ETH on Aug 18 03:59:59. Both execution transactions carried a priority fee of exactly 0 β nothing ever hit the public mempool.
ETH drain: https://t.co/G7i6OKDrks
The $8.5M figure and the addresses were first published by CertiK Insight and @osint_based. The cost of the veto, the empty books and the two live windows are ours.
Addresses
Attacker 1: 0xa908b3472d76e7744baB0A5911768a4a6300612B
Attacker 2: 0x686457a7468B9B31c5dbA43b1b16077B48520691
Funds: 0xD5183d8BfC65a50863C62aF2538198A8288FFc13
Fake strategy: 0x184f2e57b4ce135181fa2a2166ac394339016338
ETH Meta Vault: 0x26fcb50eec367ddab060ccf5e7394cecd95f7db2
Governance ETH Meta Vault: 0x5b96c5bbdcb361e1e9944baa071b237e27829be0
Measured it rather than guessed: 1.71 ETH.
That is the entire gas cost of the campaign that took the $2M β 126,339 baits across 21,977 transactions, about 13.6 micro-ETH per bait. Roughly four cents to lay one.
Method: pulled every dust transfer the controller ever sent, deduped to 21,977 transactions, sampled 45 receipts and summed gasUsed Γ effectiveGasPrice. Mean 0.000078 ETH per tx, median 0.000025, and they batch a median of 2 baits into each one.
So the return on the whole three-month operation is roughly $2,000,000 against $6,000 of gas. That is why the hit rate can sit near zero and the model still works. It isn't a business that needs to win often. It needs to win once.
ON YOUR SECOND POINT β you're right, and I should be clearer about what I actually measured.
1,206 is my sample, not the fleet. I never enumerated fleet two's full address count; I pulled 34 of its batches spread across six months and collected every lookalike in them. Its real size is larger and I don't have the number.
So "zero over $1,000" means zero in those 1,206, not zero overall. With no hits in 1,206 draws the ceiling on the hit rate is about 0.25%, and across a fleet that size 0.25% leaves plenty of room for wins I simply haven't looked at yet.
And your instinct is the right one. Nobody burns 170 ETH for nothing β especially not when the crew next door turned 1.71 ETH into $2M. That gap is the argument that fleet two has been paid too, and that I haven't found where. I'd rather say that than dress a sample up as a conclusion.
The bot that stole $2M address-poisoned itself
13 minutes after parking the stolen DAI, the same controller that funded the theft address dusted the thief's own wallet - the same 0.0008 -> 0.0002 relay it ran on the victim.
https://t.co/8uSckrLNmF
THE SELF-POISONING
Controller 0xedda4e01β¦4143 runs one fixed pattern: a single tx sends 0.0008 to a freshly mined lookalike, which relays 0.0002 to the target.
Jul 21 14:45:23 β 0xf0e63433β¦61af, 8 min after the victim's real payment
Aug 20 20:20:59 β 0xf0e6a496β¦21af, 8m36s after the victim's real payment. This one took the $2M.
Aug 21 23:44:23 β 0xe2ebba3eβ¦416a, 13m12s after the thief's own DAI move β dusting his own swap wallet 0x692729bcβ¦7251.
Same controller on both ends. The bot saw 1,999,939 DAI leave an address, mined a lookalike of the destination, and poisoned the sender. The sender was itself.
THE BAIT
The victim ran a recurring $2,000,000 USDC payment to 0xf0e67a18β¦b1af β Jul 21, then Aug 20, each funded by a Compound v3 withdrawal minutes earlier.
Both times, forged 2,000,000 USDC entries appeared in the wallet's history within minutes, sent from lookalikes of that payee by homoglyph token contracts (ΓΠ DΠ‘, USΝDC, USα DC β Cyrillic and invisible characters).
Jul 21: first forgery 4m48s after the real transfer. No bite.
Aug 21 16:31:11: the payment repeated, into 0xf0e6a496β¦21af.
THE CLOSEST MATCH LOST
Three separate operations worked this victim in August.
0xf0e620bdβ¦b1af β 4 prefix + 4 suffix β operator 0x7d459a40β¦ab22 via batcher 0x2a9617a4β¦dd7f
0xf0e6d532β¦b1af β 4 prefix + 4 suffix β operator 0xd6434d15β¦6908 via batcher 0x7ec8a30aβ¦bef7
0xf0e6a496β¦21af β 4 prefix + 3 suffix β the fleet β took the $2M
The two closer matches have zero transactions and were never funded. They exist only as the destination of zero-value transferFrom calls on real USDC and of forged fake-token events.
The winner was the only one whose entry moved a real, non-zero balance: 0.0002 USDC.
THE FLEET
0xf0e6a496β¦21af is an EIP-7702 EOA delegated to sweeper 0xecad547eβ¦8469 β deployed 2026-05-26 07:53:59, gas-golfed, recipient and amount packed into one uint256, hardcoded paths for USDT, DAI and USDC, callable only by the controller.
That controller has sent 126,339 dust transfers to 80,663 distinct addresses between May 26 08:55 and Aug 22 01:07 UTC.
120 of 120 randomly sampled recipients carry the identical delegation to the same sweeper.
63,354 of them were used exactly once.
THE FORGERY IS A SERVICE
Contract 0xde39ef67β¦27de, deployed Aug 19 08:02 by 0x161643f2β¦5003, its only caller.
320 transactions in 60 hours, a median of 278 Transfer logs each β roughly 89,000 forged history entries, batched across many fake token contracts at once.
THE MONEY
Aug 21 23:24:47 β 2,000,000.0006 USDC swept to 0x692729bcβ¦7251
Aug 21 23:28:35 β swapped via CoW Protocol to 1,999,939.4763 DAI
Aug 21 23:31:11 β parked at 0xe2ebfd6fβ¦1816a
Still there. Nonce 0. Block 25807397.
ADDRESSES
Victim 0x7ba7f4773fa7890bad57879f0a1faa0edffb3520
Real payee 0xf0e67a1896e814e30c011e36174de28caa9ab1af
Spoof 0xf0e6a49668de1195b931a3717c9cc36fc19721af
Swap wallet 0x692729bcd0887b8d02b8ff3169220ba0f4e17251
DAI vault 0xe2ebfd6f329a6330ab7eee68ce1328c21d31816a
Self-poison 0xe2ebba3e64f25f8badf35d2760473748d673416a
Sweeper 0xecad547e905892ff19d162ca57b91f0fecf78469
Controller 0xedda4e01669d30faa04a9cb75488abc366ee4143
Forger 0xde39ef679e12574279e3ed35de4b0721beae27de
Mechanism first reported by PeckShield.
Agreed on the facts β the tests are there, #5101 added cases in quorom_test.go, blockchain_impl_test.go and engine_test.go. But my open question isn't whether it's fixed. It's which hole the attacker actually went through.
The two answers say different things. Receipt replay alone means ONE was minted through a bookkeeping bug in spent-marker keys. If the quorum check was also exercised, it means a header that nothing had signed was accepted on mainnet β a different class of statement about what the chain will take. That's why I keep pointing at the all-zero bitmaps in the receipts rather than at the diff.
Which makes your last point the right one. The patch tells us the holes are closed. Only the postmortem tells us what walked through them.
Fair. But look at what they were actually shown: a line in their own history reading 2,000,000 USDC out β same amount they'd sent the day before, same direction, wrong address by three characters.
That's not a lapse in attention, it's a forgery built to survive attention. Careful wouldn't have caught it. A token-contract filter would.
They never needed 80,663 wealthy targets. They needed 80,663 recent transfers β and the chain publishes those for free, in real time.
I sampled 22 baits from this fleet at random and went looking for what triggered each one. 19 of the 22 land on the same pattern, and it answers the question exactly:
the lookalike never mimics the victim. It mimics whoever the victim just paid.
HOW A TARGET GETS PICKED
A wallet sends stablecoins to someone. The bot takes the recipient address, mines a lookalike of it, and drops a bait into the sender's history β betting that sender will pay that person again.
No list of whales. No guessing who holds what. The trigger is a public transfer, and the target is handed over by the transaction itself.
Median lag from the real payment to the bait: 1h53m. Fastest in the sample: 2 minutes.
THEY ARE NOT HUNTING WHALES
Baited payments in the sample ran from $1,000 to $3,000,200. Median around $124K.
Hardware wallet, hot wallet, individual, desk β none of it is a selection criterion. Having moved stablecoins recently is the only one.
WHY 80,663 IS AFFORDABLE
The match is deliberately shallow and remarkably consistent: 4 leading characters, 3 to 5 trailing. Mean 7.68 characters across the sample, median 8, never more than 9.
They are not mining a convincing address. They are mining exactly enough to fill a truncated display β 0xf0e6β¦21af β and not one character beyond it. That ceiling is the whole reason this scales to five figures of addresses.
Cost of laying one bait: about 92,000 gas, batched a dozen or more per transaction.
WHICH IS ALSO WHERE IT BREAKS
The attack is priced against a display convention, not against cryptography. A wallet that shows more of the address, or that renders only canonical token contracts, takes the entire economic model away.
(3 of the 22 had no prior payment in the records I pulled, so they are unexplained rather than counterexamples.)
UPDATE β the enabling step, 24 seconds before the first mint.
At 23:41:41 UTC the token was made to call the LayerZero endpoint with itself as msg.sender, and hand its own config rights to a stranger.
Base block 50,283,177:
0x149eb0eec5f1c793b094b46889059b510281a7eff3c1597bb262777a5cfaa237
Decoded, it is one call to SAND's own approveAndCall (0xcae9ca51) with
target = 0x1a44076050125825900e736c501f859c50fe728c β LayerZero EndpointV2
amount = 0
data = 0xca5eb5e1 + the caller's own address β setDelegate(caller)
approveAndCall ends in https://t.co/UTYLLeRoIV(data). That is an arbitrary call in which the token contract itself is msg.sender, and its only guard is that the first parameter of data must equal the caller. setDelegate(attacker) satisfies that guard by construction. EndpointV2 then does exactly what it documents β delegates[msg.sender] = _delegate β and the caller owns the token's LayerZero configuration.
The endpoint was not broken. The access-control hole is in the token.
DelegateSet in that block names the address Upbit itself linked in its caution notice hours later. It did not stay there: the delegate moved to the contract that went on to receive 317.69 trillion SAND, then twice more. As of 07:06 UTC it is still not an address the project controls.
Which means the hole is not closed. Containment is the two peers the multisig zeroed at 05:09:19 UTC β Ethereum and BSC, eid 30101 and 30102, both still zero. That is what is holding, not a fix to approveAndCall.
https://t.co/QICMpAmLiY
Credit for the root cause goes to CryptoTaffy, who called it Access Control at 03:06:52 UTC while the rest of the timeline was still reporting a mint. This adds the transaction and the decode.
One thing I have not verified and will not assert: what the delegate reconfigured on the endpoint between 23:41:41 and 23:42:05. A forged inbound message has to clear some verification path, but I have not decoded the setConfig calls, so that step stays open.
π΄ THE SANDBOX / SAND
PeckShield reported 14.9B SAND minted on Base. On-chain it is 329.24 trillion β 22,000x more.
But the mint was never the theft. Ethereum's adapter went from 14,769,723 SAND to 0.0056.
SAND on Base is a LayerZero OFT. The same address on Ethereum is the OFT Adapter holding the real, locked L1 SAND that backs every satellite deployment. Trillions of unbacked tokens on Base are worth nothing β the theft ceiling is whatever sits in that adapter.
It was emptied in 24 seconds.
THE DRAIN
00:32:11β00:32:35 UTC, Aug 22 β 14,753,431.67 SAND leaves the adapter in 15 events. 14,095,483.66 of it to a single EOA in six transactions.
By 01:22:11 the adapter holds 0.0056 SAND. The previous morning it held 14,769,723.07.
Sold through a router between 00:35 and 01:01. Proceeds: 78.24 WETH + 1.50 ETH β 79.74 ETH.
~$675K stolen. ~$194K realized. Not $706M.
verify: https://t.co/tD0DFioa5q
THE MINT
First mint 23:42:05 UTC Aug 21, Base block 50,283,189 β 50,000,000 SAND. Baseline supply that morning: 14,639,320.
Last mint 04:45:21 UTC Aug 22, block 50,292,287. Total 329,243,083,813,776 SAND across 703 events to 173 recipients.
Each mint was a one-shot contract deploy paying tx.origin β 379 of exactly 10M, 153 of exactly 10B. Anyone could run it.
Largest single recipient: 317.69 trillion, in 44 transactions.
BSC, unreported
The same contract address on BSC went 129,508 SAND (Aug 21 05:08) β 58.8M (02:44) β 11.98 trillion now.
CONTAINMENT
05:09:19 UTC β the project multisig zeroes the LayerZero peers for Ethereum (eid 30101) and BSC (30102), cutting Base out of the mesh. 24 minutes after the last mint.
Ethereum L1 totalSupply never moved: exactly 3,000,000,000. Holders on Ethereum, Polygon and exchanges lost nothing.
ADDRESSES
OFT / Adapter, same address on Base, Ethereum and BSC
0xac531eb26ca1d21b85126de8fb87e80e09002dcf
Received the unlocked L1 SAND
0x53eda2e80e46b804c5a47260ce04642e82d004ca
Proceeds
0xac76b04397c9296dfc00e25c96d8e51b4edfaf29
Multisig that cut the peers
0x18987794f808ee72ae9127058f1c7d079736ca45
One trap: the trail carries address-poisoning spam β fake tokens with Unicode lookalike symbols (EαΉ¬H, WΔTαΈ¨) mirroring the exact WETH/ETH amounts to lookalike addresses. Match the contract, not the symbol, or you double the loss.
Accounting closes: supply at window start + minted β burned β supply now = 0.000000.
The delegate claim is right β here is the transaction, 23:41:41 UTC, 24 seconds before the first mint:
approveAndCall(LayerZero endpoint, 0, setDelegate(caller))
But it is not a LayerZero flaw, and it is not 15B.
0x149eb0eec5f1c793b094b46889059b510281a7eff3c1597bb262777a5cfaa237
Base block 50,283,177. Calls SAND's own approveAndCall(0xcae9ca51) with target = EndpointV2 and payload 0xca5eb5e1 + the caller's address.
SAND's approveAndCall does https://t.co/UTYLLeRoIV(data) β an arbitrary call in which the token itself is msg.sender. Its only guard is that the first parameter of data must equal the caller. setDelegate(attacker) satisfies that by construction, and EndpointV2 writes delegates[msg.sender] = _delegate. So the token was made to hand its own LayerZero config rights to a stranger. The endpoint behaved exactly as documented; the access-control hole is in the token.
The delegate is still not the project's. Containment rests entirely on the peers the multisig zeroed at 05:09:19 UTC, not on the hole being closed.
On the numbers: 329,243,083,813,776 SAND across 703 events to 173 addresses, not ~15B across two. That pair is 0.0045% of it. Minting stopped at 04:45:21 UTC β nothing since.
And the minted SAND is not where the money went. Ethereum's OFT adapter was emptied 00:32:11β00:32:35 UTC: 14,753,431.67 SAND, down to 0.0056 by 01:22:11. ~$675K, of which ~79.74 ETH was realized.
https://t.co/tD0DFioa5q
500M was the supply at 01:59 UTC. It ended at 329.24 trillion, and nothing has minted since 04:45:21 UTC.
The minted SAND is also not where the money went.
SAND on Base is a LayerZero OFT, and the same contract address on Ethereum is the adapter holding the real locked L1 SAND. Unbacked trillions on Base are worth nothing β the loss ceiling is that adapter, and it was emptied between 00:32:11 and 00:32:35 UTC: 14,753,431.67 SAND in 15 events, down to 0.0056 by 01:22:11. Roughly $675K, of which ~79.74 ETH was actually realized.
https://t.co/tD0DFioa5q
One correction on the exchanges: Upbit did both. It suspended SAND deposits and withdrawals at 02:12:23 UTC, then issued the caution at 02:45:01.
Ethereum L1 totalSupply never moved β exactly 3,000,000,000. Nobody holding SAND on Ethereum, Polygon or an exchange lost anything.