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
function methodName1(uint256 _param1, address _param2, bytes calldata _data);: An example of a compliant method. It designates the trailing _data slot for behavior extension.function methodName2(uint256 _param1, address _param2);: An example of a sibling (overloaded) method without the extension argument.function methodName2(uint256 _param1, address _param2, bytes calldata _data);: An example of a compliant method that adds _data to a sibling method. When _data is an empty array, it must behave exactly like the sibling method.
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
- Follow best practices for each extended behavior: When the extension data is used for a specific extended behavior, such as a signature for on-chain verification or a commit value in a commit-reveal scheme, the best practices for each of those behaviors must be followed.
- Data arguments are public: A data argument can be seen by anyone when it is submitted to the mempool or included in a block. So the risks of replay attacks and transaction ordering manipulation attacks must be considered. Unencrypted personally identifiable information must never be put in a data argument.