Insecurity of split contracts

Dmitry Khovratovich
4 min readJul 21, 2016

--

The just happened hard fork raised an important question of how to deal with money on private accounts and contracts, which are still valid in both branches. For instance, we may want to send money from our account to another account or contract A only in the new (HF) branch and keep the money in the original branch intact.

There is a couple ways to do that. First, we recall that every transaction must come with an incremented counter value (nonce); a transaction with nonce being larger or smaller than the number of previously sent transactions is discarded. Secondly, we may create a proxy contract that forwards money to the recipient but only in a certain branch. This contract is also called a split contract. It can, for instance, be used to split the money of user’s account between branches so that operations on user’s money in distinct branches are completely independent.

Several such contracts have been proposed so far, and we have found some of them too complicated and even insecure. The first problem comes from identifying in which branch we are. Since the entire purpose of the hard fork is too deal with the DAO heist, it might sound reasonable to check the balance of the WithdrawDAO account, which is fixed differently in both branches. However, this is not a bulletproof solution. Indeed, the balance of any account in any branch can be updated in the future for whatever reason, and the seemingly secure split method will stop being so. We personally believe that the WithdrawDAO balance will go below 2M ether soon.

Buterin’s contract (0xb671c3883307cf05bb3dff77a9754e87b4347195)

Another problem is the possible outcomes of the contract. Let us look at the split contract HFConditionalTransfer proposed by Buterin. It detects in which branch it is by looking into external account’s balance (note that it costs the caller extra gas). Then it sends the money to the recipient or back to the sender depending on this check. Note that there is no failure check in the contract. For instance, the send() call may fail, which would leave all money in the possession of the contract. Worse enough, there will be no way to withdraw the money after the failure, as the contract can not send its money to anyone. The same happens if the branch is wrong but the send-to-the-sender fails. Therefore, there are at least three outcomes of the contract call: move money to the recipient, return to the sender, and acquire. By the time of writing, the split contract by Buterin has already acquired 0.08 ETH.

The idea of sending back the money in the classic branch seems insecure too. Consider a multiple-wallet contract, which holds ethers of several users and is responsive to the commands of its shareholders. Suppose that shareholder A wants to send money to some recipient B in the hard fork only via HFConditionalTransfer calling its transferIfHF. In the HF branch the transfer will be fine, but in the classic branch the money will be sent back to the wallet contract, which can not determine whose they are, and will assign them to HFConditionalTransfer’s account. This is the fourth possible outcome of Buterin’s contract.

An alternative approach by Timon Rapp is just the oracle contract AmIOnTheFork with a single public variable ‘forked’ (see below). The contract was deployed before the fork, and has to be called within the first 1200 blocks after the fork. The update() function checks the balance of the darkDAO account and sets the fork flag to ‘true’ if it is too low. An external contract is supposed to ch

The intention was clearly to update the contract before the darkDAO balance can be reduced in the classic branch. However, this again assumes the security of the DAO contract. If the hacker finds a way to reduce the balance of darkDAO before the block 1921200, he can call update() and the contract becomes useless. Worse enough, calling update() earlier does not help: the function can be called (and the variable can be updated) multiple times, so before the block 1921200 there is always a chance the contract becomes obsolete. Currently the classic mining rate dropped 5–7-fold, so the DAO hacker has plenty of time to find a way to reduce the balance of the darkDAO.

We suggest a more careful approach. First, we use the mere fact of the chain differences as the distinctive property. Recall that the Ethereum language has limited (but sufficient for our purpose) functionality of working with blockchain itself. For instance, it can look up the hash of any last 256 blocks using the blockhash() command. The contract BranchSender (see below) at the time of creation checks if in the current branch the block with the given number has the given hash and updates the internal variable IsRightBranch accordingly. In reality it checked the block with number 1920000 against the hard fork hash. This contract was deployed in the HF branch in the block 1920012 and got the variable assigned with ‘true’, whereas the same contract in the (still alive) classic branch got ‘false’ in the block 1920160.

Secondly, the special care was taken to limit possible outcomes. In the case of any failure the contract throws, which guarantees that the transaction does not finalize. Thus there is no way for the contract to acquire the money. This applies also to the default function, which always throws.

Vladimirov’s contract (0x23141df767233776f7cbbec497800ddedaa4c684)

The contract BranchSender sends money in the hard fork only; this is sufficient to split the account between them. To make the classic-only send, another copy of this contract should be created with the constructor taking the value of any recent ``classic’’ block.

We have also prepared a simple script that uses the contract and splits the account for the user. It will be ready to use as soon as the classic branch accepts the contract too. It is sufficient to insert the account number in the following command:

Write secure contracts and prosper.

Dmitry Khovratovich,

Mikhail Vladimirov

--

--