All incidents

Laundromat Free Deposit Drain

Share
Apr 08, 2025 07:28 UTCAttackLoss: 1 ETHPending manual check1 exploit txWindow: Atomic
Estimated Impact
1 ETH
Label
Attack
Exploit Tx
1
Addresses
1
Attack Window
Atomic
Apr 08, 2025 07:28 UTC → Apr 08, 2025 07:28 UTC

Exploit Transactions

TX 1Ethereum
0x08ffb5f7ab6421720ab609b6ab0ff5622fba225ba351119c21ef92c78cb8302c
Apr 08, 2025 07:28 UTCExplorer

Victim Addresses

0x934cbbe5377358e6712b5f041d90313d935c501cEthereum

Loss Breakdown

1ETH

Similar Incidents

Root Cause Analysis

Laundromat Free Deposit Drain

1. Incident Overview TL;DR

Laundromat at 0x934cbbe5377358e6712b5f041d90313d935c501c was drained in Ethereum mainnet transaction 0x08ffb5f7ab6421720ab609b6ab0ff5622fba225ba351119c21ef92c78cb8302c at block 22222687. The attacker used one public transaction to create a helper contract, occupy the remaining mixer participant slots without paying, execute a valid withdrawal sequence, and extract the only honest participant's 1 ETH deposit.

The root cause is that deposit(uint _pubkey1, uint _pubkey2) records a participant and increments gotParticipants without enforcing msg.value == payment. That breaks the contract's funding invariant and lets an unprivileged attacker convert unpaid registrations into a fully funded withdrawal set.

2. Key Background

Laundromat is a fixed-size ETH mixer. The constructor sets participants and payment. Users call deposit to append two public-key coordinates to the mixer set. Once the participant count is full, withdrawStart, repeated withdrawStep, and withdrawFinal validate a ring signature over the participant set and send exactly payment ETH to msg.sender.

Two facts matter for this incident. First, the contract economically assumes every participant slot corresponds to one funded deposit. Second, a ring signature only requires control of one private key in the ring, not every key. If unpaid registrations are allowed, an attacker can contribute multiple attacker-controlled keys for free and use the honest depositor only as a decoy.

3. Vulnerability Analysis & Root Cause Summary

The incident is an ATTACK-class vulnerability caused by broken deposit accounting. Laundromat should require each participant registration to contribute exactly payment wei, so a full ring implies a funded pool. The verified source does not enforce that rule. In the verified deposit implementation, the intended payment guard is present only as a commented line:

function deposit(uint _pubkey1, uint _pubkey2) payable {
    //if(msg.value != payment) throw;
    if(gotParticipants >= participants) throw;

    pubkeys1.push(_pubkey1);
    pubkeys2.push(_pubkey2);
    gotParticipants++;
}

That omission creates a direct code-level breakpoint: gotParticipants++ can execute while msg.value == 0. Once gotParticipants reaches participants, withdrawStart and the subsequent verification path treat the ring as valid and withdrawFinal pays payment ETH to the caller. The economic invariant "funded participants equal withdrawable pool" is therefore false, and the attacker can drain honest deposits without contributing matching capital.

4. Detailed Root Cause Analysis

The exploitable pre-state was block 22222686, immediately before the attack transaction. At that point the contract state was:

participants    = 5
payment         = 1000000000000000000 wei
gotParticipants = 1
balance         = 1000000000000000000 wei

That means the mixer expected five funded participants, but only one honest user had actually paid into the pool. Four participant slots remained open.

The attacker exploited this by taking those remaining slots for free. The verified victim code above shows why the free registration is possible: deposit checks only gotParticipants < participants and then appends the supplied public key pair. No value transfer is required. The relevant withdrawal logic then relies on the participant set being complete, not on the pool being economically sound:

function withdrawStart(uint[] _signature, uint _x0, uint _Ix, uint _Iy) {
    if(gotParticipants < participants) throw;
    ...
}

