Skip to content

Instantly share code, notes, and snippets.

@5chdn
Created December 7, 2017 12:04
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save 5chdn/a9bb8617cc8523a030126a3d1c60baf3 to your computer and use it in GitHub Desktop.
Save 5chdn/a9bb8617cc8523a030126a3d1c60baf3 to your computer and use it in GitHub Desktop.
Contract revival EIP specification drafts

Contract revival EIP specification drafts

Motivation

The motivation behind this EIP in all variants is to help people whose funds were blocked in Parity's multi-signature wallets due to the destruction of the library contract holding all wallet logic. However, it is worth noticing that this problem is not the first of its kind. @mjmau noticed in the EIP 156 discussion in August that over 3500 ETH is stuck in roughly 300 smart contracts without any code. Currently, there are more than 5000. The actual number of stuck ether may be much higher if we take into account all ether which was lost due to replay attacks where "a contract was created on ETC, funds sent on ETH but the contract not created on ETH" (quote from EIP 156).

Proposal drafts

In the following sections, four potential variants of this contract revival proposal will be introduced. Some points for an initial discussion are appended to the proposal drafts.

Proposal Variant A - createat builtin:

We introduce a new builtin createat which works precisely like create-transaction, but it has an additional parameter, which specifies a nonce used to create a contract address.

There are two additional parameters. The parameters are prepended to the data sent to the builtin. Both of those parameters are integers aligned to 32 bytes. The first one represents a nonce that should be used when computing the contract address. The second one is the initial_nonce that the new, derivative contract begins on.

Instead of computing the contract address being created as in create-transaction as:

keccak256(SENDER ++ SENDERS_NONCE) % 2**160

We compute it as :

keccak256(SENDER ++ nonce) % 2**160

The createat call succeeds if:

  • There is no code under ADDRESS
  • The nonce is < SENDERS_NONCE
  • Any regular create-transaction with the same set of parameters would succeed.

The second additional parameter initial_nonce is useful when a contract deploying another contract has been suicided and recreated. Suicided contracts have their nonces reset, so by giving them initial_nonce, no additional logic needs to be written to make their CREATE opcode always work.

Proposal Variant B - claim builtin:

Introduce a new builtin claim which accepts three parameters. nonce, initial_nonce, and transaction_data (same as if it was passed to create-transaction). nonce and initial_nonce are integers aligned to 32 bytes.

This builtin creates two smart contracts:

  1. A proxy contract at address keccak(SENDER ++ nonce) % 2**160. This contract is locked for 500_000 blocks (to be discussed). The proxy contract nonce is set to initial_nonce.

    pragma solidity ^0.4.19;
    
    contract Claim {
    
        // these variables are set directly in a contract source
        uint constant claim_block_number = block.number + 500000;
        address constant destination = 0x00d35100000000000000000000000000000000;
    
        function () public {
            if (block.number > claim_block_number) {
                require(destination.delegatecall(msg.data));
            }
        }
    }
    
  2. A target contract at address keccak(SENDER ++ SENDER_NONCE) % 2**160. Contract code is created according to the same rules as if it was created using create-transaction with data equal transaction_data.

The claim call succeeds if:

  • Call-data length is equal or longer than 64 bytes
  • There is no code under keccak(SENDER ++ nonce) % 2**160
  • The nonce is < SENDERS_NONCE
  • And regular create-transaction with the same transaction_data would succeed.

Proposal Variant C - proxy builtin:

Introduce new builtin proxy which accepts three parameters: nonce, initial_nonce, and destination. The builtin works in a very similar way to the claim builtin; the main difference, however, is the code of proxy contract.

When called, the builtin transfers all funds from address keccak(SENDER ++ nonce) % 2**160 to SENDER and then it creates two smart contracts:

  1. A proxy contract at address keccak(SENDER ++ nonce) % 2**160. This contract always behaves as if it was killed, until the caller of this contract agrees to it by calling setupProxyForContract or setupProxyForSelf on the ProxyState contract:

    pragma solidity ^0.4.19;
    
    contract Proxy {
    
        // these variables are set directly in a contract source
        address constant state = 0x0051413000000000000000000000000000000000;
        address constant destination = 0x00d35100000000000000000000000000000000;
    
        // the contract does not have payable modifier, so it cannot accept any ether
        function () public {
            ProxyState(state).isProxySetup(msg.sender).delegatecall(msg.data);
        }
    }
    
  2. The proxy state contract at the address keccak(SENDER ++ SENDER_NONCE) % 2**160.

    pragma solidity ^0.4.19;
    
    contract ProxyState {
        mapping (address => bool) private proxy;
    
        function setupProxyForSelf() public {
            proxy[msg.sender] = true;
        }
    
        // please note the usage of tx.origin
        function setupProxyForContract(uint nonce) public {
            proxy[address(keccak256(msg.sender, nonce))] = true;
        }
    
        function isProxySetup(address a) public view returns(bool) {
            return proxy[a];
        }
    }
    
    

