# Polygon Double-Spend Bugfix Review — $2m Bounty

Polygon Plasma Bridge WithdrawManager decoded the HP-encoded branchMask inconsistently, letting one burn exit be replayed up to 223 times to drain ~$850M.

- Payout: $2,000,000
- Severity: critical
- Program: Polygon
- Credited: Gerhard Wagner
- Disclosed: 2021-10-21
- Write-up quality: 8/10
- Original: https://immunefi.com/blog/bug-fix-reviews/polygon-double-spend-bugfix-review-2m-bounty/
- Library page: https://bugbountylibrary.com/r/blog%3Aimmunefi-com-blog-bug-fix-reviews-polygon-double-spend-bugfix-review-2m-bounty
- Tags: domain:web3 impact:funds-loss kind:bounty platform:blog platform:immunefi sev:critical tech:evm tech:solidity technique:parser-differential technique:source-review vuln:business-logic vuln:signature-replay

**Target:** WithdrawManager.sol / MerklePatriciaProof on Ethereum root chain
**Root cause:** verifyInclusion decodes the HP-encoded branchMask as uint256 for Exit ID uniqueness while MerklePatriciaProof decodes it as nibbles, discarding the first byte when its nibble isn't 1 or 3, so one semantic path has ~224 encodings and thus many Exit IDs.
**Steps:**
1. Deposit large ETH/tokens via Plasma Bridge to Polygon
2. After confirmation, call startWithdraw to burn tokens on child chain
3. Wait for checkpoint inclusion and the 7-day exit wait period
4. Call startExitWithBurntTokens with exit payload, then processExits with burn proof
5. Resubmit the same exit payload with a modified first byte of the HP-encoded branchMask
6. Repeat up to 223 times with different first-byte values, each yielding a distinct Exit ID
7. Collect the withdrawn funds each time
**How found:** Whitehat noticed branchMask is HP-encoded and decoded twice: as nibbles in MerklePatriciaProof (discarding first byte unless nibble is 1 or 3) and as uint256 in WithdrawManager for Exit ID uniqueness; the mismatch means one path has 14*16=224 encodings, breaking the one-exit-one-Exit-ID invariant.
**Exploitation:** Attacker needs only ~$100k to launch; by replaying a single valid burn exit with mutated branchMask first bytes they can exit the same burn up to 223 times, draining the DepositManagerProxy which held ~$850M at disclosure.
**Escalation:** Replaying the same proof across all 223 valid encodings multiplies a single deposit into ~223x withdrawal, depleting the entire bridge deposit pool.
**Insight:** When a value is decoded by two different routines for security checks (uniqueness vs. proof verification), enumerate all encodings each decoder accepts and check whether the uniqueness key covers them; HP-encoded Merkle paths are a classic source of many-to-one encodings.
