Over on Twitter, @kelvinfichter gave an analysis (included below) of the recent Poly Network hack.
Without any of the benefits of hindsight, are there reasons why the exploit would have been less likely to occur in Plutus on Cardano?
Poly has this contract called the
EthCrossChainManager. It's basically a privileged contract that has the right to trigger messages from another chain. It's a pretty standard thing for cross-chain projects.
There's this function
verifyHeaderAndExecuteTxthat anyone can call to execute a cross-chain transaction. Basically it:
- Verifies that the block header is correct by checking signatures (seems the other chain was a poa sidechain or something) and... then
- Checks that the transaction was included within that block with a Merkle proof. Here's the code, it's pretty simple: EthCrossChainManager.sol#L127
One of the last things the function does is call
_executeCrossChainTx, which actually makes the call to the target contract. Here's where the critical flaw sits.
Poly checks that the target is a contract EthCrossChainManager.sol#L185. But Poly forgot to prevent users from calling a very important target... the
Why is this target so important? It keeps track of the list of public keys that authenticate data coming from the other chain. If you can modify that list, you don't even need to hack private keys. You just set the public keys to match your own private keys. See here for where that list is tracked: EthCrossChainData.sol#L22.
So someone realized that they could send an cross-chain message directly to the
What good does that do them? Well, guess which contracted owned the EthCrossChainData contract... yep. The
EthCrossChainManager. By sending this cross-chain message, the user could trick the
EthCrossChainManagerinto calling the
EthCrossChainDatacontract, passing the
Now the user just had to craft the right data to be able to trigger the function that changes the public keys. Link to that function here: EthCrossChainData.sol#L45.
The only remaining challenge was to figure out how to make the
EthCrossChainManagercall the right function. Now comes a little bit of complexity around how Solidity picks which function you're trying to call.
The first four bytes of transaction input data is called the "signature hash" or "sighash" for short. It's a short piece of information that tells a Solidity contract what you're trying to do. The sighash of a function is calculated by taking the first four bytes of the hash of "()".
For example, the sighash of the ERC20 transfer function is the first four bytes of the hash of
transfer(address,uint256). Poly's contract was willing to call any contract. However, it would only call the contract function that corresponded to the following sighash: Errr but wait...
_methodhere was user input.
All the attacker had to do to call the right function was figure out some value for
_methodthat, when combined with those other values and hashed, had the same leading four bytes as the sighash of our target function.
With just a little bit of grinding, you can easily find some input that produces the right sighash. You don't need to find a full hash collision, you're only checking the first four bytes.
So is this theory correct? Well... here's the actual sighash of the target function:
> ethers.utils.id ('putCurEpochConPubKeyBytes(bytes)').slice(0, 10) '0x41973cd9'`
And the sighash that the attacker crafted...
> ethers.utils.id ('f1121318093(bytes,bytes,uint64)').slice(0, 10) '0x41973cd9'
Fantastic. No private key compromise required! Just craft the right data and boom... the contract will just hack itself!
The author then goes on to give lessons learned.
Do Plutus developers also need to learn from these lessons or are they specific to Solidity?
One of the biggest design lessons that people need to take away from this is: if you have cross-chain relay contracts like this, MAKE SURE THAT THEY CAN'T BE USED TO CALL SPECIAL CONTRACTS.
EthCrossDomainManagershouldn't have owned the
EthCrossDomainDatacontract. Separate concerns.
If your contract absolutely need to have special privileges like this, make sure that users can't use cross-chain messages to call those special contracts.
And just for good measure, here's the function to trigger a cross-chain message: EthCrossChainManager.sol#L91. Nothing here prevents you from sending a message to the
@kelvinfichter also gives a simplified summary of the attack: https://twitter.com/kelvinfichter/status/1425290462076747777