ERC-1066: Ethereum Status Codes (ESC)

Here is my brief review of EIP-1066. I have not reviewed everything but a few items stand out. First are notes about the standardization.

Standardization is premature

No existing implementations are deployed. Nor are similar deployments cited that show a demand for these features. Therefore it is impossible to know if this EIP provides any value.

Reasons are not motivated

No documentation is shown to explain why these codes were chosen.

Nibbles

I recommend encouraging (SHOULD not MUST) people to use two-nibble returns hex"01" in all cases to promote clarity.


Here are more opinions on the topic itself.

It is wasteful in the default case

This ads one word of output for function calls to return a SUCCESS status code. Competing proposals do not require this extra cost. The cost of including an extra output word is not explained or experimented in this EIP.

Another proposal returns status and other outputs during a revert.

It is not backwards compatible

Adding an additional function return makes this incompatible with every existing contract and other standard, including ERC-20 and ERC-721.

It is unsafe

We learned from ERC-20 that returning (rather than reverting) from a “failed” function call is dangerous. Citation --> https://twitter.com/eth_classic/status/971030641406980096

Stronger motivation should be provided to rewrite this history with a new narrative.

The async DEX examples are not realistic

A DEX will not implement in this way.

Codes are ill-defined

Should I return 0xE1 Decrypt Success or 0x01 Success or 0x21 Found?

If there are so many new codes being introduced, they should have well-defined meanings, including motivations.

In other words, this EIP is basically proposing 100 different mini standards so I am expecting the rationale to be an order of magnitude longer than other EIPs I’ve read.


In all, this is a noble effort. And I think it is close to a good solution. Here’s my overall recommendation and a path forward.

Recommendation

As is, two issues are show stoppers for me:

  • Unsafeness on returning when a function fails
  • This ERC is not backwards compatible with any existing contract or standard

Therefore I do not recommend anybody to use this EIP as-is.

BUT

With one quick modification, this can be easily addressed…

Please consider to update this proposal to only use status codes through reverts.

AND that makes it compatible with another proposal — https://github.com/ethereum/EIPs/issues/838#issuecomment-462363456

The result is efficient, backwards compatible and allows very semantic use.

Also, feel free to reach out to me directly.

1 Like

Thanks for the mini-review! Yes, these are the things we hear, explain to folks that it’s a paradigm shift, and they tend to go “oh, yeah that actually makes a lot of sense.” I’m not totally sure how to more get it across in text, so I’m very open to suggestions. Anyhow, in the meantime our standard short rebuttals are below:

In all, this is a noble effort. And I think it is close to a good solution.

Thanks!

No documentation is shown to explain why these codes were chosen.

Fair! You’re right that it should be right in the proposal itself. I’ll copy paste a bunch from the Medium post and the notes from when we did those several dozen company interviews. Thanks :smile:

No existing implementations are deployed

In fact, some contracts are already using this (typically in the the security token world). The ERC1400 Security Token proposal depends on it and we have had discussions with folks looking to use it for blockchain identity use cases, as well.

Nibbles
I recommend encouraging ( SHOULD not MUST ) people to use two-nibble returns hex"01" in all cases to promote clarity.

I’m confused. In all cases, they’ll get cast to two nibbles, even if the first nibble is 0, since you can only work with full bytes. Am I misunderstanding something?

It is wasteful in the default case

Yes, but in what way was it successful? This is important information for many use cases that involve automation. Was it successful accepted but will be run later when quorum is reached? Did the transfer go through now? There are lots of cases here.

Adding an additional function return makes this incompatible with every existing contract and other standard, including ERC-20 and ERC-721.

Good news! You don’t need to change those contracts to wrap them in an interface (in a proxy contract), effectively giving backwards compatibility for those that already rely on ERC20 or ERC721, but giving a separate interface for others. Future contracts can make use of this functionality.

But I should emphasize that this proposal was created to solve problems that we were having when developing security tokens, and enables new use cases that are currently difficult to do interoperatively on Ethereum. Calling ERC20s and NFTs directly as is most commonly done today (largely because of how difficult it is to communicate between contracts autonomously) is not an amazing use case to show off this design pattern.

It is unsafe

As per the EIP, this is in no way a replacement for revert! Exceptional, state-breaking, or dangerous cases should absolutely 100% revert! In fact, the library provides helper functions to aid with a number of revert scenarios!

The async DEX examples are not realistic

