All vulnerabilities
CRITICALWeb3exploited in the wildcurated

WEB3-POLY-NETWORK-2021

Web3 · Ethereum · Poly Network

Summary

On 10 August 2021, an attacker drained about $611 million from Poly Network, a protocol that moves assets between blockchains, in what was then the largest DeFi theft ever. No private keys were stolen and no cryptography was broken. The attacker found a flaw in how Poly's cross-chain contracts handled permissions and simply instructed the system to make them the owner, then signed their own withdrawals across three chains. The strangest part came next: over the following two weeks the attacker gave almost all of it back, claiming they had done it "for fun" and to expose the bug, and Poly Network ended up thanking them and offering them a job. It is a vivid lesson in smart-contract access control, and in the difference between stealing money on-chain and keeping it.

How it happened

This was not a key theft but a smart-contract access-control failure. Poly's cross-chain manager contract (EthCrossChainManager) had a function (verifyHeaderAndExecuteTx) that dispatched incoming cross-chain calls onward to other contracts, and it placed no allowlist on which target or method those calls could hit. Separately, the contract that stored the bridge's trusted "keeper" public keys (EthCrossChainData) was owned by that manager, and the function to replace those keys was protected only by an owner check.

The attacker connected the two. Solidity identifies a function by a four-byte selector derived from a hash of its name, and four bytes is small enough to brute-force a collision. The attacker found an innocuous-looking method string, f1121318093, whose selector collides with the protected key-replacement function putCurEpochConPubKeyBytes (both hash to 0x41973cd9), then used the dispatcher to make the manager contract, the legitimate owner, call that function. Poly's own contract dutifully replaced the entire keeper key set with the attacker's key. From that moment the attacker could sign any withdrawal they liked, and they emptied reserves across Ethereum (about $273 million), BSC (about $253 million), and Polygon (about $85 million), about $611 million in all.

The white-hat ending

What followed was unprecedented. Tether froze about $33 million of the stolen USDT, but the rest sat in the attacker's hands, fully traceable on-chain. Calling themselves "Mr White Hat," the attacker began returning the funds, and over roughly fifteen days gave back nearly all of it. In a Q&A embedded in their own transactions they said it had been done "for fun" and to expose the bug "before any insiders" could exploit it, and they even sent 13.37 ETH (about $42,000) to a stranger who had warned them on-chain not to move funds that had been blacklisted. Poly Network publicly thanked them, offered a $500,000 bug bounty, and even floated a security-advisor role. Part of the motivation was surely the simple reality that $600 million in watched, traceable crypto is extraordinarily hard to launder.

Why Poly Network still matters

Poly is an access-control lesson first. The most dangerous bugs are not fancy cryptography; they are a privileged function that can be reached from untrusted input, here a manager contract that could be tricked into calling its own owner-only setter. The selector collision is a neat reminder that four-byte function selectors are not unique and should never be trusted from attacker-controlled input. The defences are structural: allowlist which targets and methods a dispatcher may call, never make the executing contract the owner of the privileged config it can call into, put sensitive setters behind multisig or timelock governance, and validate full function selectors. And it stands as the clearest demonstration that on a public blockchain, taking the money and getting away with it are two very different problems.

How to fix it

  • Pause the bridge and replace the compromised keeper key set, then re-secure the privileged setters behind multisig or timelock before resuming.
  • Separate the executing contract from ownership of the keeper or config contract so no dispatched call can reach an owner-only function.
  • Trace and recover funds; Poly recovered nearly everything, helped by how hard $600 million on-chain is to launder under watch.

How to avoid it

  • Allowlist permitted call targets in the dispatcher; forbid calls into keeper and privileged config contracts.
  • Never make the executing manager contract the owner of the keeper/consensus data contract; separate execution from ownership.
  • Place privileged setters like the keeper-key setter behind multisig or timelock governance, not a single contract's owner check.
  • Validate full function selectors against an allowlist instead of trusting four-byte selectors from attacker-controlled method strings.
  • Audit every cross-contract ownership edge; assert no untrusted-input path reaches an owner-only mutating function.

References

Related vulnerabilities

All Web3 →