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.
createat builtin
:
Proposal Variant A - 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.
claim builtin
:
Proposal Variant B - 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:
-
A proxy contract at address
keccak(SENDER ++ nonce) % 2**160
. This contract is locked for500_000
blocks (to be discussed). The proxy contractnonce
is set toinitial_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)); } } }
-
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 usingcreate-transaction
withdata
equaltransaction_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 sametransaction_data
would succeed.
proxy builtin
:
Proposal Variant C - 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:
-
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 callingsetupProxyForContract
orsetupProxyForSelf
on theProxyState
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); } }
-
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
.
multi-proxy builtin
:
Proposal Variant D - 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:
-
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 callingsetupProxyForContract
orsetupProxyForSelf
on theProxyState
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); } }
-
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.
createat builtin
:
Proposal Variant A - 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.
claim builtin
:
Proposal Variant B - 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.
proxy builtin
:
Proposal Variant C - 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 contract0xX
. - 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.
multi-proxy builtin
:
Proposal Variant D - 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.
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.