Yes, it’s true that today’s DEXes aren’t very smart. They could be smarter and more autonomous by using such a standard, but still IMO doing one on-chain is impractical for most scenarios. And fair, it’s not the greatest use case, but it’s short, and people in the ecosystem understand token-related flows today. Essentially it’s a toy example for illustrating how codes can flow through a system, as per HTTP or BEAM. If you can think of a better example that’s widely understandable to an audience with experience limited to Solidity and JS (and that shockingly often doesn’t fully understand HTTP), I would love to include it!

Codes are ill-defined
Should I return 0xE1 Decrypt Success or 0x01 Success or 0x21 Found?

I mean, it depends on your use case. If you decrypted something, you should use 0xE1, if you looked something up in a table or found something you should use 0x21, and if you want straight dumb compatibility with bools you should use 0x01. HTTP Status Codes have a similar range of more specific codes to help in control flow.

They should have well-defined meanings, including motivations

These are all taken from real-world scenarios from interviews that we did with Ethereum companies, so there are motivations for every code (some cases joined or abstracted), even if they’re not all spelled out. As always with code, we think more documentation and use cases will be useful.

Please consider to update this proposal to only use status codes through reverts.

Evidently the paradigm and purpose of this document is not clear enough at the moment, for which I apologize. I do wonder if people come to this EIP with preexisting assumptions, since (as mentioned before) when I spell it out in detail that “no it doesn’t work like that and isn’t written in that way anywhere”, a light bulb goes off for a lot of people. It’s purpose is to give more semantic information to other contracts, developers, and users, in an automated way, in a similar vein to HTTP status codes. It is not about propagating exceptions: it’s about sending context around for both success and failure, and true errors/exceptional cases should promptly exist this flow and revert.

I’ll give you a simple illustrative real-world example for status codes:

You need to check if someone is 1/n whitelists maintained by several parties. This is key in regulated scenarios, so that not every token has to (for example) check everyone’s photo ID separately, which is time consuming, expensive, and error prone, and does not easily work across borders without multiple domain experts handling this per-domicile.

Since you need to check several of these lists, you need to not revert if it fails, and it’s not an irrecoverable error, it’s a normal part of flow (chances are that you’ll need to check several of these lists). You may not be allowed to read from this list (a closed list), the user being checked may be actively blacklisted, they may not be on that list, or their verification has started off-chain (someone is reviewing their passport) but hasn’t completed. If you need 2/3 to succeed, and 2 have blocking codes (ie: even numbered codes), then you should revert. Likewise, if there is an overflow, it should also revert.

AND that makes it compatible with another proposal — EIP 838: ABI specification for REVERT reason string · Issue #838 · ethereum/EIPs · GitHub

As per the EIP text, this is already fully compatible with revert-with-reason, and the helper lib provides ways of bridging the two with a nice, semantic interface.


Thanks again for the review :tada: Looking forward to further feedback on the above responses!

Relevant Resources

Regarding wasteful.

“In what way was it successful”? This will /always/ depend on the application. If a function casts a vote that is effective later when a quorum is reached then that is something that should be documented and already understood by the caller. When I cast I vote I expect that to work like a vote. When I transfer a token I expect the token to be transferred. “Waiting for a quorum to be achieved” is a ridiculous outcome for a standard, generic token transfer. It might be cool for a DAO action, but again that is application-specific.

Regarding upgrades.

Please explain how to make a proxy for the existing CryptoKitties contract to add this feature using less than $100,000 of gas.

Regarding revert with reason.

The abstract says “They are fully compatible with both revert and revert -with-reason.” This conflicts with the specification “Codes are returned either on their own, or as the first value of a multiple return” since the specification only relates to returns, not reverts.

since the specification only relates to returns, not reverts.

Yes, because this spec isn’t about the exceptional cases, it’s about the rest. It doesn’t preclude reverts, and the information gleaned from a code can be used in a revert. As an example:

require(someCheck(msg.sender) == 0x41, localize(0x41));

Or with the helper lib

pragma solidity ^0.5.0;

import { FISSION } from "fission-codes/contracts/FISSION.sol";
import { SimpleAuth } from "./SimpleAuth.sol";

contract Portfolio {
    SimpleAuth private auth;
    mapping (address => bool) private holdings;

    constructor (SimpleAuth control) public {
        auth = control;
    }

    function isHeld(address token) external view returns (byte status, bool held) {
        byte permission = auth.min(SimpleAuth.Level.Unregistered);
        if (FISSION.isBlocking(permission)) { return (permission, false); }
        return (FISSION.code(FISSION.Status.Found_Equal_InRange), holdings[token]);
    }

    function setTracking(address token, bool track) external returns (byte status) {
        FISSION.requireSuccess(auth.min(SimpleAuth.Level.Member));
        holdings[token] = track;
        return FISSION.code(FISSION.Status.Success);
    }
}

