In 2017, Parity had set up wallet that enabled users to have multi-signature abilities for their wallet address.
In July 2017, a hacker noticed a vulnerability in how a Wallet contract calls it's Wallet Library contract. This was intentional and a common use of the delegateCall
function (more on that to come). What was uncommon was how the wallet library had a initWallet
method that wasn't internalized and allowed any user to effectively make themselves the owner of the Wallet.
The hacker exploited this and drained 153,037 ETH,~$30M in USD at the time (you can still see the ETH flowing around to different addresses here). At the same time, a group of white hat developers scrambled to protect the remaining wallets and froze ~513k ETH from 500+ wallets.
After the dust settled, post-mortems were performed by different teams, the Parity team applied a patch for the affected code, and, unbeknownst at the time, the redeployed contract contained related bug that remained in the code.
Later on in November 2017, a developer by the name "devops199" stumbled another critical issue when interacting with the same contracts. They claimed to being doing research on a post-mortem by Open Zeppelin on Hack #1. In the process, they unknowingly found a vulnerability that could call upon the Wallet Libary contract, assume ownership of it, and kill the Library that the Parity Multi-Sig wallets relied on.
Note: Hack #2 is a little more controversial than the first if for any reason because it is not clear what the intentions of the developer were. They could be identified as a naive user, a malicious actor, or a hacker like in the first hack.
In any case, this is the hack that I have chosen to replicate in the repository and can test. The end result was that the ownership of the Wallet Library was changed, instructed to self-destruct, and then effectively rendered the Wallet Contract (and all the funds in the Wallets) useless.
The smart contract developer community learned valuable lessons about security from both these hacks as they both involve the same code. We also got this classic quote:
Read below for more details.
The repository is a Hardhat based snapshot of before and after Hack #2 occurred at Block 4501736. It includes the following relevant files:
- parity.sol - a wallet contract that we can use to target the vulnerability
- parity.ts - a test that we can use to fork from a specific transaction in the blockchain's history. This is used to demonstrate how the wallet was functional before the wallet library was compromised, and then unable to withdraw funds immediately thereafter
- .env- users need to generate a environment variable file. It's only contents will include
ARCHIVE_URL=name_of_Alchemy_API_key
. This will be necessary to 1) run the tests from the historical placemark in the blockchain, and 2) impersonate a wallet contract address for the purpose of the test. To access an archive node, I suggest a free one from Alchemy which requires a simple sign-up.
Instructions:
- clone the repository
- install the dependencies with
npm install
- create an .env file (discussed above)
- run the test file in your terminal using
npx hardhat test test/parity.ts
- the output will describe the intended transaction behavior before and after the hack occurred
This test file highlights the severity of the exploit and how it impacted Wallet holders in real time. To understand the full extent of how the contracts were compromised, the Details_of_the_vulnerability section below outlines how the smart contracts were compromised.
Originally the multi-sig Wallet contract in question was written by Parity, audited by the Ethereum Foundation Dev team (as well as by other known solidity developers at the time), and had no known security vulnerabilities.
What is a multi-sig wallet? Generally speaking it is a wallet that needs more than 1 signer to approve a transaction. It can be a crucial wallet feature for organizations that want to eliminate key person risk. Read more here.
When Parity sought to upgrade their wallet using this contract and a new Wallet UI, the team introduced more than 4000 lines of code, however most of it front-end using Javascript, CSS, and HTML. Most of the changes were cosmetic, but when they merged changes, there were key solidity oversights. The solidity code affected by the merge was reviewed by a single reviewer, the written smart contract was deployed, and then a solidity expert reviewed the code later only to miss key vulnerabilities as detailed below.
Dissecting the Parity Hacks highlight how smart contract security is sometimes not as simple as a single bug found in code. In the case of Parity hacks, the contract vulnerability can be attributed to a confluence of programmer choices. It makes sense to start with the first hack and see how the contracts played out in the real world setting.
Starting with Parity Hack #1, let's consider the role of the fallback function. In lines 432 in our Parity.sol
Wallet contract, the following code gets us to the source of our issue:
function() payable {
// just being sent some cash?
if (msg.value > 0)
Deposit(msg.sender, msg.value);
else if (msg.data.length > 0)
_walletLibrary.delegatecall(msg.data);
}
There is an issue with this code. However, it is important to understand the role of a fallback function in solidity and the the official documentation here gives the following definition:
Fallback Functions: A contract can have exactly one unnamed function. This function cannot have arguments, cannot return anything and has to have external visibility. It is executed on a call to the contract if none of the other functions match the given function identifier (or if no data was supplied at all).
In the context of this Wallet contract, using fallback function was fairly standard. So what is the issue with using it? Well, from this fallback function, any user could use code from the Wallet contract to introduce changes to a completely different contract.
- in this else if statement, a user defaults to a
delegateCall
on Wallet Library. - later in the contract with lines 456, you can see that the
_walletLibrary
is hard coded to a specific address. At the time, Wallet contracts were coded to "0x863df6bfa4469f3ead0be8f9f2aae51c91a907b4".
Before critiquing the these issues, it is important to explore why we use delegateCalls.
DelegateCalls: At a high level, it is a call that "is identical to a message call apart from the fact that the code at the target address is executed in the context of the calling contract and msg.sender and msg.value do not change their values." They are foundational to programming solidity and are generally invoked when utilizing libraries. They benefit of library contracts in Ethereum are they promote code re-usability and gas efficiency.
In this case the delegateCall
was being used in a fairly standard fashion. However, caution with using the delegate call pertains more to the Wallet Library contract. Since you can call the Library directly, it is important that the Wallet Library contract have secure code.
This is where the delegateCall
within the Fallback Function becomes an issue. ...
Turning to the Wallet Library code, below is code for the initWallet
function:
// constructor - just pass on the owner array to the multiowned and
// the limit to daylimit
function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized {
initDaylimit(_daylimit);
initMultiowned(_owners, _required);
}
Calling the initWallet
function wouldn't normally be an issue because the initWallet
function on the Wallet contract was in the constructor which is available only at the creation of the contract. Therefore, the Wallet Library wouldn't be able to be called at an already created Wallet contract, right? The Wallet Library should be safe.
If you follow the initWallet
, initDaylimit
and initMultiowned
functions, you will see that none of them have explicit visibility written into the code. When there is nothing explicit assigned, the default is external which means anyone could call these functions. The solidity documentation defines the scope of external and internal visibility as such:
external External functions are part of the contract interface, which means they can be called from other contracts and via transactions. An external function f cannot be called internally (i.e. f() does not work, but this.f() works). External functions are sometimes more efficient when they receive large arrays of data, because the data is not copied from calldata to memory.
and
internal Those functions and state variables can only be accessed internally (i.e. from within the current contract or contracts deriving from it), without using this.
Here is where it all comes together and the problem starts to become clear. The Wallet Library contract had these functions that defaulted to external. This same contract could accessed by anyone from the Wallet contract. So any user could
- pass through data that doesn't match with the normally defined functions (in the Wallet contract),
- which invokes the fallback function to
delegateCall
to theinitWallet
method (in the Wallet Library contract) - which wasn't properly checked by the
isMultiowned
method - and then becomes the owner of the Wallet contract.
This is what the hacker with this address did with this transaction.
Sure, but, more importantly, you can become owner of the previously existing wallets.
Once this became known, this chapter in Ethereum lore was written. The hacker targeted 3 of the largest wallets that had ETH as well as other ERC20 tokens raised during ICOs. You can track the hacker in Etherscan as the original wallet they used is marked as "Multisig Exploit Hacker"
The hacker has diverted some of the ETH (and other tokens) over time and reporters still track their activity.
Just as important to the lore is that the Hacker didn't get to more wallets because a group of whitehat hackers stepped in to prevent the malicious hacker from doing any more damage. A group of Ethereum developers effectively teamed up to divert the funds from the remaining wallets to a Whitehat address. There was live tweeting from the whitehats, and even a Vice article on the escapades. In the race against time, a group of Ethereum Foundation developers and well meaning developers were able to use the same code as the hacker and safeguard the funds.
The code was patched by the Parity Lead (and Solidity Creator) Gavin Woods. There was speculation about whether he had written the original code, but he ended up submitting a change to the original code by adding a modifier called only_uninitialized
which was designed to prevent the initWallet
function from being called twice. Additionally, the initDaylimit
and initMultiowned
methods that were part of the initWallet
were changed to have internal visibility.
The Parity team made the fix and then redeployed the contracts. This appeared to work till November of the same yea.
Returning to the same logic from the initWallet
function in Hack #1, users were no longer able to get ownership of the Wallet contract. The original delegateCall
to the initWallet
now had a modifier that protected it from being invoked by just anyone. So the Wallet contract should be safe from hackers assuming ownership of it.
Parity Hack #2 isn't as much about a hacker as it is a curious developer. Without getting into labels or assuming their intent, a self-proclaimed "eth newbie" stumbled upon another vulnerability - any user could assume ownership of the Wallet Library. Keep in mind that Parity Hack #1 was about protecting the Wallet contract, but Hack #2 is about the Wallet Library contract that the delegateCall
was originally referring to.
During the redeploying of Wallet Library contract to fix Hack #1, most of the code was kept in tact during the patch. During the fix, ownership of the actual Wallet Library contract was overlooked. Within the contract's constructor, there is the initialization of the wallet. In the new contract, the code just needed to be initialized for the initWallet to be invoked
// constructor - just pass on the owner array to the multiowned and
// the limit to daylimit
function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized {
initDaylimit(_daylimit);
initMultiowned(_owners, _required);
}
So if a user called initWallet
, this didn't impact the existing Wallet contracts, but it did impact the Wallet Library contract and the isOwner
function:
function isOwner(address _addr) constant returns (bool) {
return m_ownerIndex[uint(_addr)] > 0;
If signature count was 0, this allowed the next person to call it to become the 1st and sole required owner. This confirmed by the confirmAndCheck
function:
function confirmAndCheck(bytes32 _operation) internal returns (bool) {
// determine what index the present sender is:
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return;
var pending = m_pending[_operation];
// if we're not yet working on this operation, switch over and reset the confirmation status.
if (pending.yetNeeded == 0) {
// reset count of confirmations needed.
pending.yetNeeded = m_required;
// reset which owners have confirmed (none) - set our bitmap to 0.
pending.ownersDone = 0;
pending.index = m_pendingIndex.length++;
m_pendingIndex[pending.index] = _operation;
}
Combine these conditions as laid out by the isOwner
and confirmAndCheck
with the fact that we have the initWallet
function in the constructor.
This contract is visible for everyone on etherscan in its previous state here.
Under these conditions the following transaction could take over the Wallet Library with the simplest of ABI code:
Function: initWallet(address[] _owners, uint256 _required, uint256 _daylimit)
MethodID: 0xe46dcfeb
[0]: 0000000000000000000000000000000000000000000000000000000000000060
[1]: 0000000000000000000000000000000000000000000000000000000000000000
[2]: 0000000000000000000000000000000000000000000000000000000000000000
[3]: 0000000000000000000000000000000000000000000000000000000000000001
[4]: 000000000000000000000000ae7168deb525862f4fee37d987a971b385b96952
...
After all was complete the question remains, what happens when someone assumes ownership of a Library contract?
At this point, the punchline is all too obvious. Soon after this developer, named "devops199" made themselves the owner of the Wallet Library contract, the invoked thekill
function which invoked the suicide
method (since renamed selfDestruct
). The only arguement was their address which keeps them the beneficiary address. The damage was done as this effectively froze all the funds in Parity Multi-Sig Wallet contracts. Without a Wallet Library contract, the Wallet contract were rendered useless.
Trying to make sense of what the developer had just done, they alerted the Parity team. The original thread is still on Github, although you'll notice the user name has been deleted is now labeled as a ghost.
Additionally, there are still screenshots of original threads where devops199 comes clean as an "eth newbie" that was trying to follow along with the Open Zeppelin post from Parity Hack #1.
The rest of this hack isn't technical in nature. Since the wallets were frozen and the contracts were rendered useless, there wasn't much that Parity could do to fix the issue. There was much speculation about the role in the developer as whether they were a hacker or an amateur. Some of the speculation turned into blame towards the Parity team as it became clear that this bug could have been identified (and fixed) after Parity Hack #1 when the team redeployed the contract.
In the months after, some Wallet holders pushed for there to be a fork in Ethereum blockchain in order to restore the funds that were frozen. A similar debate happened around the DAO hack which led to a successful Ethereum fork and a restoration of the DAO funds. However, there is no documented vote on whether Ethereum ever seriously considered forking the blockchain based on the events of Parity Hack #2.
At the time of this writing, both hacks server as learning resources for smart contract developers about:
- using explicit visibility when it comes to library functions
- the code that patched up the contract after Parity Hack #1 simply added the
internal
parameter to 2 of the functions (and added a modifier to a 3rd function) that would have effectively removed this vulnerability
- the code that patched up the contract after Parity Hack #1 simply added the
- being deliberate of what a
delegateCall
can do to the contracts that it invokes- this is a little more broad concern, but there is a careful tradeoff here. While most contracts can benefit from the efficiency of using
delegateCall
, almost all the vulnerabilities from these 2 hacks stem from behavior between using this call between the Wallet and Wallet Library contracts
- this is a little more broad concern, but there is a careful tradeoff here. While most contracts can benefit from the efficiency of using
- creating libraries that allow itself state-modifying functions
- Solidity explicitly calls this out in their library documentation and in recent updates has added opscode to inspect this [behavior].(https://docs.soliditylang.org/en/develop/contracts.html#call-protection)
- Hack # 1
- The Multi-sig Hack: A Postmortem by Parity
- The Parity Wallet Hack Explained by Open Zeppelin
- An In-Depth Look at the Parity Multisig Bug by Lorenz Breidenbach, Phil Daian, Ari Juels, and Emin Gün Sirer
- How not to destroy millions of smart contracts part 2 by Omer Goldberg
- How Coders Hacked Back to ‘Rescue’ USD208 Million in Ethereum by Jordan Pearson
- Hack # 2
- Security Alert by Parity
- The Parity Wallet Hack Reloaded by Open Zeppelin
- Parity Hack: How It Happened, And Its Aftermath by Christopher Durr
- Bug that deleted USD 300m could have been fixed months ago by Naked Security
- Replaying Ethereum Hacks Introduction by Christoph Michel
- Parity Wallet Hack 2: Electric Boogaloo by Matt Condon
- Another Parity Wallet hack explained by Sergey Petrov