ERC-5750 A trailing bytes argument for method extension (General Extensibility for Method Behaviors)

Final · Other · created 2022-10-04

A standard that makes the last argument of a function a variable-length byte string (bytes) and establishes that slot as extension space for behavior to be added later.

Why it is needed

To add a new feature to a function that is already deployed and in use, there are only two ways: add a new function, or change or add to the existing arguments. Either way, each project gives its own answer to questions such as what to name it, how many arguments of which types to use, and whether to support off-chain signatures. As a result, functions with similar purposes multiply in inconsistent shapes, and wallets and other contracts find it hard to handle them uniformly. This standard settles on a single answer. If the last argument is a bytes and that slot is left empty and reserved for extension, new behavior such as attaching a signature or commit-reveal can be added without changing the function declaration. Many standards, such as EIP-721 and EIP-1155, already have this shape, so the aim is compatibility with both past implementations and future extensions.

What to implement

In depth

Compliance is judged per method, not per contract

Most ERCs check whether an entire contract follows the specification. This standard is different. It asks whether each individual function is compliant. So a contract can contain compliant methods mixed with unrelated methods without any problem. The Foo contract in the original example has methodName1 along with firstNonRelatedMethod and secondNonRelatedMethod, but compliance is asked only about methodName1. The requires field in the header lists EIP-165, but the body does not explain how it is used.

Eligible methods and compliant methods

Every method whose last argument is bytes is an eligible method. The form is function methodName(type1 value1, type2 value2, ... bytes data). Here bytes must be interpreted as Solidity's dynamically sized byte array. Among eligible methods, only those that designate the last bytes for extending behavior are compliant methods. In other words, even if the declaration has the same form, a method is not compliant if it does not open that argument up for extension. Non-compliant example 3 in the original is identical to methodName1 down to the last character in its declaration, but it is classified as non-compliant because it does not allow extending behavior through _data.

If a sibling overload exists, it must behave identically when the array is empty

If there is an overloaded method with the same name as the compliant method whose preceding arguments are exactly the same except for the last bytes, the two are siblings. In that case, passing an empty array to the compliant method MUST produce the same behavior as the sibling method. methodName2(uint256, address) and methodName2(uint256, address, bytes) in original Example 2 are such a pair. Example 4 has the same declaration but behaves differently from its sibling when _data is an empty array, so it is non-compliant. Thanks to this rule, existing callers that know nothing about the extension argument can use the method without worrying that results will change.

Why put dynamic bytes last

Using dynamically sized bytes can hold a payload of any type, which gives the greatest flexibility. The reason for placing it last is that it fits Solidity's calldata layout. The original Motivation gives examples of what can go in this slot: reason strings in transfer or vote overloads, values that prove an endorsement, salts, nonces, and commit values for commit-reveal schemes. It also mentions passing the data straight through to a callback. It cites EIP-5732 as a commit-reveal case, and another case where a digital signature is sent along with a method call.

Existing standards whose methods already comply

Several ERCs already have methods of this form and so follow this standard fully or partially. The list given in the original is as follows. safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) in EIP-721. safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) and safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) in EIP-1155. burn(uint256 amount, bytes calldata data) and send(address to, uint256 amount, bytes calldata data) in EIP-777. The original states that this list is not exhaustive.

A method whose last bytes is used for its functionality is not compliant

Even if the last argument is bytes, the method is not compliant if that value is used for the function's own functionality. The original gives two cases: diamondCut(FacetCut[] calldata _diamondCut, address _init, bytes calldata _calldata) in EIP-2535 and isValidSignature(bytes32 _hash, bytes memory _signature) in EIP-1271. In each, _calldata and _signature take part in the functionality. To make them compliant, one of two approaches MUST be chosen: create a new overload that appends bytes calldata _data after all existing arguments, or loosen the purpose of the existing last bytes argument so that it also allows extended behavior.

Things to watch out for

Original ERC · ← ERCs