EVM Object Format (EOF)

No, current plan is to keep it as a collection of EIPs. But there is also unified spec at https://github.com/ipsilon/eof/blob/main/spec/eof.md

Just want to confirm because Iā€™ve missed some discussions on this, but it looks like the changes previously meant to be in EOF2 are all merged into EOF1, is this correct?

I like the gas parameters for security because I can use them to prevent re-entrancy for eth payments. Without them I would need the PAY opcode for the same level of security.

This pattern breaks some contract wallet recipients and shows why (IMO) we need to remove GAS parameters. A wallet should be able to do internal accounting when it receives ETH.

If we believe that contracts should not be able to do internal accounting on receipt of ETH then we should make that explicit at the EVM layer by providing a mechanism to send ETH without the recipient getting an opportunity to execute code. This used to be possible in a very hacky way with self destructing, but if we feel that pattern is good we should just have an ETH_SEND opcode that doesnā€™t transfer execution over to the destination.

1 Like

I can imagine another solution. There could be a new type of call (PAYCALL) that allows sub-frames to delegatecall and staticcall but not call and paycall. It would be more powerful than PAY because the recipient can update their own storage, but they canā€™t cause mischief in other accounts. It would differ from staticcall in that it would allow storage and log for the recipient account.

But I donā€™t think recipient reaction and right of refusal are important (see ERC20) and I prefer PAY for simplicity. The only accounts with good eth recipient action without calldata are WETH and UniswapV1, and I wouldnā€™t use PAY in those circumstances. The others Iā€™ve seen are just wasting my gas or trying to steal my money.

1 Like

Yes! Letā€™s do PAY! (and/or EIP-7609)

1 Like

(removed, meant to reply to the whole thread as opposed to any specific comment)

I have been providing feedback to the ipsilon team over the last year and have helped to draft / improve some of the EOF proposals. I am in general in favor of any changes to the EVM which improve execution efficiency, provide features for compilers and make it more future proof. That being said, I think there are two areas in the design (current: eof/spec/eof.md at 890ef566b86e8f25215f5cad65e171e2c1de71d4 Ā· ipsilon/eof Ā· GitHub) which I think could benefit from more work before mainlining it.

  1. VLQ. Using variable length quantities instead of fixed-size quantities, both in headers and as instruction immediates. This is a nice-to-have, but I think it will save significant pain if any limits (e.g. codesize limits) need to be upgraded in the future, since they can be implemented in a ā€œsoftā€ limits change, rather than needing a format change. It also can provide some codesize savings (I estimate 2-5% improvement), as well as potentially save opcodes (e.g., SWAP_N vs EXCHANGE) since we donā€™t need to optimize opcodes for different immediates sizes anymore. I wrote about this more here: update EOF container specs with variable-length encodings by charles-cooper Ā· Pull Request #37 Ā· ipsilon/eof Ā· GitHub. Again, I think it could be ok if EOF shipped without VLQs since it can theoretically be changed later, but the cost of changing this later after mainlining would be high since it would require a format change. Therefore I think the benefit to changing it now (which, per feedback on the above PR, the only downside of which is needing to redo some testing work) outweighs the cost.
  2. Global code section. Right now EOF provides subroutine functionality mainly through code sections. However, to my knowledge an EIP-2315 style design which has a single global code section (effectively allowing cross jumping between routines, which EOF does not allow) was not seriously considered. The benefit of a global code section is mainly about efficiency of compiler-generated code; there are, by definition, some code-sharing optimizations available to a global code section which are not available to the nonglobal code section design. This is a deeper and more permanent(!) design issue that Iā€™m not sure it would be possible to easily fix in a future hard fork. It may be that the benefit of disallowing cross-jumping outweighs the cost to compilers / code generators, at this stage Iā€™m not sure, in any case I donā€™t think the analysis has been done yet. But in my mind this is a serious issue that should at least be investigated and addressed in the rationale section of the EOF proposals.
1 Like

I agree technically on VLQs ā€“ you get better code size now and you future-proof a system that in principle will run forever. I canā€™t speak to schedule issues, but I imagine they are tight. If it helps ā€“ and Iā€™ve forgotten the details of the VLQ scheme you are using and of the EOF immediate size issues ā€“ but some schemes would let you specify a fixed-size field with unused bits now which could become variable length later when those bits start getting used.

If someone wants to pick up the 2315 ideas in the future I donā€™t think that EOF forecloses them. Somewhere there is a old PR of mine proposing that EOF code sections not be functions, but modules with multiple EOF-defined entry points and no cross-module jumps, but with EOF-2315 supported inside of modules. We never discussed it much, but something along those lines should still be doable.

EIP-2315 wasnā€™t seriously considered in part because it I never seriously proposed it again. Itā€™s last-minute rejection simply set my research back too far, and I became content to join a startup doing other VM work and in my spare time get the proposal to where I wanted it and make sure that EOF functions provide at least the same level of init-time validation, which earlier versions did not.

1 Like

There are popular ERCs such as ERC-721 (NFTs) that rely on checking extcodesize(addr) > 0. It would not be possible to implement them as EOF contracts.

Has this been discussed before?

2 Likes

If the intent is to differentiate EOAs from Contracts this runs headlong into the AA and Smart Contract wallets also being smart contracts. While it used to be a valuable check current user practice has significantly curtailed itā€™s value, as an NFT collection that only allows EOAs to own the NFT will be chasing a shrinking market share.

So it has been discussed, and the conclusion was that the AA roadmap removes the value of that check.