function withdrawFinal() returns (bool) {
    ...
    safeSend(withdraw.sender, payment);
    return true;
}

The attack transaction deployed helper contract 0x2e95cfc93ebb0a2aace603ed3474d451e4161578, which then called Laundromat four times with the deposit selector 0xe2bbb158. The execution trace shows the repeated public calls and the final withdrawal sequence, including withdrawFinal() returning true and transferring exactly 1 ETH to the helper:

Laundromat::withdrawFinal()
  0x2E95...1578::fallback{value: 1000000000000000000}()
  <- [Return] true

The attack only works because the attacker controls at least one private key inside the final five-member ring. The attacker supplied four attacker-controlled public keys via zero-value deposits and combined them with the single honest participant already present in the mixer. That was sufficient to generate a valid ring-signature withdrawal flow over the now-complete participant set.

The balance diff confirms the exploit outcome. Laundromat dropped from 1000000000000000000 wei to 0, and attacker EOA 0xd6be07499d408454d090c96bd74a193f61f706f4 increased from 50000000000000000 wei to 1046407109528025240 wei, a net gain of 996407109528025240 wei after gas. The core violated invariant is therefore:

Each increment of gotParticipants must correspond to receipt of exactly payment wei.

Laundromat breaks that invariant at deposit, and withdrawFinal later cashes out against the honest depositor's ETH.

5. Adversary Flow Analysis

The attacker strategy was a single-transaction helper-contract exploit.

  1. Pre-state setup: Before block 22222687, Laundromat held 1 ETH from one honest participant and still needed four more participants.

  2. Helper deployment and free participant fill: EOA 0xd6be07499d408454d090c96bd74a193f61f706f4 submitted transaction 0x08ff...302c, which created helper contract 0x2e95cfc93ebb0a2aace603ed3474d451e4161578. The helper executed four public deposit calls with attacker-controlled public keys and zero ETH, moving gotParticipants from 1 to 5 while the contract balance stayed at 1 ETH.

  3. Ring-signature withdrawal: The same helper called withdrawStart, then withdrawStep five times, then withdrawFinal. The trace records exactly that sequence and shows withdrawFinal succeeding.

  4. Profit realization: withdrawFinal sent 1 ETH to the helper contract. The helper then forwarded value to the attacker EOA via selfdestruct, leaving the attacker with a net profit after gas and leaving Laundromat drained.

The ACT framing is supported directly by the interface. deposit, withdrawStart, withdrawStep, and withdrawFinal are public and require no privileged keys or protocol-side permissions. Any unprivileged actor with public chain access could have executed the same sequence from the exploitable pre-state.

6. Impact & Losses

The measurable loss was the full 1 ETH held by the mixer for the only honest participant. The contract balance was reduced to zero in the exploit transaction, and the honest depositor's funds were transferred to the attacker-controlled flow.

Loss summary:

[
  {
    "token_symbol": "ETH",
    "amount": "1000000000000000000",
    "decimal": 18
  }
]

The broader impact is that Laundromat's accounting model is unsound: participant registration is not bound to payment, so the mixer can be completed and drained by unpaid adversarial participants whenever at least one honest depositor has already funded the pool.

7. References

  1. Exploit transaction: 0x08ffb5f7ab6421720ab609b6ab0ff5622fba225ba351119c21ef92c78cb8302c
  2. Victim contract: 0x934cbbe5377358e6712b5f041d90313d935c501c
  3. Attacker EOA: 0xd6be07499d408454d090c96bd74a193f61f706f4
  4. Attacker helper contract: 0x2e95cfc93ebb0a2aace603ed3474d451e4161578
  5. Verified Laundromat source from Etherscan, including the commented-out payment check in deposit
  6. Collector seed metadata for the exploit transaction
  7. Collector transaction trace showing the repeated deposit calls and the successful withdrawFinal
  8. Collector balance diff showing Laundromat losing 1 ETH and the attacker EOA gaining 996407109528025240 wei net