Potential security implications of CREATE2? (EIP-1014)

Unfortunately, that would require keeping around a flag for every contract that has ever existed, after it’s gone. That can have a significant impact on the account trie size/structure over time.

1 Like

But one bit per contract (only those created using CREATE2) is worth it if the alternative is that such a contract might change in-place in future and it is hard (?) to verify beforehand that a contract is capable of doing so. Are there any other possible mitigations?

You can actually do this with regular CREATE contracts too, if you layer them underneath CREATE2 ones.

1 Like

I see. So based on your analysis and prototypes (and ongoing discussion with @Arachnid on gitter - pasted below), this is a real exploitable concern, correct? If analysing init_code is hard and not sufficient, then besides keeping a flag-bit history for created contracts, are there any other options we could consider?

From @carver:

I’m not even sure if looking at the init code used is enough. I don’t have a prototype of this one, but you can theoretically deploy a malleable contract which itself calls a regular CREATE. Then when you reset the malleable deployer contract, you can do another CREATE to the same address, because you reset the nonce on the deployer contract. Now you don’t have any constraint on the init code being the same.

From @Arachnid:

This is a really good point. You’d have to verify everything back to the first EOA-created contract.

1 Like

By ‘reset’, do you mean re-create the malleable deployer contract using CREATE2 at the same address? Keeping a flag-bit for CREATE2 only contracts would prevent this, won’t it? You won’t be able to reset the deployer contract and hence the nonce.

It’s not an EVM exploit, exactly, but it is a way to maybe make auditors’ jobs more difficult. There are ways around each of these “social attacks”, but most of them require education. That will surely lag behind the Constantinople upgrade itself.

Right, if you prevent CREATE2 redeploys, then you prevent CREATE redeploys, as far as I can tell.

2 Likes

Agree but at some level this further reduces the trust end-users expect from contracts being “immutable.” Not only can they be upgraded but now they may be redeployed as well. There is a perceptional risk here and we can’t blame the users (or the auditors).

So the additional redeploy-tracking flag-bit only for CREATE2 contracts might not add much (?) to the state assuming that most use cases will continue to use CREATE, correct? But as @Arachnid indicated, this will be a consensus change.

  • Regarding the fact that CREATE2 ->A, CREATE -> B+2 x SELFDESTRUCT and then CREATE2 -> Amod, CREATE -> Bmod

I don’t think that changes things a lot. It simply further an argument against an anti-pattern: the anti-pattern of trying to establish trust through investigating history of a contract.

The pattern that should be used, is:

  • Use EXTCODEHASH to verify that the code you are calling matches what you expect. This might mean that during either compilation, construction or initialization (not necessarily same as construction), you store the expected EXTCODEHASH for other contracts that you will interact with.
    • Example: A DEX, when enrolling new ERC20 assets, may have to store the EXTCODEHASH when the new asset is enrolled.
      • Bonus: When solidity does a call, today it uses EXTCODESIZE to verify that the contract exists. It could use EXTCODEHASH instead, and internalize it so the call would look maybe like theToken.verify(codehash).transfer(from, to, value).
      • Alternative: when enrolling the token, it would do a safety-check, to verify once and for all that the contract is immutable.
    • There may arise an immutability-registry. That would simply run a check that contract X does not have SELFDESTRUCT, DELEGATECALL, CALLCODE (that’s it right?). EDIT TO CLARIFY: A registry of codehashes, not addresses.

That was the on-chain scenario, so how about the EOA scenario, interacting with a contract Casino?

  • Attack vector: When user sends large amount of ether to Casino, then the operator front-runs with two transactions:

    1. SELFDESTRUCT Casino.
    2. CREATE2 a new Casino which steals the funds.

OBS: The SELFDESTRUCT and recreation cannot happen within one transaction, due to how SELFDESTRUCT operates.

This scenario is flawed from the beginning:

  1. The EOA should not send money to a contract which can SELFDESTRUCT without prior notice. That’s dangerous even today (but there is no payout for the operator, only the wreak-havoc factor).
  2. If the user, despite this, wants to interact with the contract, he can do so e.g. via a personal on-chain wallet.

At the end of the day, auditors and developers NEED to get up to speed with these new changes. It’s been discussed ever since the suggestion of the first CREATE2 variant, and then further discussed with the current variant.

3 Likes

Thanks for the detailed explanation with examples.

As @Arachnid explained on gitter, the “bytecode invariant” may not hold for contracts deployed with CREATE2 or even with CREATE if one of its ancestors were deployed with CREATE2.

Therefore, it’s going to be even more critical going forward that the security pattern leverages EXTCODEHASH before interacting with other contracts.

So it sounds like the bottomline is that these concerns are not new, have been deliberated, no mitigations are necessary and what is needed is incorporating this guideline in the security best practises.

This is a very interesting idea.

I really think this is an antipattern; for interoperability and for permissionless innovation, security models should not depend on called contracts having specific bytecode. Contracts should be designed to be secure regardless of the callee’s code.

…and it won’t help here; a malicious token contract could be deleted and replaced with one with a constructor that produces the same byte code, but allocates the initial balances arbitrarily.

4 Likes

Yes, that naturally depends on what properties you want guarantees on. I was more thinking of the scenario where you audit a potential ERC20 token before enrolling it, to ensure that it will behave in certain ways when invoked.

Darn! So EXTCODEHASH offers no protection against this scenario. Doesn’t this alone justify a mitigation measure? @Arachnid’s below suggestion on gitter seems elegant:

I honestly think the simplest solution to all of this would have been to modify self destruct to leave an account’s nonce intact. Selfdestruct is already an ineffective way to encourage freeing state, and this would solve the issues.

Does this have any other side-effects?

Some of the text at Prevent overwriting contracts · Issue #684 · ethereum/EIPs · GitHub from @vbuterin seems to perhaps raise this concern as well, but I don’t have all the insight/context and could be mistaken:

Specification

If a contract creation is attempted, due to either a creation transaction or the CREATE (or future CREATE2) opcode, and the destination address already has either nonzero nonce, or nonempty code, then the creation throws immediately, with exactly the same behavior as would arise if the first byte in the init code were an invalid opcode. This applies retroactively starting from genesis.

Rationale

This is the most correct approach for handling the case where a contract is created in a slot where a contract already exists, as the current behavior of overwriting contract code is highly unintuitive and dangerous.

Currently this is not an issue because there is no way to create a contract with the same address twice without spending >2^80 computational effort to find an address collision, but with #86 this will change. Hence it is important to have correct behavior for this situation in the long term. This can be safely applied retroactively for simplicity, because currently creating a contract with the same address twice is computationally infeasible.

1 Like

Is there any way to characterise the overall risk here given that EXTCODEHASH does not fully protect against all scenarios?

If the risk is minimal, that would justify status quo. If the risk of misuse is high and all mitigations require consensus-changes, then what is the best way forward?

“Can’t change the balances arbitrarily” seems like a pretty important property to preserve!

Again, relying on a callee contract to have specific behaviour is a bad pattern. We should be authoring contracts to be secure regardless of what a third-party contract does.

1 Like

Yes, defensive programming is something everyone should ideally practise and educate, but if there is a design flaw, it should be addressed as such without shifting that burden to the user/developer/auditor.

And this unintended/unintuitive side-effect of CREATE2 which can be maliciously exploited in multiple ways certainly looks like a flaw in opcode semantics. Yes, it may be too late to fix/remove it now before Constantinople (assuming the cost to fix > risk of exploitation) but has to be addressed soon if we believe this is indeed a design flaw.

If I’ve understood correctly then for it to be possible to mutate contract A’s byte code, contract A must somewhere call selfdestruct. If that’s the case, a big red warning on etherscan.io for post-C contracts which contain selfdestruct opcode would probably make this exploit / obfuscation near useless.

I do think it would pretty much kill off use of selfdestruct but don’t see that as problematic as it is fairly rarely used today due to their being little incentive to include it.

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