The Polygon proof of stake network relies on three different parts: a consensus layer called Heimdall, an execution layer called Bor (fork of Geth) and a set of smart contracts. For this vulnerability, we'll be looking into the smart contracts and Heimdall layer.
Heimdall is a forked version of Cosmos and Tendermint. Unlike most blockchains, the staking is implemented within the smart contracts instead of natively. To do this, all events from Ethereum are picked up and processed natively if they come from the proper location. To prevent stakers from creating their own stake Heimdall uses side handlers to verify that the event occurred.
Any verification of Ethereum logs is critical code to get correct. Two common issues are equality checks that don’t verify all fields and insecure parsing of Ethereum log messages. Within the function responsible for decoding all of the event information, it properly verifies the emitting address. Once it calls UnpackLog() to get further information, we have a problem though.
UnpackLog() is used to take an Ethereum event log into a Golang struct. However, there is a missing check for the Topic of the code. Each event type has its own selector, just like functions do. From parsing, the only restriction was that it had to have the same amount of indexed parameters. With this, we have a type confusion vulnerability.
The StakeUpdate is call is the most interesting one to exploit but this affected a bunch of other issues. The goal is to use a SignerChange() event to trigger an StakeUpdate() function to steal all of the funds. The validatorIds correspond to the same field and the address in SignerChange is the amount (which would be a crazy large value). The final thing that lines up is the nonce from the change with the address on the update.
Is it feasible to line up a nonce as an address!? There is an integer truncation that occurs on the nonce while processing. So, we need the final 8 bytes of an address to line up with a valid nonce! Is this possible?
If we consider a valid nonce to be 0x0-0xFFFF then we have to generate 2**51 addresses for a 50% chance to hit. According to Felix, an EC2 P5 instance with 8 Nvidia H100 GPUs could do this in a fairly reasonable amount of time for some value between 50K-100K. There's a MEV bot whose address is
06f65 but ours would be 16 times more difficult to do.
To exploit this, perform the following steps:
- Change the signing key of the validator to be the address that we brute forced.
- Increase the nonce of the validator to match the address by performing various operations.
- Perform a signer change with an address under our control. This will process the fake
MsgStakeUpdate() event.
Once the fake message is accepted, we have a crazy amount of MATIC staked. With this, we gain a supermajority over the network. Although the smart contracts hold the actual funds (which we simply can't steal) we can still do quite a bit. So, Felix chose to attack the state-sync to actual steal funds.
State-Sync is a mechanism used by Polygon PoS to push events from Ethereum L1 to the network. Since it processes incoming transfers from the Polygon bridge to Plasma bridge, it's a super interesting attack surface. The state sync is a feature that allows users to participate in Consensus even if there's an RPC outage by agreeing with the super majority.
Felix's idea was to use his inflated voting power on the Heimdall network to claim that arbitrary events occurred within this secondary voting process. By triggering arbitrary events, side messages can be created into the L1 state to create an infinite mint on the network. There was roughly $2B at the time of reporting.
The author offers a couple of suggestions for limiting impact. First, adding in time locks/withdrawal delays on large amounts in order to allow for actions to be performed during attacks. Second, set transfer limits on the inflow and outflow to limit how much an attacker can steal. Finally, invariant testing on things like the staked amount, single validator holding a super majority and whatever else. Overall, amazing write up on how Polygon actually works and ways that parsing can go wrong!