Polygon Double-Spend Bugfix Review — $2m Bounty

Immunefi
Immunefi
Published in
7 min readOct 21, 2021

Summary

Once in a while, the Immunefi triaging team receives a bug report that changes everything — a report that shows how vital bug bounty programs are, and how Immunefi helps to save hundreds of millions of dollars from being hacked. Such a bug report was submitted to Immunefi earlier this month.

Whitehat Gerhard Wagner submitted a critical vulnerability on October 5th, 2021 that affected the Polygon Plasma Bridge. The vulnerability allowed an attacker to exit their burn transaction from the bridge multiple times, up to 223 times. There was around ~$850M at risk. Having just $100k to launch the attack with would result in $22.3M in losses! This means the DepositManager for the Plasma Bridge could be depleted with a sufficient amount.

The Immunefi triaging team confirmed the underlying issue and escalated it to the client. After 30 minutes, Polygon confirmed the bug and immediately began fixing the underlying issue. In the meantime, the triage team decided to calculate the funds at risk. The whitehat also confirmed our calculations, and Polygon agreed to pay the maximum for this submission.

What’s the maximum? According to Polygon’s bug bounty page, the figure is $2,000,000.

The whitehat received a payout of $2m from Polygon, which is the highest bounty ever paid out in history. We congratulate Gerhard for his fantastic work and excellent report. We also want to thank Polygon for a swift answer and subsequent fix.

The issue was mitigated within one week, which includes:

  • Payout to the whitehat
  • Payout of the commission to Immunefi
  • Testing the fix
  • Deploying the fix onto the mainnet

Before we jump into a technical analysis of the exploit, we first need to understand what Bridges are and, in particular, the Plasma Bridge on Polygon. It will help us better understand the underlying issue.

Vulnerability Analysis

There are many takes on how blockchains should work. A few of the leading blockchains are Ethereum, BSC, Polygon, and BItcoin.

The rise of DeFi means many assets are created on one of the chains mentioned above and live there. But what if we wanted to move our NFTs or tokens from one chain to another? Is this possible?

In comes the concept of a blockchain bridge. A blockchain bridge is a way to connect two distinct blockchains and enable communication between them. Polygon offers a trustless, two-way transaction channel between Polygon and Ethereum. It introduced the cross-chain bridge with Plasma and Proof of Stake (PoS) security.

As we can read in the Polygon documentation, “A bridge is basically a set of contracts that help in moving assets from the root chain to the child chain. There are primarily two bridges to move assets between Ethereum and Polygon.”

Polygon offers two bridges. The first one is the Plasma bridge, and the other is the PoS bridge. Plasma is considered a more secure bridge, due to its exit mechanism.

The pros and cons of the PoS bridge vs. the Plasma bridge can be further analyzed by reading the official Polygon docs.

High Level Flow of Assets in the Plasma Bridge

1. User deposits tokens to a Polygon contract on the Root Chain (Ethereum)

2. When the token deposit transaction is confirmed on Ethereum, corresponding tokens will be minted on the Polygon chain. These tokens are ready to be used on the Polygon network

3. When a user is ready to withdraw from the Child Chain (Polygon), they can do so by initiation from Polygon

3.1. A checkpoint interval needs to pass (around 30 minutes) where all blocks have been validated since the last checkpoint

3.2. Checkpoint is then submitted to the Root Chain contract

4. An EXIT NFT token is minted of the value the user wants to withdraw

5. A wait period is initialized, and the user needs to wait for seven days before being able to withdraw their funds

6. Using the process-exit procedure, once the waiting period finishes, a user can claim back funds to their Ethereum account.

The withdrawal procedure contained a vulnerability.

Withdrawal

The withdrawal process starts with burning tokens on the child chain. The Polygon Plasma client exposes the startWithdraw method to call the withdraw function of getERC20TokenContract. This function burns the tokens.

After the burn is confirmed, a user can call startExitWithBurntTokens function of erc20Predicate contract. It is the time where the initial checkpoint (30 minutes) is being followed. Additionally, the exit payload needs to be passed to the function. The exit payload contains all important information about the funds that are being transferred from L2->L1.

