Polygon Consensus Bypass Bugfix Review
The consensus mechanism in the blockchain plays a significant role in validating the authenticity of transactions and maintaining the underlying blockchain’s security. It is a system that users of a blockchain network follow to agree on the legitimacy of transactions. But what would happen if someone were able to bypass checks in the consensus requirement check to gain an advantage over the system?
On January 15, whitehat Niv Yehezkel submitted a report to Polygon along with a local mainnet fork proof of concept (PoC) to demonstrate a consensus bypass vulnerability. Niv discovered a vulnerability in the proof of stake (PoS) system in Polygon’s smart contract on Ethereum, which would have allowed an attacker to decrease the total staking power, allowing a consensus (⅔ threshold) bypass that could potentially have allowed an attacker to drain all funds from the deposit manager, engage in unlimited withdrawals, DoS and more.
The bug was given a severity level of
high due to the complexity of the exploit, and the whitehat was rewarded with a bounty of $75,000.
For the attacker to have exploited this vulnerability, specific market conditions would have had to have been met. For example, a validator spot had to have been open, and the capital requirements were high (less capital means longer the attack takes). The amount to pay the miners directly to stay in the validator spot using flashbots was also high. Additionally, the checkpoint time for the Polygon network happens every 30–45 minutes, and the attacker would have needed to maintain the validator spot for a long time, thus increasing the costs of the attack due to time requirements.
Intro to the Consensus Mechanism
A consensus mechanism is a method by which the network agrees on a single source of truth. Unlike in centralized systems, where a single controlling entity decides upon a source of truth, distributed systems rely on large numbers of autonomous authorities to cooperate to maintain a single network. In the blockchain, the process is formalized, and reaching consensus means that at least 51% of the nodes on the network agree on the next global state of the network in a proof of work (PoW) consensus mechanism.
A consensus mechanism also helps prevent certain kinds of economic attacks. In theory, an attacker can compromise consensus by controlling 51% of the network. It was designed to make this “51% attack” unfeasible. A similar attack could be performed on the PoS consensus mechanism but instead of holding 51% of nodes, you need ⅔ +1 of the total staked amount.
To recap, in the context of blockchains and cryptocurrencies, there are two most prevalent consensus mechanisms.
- proof-of-work (PoW)
- proof-of-stake (PoS)
With an Ethereum Layer-1 PoW consensus mechanism, transactions are broadcasted to the mempools, with miners picking the transactions and processing them. However, due to high demand, this process is slower because of network congestion and nonviable gas prices. The goal of the scalability solution is to increase the transaction speed without sacrificing security or decentralization.
Polygon PoS is a Layer-2 scalability solution that relies on a set of validators, who act like operators by staking the tokens into the system to secure the network. Anyone holding a native blockchain base cryptocurrency (ex: MATIC for Polygon blockchain) can become a validator by locking up their staking token into a system and running Heimdall validator and Bor block producer nodes to help run the network. The role of validators is to run a full node, produce blocks, validate and participate in consensus, and commit checkpoints on the Ethereum mainnet. The rewards are distributed to all stakers proportional to their stake at every checkpoint. One can opt-out of the system at any time and withdraw tokens once the unbonding period ends. Validators in the Polygon network are selected via an on-chain process which happens at regular intervals. These selected validators participate as block producers and verifiers. Once a checkpoint is validated by the participants, updates are made on the parent chain (the Ethereum mainnet) which releases rewards for validators depending on their stake in the network.
Those who are interested in securing the network but are not running a validator node can participate as delegators. The delegator role stakes their MATIC tokens to secure the Polygon network with existing validators without running the nodes themselves. Delegators delegate staking tokens to a validator and obtain a part of their rewards in exchange. Anyone can delegate any amount of tokens to any validator he wishes. When a validator unstakes, the counter of the total staking power updates together with the delegated amount in that validator.
As long as ⅔ of the weighted stake of the validators is honest, the chain will progress accurately and the funds on the chains are secure. The validators stake their MATIC tokens as collateral to work for the security of the network and earn rewards in exchange for their service.
Polygon’s staking manager contract is the main contract for handling validator-related activities like checkpoint signature verification, reward distribution, slashing validators, and stake management.
As stated previously, anyone can participate in the PoS system by staking MATIC tokens into the contract, and the contract mints the NFT with
validatorID as a source of ownership for participants in the system. The contract also has a fixed slot limit (
validatorThreshold) for validators to participate in the system. Currently, the slot limit is set to 100. If you want to stake into the system, then you have to wait until one of the validators unstakes and you fill the slot via a selection process.
For every validator participating in the contract via
stakeFor() function, the contract asks the validator if they want to allow delegators to stake tokens into the validators
acceptDelegation(bool). If the validator wants to accept delegators, then the contract creates a
validatorShare delegator contract for the validators. The following screenshot represents the
stakeFor() function and the described behavior.
ValidatorShare contract holds all the logic for delegators participants in the system such as buying and selling shares of the validators, calculating the rewards earned, etc. Delegators can participate in the system by providing staking tokens (MATIC) to the validators in the
stakeManger contract and in return the contract mints the desired amount of shares to the delegator. Similarly, delegators can sell the shares of the validators at any time in the future and get back their staked MATIC tokens along with the rewards earned.
Delegators can participate in the system by buying shares of the validators via
Every time staking tokens or stakers are added/removed from the system, the following function is called
updateTimeline(amount, stakerCount, epoch) in a
stakingManager contract which is responsible for accounting the number of stake tokens and number of stakers in the contract.
StakingManager contract holds information about the validator state in the
validatorState struct. The following are the two variables that are important to the vulnerability:
validatorState.amount— The total number of tokens staked by the validators. In other words, it represents the total staking power.
validatorState.stakerCount— Represents the total number of stakers in the contract.
When a validator unstakes, the counter of the total staking power updates together with the delegated amount and the amount of the validator. This can be seen in the following line:
updateTimeline(-(int256(amount) + delegationAmount), -1, targetEpoch);
The above function decreases the total staking power by
amount + delegationAmount, where the
amount is the validator staked amount and
delegationAmount is the total delegated amount provided by the delegators to the validator.
StakeManager has a
migrateDelegation functionality where the system allows the delegators to migrate their delegated amounts from one validator to another validator.
When the delegator migrates staking tokens to another validator via the
migrateDelegation function, the system first calls
migrateOut on the
validators[fromValidatorId].contractAddress, the contract that will withdraw the staking tokens deposited by the delegator along with the rewards from the
fromValidatorId validator. Then, it burns the shares held by the delegator for the validator and updates the validator state with the removed amount on the
The system then calls the
migrateIn function on the
validators[toValidatorId].contractAddress that will buy the shares of the
toValidatorId validator by depositing the staking tokens to the validator and updates the validator state with the added amount on the
stakeManger contract. This covers the entire migration process of the delegator.
The vulnerability arises when delegators migrate their delegations from one validator to another. The contract calls
updateTimeline(-amount), which ends up subtracting the total validator power from the
stakeManager contract, and once that validator unstakes, the counter of total staking power will be updated again by decreasing the
validator amount + delegated amount again from the contract.
Imagine if the validator unstakes, a delegator can migrate from one validator to another. When a delegator migrates, the delegated amount is migrated to another validator. Once that validator unstakes, the counter of total staking power will update again and decrease the delegated amount again from the total staking power.
So the exact issue lies in the
updateValidatorState(validatorId, -int256(amount)) function on the
stakeManager contract, where it modifies the timeline
updateTimeline(amount, 0, 0); which reduces the total staking power
validatorState.amount with the
amount the delegator is migrating, without first checking if the current validator is currently staking or not in the contract.
Steps to Reproduce
- Create a new validator using the
- Call the
buyVoucherfunction with a big delegated amount to buy the shares of the validators by staking tokens.
The attacker can now repeat the following steps until
validatorState.amount (total staking power) is low enough to bypass the consensus majority check (⅔) requirement.
- Catch an available validator slot via an on-chain auction process which happens at regular intervals.
- Migrate staking tokens into that validator by calling a
- Unstake the validator. (
validatorState.amountis decreased again)
- Wait for a checkpoint (for this validator slot to open)
These steps will repeatedly decrease the total staking power by the same amount of delegated amount for each iteration. An attacker can repeat this until the total staking power is low enough to start accepting new checkpoints. He can bypass the required ⅔ consensus majority check. An attacker can lower the total staking power up to a low point that a sole validator can pass the majority check.
After this consensus bypass, the attacker can send malicious checkpoints that fake a withdrawal of tokens from Polygon that basically drains all tokens from the deposit manager, claiming all heimdall fees stored and more.
migrateOut() is called in the
validatorShare contract, it calls
stakeManager.updateValidatorState(validatorId, -int256(amount)); to subtract the amount from the
The fix was deployed on the Matic Github repository by adding a check to update the total staking power only if the validator didn’t unstake from the
We would like to thank whitehat Niv Yehezkel for doing an amazing job and reporting this finding. Props also to the Polygon team for patching and paying out the bug.
This issue was reported responsibly and securely via the Immunefi platform, leading to a happy outcome for everyone, especially the users.
If you’re a Web2 or Web3 developer who is finally thinking about a bug-hunting career in Web3, we got you. Check out our ultimate blockchain hacking guide, and start taking home some of the $84m in rewards available on Immunefi — the leading bug bounty platform for Web3.