Potential security implications of CREATE2? (EIP-1014)

Right, and if that’s a concern, you should not let in contracts that can SELFDESTRUCT.

If, however, your main concern is that it doesn’t do a reentrancy-attack against you, then you might allow that but don’t allow change of code.

For this to work, you would also need to check for delegatecall and callcode

This might sound crazy, but hear me out: couldn’t this also be considered a feature? Obviously, there will be a lot of education, mitigation, etc. required in cases where immutability is the desired behavior, but in the case of upgradeable contracts this could be a great way to cut down on the overhead of needing to delegatecall out to a logic contract, especially in cases where persistent contract storage isn’t required.

I know this is likely a controversial perspective, and any upgradeability mechanism should of course be paired with adequate precautionary measures and / or governance, but if it works, it would be a pretty cool positive unintended consequence!

Interesting thread. I suggest modifying the semantics of CREATE2 so that the created contract starts with a unique nonce, using mechanism similar to what was suggested in dust removal replay protection EIPs

1 Like

disagree.
We need a robust immutable identity for the things we are assume to be immutable. Immutability check should work without lookup through the chain history.

In case of upgradable contracts we should just follow version paradigm: any change is a new version (with own immutable ID). It should be an explicit decision which version (inclusive HEAD) to follow. That is why I am not quite happy with current proxy based solution, but your proposal will make things even worse.

For my taste I would implement upgradable contracts by using ENS resolution.

Look, I agree that this will make things a little trickier than before. However, there will still be ways to perform the immutability check without looking at the history:

  • Ensure that no selfdestruct, delegatecall or callcode opcodes appear in the contract,
  • If there are delegatecall or callcode opcodes, ensure that it’s not possible to somehow reach a selfdestruct opcode,
  • Enforce that the contract has been created by some trusted source (such as an established CREATE2 factory contract that maintains a mapping of deployed contract addresses and will not allow a contract to be redeployed) via a require statement in the constructor or some other means.

The need for upgradeable contracts has been well-established at this point, and many of the most high-impact issues in Ethereum’s history thus far could have been mitigated with adequate upgradeability mechanisms in place. Obviously it’s not a paradigm that should be forced onto everyone, but if there are ways to implement it more efficiently, it will greatly benefit those that do need it. Both the proxy pattern and ENS resolution result in increased overhead vs. a direct equivalent, but an optimal upgradeable contract would operate on par with a standard contract from an efficiency standpoint. This could be a way to achieve that aim, assuming the corresponding risks can be properly addressed.

This is a good idea! A proxy contract which is rather easy - you send the seed and the init_code to this contract and it has a mapping(address => bool) which holds if a contract is deployed at this address (hence the return code of CREATE2 or generated by using the definition of address generation by CREATE2). It sets the value of the address key to true if a contract has been created successfully at this address. If the value of this key is true before creating the contract it reverts.

1 Like

Is there any design pattern that would want a contract deployed with CREATE1 by a contract deployed with CREATE2 that isn’t looking to take advantage of this? If there isn’t, any contract deployed by this method can just be flagged as being able to have its code change.

I think it might end up being a lot trickier than before. For example, it seems that the only possible secure contract is one that contains no selfdestruct, delegatecall, or callcode. Otherwise, you’d need to verify not only that delegated calls cannot possibly hit a selfdestruct opcode but also that the delegate contract contains no selfdesctruct opcode itself. What if the delegate contract, itself, contains delegated calls? The dynamics of this question get complicated pretty rapidly.

Also, as @carver pointed out, there are cases where you have to perform a similarly chained verification process in the reverse direction, verifying the properties of creator contracts and the creators of those creator contracts, and so on.

The recursive nature of this problem gives me the intuition that there are even more complex dynamics still lurking beneath the surface that may yet be discovered.

2 Likes

We can deploy a “Safe CREATE2 deployment” contract where it is not possible to recreate at the same address.

1 Like

Just in case this is actually a debate about removing CREATE2 from Constantinople, or delaying it, then I oppose both.

I think the checks @0age lists are easy enough to make:

Any system not following those parameters should raise red flags.

1 Like

Not to disagree with myself, but I will admit I am worried about an attack that includes some social engineering.

Specifically:

  1. Project team gets and audit on system using a DELEGATECALL proxy pattern for upgradability
  2. Project team “Forgets” to mention that it will be deployed using CREATE2.
  3. System looks OK to auditors.
  4. … the rest is self explanatory