I’ll add this as an example to the spec. Thanks for raising that!

Regarding upgrades.

Please explain how to make a proxy for the existing CryptoKitties contract to add this feature using less than $100,000 of gas.

I see why you’re confused; not proxy but rather adapter. You create a contract that calls out to regaulr CryptoKitties, and returns the value plus some status code. The extra 700 gas from a CALL isn’t going to break the bank.

This will /always/ depend on the application.

I sympathize with this position! People should be application-specific enums and returning them along with their data. The problem is that there are a lot of contracts and downstream stanards out there that are looking for a high level of genericity and/or can benefit from helper libraries, collaborator contracts, and so on. These codes are indeed intentionally designed to handle common use cases, no all use cases.

“Waiting for a quorum to be achieved” is a ridiculous outcome for a standard, generic token transfer

For a bog-standard OZ ERC20? Of course I agree with you. Again, that’s not really what this enables. For a permissioned or multisig wallet this is a totally valid return! At the end of the day, most use cases fall into a handful of categories. Why make these contracts pluggable to a large number of producers and consumers of these messages that can’t know about your specific contract beforehand? I agree that this is not how things are done today: we’re stuck writing bespoke applications that don’t interoperate with each other, and live in isolation. If you start to see the blockchain as a web of autonomous actors rather than one-off single-call programs, we start to need some way of communicating more generically.

Does that help?

Here is the balance of my notes before.

Standardization is premature

Still, no existing implementations are deployed to mainnet. That makes it premature to standardize anything.

Reasons are not motivated

:white_check_mark: Documentation will be or has been added.

Nibbles

I was just recommending against short form. It will be more clear if all references to Success use the form 0x01 or hex"01" (pick one). This consistency can improve clarity.

It is wasteful in the default case

I still have yet to see any single instance where it would be helpful, in a general sense, to know that a call to castVote has the resulting status APP_SPECIFIC_SUCCESS rather than simply SUCCESS . EVM already has a mechanism for showing successful outcomes and the default successful outcome already indicates that the function did what it advertises to do.

I have not seen any counter point here yet.

This proposal would be great as a way to return semantic information during a revert (i.e. throw values instead of return values). I cited a comment that implements throw values. This would be a welcome paradigm shift.

It is not backwards compatible

It is claimed that an existing deployed ERC-721 could use an “adapter” to implement transfers using ERC-1066 DRAFT status codes with an additional 700 gas. But I have yet to see how.

I assert that this would require ~$100,000 worth of gas to implement just for the CryptoKitties contract.

It is unsafe

I disagree with the statement in the text:

The current state of the art is to … return a Boolean pass/fail status

My understanding is that current state of the art is to revert on failure. Also the statement:

Exceptional, state-breaking, or dangerous cases SHOULD revert!

Could be included in the EIP for clarity.

The async DEX examples are not realistic

:white_check_mark: We agree this example is not a good use case.

Realistic and useful use cases should be included in the specification. If only contrived examples can be provided then the value of the document is limited.

If a standard will be passed, the examples should be both conceivable and also deployed on mainnet.

Codes are ill-defined

Regarding successful outcomes, it should not be necessary for me to choose between “compatibility with bools” or other choices. The standard should have a prescriptive answer.

Regarding codes for unsuccessful outcomes, I have no complaints.

Again, I am a fan of using this proposal to describe unsuccessful outcomes.


Here are new notes introduced now.

The language is not strong enough

Please adopt language such as RFC 2119. This will improve clarity of the proposal.

Currently the specification is vague with statements such as:

Codes are returned as the first value of potentially multiple return values.

A much stronger and clearer statement would be:

A function that implements ERC-1066 MUST include a byte -typed value as the first return value and this value MUST follow the specification of the code table below.

It is currently unclear whether a contract can comply with ERC-1066 by having only some functions implement the protocol or if it is necessary for every function to implement the protocol.

Bytes32 is contradictory

The document states:

byte is quite lightweight, and can be easily packed with multiple codes into a bytes32 (or similar) if desired. It is also easily interoperable with uint8 , cast from enum s, and so on.

but later contradicts (using incorrect math, 4≠32):

Packing multiple codes into a single bytes32 is nice in theory, but poses additional challenges. Unused space may be interpeted as 0x00 Failure , you can only efficiently pack four codes at once, and there is a challenge in ensuring that code combinations are sensible. Forcing four codes into a packed representation encourages multiple status codes to be returned, which is often more information than strictly nessesary. This can lead to paradoxical results (ex 0x00 and 0x01 together), or greater resorces allocated to interpreting 2564 (4.3 billion) permutations.

