11

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 verifyHeaderAndExecuteTx that anyone can call to execute a cross-chain transaction. Basically it:

  1. Verifies that the block header is correct by checking signatures (seems the other chain was a poa sidechain or something) and... then
  2. 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 EthCrossChainData contract: EthCrossChainData.sol.

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 EthCrossChainData contract.

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 EthCrossChainManager into calling the EthCrossChainData contract, passing the onlyOwner check.

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 EthCrossChainManager call 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... _method here was user input.

All the attacker had to do to call the right function was figure out some value for _method that, 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.

The EthCrossDomainManager shouldn't have owned the EthCrossDomainData contract. 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 EthCrossChainData contract.

@kelvinfichter also gives a simplified summary of the attack: https://twitter.com/kelvinfichter/status/1425290462076747777

1

2 Answers 2

3
+200

This is a very broad question but here's what I think regarding this topic.

Without any of the benefits of hindsight, are there reasons why the exploit would have been less likely to occur in Plutus on Cardano?

First of all, this could have happened on Cardano too. As Charles said in his recent update:

This is not to say that hacks won't happen on Cardano, it will! (...) The hacks will occur in uncertified software. Applications that are not rigorously debugged or checked. (...) You can't control people maliciously or incompetently writing software that can create problems. But the existence of certified software against known standards and auditing means that the consumer can have a visual representation (...) that demonstartes the difference between something that is known and reliable and something that is unknown.

See: https://youtu.be/xSXSZ5xLpYI?t=665

What this essentially means is that a similar exploit could happen on Cardano, but it is less likely for certified smart contracts that have been audited. This will not give 100% guarantee that there are no vulnerabilities in the contract. Some vulnerability may be missed in the audit too, but the risk can be greatly reduced.

Do Plutus developers also need to learn from these lessons or are they specific to Solidity?

I'm no expert when it comes to cross chain relay contracts, but I would answer this question with yes. It's always good to know about possible vulnerabilities and if you ever come across some Plutus code that makes calls to other smart contracts, the polygon hack is something to remember: "Are there any special contracts that I should not be able to call here?". Knowing about these things can make you write better and more secure code.

2
  • 1
    It was those words from Charles' that prompted me to ask the question. There is a strong narrative in the community that Cardano smart contracts are going to be more secure. Given that solidity code could also be audited, I’m looking for specific evidence that may support the increased security narrative (e.g. tooling). Otherwise there is perhaps a risk that the increased complexity/lack of familiarity with functional programming could actually make bugs more common on the The Island.
    – isedwards
    Aug 13, 2021 at 11:51
  • 3
    Haskell is a functional programming language, which is well-suited for high-assurance code and programs that require a high degree of formal verification because every line of code can be mathematically proven to be correct. This is one of the main advantages Plutus smart contracts have in comparison to e.g. Solidity. However, there is a difference between mathematically correct and secure. Plutus can't protect you from making mistakes like getting your access rights wrong as in the polygon example.
    – eddex
    Aug 13, 2021 at 12:01
3

[In short]

"Things can go wrong in any software" is a statement that misses a mammoth fact. The probability of occurrence can vary on an astronomical scale, like the probability of a bug in a Javascript program( written by me in college) and the code driving the Curiosity rover. There have been bugs even in spacefaring vehicles that had caused catastrophic accidents like thee Flight 501 Failure. It may not be prudent to compare them at any level. Cardano is written in a Functional Programming language, Haskell, which has existed in the academic world for 30 years. The functional programming paradigm prevents [make it impossible] for programmers to make stupid mistakes and, on top of it, adds formal verification that makes the software space grade. Therefore if we have to follow the human scale and not some astronomical scale, we can say Cardano will not face hacks like what these mushroom networks face.

[In Detail]

The piece of the code from the Poly Network which had the vulnerability is referenced below.

enter image description here Vularable part of Poly Code

Analysis

There are loops, jumps, and variables. The code can exist in various states. And it becomes impossible to test the code in all the possible states. After learning Haskell and Plutus, I can say with certainty loops, backward and forward jumps, and variables are the sources of all problems. Now for any new programmer, these are the first three steps that are taught. When I started learning Haskel, it was unbelievable that it is possible to program without these basic constructs. Haskell was heavily used by the Math community, which defines function as one input that gives one output irrespective of the date, time, or places it is executed. Everyone knows the square root of 2, but try calculating in C/C++/Java, depending on the floating-point implementation in a particular computer, the result varies by tiny amounts. However, due to the compounding effect of software, these tiny changes add up leading to a noticeable difference downstream. People working in Digital Signal Processing can vouch will vouch for this issue. Like the duration of a second determined by the pendulum clock with a temperature change.

Haskell is based on purity and has no variables and loops. It aims to achieve a lookup table-like behavior in which the values are calculated and pre-stored. Lookup table-like behavior prevents change in output due to state change. The functions are written in Haskell behave like mathematical functions. It goes to great lengths to achieve purity resulting in deterministic behavior. For example, see the following codeTake From

function main():
print assemble(3)
function assemble(x):
  var y = 0
  var s = “”
  do until (y > x)
    s += y++
  loop
  return s

and same logic implemented in Haskell

main = putStrLn $ assemble 3
assemble 0 = "0"
assemble y = assemble (y-1) ++ show y

Now it's not easy to appreciate Haskell's clarity unless we understand the type system. In the first code class, most of the languages accept any type of input to function assemble and try to convert to Int for adding and comparing. Haskell, does not allow writing codes without explicit type constraints. Haskell has a robust type system preventing many logical errors so common in languages like python and javascript. Even functions that have to subtract 2 from a given number can be a source of potential mistakes.

Now that's all good. But if there are no checks put in place, even the most saint of a person can become corrupt. Therefore all software is tested to ensure there is no error. The funny part is that it is some software that tests the other software, and the testing software can only generate fixed-size test scenarios. Therefore there is a need to reduce the surface required to test. The surface of testing is reduced by adopting functional programming. The added advantage is that it inherently helps in carrying out what is called "Formal Verification." Formal verification is an integral step in VLSI chip manufacturing. That is why there are very few "Bugs" in hardware chips. ( Have you ever come across any bug in the Intel I7 processor? now compare it with the number of bugs you had seen in Microsoft windows!!!.)

Formal Verification is the process of proving the correctness of systems (hardware/software/algorithms) using mathematical methods. Essentially it is used for ensuring, that something (be it a motherboard, a small chip, a library, a whole program, or an operating system) works as expected. For doing so, one formulates the “thing” mathematically, then formulates theorems that should or shouldn’t hold, and finally proves or disproves these theorems in mathematical ways.

When using the process in Software Development, it is often referred to as Formal Software Verification.

A pedestrian example is the square of an Integer is Integer. Math can prove this. We don't have to try all possible Integers and check that its square is an Integer. Programs that are written in functional languages can be formally verified. Unfortunately, a purely functional program will have no "so-called" side effects like any interaction with the outside world, like reading and writing into a network socket (used in the propagation of blocks in the blockchain). Therefore, Haskell has come up with very clever ways to permit side effects through strictly controlled mechanisms, preventing expansion of the surface required to carry out the testing.

These are some of the mechanisms which Cardano is using to ensure Secure Operation. Charles' Statement, "This is not to say that hacks won't happen on Cardano, it will! " is a fact. Still, comparative probability and the scale of impact on Risk Management are most likely to be astronomically minimal, compared to ad-hoc blockchains mushrooming out in the wild.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.