The proxy call succeeds if:

  • Call data length is equal to 96 bytes
  • There is no code under keccak(SENDER ++ nonce) % 2**160
  • The nonce is < SENDERS_NONCE.

Proposal Variant D - multi-proxy builtin:

We introduce a new builtin multi-proxy which accepts two parameters: address and nonce. The builtin works in a very similar way as proxy builtin, but it allows everyone to set their destination of the proxy.

When called, the builtin transfers all funds from address(address, nonce) to the creator of this address and then it creates two smart contracts:

  1. A proxy contract at address address(address, nonce) % 2 ** 160. This contract always behaves as if it was killed until the caller of this contract sets up the proxy by calling setupProxyForContract or setupProxyForSelf on the ProxyState contract:

    pragma solidity ^0.4.19;
    
    // contract deployed at address
    contract Proxy {
    
        // this variable is set directly in the contract source
        address constant state = 0x0051413000000000000000000000000000000000;
    
        // proxy function, proxies all calls from sender
        function () public {
            ProxyState(state).getProxyAddress(msg.sender).delegatecall(msg.data);
        }
    }
    
  2. A proxy state contract at address keccak(SENDER ++ SENDER_NONCE) % 2**160:

    pragma solidity ^0.4.19;
    
    // contract deployed at address(msg.sender, nonce(msg.sender))
    contract ProxyState {
        mapping (address => address) private proxy;
    
        function setupProxyForSelf(address destination) public {
            proxy[msg.sender] = destination;
        }
    
        function setupProxyForContract(uint nonce, address destination) public {
            proxy[address(keccak256(msg.sender, nonce))] = destination;
        }
    
        function getProxyAddress(address a) public view returns(address) {
            return proxy[a];
        }
    }
    

The multi-proxy call succeeds if:

  • Call-data length is equal to 32 bytes
  • nonce(address) >= nonce
  • There is no code under address.

Consequences

At present there are two ways a contract can alter how it handles a transaction: it can call another contract (particularly with DELEGATECALL) with a dynamically determined address, or it can SUICIDE. If no such operation is present in its code, then it will never change.

The same is true with these EIP drafts. The only difference is the SUICIDE opcode:

If there is a SUICIDE opcode, then there is a possibility that, according to the logic surrounding it, the code will have changed by the time the transaction is executed. Before the consideration of these EIP drafts, SUICIDE would have meant the absolute loss of anything sent into its custody that required any action on its part for access, in particular ETH, but also ERC20 tokens, badges, etc. With these EIP drafts, there is a possibility to attach new code to the address, and thereby potentially claim ownership over any such assets rather than leave them in limbo forever. Strictly for the sender, the difference is negligible: they lose the asset in both cases.

Discussion

Each version of the proposal has different advantages and disadvantages that will be discussed in the following paragraphs.

Proposal Variant A - createat builtin:

Pros:

  • The creator of the killed smart contract at 0xX can fix it by redeploying new smart contract at the same address.
  • This proposal variant introduces only a low level of complexity.

Cons:

  • If the creator of the smart contract at 0xX has malicious intentions and has the ability to kill the smart contract, he can replace it with malicious code.
  • Currently, the most significant risk of using contracts with selfdestruct is having the funds locked in contracts referencing to them. With this proposal variant, someone may additionally take control over those locked funds.
  • This requires a creator of the killed contract at 0xX to send a transaction to rescue the funds.
  • This changes EVM semantics.

In real-world applications, the user should never trust and use a contract which can be killed by anyone but himself. Simply because, by doing this, he gives someone an ability to lock his assets. But even if those funds become locked, let's see if it's possible to reduce the risk of someone taking control of them.

Proposal Variant B - claim builtin:

