What can be the consequences of errors in contracts? One of them is the loss of funds, as some Parity wallet users had a painful experience in July this year. Blockchain does not forget, what has occurred in it cannot be revoked. This is its undoubted advantage, which in certain situations becomes a disadvantage. The contract is governed by the rules as implemented in it. These are the only ones we can use to save our funds if there is a suspicion that they are no longer safe in the contract. Even if the rules are wrong they are still rules in the contract.
Parity hole
Parity is one of the client implementations along with the wallet contract for Ethereum. In a previous post I discussed another wallet implementation which is Ethereum Wallet.
In simple terms, the Parity wallet implementation consists of two contracts. One of them acts as the WalletLibrary
, which provides an implementation of the functions used by the Wallet
contract. Such a technique is used in order not to incur high costs when creating instances of contracts, if some of their code can be shared. The library is created in the blockchain only once, wallet instances will be many.
The WalletLibrary
library provides, among other things, the initWallet
function, which was called from the wallet constructor, and the changeOwner
function, which is used to change the owner, and was called from the changeOwner
method. Below I have provided a simplified implementation of this part of the Parity contract.
1contract WalletLibrary { 2 address public owner; 3 4 // funkcja wołana przy tworzeniu portfela 5 function initWallet(address _owner) { 6 owner = _owner; 7 // ... 8 } 9 10 // zmiana właściciela portfela 11 function changeOwner(address _new_owner) external { 12 // sprawdzamy czy funkcję wywołuje obecny właściciel 13 if (msg.sender == owner) { 14 owner = _new_owner; 15 } 16 } 17} 18 19contract Wallet { 20 address public _walletLibrary; 21 address public owner; 22 23 // konstruktor portfela, woła initWallet 24 function Wallet(address _owner) { 25 // ... 26 _walletLibrary.delegatecall(bytes4(sha3("initWallet(address)")), _owner); 27 } 28 29 // zmiana właściciela portfela 30 function changeOwner(address _new_owner) { 31 _walletLibrary.delegatecall(bytes4(sha3("changeOwner(address)")), _new_owner); 32 } 33 34 // funkcja awaryjna, wołana w razie braku możliwości dopasowania innej funkcji 35 function () payable { 36 _walletLibrary.delegatecall(msg.data); 37 } 38}
At this point it is necessary to explain how delegatecall
works. Calling a function of another contract from the contract level using delegatecall
causes code execution for the calling contract. This means that a delegatecall
to the changeOwner
function will actually modify the owner
field of a specific Wallet
contract instance and not the owner
field in WalletLibrary
. This is sensible behavior, after all, all wallets do not share a common owner.
The above implementation performs a delegatecall
from the constructor to the initWallet
function and from changeOwner
to the corresponding function in the library. Note that the initWallet
function changes the owner of the contract unconditionally. This seems correct, in the constructor we just create the contract, so it will be the first owner. The constructor can only be called once. The changeOwner
function verifies that it is the current owner who calls it, and only then allows the new address to be set.
Inside the Ethereum Virtual Machine, calling a contract function from another contract involves calculating its signature and passing (pasting to the signature) the parameter values. The signature of a function is four bytes from the abbreviation of its name along with the types of parameters. It is calculated by the contract from the string. In the above example for initWallet(address)
it will be 9da8be21
. So such a signature can be calculated independently outside the contract. In the presented contract, is there any way to call the initWallet
function when the contract is already created? Let's look at the implementation of the unnamed function, which is a so-called fallback function called when no other function can be matched. It performs a delegatecall
by passing as a parameter the data passed in the transaction. Bingo! So, all you need to do is to bring the fallback function to a call with the signature initWallet(address)
passing the new owner's address of your choice as a parameter. This worked because the Wallet
contract didn't have a function implementation with the signature initWallet(address)
, so the call would go to the fallback function and be passed to the WalletLibrary
, after which it would execute unimpeded for the calling contract.
The bug in the Parity contract was that the ability to call the initWallet
function was not limited to the case of the new wallet creation process only. It was used by attackers to move Ether out of existing contracts. Losses reached tens of millions of dollars, and would have been even greater had the good hackers not reacted in time to secure funds from the erroneous contracts by using the same attack. The difference was that they later returned the diverted funds to the rightful owners of the contracts.
The fix in the Parity contract additionally included functions that unconditionally modify the owner's address. They should be internal
functions, which means there is no way to call them from outside the contract. It is important to remember that in Solidity, by default, functions have public
status.
The patch in the Parity portfolio that eliminated these bugs can be seen at this link. To quote some of the comments: internal
worth $30 million.
Summary
The above example shows how much care must be taken when implementing contracts in Solidity and using the prepared code as their user. The nuances of the Solidity language and EVM can have dire consequences for contracts and their owners. Many tools are being developed to help analyze contract code. Various techniques, such as a daily payout limit, are also being used to minimize the impact of exploiting potential holes. Many clever attacks are yet to come. Certainly one way to avoid them is to learn by implementing various contracts and analyze them in your own Ethereum network.