Unless you’re living under a rock, you might have read that last Tuesday the largest “crypto hack” in history targeted Cross-chain decentralized finance (DeFi) platform Poly Network, and allowed an undisclosed attacker to steal the equivalent of a whooping 610 million USD of crypto tokens.
The situation is in rapid development, and preliminary analysis of the attack started to circulate in the last hours. In this post, I am trying to explain what happened based on the existing available information.
The very basics
In this blog post I assume familiarity with the basic working of a permissionless decentralized ledger, and in particular with the concepts of blockchain and consensus, and related cryptocurrency token applications (for instance, Bitcoin), but I will recap anyway the most basic concepts.
In a nutshell, we can say that a “cryptocurrency token” is a virtual amount of “something” that is owned by someone. You claim possession of a token by publishing a “wallet address” (basically, a public verification key) and by “doing something” that makes the consensus protocol agree to bind the token to that address. Proof of possession of that token and the possibility of transferring the token to some other wallet is provided by a digital signature, which is generated by the wallet’s secret signing key. Issuing a “transaction” simply means embedding some metadata and a related valid signature in the blockchain, which authorizes transfer of funds from wallet A to wallet B. In a fully distributed ledger, the consensus mechanism makes it so that it is in the “best interest” of the participants to accept a valid signature within the blockchain.
The next step is to realize the following: As explained above, issuing a cryptocurrency transaction is basically a process where multiple participants “agree” to change the state of a virtual ledger in the same way. And a ledger is nothing else than a replicated database with entries of the form “address: amount”. However, there is nothing special in this particular form of entries: nothing described so far forbids to apply the same process to change the state of a different kind of replicated database. What if we consider as our “database” the memory of a virtual machine?
This is where the idea of “smart contracts” came into play, initially adopted within the Ethereum application. In a nutshell, a smart contract is a piece of software that is designed to run on a distributed virtual machine. On traditional computers, software is “executed” by changing, step by step, the state of the computer’s internal memory according to a set of predefined rules. A smart contract works in a similar way: the initial internal state of the machine is embedded in the blockchain ledger by issuing a transaction (from a creator’s wallet) that contains a compiled (“compressed” and machine-readable) version of the code. The code is executed by changing the state of the blockchain, and the consensus protocol makes it so that it is always in the best interest of the participants to execute the code (that is, applying the rules that govern the state change to the next block) in a correct way. As in cryptocurrency transactions, the execution of a smart contract is authorized by a digital signature issued by the “owner” of the smart contract (usually, but not always, its creator), but must be validated by the consensus.
This brings up the important concept of ownership and interaction between contracts. How do smart contracts interact? In a typical code environment, you might have different subroutines calling different functions, often collected into libraries, and you need to manage input and output of these. Smart contracts work in the same way, but the difference is that, given the distributed nature of the computation, safeguards have to be put in place (and enforced by the consensus mechanism) to make sure that, for example, if contracts A and B both call a function F, F will return to each of them the right output, or that if you create and execute a smart contract, I cannot change arbitrarily its state since I’m not the owner.
In practice, smart contracts (or their running platforms) usually embed safeguards of the form “If the caller of this program is not authorized by a certain key, then abort”. The authorization does not always come in the form of a direct signature: a contract A might be issued specifying another contract B as its “owner”, and different permissions can be given to different sets of users.
Cross chain networks
There is already too many blockchains out there to keep track of them all. A common problem that arises in the cryptocurrency scenario is when you own a certain kind of token (say, Bitcoin) and you want to convert it to another one that uses a different kind of blockchain technology or consensus (say, Ripple). This usually involves swapping tokens at an exchange, with associated fees. In the smart contract landscape, you run into similar problems: what if you are running a smart contract on Ethereum but you want it to interact with another one, say, on Cardano?
Now, if you think about it, you can see a cryptocurrency exchange as a sort of contract: I give you X Bitcoin and you give me back Y Ripple. Analogously, centralized services can act as “cross-chain interpreters” by, let’s say, allowing a program A running on Ethereum to interact with another program B running on Cardano in the following way:
- Read the query (intermediate output) of A from the Ethereum blockchain
- Input that value to B by issuing a transaction within the Cardano blockchain
- Wait that B finishes its execution, then read its output from the Cardano layer
- Return that value to A by issuing an input Ethereum transaction.
Services of this type are sometimes called “oracles” and, like exchanges, require a fee to operate. Also, like exchanges, they require the user to fully trust a single entity. However, the next observation is: since these nodes are basically running a contract or script, why don’t we decentralize that one as well as we do for smart contracts?
Enter the concept of “cross-chain network”, which is basically an additional abstraction layer on the concept of distributed virtual machine. A cross-chain network allows two different blockchains to “communicate with each other”. Or, to be more precise, allows users of a certain blockchain to perform operations on another blockchain in a distributed and autonomous way. However, exactly because they operate across different blockchains, these networks usually require their own “virtual blockchain” to run smart contracts that govern the communications rules within the underlying networks.
Poly Network is a cross-chain network that sits on top of different blockchains, including Bitcoin, Ethereum and Elrond. To overly simplify its architecture, Poly can be described by the following components:
- A master wallet for each one of the underlying Layer-1 networks (e.g., one for Bitcoin, one for Ethereum, …) each of them containing a certain amount of funds.
- A set of smart contracts that interpret and execute users’ instructions (e.g. “please exchange this amount of Bitcoin that I am sending to you into Ether tokens”) by calling functions on the related above wallets.
- A blockchain layer (the Poly network) where the above smart contracts run.
It is common for cross-chain networks, including Poly, to store at any moment large amount of liquidity in their underlying wallets, because there are many users performing cross-chain operations at the same time. Therefore, it is crucial to properly secure the very privileged cross-chain smart contracts that administer these wallets. Unfortunately, this is exactly what did not happen here.
On August 10th, Poly Network reported that an undisclosed attacker hacked a smart contract of the network, transferring the equivalent of roughly 610 million USD (mainly in Ether, Binance Coin and USDC) and moving them to external wallet addresses.
According to cybersecurity firm SlowMist and security researcher Kelvin Fichter, the hack was made possible by a mismanagement of the access rights between two important Poly smart contract. The first one is EthCrossChainManager and the second one is EthCrossChainData.
Let’s first talk about EthCrossChainData. This is a very high privileged contract that is not supposed to be invoked by anyone within the network, except by its owners. The reason is that this contract is responsible for setting and managing a list of public keys of “authenticator nodes” (Keepers) that manage the wallets in the underlying liquidity chains. In other words, EthCrossChainData can decide who has the privilege of moving the large amount of funds contained within Poly’s Binance wallet, Ethereum wallet, etc. If an attacker could call the right function (putCurEpochConPubKeyBytes) within EthCrossChainData, there would be not even need to attack a Keeper’s secret key: the attacker could simply set their own public key to replace that of a Keeper, and then they would have the right to execute a high volume transaction within the Poly network to exfiltrate a large amount of funds to other wallets. Clearly not something you want to happen.
Now, about EthCrossChainManager. This is another high privilege contract that has the right to trigger messages from another chain to the Poly chain. Which means: anybody can call a cross-chain event by issuing a transaction on the source chain that invokes the verifyHeaderAndExecuteTx function within EthCrossChainManager, and specifying a target Poly contract to execute. However, because of its particular intended use, EthCrossChainManager would not call any function within the target contract, but only the one with a very specific “Solidity function ID”. Namely, it would only call a function whose 32-bit ID is computed this way:
In other words, the ID of the function called is computed as the 32-bit truncation of a 256-bit Keccak hash of the string _method and a suffix.
The attacker exploited two problems here.
The first one is, it turns out that EthCrossChainManager is an owner of EthCrossChainData, and can therefore execute privileged functions within this one! The second one is that the field _method in the code snippet above is actually user-defined, and can therefore be set at will. In particular, it is possible for the attacker to brute-force a _method field that hashes to a 32-bit value that is exactly the ID for putCurEpochConPubKeyBytes!
This is the attack in detail:
- The attacker computed the 32-bit ID for putCurEpochConPubKeyBytes:
ethers.utils.id ('putCurEpochConPubKeyBytes(bytes)').slice(0, 10)'0x41973cd9'
- The attacker brute-forced a string that, if set as _method in the code snippet above, gives the same 32-bit value. In this case the attacker used the string “f1121318093”:
ethers.utils.id ('f1121318093(bytes,bytes,uint64)').slice(0, 10)'0x41973cd9'
- The attacker called a cross-chain transaction from the Ethereum network to the Poly network by triggering EthCrossChainManager and targeting EthCrossChainData, and passing the string f1121318093 as _method, and the public key of their own Ethereum wallet as a parameter.
- This triggered EthCrossChainManager into calling the function putCurEpochConPubKeyBytes within EthCrossChainData, and demanding the attacker’s public key to be registered as a Keeper’s. EthCrossChainData executed such command, since EthCrossChainManager is its owner.
- Once the transaction was executed and the attacker was granted the status of Keeper for the Ethereum blockchain, the attacker proceeded into using the corresponding secret key in their possession to funnel tokens out of Poly’s Ethereum wallet into their own wallet.
- The attacker repeated the above for other Poly liquidity wallets: Binance, Neo, Tether, etc.
A total worth of over 610 million USD was stolen this way. That’s a lot.
Things are moving fast and it is difficult to keep track in real time of the evolving situation.
After the hack, the next reasonable step would have been for the attacker to transfer the stolen funds into the liquidity pool of an anonymous decentralized exchange like Curve. For this reason, Poly immediately issued a request for crypto miners and exchanges to “blacklist” the stolen funds, making them de facto unavailable to the attacker. This is in theory possible to do, but cumbersome for the exchanges for many reasons, and it is unclear whether all exchange platforms answered the plea.
However, administrators of the stablecoin Tether (who have greater control over their blockchain compared to other DeFi applications) managed to freeze the stolen Tether-USDC funds in time just 9 blocks before the attacker tried to launder them on the Curve liquidity pool. An anonymous user alerted the hacker by issuing an Ethereum transaction to the hacker’s address with the freeze warning message in the metadata, and the hacker rewarded this user with part of the stolen Ethereum. After that, the address of the hacker was flooded with transactions of hundreds of people begging for money.
Poly Network asked the hacker to return the funds. The security company Slowmist published findings on the alleged hacker, claiming that the hacker’s identity had been exposed and that the group had access to the hacker’s email and IP address. According to Slowmist, the hacker was able to take advantage of a relatively unknown crypto exchange in Asia and they claimed to have a lot of information about the attacker.
Whether this is true or not, the hacker started returning funds to Poly on Wednesday. By August 11th 15:00 UTC nearly half worth of tokens have been returned, and the hacker claims to be ready to return more in exchange for the unfreeze of the Tether tokens. A second message embedded in a transaction reads: “IT’S ALREADY A LEGEND TO WIN SO MUCH FORTUNE. IT WILL BE AN ETERNAL LEGEND TO SAVE THE WORLD. I MADE THE DECISION, NO MORE DAO”.
While this story develops, it is not superfluous to remind that “blockchain” is not synonymous with “security”. It is very important to audit the security of your applications, including smart contracts.