The only real protection against this requires a lot of UX and engineering work by etherscan et al.

1 Like

From @rajeevgopalakrishna:

For example, the init code could load bytecode from another contract and return it. So the init code doesn’t change, but returns arbitrary contract code to deploy.

I’m assuming this refers to the EXTCODECOPY opcode. Wouldn’t the returned value always be the either:

  • The same code or
  • Empty (in the case that contract has been selfdestructed)

This is with the assumption that the init_code is exactly the same, due to the collision resistance property of the keccak hash function.

However, (I realized this as I was typing this reply) maybe this can be used as a branch in the init_code to deploy different bytecode?

Good question, it doesn’t have to use EXTCODECOPY. A couple things you could do with identical init code to deploy different code:

  1. init code returns a different value after a specific block height
  2. init code calls to a contract which returns code from an address held in storage, which can be updated.

I’m sure there’s more.

1 Like

By ‘audit process’ do you mean something wallets and interfaces do automatically to see if it was instantiated by CREATE2? I agree, that would be hard, and probably poorly standardized.

Would it be a bad idea to somehow mark contracts as originating from a CREATE2 op?

Is it intentional that this EIP allows selfdestructed contracts to be redeployed? It seems to me like more of an unintended side effect.

That’s my understanding.

I’ve been thinking more about this edge case over the last few days (as I’m sure many of us have been), and I do agree that this is going to complicate things significantly on multiple fronts. That being said, I’m starting to realize that this could open up a whole new paradigm of contract that could improve the user experience immensely, by enabling the creation of transient workspaces that can be used in place of EOAs or simple proxies.

Here’s what I mean by transient workspaces: right now, if you want to interact with a contract, you have three main methods of doing so, each with its own limitations:

  • Call right into the smart contract from the originating EOA, passing in the function selector and arguments as calldata. This is the standard method, but prevents the caller from accessing many “power features” like preconditions based on current EVM state (for instance, as discussed above you cannot use EXTCODEHASH when calling from an EOA), requires that the entity in question is intrinsically tied to a particular keypair, and does not allow for the same address to also perform its own processing when receiving calls.

  • Deploy a static contract that will relay the call into the target smart contract for you. Basic proxies and multisigs that simply pass along a bytes payload to the target will solve the issue with keypair coupling, but still cannot adequately tap into “power features” or intelligently send and receive calls. Alternately, you can deploy contracts with actual logic that employs “power features” and processes incoming calls, but then you are trapped: the logic will be permanent, and the identity associated with that address will not be portable in the same way that an EOA would be.

  • Deploy an upgradeable contract (such as a transparent proxy with assignable “implementation” logic contracts) that will relay the call into the target smart contract. At first glance, this method seems to solve both the feature-deficit of EOAs and basic proxies as well as the malleability-deficit of standard contracts, but it falls short of the ideal outcome for both. In the former case, a proxy will not accurately convey its true behavior when it is the target of operations other than a standard call - for instance, its EXTCODEHASH will remain unchanged even when the implementation has been altered - and resists straightforward introspection. (If we’re really optimizing for the end-user and want to make contracts auditable, then this upgradeability pattern is not exactly making it easy!) For the latter, the logic may be upgradeable but the state is still persistent, which is also difficult to properly manage even with the proper education and tooling. On top of all that, the added overhead from the delegatecall to the implementation contract makes it more expensive to use.

Armed with a transient workspace, a user can set up an identity contract that contains whatever logic they want and track internal state however they want, accessing all the “power features” available to contracts and handling incoming calls, then tear the whole thing down and start from scratch while still retaining ERC20 balances, accumulated reputation, or other external state referencing the address. Applications can set up “guard-rails” for new users, helping to on-board them with a few pre-loaded methods while they start to accrue a reputation but enabling them to wipe the slate clean and take full control of the address when they’re ready. And once the “transient” aspect is no longer desired, the deployed code can be made immutable by restricting access to selfdestruct or removing it entirely.

4 Likes

An article on this topic from @carver which explains the various scenarios in detail with some examples: https://medium.com/@jason.carver/defend-against-wild-magic-in-the-next-ethereum-upgrade-b008247839d2

I think that would be helpful, as long as you mean that a contract is flagged if it or an ancestor contract was deployed with CREATE2.