In part 1 of this series, we learned about the Solidity programming language, smart contract primers and looked at some of the basic hacks and vulnerabilities exploited to drain funds out fo smart contracts. In this article, we’ll take this knowledge a step further by examining real-world hacks and walking through them.
So what happened here?
Parity had a library called WalletLibrary. It was deployed to the Ethereum network and reused by the multisig wallets smart contracts, so that people wouldn’t spend too much gas redeploying the library all the time. This wallet library had nice modifiers, onlyOwner and onlyManyOwners. What’s important to notice is that all these modifiers used state variables to see who is the owner, how many confirmations there are etc. Look at the two snippets below.
Then we had this interesting and suspicious function called initWallet that was used to initialize the wallets in the constructor. The argument of this function is an array of owners required which represents the number of owners needs for confirmation of transaction. What stands out here? This function has no visibility explicitly stated!
Wait but maybe initDayLimit or initMultiOwned has some type of protection or modifier?
No, they don’t have visibility defined either! So basically initWallet can be called by anyone!!!!
But, wait … until this point we have examined the WalletLibrary. This library is utilized by the Wallet contract, it’s not actually a part of it. So maybe if the Wallet contract is fine this won’t be a problem? Let’s take a look at the Wallet Contract.
You can see that in the Wallet we had a Wallet Library state variable and we have those delegateCalls.
Aside: delegateCalls accepts parameters which are encoded msg data, encoded name of a function and encoded params.
This is exactly what happened.
Focusing on the fallback function with a payable modifier, we see that it is public. We can notice that the 3 delegate calls above are all from public methods! This makes sense because we want anyone to be able to call it. But because the rest of the functions were also public an attacker could invoke delegateCall with encoded data that represents the initWallet function name, data and expected parameters, and set themselves as owner! This is exactly what happened. The attacker exploited this and simply changed the contract’s m_owners state variable to a list containing only their addresses and requiring just one confirmation to execute any transaction.
Parity developers did two things.
Here is a link to the patch deployed to fix this hack. The comment thread is interesting!
There is a member called creator which is initialized in the constructor. We have the onlyOwner modifier that will only execute code if you are the proper owner. Additionally, we have ways to collect fees based on this modifier.
The original contract name and constructor was DynamicPyramid.
The creators had a change of heart and decided that perhaps something with the word Pyramid would not market well. They changed the name to Rubixi and forgot to change the name of the constructor method ;) Because the DynamicPyramid function was public, anyone could call it and set themselves are the contract creator! There was 1 contract where 100 Ether was lost and another where ~0.1Ether was lost.
We have a smart contract called Bank. Each user has balances, and users can deposit funds which will immediately update the state variable with how many funds are available. We can use the withdraw function to take out funds, and then these funds are sent to us.
First, it is important to note that assert is a blocking synchronous call. Therefore, the balance will not get updated until the assert statement is completed.
This works well if the depositing entity is a user, but what happens if it is a smart contract? If the entity that deposited funds is a smart contract and not a user, then sending those funds to the smart contract will trigger a payable fallback function (if it exists or fails). This fallback function will go back to the bank and do another withdrawal before the original transaction is complete. The bank contract guards against this by updating the balance, but this is only after the potential attacker can make two withdrawals. This type of attack (bug?) is called reentrance.
Let’s look at an example of a malicious smart contract called Robber which will exploit the Bank smart contract via a reentrancy bug.
We can only do this twice per contract. But if it works we can create and deploy many contracts and do this repetitively until we drain the bank!
Mitigation #1: Checks-Effects-Interactions Pattern
By utilizing this pattern we can ensure that even if there is a reentrance issue, the check will fail. So following this pattern the fix for our Bank contract will be first effecting state variables — in this case, the balance, and then calling to the smart contract/user:
In Ethereum there are 3 ways to interact with other smart contracts implicitly. When sending Ether, we should be aware of the relative tradeoffs between the use of them:
The community responded by splitting into two, aka hard forking! Those who believed that the hack was legit (code is law) stayed on the network and came to be known as Ethereum Classic. The forkers formed a new network where they restored all the lost money and became known as (plain-old) Ethereum.
Here we have an Asset smart contract. This is a toy example to demonstrate the concept so bear with me ;) Originally the creator of the Asset contract is the owner of this asset. Anyone can send funds to this contract. If someone outbids the amount of funds that reside in this smart contract, then you become the owner of this asset and receive all the funds of the asset.
What are the cases where the if statement can hold true?
This will never be true! This is because the balance is updated with the value before this code is ever reached. This type of contract had 2 version, where the first one drained 20 Ether and the second drained 5 Ether, from people who bid
Until now we’ve examined explicit methods for transferring Ether. Let’s look at some options for transfers which are less known / popular.
In addition to the regular means to send Ether (e.g. call, send/transfer), there are two more ways which bypass the fallback function.
Let’s see an example. Every time someone sends funds to the GenerousAttacker contract we will selfdestruct and send the funds to an address determined in the GenerousAttacker constructor.
So every time a user transfers money to this contract, the value stored in the contract is transferred to a pre determined target, without explicitly transferring. Beware! Without knowing the language thoroughly you could read through this and think that it is innocent!
Somebody opened a Github issue with the Parity wallet, and said “hey I accidentally killed it”. Essentially he figured out a way to delete the smart contract. Suddenly, their were many wallets that were using a library that could be deleted and cease to exist!
If you recall we had this only_unitialized modifier. You can see the library uses many state variables. But where are these state variables declared and initialized?
We search all the contracts used to create the parity wallet and find that these state variables are defined in the Wallet Contract.
The WalletLibrary contract contains state variables that it expects to be shadowed by the calling contract’s own state.
Once deployed, the WalletLibrary contract is simply uninitialized, so m_numOwners is 0.
A developer invoked initWallet and set himself as the only owner, and then proceeded to kill it by invoking the kill function. The developer who found this bug had a Github handle of devops199 and then tweeted this
So a user can make an Auction bid, and if it’s the highest they get the funds. But what happens if the transfer methods fails?
Imagine, just as the auction starts we make a cheap bid and become owners. Then, if someone outbids us and we somehow make the transfer fail then we become the Auction leaders forever but still hold on the new highest bid! So let’s take a look at how this would work. Let’s simply create a contract that doesn’t implement a payable fallback function, thus making it impossible to send us funds.
We can additionally mitigate this by implementing a refund mechanism. Essentially, we will hold a map of the funds that each user contributed and implement a withdraw function that allows everyone to request their funds back.
Smart contract is a term that is often thrown around synonymously with trust less environments. Although smart contracts remove middlemen, and centralized contract enforcers, these hacks prove that there is indeed an element of trust still involved. Instead of trusting a corporation or a fleet of lawyers, we lay our trust in code and the developers who write and audit these contracts.
Although the code is error-prone, I believe that the smart contract community will continue to improve their proficiency in writing smart contracts. Developers are becoming more experienced, learning from past mistakes, and better tools are constantly being developed to aid in analyzing contract logic/testing. I’m excited by the future smart contracts hold and believe they will grow in popularity in the coming years.
My next posts will explore smart contracts and distributed application development (dapps), so stay tuned!
If you’re looking for resources to ramp up on the blockchain world, I highly recommend (again) checking out the Kin Ecosystem Youtube channel, as it has a wonderful inventory of high quality, technical discussions and lectures. Huge thanks to Leonid Beder for building this lecture and teaching it at Blockchain Academy!