Pros:

  • The creator of the killed smart contract at 0xX can fix it by redeploying new smart contract at the same address.
  • Even if a creator of the contract redeploys malicious code at 0xX, for some period (e.g., 500_000 blocks or ~90 days), the contract works as if it was killed. During this time, the user can review the code himself and decide whether he wants to use it.

Cons:

  • This requires a creator of the killed contract at 0xX to send a transaction to rescue the funds.
  • If the contract at 0xX is replaced by a malicious contract, funds will be still locked.
  • This introduces additional complexity.
  • This changes EVM semantics.

Another version of this proposal (C) is also trying to reduce the risk of losing control over funds. It requires the user to agree to the new code of contract. If he doesn't accept this, the code works as if it was killed.

Proposal Variant C - proxy builtin:

Pros:

  • Even if a creator of a contract at 0xX redeploys malicious code, the user has to agree to use those changes.

Cons:

  • A user can setup the proxy for himself and the contracts he created, but creation does not equal ownership.
  • At least two parties need to cooperate to unlock the funds from locked the contract that is: a creator of killed smart contract at 0xX, and a creator of a contract which uses contract 0xX.
  • If the contract at 0xX is replaced by a malicious contract, funds are still locked.
  • This introduces additional complexity.
  • This changes EVM semantics.

Again, version C is addressing one problem but introducing another. Additionally, it requires the trust of the other party. Variant D is trying to simplify this version of the proposal by removing a dependency on the creator of the killed contract.

Proposal Variant D - multi-proxy builtin:

Pros:

  • Anyone can create a proxy at the address of the killed contract. By default, the proxy behaves as if it was a destructed contract.
  • The proxy destination can be set per user. A malicious contract creator cannot harm anyone.

Cons:

  • A user can setup the proxy for himself and the contracts he created, but creation does not grant ownership.
  • This introduces additional complexity.
  • This changes EVM semantics.

The proposal variant D is the only proposal, where nothing depends on the creator of the killed contract. At the same time, it gives a lot of power to the creator of a contract using the killed contract. This creator may be at the same time the owner of the contract but doesn't have to be.

Rationale

This proposal is both a rescue and a technical improvement to the protocol. We need to consider how much has already been lost, and how much will be lost in the future. (quote).

Feedback, critics, and suggestions very welcome. Discuss it either here on Github, on Reddit, or send a mail to community@parity.io to get in touch.

@gcolvin
Copy link

gcolvin commented Dec 12, 2017

@Arachnid @5chdn @marek On first read Nick makes some good points. I couldn't always tell which variation which critique applied to, but the only one that really bothered me (and I don't understand it well) is the issue of new contracts misusing approvals held by the old contract. I'd also like to see more analysis of how existing contracts would be affected.

@fcecin
Copy link

fcecin commented Dec 15, 2017

Parity could also hard fork the Ethereum project itself and add all the missing governance. A simple referendum system, where balances vote on administrative (arbitrary) transforms to the state, and the biggest vote wins after e.g. 30 days, would cleanly solve all issues where the majority has a stake in fixing it. Then, only thefts and bugs with minor support (neither the case of the DAO theft nor the Parity losses) would require those affected to push technical improvements or actual forks.

@branciard
Copy link

I was thinking of existing contracts impacted by this contract revival proposal: stuck omise go token on suicide multi sig.

The story :
On july, with the initWallet parity vulnerability exploitation, WHG (thanks to them again!) rescue fonds of hundred of multi sig wallets. Then WHG call suicide on those rescued wallets. OmiseGo airdrop omg token to those suicided multi sig addresses. (wrong block timing choice of the eth getBalance i suppose).

To analyse the impact of the above proposals, we can ask ourself three questions :

1- What was the human intention. An airdrop of omisego token according to the eth holder balances. The eth holder of a multi sig contract are the owners address list field.

2-What will be technically possible according to the smart contract revival and who will own those omisego token at the end :

  • The multi sig smart contract creator : yes or no
  • The owners list stored in the smart contract : yes or no
  • WHG : yes or no
  • Parity : yes or no
  • first arrive, first serve : yes or no

3- Is it better than stuck fund ?

Bonus questions :
WHG create multi sig wallet to give back the money. It will be good to ask them if they want to have the power of the contract creator explained above. Also, It will be good to ask the people that have made the choice ( thanks to ChooseWHGReturnAddress.sol contract) of using the multi sig wallet created by WHG. Do they will have made another choice if they had known that, one day, creator will have that power. Other choice, like created themself the multi sig contract...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment