EIP-7761: HASCODE instruction

Introduce HASCODE instruction to replace the EXTCODESIZE > 0 check in EOF


Metadata
Status: DraftStandards Track: CoreCreated: 2024-09-01
Authors
Andrei Maiboroda (@gumb0), Piotr Dobaczewski (@pdobacz), Danno Ferrin (@shemnon)

Abstract


Allow EOF contracts to discriminate between EOAs (Externally Owned Accounts) and contract accounts by introducing an HASCODE instruction.

Motivation


EOFv1 as scoped in EIP-7692 removes code introspection capabilities from the EVM, including the EXTCODESIZE instruction (in EIP-3540). This makes it hard for ERC-721 and ERC-1155 standard contracts to be implemented, as they rely on discovering whether a token's safeTransfer call target was an EOA or a contract account:

  • safeTransfers to EOAs succeed
  • safeTransfers to contract accounts call an onERC721Received (onERC1155Received) on them and expect to get a special magic return value, otherwise the transfer reverts (on the assumption that such a receipient may not be able to interact with the token)

HASCODE is aimed to fill this gap and bring back the possibility to easily implement ERC-721 and ERC-1155 standard contracts in EOF.

Specification


Parameters

ConstantValue
FORK_BLKNUMtbd
GAS_COLD_ACCOUNT_ACCESSDefined as 2600 in the Ethereum Execution Layer Specs
GAS_WARM_ACCESSDefined as 100 in the Ethereum Execution Layer Specs

We introduce a new EOFv1 instruction on the block number FORK_BLKNUM: HASCODE (0xe9)

EOF code which contains this instruction before FORK_BLKNUM is considered invalid. Beginning with block FORK_BLKNUM 0xe9 is added to the set of valid EOFv1 instructions.

Execution Semantics

HASCODE

  • deduct GAS_WARM_ACCESS gas
  • pop 1 argument target_address from the stack
  • if target_address has any of the high 12 bytes set to a non-zero value (i.e. it does not contain a 20-byte address), then halt with an exceptional failure
  • deduct GAS_COLD_ACCOUNT_ACCESS - GAS_WARM_ACCESS if target_address is not in accessed_addresses and add target_address to accessed_addresses
  • push 1 onto the stack if target_address.code is not empty, 0 otherwise

If target_address.code contains an EIP-7702 delegation, the result of HASCODE should follow the delegation and return a result according to the delegation designator account. Additional gas costs and rules concering accessed_addresses apply as specified in EIP-7702 for code reading instructions like EXTCODESIZE.

If target_address points to an account with a contract mid-creation, it behaves aligned with similar instructions like EXTCODESIZE and returns 0.

Rationale


Alternative solutions

There have been other solutions proposed to alleviate the problems related to lack of code introspection required for ERC-721 and ERC-1155 standards:

  1. Extra status code for EXT*CALL instruction - allowing to discriminate a result coming from calling an EOA
  2. Extra argument for EXT*CALL (a "fail if EOA" flag)
  3. Two return values from EXT*CALL (status code + whether it was EOA)
  4. EXT*CALL setting a new callstatus register (+ a new CALLSTATUS instruction)
  5. Reenable EXTCODESIZE in EOF, keeping its behavior same as in legacy

HASCODE has been chosen as the most elegant and minimal solution satisfying the requirements at hand and still able to be introduced in EOFv1.

Reuse the 0x3b (EXTCODESIZE) opcode for HASCODE

A new opcode is prefered by a general policy to not reuse opcodes. Also HASCODE can be rolled out in legacy EVM if desired.

Keep code introspection banned

Removing code introspection is one of the tenets of EOF and HASCODE would be an exception from the principle. Without HASCODE, ERC-721 and ERC-1155 standard implementations have to resort to either:

  1. Leveraging a "booster contract" which would be legacy and would call EXTCODESIZE for them. This has been deemed inelegant and inconvenient from the point of view of library implementers, requiring them to hard code an address of such a contract (with the usual address-related problems arising on different EVM chains)
  2. Continuing to use legacy EVM themselves. This is sub-optimal, since EVM compilers are likely to at some point deprecate legacy EVM as compilation target
  3. Updating the standards to not rely on code introspection patterns in safeTransfer safe guards. This can be accomplished for example by leveraging ERC-165, leaving out only contracts which do not implement ERC-165, and at the same time have non-throwing fallback functions, as indistinguishable from EOAs. This is not easy to achieve since ERC-721 and ERC-1155 are Final and adopted in practice.

"Endgame Account Abstraction" issues

HASCODE (and earlier EXTCODESIZE available in legacy EVM) are claimed to slow down AA adoption, because they encourage patterns which discriminate between smart contract and EOA accounts, e.g. not allowing the former to interact. However, there are counter arguments that it is up to other factors which slow down AA adoption (assumption that accounts can produce ECDSA signatures, and the lack of adoption of smart contract signatures).

Relation to EIP-7702 "Set EOA account code"

After EIP-7702 is activated, the discrimination between EOAs and contract accounts using EXTCODESIZE (or HASCODE) has an edge case: Whenever an EOA sets its code to a contract account which does not respond as expected to an onERC721Received (onERC1155Received) callback, transfers to it will revert, despite the recipient being able to interact with the token. This has been deemed unlikely to be a problem, as for the intended real-world uses of EIP-7702, those callbacks will be implemented by designator codes.

Including safe guarding against proxy bricking

In parallel to the ERC-721 / ERC-1155 problem, another potential risk has been brought to attention. Since EOFv1 prohibits EXTDELEGATECALL targetting legacy contracts, there exists a scenario where an EOF proxy contract accidentally upgrades its implementation to a legacy EVM one. Since reverting this or upgrading again (using current proxy standards) requires the implementation contract to be called, it would effectively render the contract unusable.

One potential solution to this would be to have a generalized CONTRACT_KIND instruction instead of HASCODE which would further discriminate the account into legacy or EOFv1 with a 0 / 1 / 2 return value, thereby providing means for an additional safeguard against such a scenario.

This problem is potentially solvable on the application layer (proxy upgrade would include a direct check of the implementation contract being EXTDELEGATECALL-able) or even by re-enabling EXTDELEGATECALL to a legacy target in a future EVM upgrade.

Backwards Compatibility


HASCODE at 0xe9 can be introduced in a backwards compatible manner into EOFv1 (no bump to version), because 0xe9 has been rejected by EOF validation before FORK_BLKNUM and there are no EOF contracts on-chain with a 0xe9 which would have their behavior altered.

Security Considerations


Needs discussion

Copyright


Copyright and related rights waived via CC0.