This is not what the check is used for in these ERCs (see ERC-721, ERC-1155).

The logic they specify is roughly this: when an asset is transferred to a recipient that has code, invoke recipient.onReceived(...), which must succeed and return a specific magic value, otherwise the transfer is reverted.

The purpose is not to prevent smart contracts from owning the asset, but to prevent errors where the asset is transferred to the wrong contract with no way to recover it. This is an error that happens a lot, so many developers consider this an important feature. Smart contract wallets are supposed to implement onReceived (and they do, see for example Safe).

Itā€™s also used to allow a recipient contract to react to a transfer and do something with the asset as soon as itā€™s received.

We can debate whether these are good design patterns or not, but I think they are at least legitimate and donā€™t conflict with the AA roadmap (unlike patterns that force the use of EOAs). The fact is that what is arguably the second most important ERC used on the network relies on this ability and the specification canā€™t be changed.

So are we okay with ERC-721 being unimplementable on EOF(v1)? My assumption is that we want EOF to become the standard way to deploy new contracts, if so this should be addressed.

It would be enough to add an opcode to check if an account has code set (or to change the semantics of EXTCODESIZE on EOF to return 0/1, but I assume thatā€™s not a good idea). Granular code introspection is not necessary.

2 Likes

I think this may be the use case we were struggling to find when we were discussing it.

One option is to change the error numbers to distinguish a return from a contract from a return to a non-contract address (empty or EOA).

i.e. instead of

0 - success
1 - revert
2 - failure

we could do

0 - successful call
1 - successful non-call
254 - failure (-2 uint 8)
255 - revert (-1 uint 8)

So we can determine from the call what happened. The downside is it would eliminate the (ISZERO RJUMPI[n]) check and require a couple of exra opcodes per call (PUSH1[127] LT RJUMPI[n]) but allow an executed onReceived to be distinguished from a transfer or no-op call. Although in cases where you only want to accept successful and want to hot-path that handling ISZERO still works

Other alternatives that were rejected

  • Use only PAY to do transfers, this prevents some calls from requiring payment, such as WETH
  • Require Value when calling a non-contract and if not failure, it felt too special cased.

Iā€™ll bring this up in the EOF call this wendsday, as I think it is the example we were missing. Solidity had brought it up but the collective seeme persuaded by the AA argument.

1 Like

Yes this would work!

Yeah this is unfortunate, I think it makes sense to prioritize keeping that code sequence short. Another alternative could be to switch back to returning 0/1 (0: success), and instead of returning the status code immediately from the *CALL instructions add a separate opcode CALLSTATUS to put it on the stack if necessary. I assume it will very rarely be used anyway?

As a side note, in EIP-7069 itā€™s really not clear to me when the failure status 2 will be returned on the stack. I canā€™t find a definition of ā€œfailureā€. This part in particular confuses me: ā€œ9. Fail with status code 1 returned on stack if any of the following is true [ā€¦]ā€, is that correct or is it an error and it should return status 2?


Regarding another point:

EOF1 contracts can only DELEGATECALL EOF1 contracts

Is this restriction still necessary given that SELFDESTRUCT has been mostly neutralized in EIP-6780: SELFDESTRUCT only in same transaction?

There is a potentially severe failure mode for EOF upgradeable proxies: if the proxy target is upgraded from an EOF to a legacy contract, and the upgrade logic is located in the target, the proxy becomes frozen and unusable. This error could be prevented by having the upgrade code always test that DELEGATECALL works, but this seems like a footgun that should be avoided from the outset.

The rationale proposes this restriction as a way to prevent attacks/errors like the Parity Multisig, but actually introduces a new way in which a proxy contract could end up irrecoverably broken!

1 Like

From this reference, extcodesize of EOF code called from legacy code should return 2. I think that suffices, no?

Given EXTDELEGATECALLā€™s restrictions. Iā€™ve also thought of is how EOF contracts can recognize other EOF contracts to know if EXTDELEGATECALL is okay to make without wasting initial gas

So currently, compilers add a check of extcodesize before making calls since calls to EOAs return 1. From this reference EXTDELEGATECALL to legacy code also behaves as though its an EOA. This means that EOF code would need to be able to detect other EOF code. Doing extcodesize wonā€™t work as legacy code has size too.

The only solution i could think of was EXTCODECOPY the first 2 bytes and asserting that it is 0xEF00. Thatā€™s means a cost of a few more opcodes during EXTDELEGATECALL for EOF

The two issues Iā€™ve described are about EOF contracts, where EXTCODESIZE is not available, so neither of those are relevant.

1 Like

Yeah youā€™re right, missed that part (extcodesize etc deprecated)

Also highly interested to know this. Since this has a huge impact on interoperability and de facto splits EVM ā€œlegoā€ to 2 worlds.

1 Like

I donā€™t think the lack of extcodesize check will affect these EIPs. Technically, yes, they both state that a correct ā€˜safe transferā€™ function implementation must check whether the target is a contract and not call it if it is not. This check will indeed be impossible on EOF. But practically speaking, this check is redundant. Proceeding with a call in case of EOA will simply result in a success with no return data. As long as the transfer function does check the return value, this will still result in a revert. And specifically in Solidity, unless intentionally implemented as a low-level call, the compiler itself will issue a revert when the size of returned data does not match what the called function declares.

As much as I think that having a way to perform this check would still be useful as an extra sanity check, it does not seem like not having it would prevent EIPs like this from being implemented. At most, the wording of those EIPs may need to be revised to stop requiring this check on EOF (or in general).