Skip to content

Instantly share code, notes, and snippets.

@5chdn
Created December 7, 2017 12:04
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
Star You must be signed in to star a gist
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.

@LefterisJP
Copy link

LefterisJP commented Dec 11, 2017

What I personally dislike in all four proposals is the fundamental change that this would introduce to all contracts deployed so far that contains the SELFDESTRUCT opcode. I feel that this is quite a big price to pay simply for getting some ETH back.

It is proposed as a means of improving the protocol but I can't see it as such. Is there something wrong with the way that SELFDESTRUCT acts at the moment? Sure it is a very final and unforgiving opcode to invoke but it does what it's been designed to do.

People have utilized it in their smart contracts already deployed in the network. Under what authority can we change what those contracts mean?

I would still like for the funds to be somehow recovered. I feel your pain, I really do, especially considering my involvement with the DAO. But this does not feel like the right way.

I would also like to propose (as I already mentioned in twitter) to move these proposals into a single EIP in the EIP repository. That's what it's there for, to foster discussion of proposed changes to Ethereum and make them visible to all interested parties.

And yes including multiple variations of one proposal in a single EIP is totally fine.

@pdaian
Copy link

pdaian commented Dec 11, 2017

D seems problematic. In general these proposals break the use of msg.sender for authentication, e.g. in the cases where contracts are holding ERC20s.

If I am reading (D) correctly, if I have a token basket contract that self-destructs and then is re-instantiated as a proxy, a miner can then set their own proxy to a contract that transfers the balance of that contract in any ERC20 tokens to themselves. In fact, miners should do this as a matter of policy.

@lorenzb
Copy link

lorenzb commented Dec 11, 2017

There seems to be a spurious comment about tx.origin?
// please note the usage of tx.origin

@taoeffect
Copy link

I think it's bad when people lose money because of other people's mistakes.

But in attempting to recover those funds, has Parity explored possibilities other than those listed here?

Has Parity, for example, done an audit of its own finances, and, if deeming it necessary, began preparation for a crowd-fund to compensate those who lost funds due to Parity's mistakes?

Would Ethereum, in accepting any of the changes listed above, have any semblance of a coherent vision remaining, or would the new vision become, "A centralized financial system, but where we invent new words for the same things." ?

@3esmit
Copy link

3esmit commented Dec 12, 2017

I don't think being able to revive a dead contract is right, I agree with all words of @LefterisJP. I agree in a HF to revive only the multisig library.
If is not possible (or too complicated) to irregularly revive the dead library in a HF, I would just do a new irregular state change and move at least 99% of the lost funds to control of a multisig between WHG, Paritytech and EF.

@kayabaNerve
Copy link

We shouldn't do any hard fork.

Parity contracts were hacked.
Whitehat hackers saved most of the funds.
Non-stolen funds returned.
Parity contract 'patched'.
Parity contract launched.
Parity contracts no longer usable.

Parity needs to own up for this ridiculous nonsense (that has happened twice) before we make a hard fork for every single group that messes up. Look at say, swarm.city (I used to do some work for them; first group that came to mind). If they mess up, and lose 10,000 SWT via one of their hashtags, should we hard fork for them? If a millionaire sends 1 million dollars to 0x0, should we hard fork for them?

We hard forked once, for the larger DAO hack that was even more of a mess, but we cannot let this become a practice. How many times must Parity mess up for them to own up and move on?

Instead of a hard fork, its users should look into all legal options. There's likely none, and if they are, that could destroy the group behind Parity, but the group behind Parity hasn't shown an iota of competence to the public lately. The node software, which I like and use, is open source, so the community can continue it without the group behind Parity.

And they're not even suggesting reviving the library, they're suggesting changing the EVM and potentially breaking other contracts. It's ridiculous!

@0x647262
Copy link

0x647262 commented Dec 12, 2017

I feel as if simply proposing reviving the library during a scheduled hard fork would be a more elegant solution than the proposed solutions in this draft.

@bernardpeh
Copy link

bernardpeh commented Dec 12, 2017

I feel the solutions presented here introduces more complexity and potential problems. Allowing redeployment of contract at the same adddress is fundamentally wrong. How can I trust you if I know you have the power to do that? We all know blockchain transactions are immutable and we should take extra precautions before deploying it. If a contract has no rescue mechanism in the code, then it should not be allowed to be rescued post deployment. The converse statement should hold true. The contract should behave as its intended, no more, no less.

I also don't agree to revive the library during the next hard fork. How are you going to do that? drill a hole in the evm just for that purpose? Might as well announce to the world "dont worry if your contract is hacked because we can always reverse it".

I noticed parity contacts are often more fanciful than the others, should have done more due diligence when using things like delegatecall and extending deployed libraries. Like others, I feel the pain but I'm sorry, we should absorb the pain and move on.

@maurelian
Copy link

@5chdn: A, B, and C would not allow you to resurrect a contract created by another contract (at least contracts which pre-date the proposed opcodes).

Is that correct? It seems like a serious asymmetry.

@gcolvin
Copy link

gcolvin commented Dec 12, 2017

Proposals C & D looks to be refinements of an idea of mine that I still like. I prefer D, the multi-proxy built-in. It is not a Parity-specific change, but is helpful now and in the future for recovering from similar situations. This is good, as I would object vehemently to rescuing Parity alone. And the cons look to be livable, given the scale of the problem.

@gcolvin
Copy link

gcolvin commented Dec 12, 2017

Luckily, my email notifications just happened to point me at Nicks analysis on Medium. (Luckily, as otherwise I would have missed it in the usual chaotic stream of Ethereum discussion, and Nick has not spoken up here.) I generally agree that breaking changes to semantics should be avoided, but not at all costs. I might have more to say when I'm studied Nick's arguments.

@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