To proceed with the exit, the burn transaction needs to be a successful and valid one. What’s very important to note here is that exit can only be called after the checkpoint is included in the root chain alongside the burn transaction. A user should call the processExits function of the withdrawManager contract and submit the burn proof.

The main vulnerability lies in how Polygon’s WithdrawManager verifies the inclusion and uniqueness of the burn transaction in previous blocks.

Vulnerability

The WithdrawManager.sol implements verifyInclusion() function.

The goal of this function is to, as the name suggests, verify the inclusion of the burn transaction receipt during a checkpoint. It does that by checking Merkle Proof for receipt and transaction itself. All of the above information is contained inside the exit payload. If you’re interested in what the exit payload contains exactly, you can check this here.

One crucial parameter the exit proof contains is Merkle proof’s branchMask for the receipt. branchMask is an essential security guard that helps keep the system secure. That’s why the branch mask must be unique, as it is used to generate Exit ID. Property that needs to be behold is one exiting transaction == one Exit ID. But as the whitehat found, that’s not necessarily the case.

The branch mask is HP encoded and later on decoded in the MerklePatriciaProof.verify call inside the WithdrawManager.sol. The verify function decodes the encoded path by calling _getNibbleArray function. Separately from the decoding in MerklePatriciaProof, the WithdrawManager.verifyInclusion function also decodes the path as a uint256. Because the decoding into an array of nibbles ignores some of the value and differences in the ignored portion are not rejected by the uint256 decoding, the same value as decoded by MerklePatriciaProof may have many encodings as a uint256. The uint256 decoding is the one used to avoid replays; therefore the same proof can be replayed due to the differences in the decoding.

We can dig deeper into the decoding in MerklePatriciaProof to see why one semantic value may have multiple encodings. We can see here that if the first nibble of the HP-encoded value is 1 or 3, we interpret the second nibble. However, if the first nibble is not 1 or 3, the entire first byte is discarded. Excluding the values where the second nibble is interpreted, we find that there are 14*16 == 224 ways of encoding the same path. A malicious user can create different exit ids for the same exit transaction.

What does the step-by-step exploit look like in this case?

1. Deposit a large amount of ETH/tokens to Polygon through the Plasma Bridge

2. After confirmation of the funds being available on the Polygon, start the Withdrawal process

3. Wait for seven days for an exit to be valid

4. Resubmit the exit payload but with a modified first byte of the branch mask.

5. The same valid transaction can be resubmitted up to 223 times with different values for the first byte of the HP-encoded path.

6. Profit

As mentioned, the impact was colossal. At the time of the bug submission, around $850M was inside the DepositManagerProxy. How did the team and the whitehat proceed with the fix?

Vulnerability Fix

As it turns out, the first byte of the encoded branch mask is supposed to always be 0x00. The fix is to check if the first byte of the encoded branch mask is 0x00 and not to disregard it as an incorrect mask.

You can find the commit with the fix here: https://github.com/maticnetwork/contracts/commit/283b8d2c1a9ff3dc88538820ffc4ea6a2459c040

New implementation of WithdrawManager: https://etherscan.io/address/0x4ef5123a30e4cfec02b3e2f5ce97f1328b29f7de#code

Acknowledgments

We want to thank Gerhard Wagner for a critical find and detailed report. We want to also thank Polygon for their swift response and fast handling of the issue and the payout.

If you’d like to start bug hunting, we got you. Check out the Web3 Security Library, and start earning rewards on Immunefi — the leading bug bounty platform for web3 with the world’s biggest payouts.

To report additional vulnerabilities, please see Polygon bug bounty program with Immunefi. If you’re interested in protecting your project with a bug bounty, visit the Immunefi services page and fill out the form.

--

--

Immunefi
Immunefi

Immunefi is the premier bug bounty platform for smart contracts, where hackers review code, disclose vulnerabilities, get paid, and make crypto safer.