Also, the example implementation does not follow this specification.

This document badly needs to use RFC 2119. At current, I don’t know if an application using multiple status codes packed into one bytes32 would be valid under this EIP. Presumably only the code >> 31 part would be covered by the specification, but this is currently ambiguous.

The output is wasteful

The proposal is rigid in limiting extensions:

Unspecified codes are not free for arbitrary use, but rather open for further specification.

but developers will be sure to demand extensions.

The Ethereum contract ABI specifies that return values will use a full word.

Reference: Contract ABI Specification — Solidity 0.8.26 documentation

This means the current ERC-1066 DRAFT specification (in one possible interpretation) prescribes one status byte and 31 bytes of zeros.

It may be helpful to use this specification:

  • A compliant function MUST have the status code as the first return value
  • The return value MUST be either byte or bytes32 type
  • The left-most 8 bits of the value MUST use the code table. i.e. byte(code)
  • A function that uses bytes32 return type MAY use the right-most 248 bits (i.e. bytes32(code) & ~bytes32(byte(0xff)) ) for application-specific status messaging.

If you choose this approach, definitely check with the Solidity and Vyper teams. You want to get a committment that if a function call returns a bytes32 when you are expecting a byte will not cause a problem. That committment should be documented in the Solidity and Vyper docs and should state that the return value will be truncated in this situation.

(And, per my notes above it will be better if this specification applies to throw values rather than return values.)

When there is revert with reasons, as there is now. I fail to see why a smart contract should ever return a status code. Smart contracts should not care about contracts independent outside of themselves. If an error occurs execution should stop, from a security perspective this is the most responsible thing to do.

Hi @decanus,

Thanks for your question!

You may be missing some of the idea behind this EIP. Other than the EIP text (which covers a lot of this), an explainer that a lot of people find helpful is the helper library’s README.

As mentioned in the spec, and in the thread above, the concept is not to handle errors or replace revert in any way whatsoever.

They are fully compatible with both revert and revert -with-reason.
~ The ERC-1066 Abstract

If you have an actual error (as opposed to an exception), you should absolutely revert immediately. In fact, this work makes it easier to revert with a message when combined with ERC-1444 (which was split out from ERC-1066). The helper library provides several functions for exactly this.

That’s great if your smart contract doesn’t depend on any collaborator contracts. This is possible for some, but not for many. In fact, many of the most powerful uses cases of smart contracts involve communication between them. You won’t own all of the state that you need to interact with (e.g. a regulation whitelist, NFT ownership, ACLs, &c), and need to move context around the system. To do this mechanically in a generalized way, you need some kind of system of messages. Yes, a message recipient with more context about a call may want to additionally revert.

You should think of this like HTTP responses. In API-driven services, you get both a payload and a status code. If you get a 202 Accepted on a request to create a resource, you can’t assume that the resource exists yet (that’s a 201 Created). It’s also not an error since it hasn’t been rejected (e.g. 422 Unprocessable Entity). You can assume that your resource may (or may not) be created at some point in the future, and either listen for a callback, or poll the collaborator. The problem space is different, which is why we have a more structured and generlizable system for statuses, but this is the general sort of thing that we’re doing with this ERC.

Does that help clarify?

Are there any examples (i.e. addresses) of smart contracts that are currently using status codes? If you provide a few of them, I can use them as test cases for QuickBlocks. One on of the things I’m interested in is how these status codes behave. If I understand correctly, they insert a single byte in the ‘output’ data field of a Parity trace. I’d like to dig into the details of what that looks like, and will be happy to write a Medium post about what I find.

Hi @tjayrush , I’ll likely be using it similar to how @0age mentioned above within the TPL protocol, i.e. returning just hex values that can be interpreted off-chain.

Mind you, the proposed way of using it is making the contracts much harder to read. Take a look at https://github.com/fission-suite/fission-codes/blob/ce563bddc98d0fb42a745cb316b93213073de292/contracts/examples/AgeValidator.sol#L30 … This makes it unclear as to what the function is all about, with most of the code just about figuring out the status code …

I prefer simply assigning the code, add comment and let the off-chain part figure it out from a lib and display to the user. Like:

require(_thisId != bytes32(0), hex"20"); //FISSION.code(FISSION.Category.Find, FISSION.Status.NotFound_Unequal_OutOfRange)

Nonetheless, I like ERC-1066 very much and am following it closely.

2 Likes

@thecryptofruit Is there a deployed contract on the mainnet?