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.
Example #1: Parity “hack” #1
- TL;DR — A vulnerability was found on the Parity Multisig Wallet version 1.5+, that allowed an attacker to steal over 150,000 Ether ($30,000,000 at the time, $105,000,000 today ETH/USD 700).
- Gavin Woods, one of the original Solidity developers was the CTO of Parity. He actually wrote the code that we are going to examine.
- One of the things that Parity provided to its users is multisig. Multisig is a wallet with M owners, and it requires N out of M signatures (confirmations) to do something with these funds. Basically, Parity offered “custodians” out of the box.
- A malicious hacker was able to target a specific multisig wallet, and steal the above mentioned amount. The attacker could have stolen more, if it was not for a white hacker group. The white hackers drained all the wallets they could find with this exploit so that the malicious hacker couldn’t get his hands on more funds. A couple of weeks later the white hackers returned all the funds they had drained. The white hackers saved ~$264 million dollars in todays terms.
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.
- Complexity is a vulnerability. Keep it Simple Stupid. Over optimisation and complexity is the root of all evil!
- Always define visibility explicitly.
- Don’t extract the constructor logic into the library contract. Avoid premature optimisations!
- Don’t use delegateCall as a catch-all forwarding mechanism.
Mitigation: Parity Developers’ Fix:
Parity developers did two things.
- They declard initDayLimit and initMultiowned to be internal.
- They added a modifier called only_uninitialiszed that said if I already have owners, then revert. They added this modifier to initWallet, so essentially it could not be called twice. It looks good… But there is a huge bug! We’ll revisit this later!
Here is a link to the patch deployed to fix this hack. The comment thread is interesting!
Example #2: Rubixi
- Rubixi is a contract which its implementation is reminiscent of a pyramid scheme allegedly. (Although it is not a pyramid scheme!)
- Investors can deposit funds.
- The owner can collect all of the funds.
- If you bring more people to join this smart contract you get part of their fees.
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.
So what’s wrong here?!
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.
- Well, for starters, try not to misname functions…
- Stay vigilant!! The scammers are getting better and better.
- Starting from 0.4.22, you can now use the safe constructor method instead. This means you can do the following instead.
Example #3: Bank, Smart Contract (Reentrance)
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.
What’s the problem?
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
- Perform checks (who called the function, are the arguments in range, did they send enough Ether, does the person have tokens, etc.).
- If all checks passed, effects to the state variables of the current contract should be made.
- Lastly, perform any interaction with other accounts/contracts. Just to make the example clearer, this is where our contract failed. It interacted with the Robber smart contract before it completed effecting all state variables.
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:
Mitigation #2: Avoid call.value()()
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:
- address.call.value(): will send the provided Ether and trigger code execution given all available gas. So, in Bank / Robber example, if the user / smart contract sent enough gas he could complete executing the reentrance.
- address.send(): will send the provided Ether and trigger code execution given a limited stipend of 2,300 gas. This is similar yet the gas is capped and is enough for basically always receiving funds. Anything more complicated would fail with an Out of Gas exception.
- address.transfer(): is equivalent to require(address.send()). It will automatically revert if the send fails.
Example #4: “The DAO”
- The “DAO” is the name of a particular DAO (Decentralized Autonomous Organization), conceived of and programmed by the team behind German start Slock.it, a company building “smart locks” that let people share their things (cars, boats, apartments) in a decentralized version of Airbnb.
- It would as a decentralized venture capital fund to finance DApps (decentralized applications) wherein participants could vote to determine which DApps received funding.
- It was launched on April 30th, 2016, with a 28-day funding window.
- It was the largest crowdfunding in history, having raised over $150,000,000 (with today's prices it is a couple of billions) from more than 11,000 enthusiastic members.
- On June 18th, 2016 the attacker started to drain “The DAO” using a relatively sophisticated reentrancy attack.
- The attacker has managed to drain more than 3,600,000 Ether ($72,000,000 at the time; astounding $2,520,000,000 today ETH/USD 700)
- How did the Ethereum community respond?
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.
Example #5: Honeypot
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
‘’Alternative” Ether Transfers
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.
- selfdestruct: The only possibility that code is removed from the blockchain is when a contract at the address performs the selfdestruct operation (previously called suicide). This is incentive to implement a self destruct call to remove contracts from the network that are no longer useful. selfdestruct receives an address parameter which specifies where to move the funds of the contract to be destroyed.
- If the receiving address is a contract, its fallback function does not get executed. This is a weird corner case/design choice in Solidity.
- As a miner, set the target address as the coinbase address in order for it to receive block mining rewards.
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!
Mitigation: Beware of Assumptions
- Never use a contract’s balance as a guard
- In general, be mindful of language/framework specific features and updates.
- Beware of compiler optimizations and bugs and test accordingly.
- Beware of compiler specific bugs and always use strict compiler version.
- Beware of potential miners’ intervention (eg. front-running, chain re-org, etc).
Example #6: Parity “hack” #2
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!
- Approximately 513,000 ETH had been locked in the affected contracts.
- No funds were “stolen”; only made unreachable, by an accident.
- There are a few proposals for methods to restore the lost funds, and even for a new governance model, but it’s unlikely to happen any time soon.
So what happened here?
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.
- If the WalletLibrary isn’t executed in a Wallet contract’s context, m_numOwners is 0, allowing anyone to call methods that this modifier guards, one of which is initWallet.
How was this exploited?
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
Example #7: Auction Contract
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.
Mitigation #1: Favor Pull over Push
- Always remember that you’re not only interacting with human beings, but also with other contracts.
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.
Mitigation #2: Ignore Contracts
- It's usually not recommended or desired, but it’s also possible to opt-out from interacting with the contract using the following check:
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!