--- eip: 7899 title: On-Chain Function Return Type Encoding description: This EIP encodes on-chain return type encoding improving interoperability between smart contracts. author: Paul Edge (@genkifs) discussions-to: https://ethereum-magicians.org/t/on-chain-function-return-type-encoding-for-solidity-functions/23040 status: Draft type: Standards Track category: Interface created: 2025-03-01 requires: 165 --- ## Abstract This EIP proposes an on-chain encoding format for Solidity function return types, allowing contracts to algorithmically determine the structure of returned data. The encoding scheme is stored in a `bytes` dynamic array and supports elementary types, tuples, fixed arrays, and dynamic arrays, making it efficient for storage and computation. This enables interoperability between smart contracts, particularly in cases where the source code is not published or where return types need to be introspected dynamically. ## Motivation Currently, solidity does not provide a standardized on-chain method for discovering the return types of a function. ABI documentation of outputs is only provided in an off-chain JSON file, not on-chain bytecode. Existing workarounds (such as off-chain ABI parsing or predefined mappings) introduce trust assumptions and inefficiencies. **ERC-165** is used to confirm if a contract has implemented this EIP to provide encoded return types. This makes the approach an optional parallel implementation that augments existing ABI practices without interference. The contract requesting and decoding return type information is not required to implement any interface or functionality. **Use Cases** Cross-Contract Calls: Allow smart contracts to safely interpret function return values without predefined knowledge of their structure. Future Proofing: New functions may be added existing contracts or new contracts linked. This EIP is particularly useful for when interface definitions of called contracts are not available at compile-time. Adapting to change: Mutable functions in proxies may require the ability to change their return type (in particular add new return variables). Documented Automation: This enables bytecode analysis and process automation documentation without requiring source code access. Abstract functions: Can be used when the function being called has encrypted/obfuscated/unverified code but the interface for the returned result needs to be known in advance. Middleware and Proxies: Enable middleware contracts (such as meta-transactions and upgradable proxies) to process unknown function outputs dynamically. Interoperability & Reflection: Facilitate contract self-introspection and interoperability between different smart contract frameworks. On-Chain Oracles & Data Feeds: Allow consumers to verify the expected structure of returned data without relying on off-chain ABI metadata. ## Specification ### Encoding Scheme A function’s return type **MUST** be encoded as a `bytes` variable, where each byte represents a type token. The first `N` bytes (starting at index `0`) **MUST** contain type tokens filling the length of the array. To identify that the contract can broadcast return types, it **MUST** implement the **ERC-165** standard with the `ITypeReturn` interface exposing the function `funcReturn(bytes4)`. ### Token Definitions * 0x01: bool * 0x02: address * 0x0C: complex numbers * 0x10–0x2F: uint8 ... uint256 (each increment means +8 bits) * 0x30–0x4F: int8 ... int256 * 0x50–0x6F: bytes1 ... bytes32 * 0xB0: bytes (dynamic) * 0xB1: string * 0xE0: dynamic array marker; a dynamic array is encoded as [0xE0, elementType] * 0xE1: fixed array marker; a fixed array is encoded as [0xE1, length, elementType] * 0xF0: tuple start, 0xF1: tuple end --- ### ELI5: The Mystery Pump Dispensers Imagine you're at a buffet, and there’s a row of pump dispensers filled with different sauces—hot sauce, mustard, and chocolate syrup. But here’s the problem… none of them have labels! Right now, if you want to know what’s inside, you have to: * Find the manual (like trusting external documentation). * Go to a different room to read a poster (like looking up an off-chain ABI repository). * Ask the chef and hope they remember (like checking the contract’s source code). That’s slow, messy, and confusing. You just want hot sauce for your taco, but you have no idea which pump to press! This proposal is like adding simple, encoded labels to every pump,like a color-coded dot on each one. Now you can just glance at it and instantly know: * Red dot = Hot sauce! (for tacos & wings) * Brown dot = Chocolate sauce! (for ice cream & waffles) * Yellow dot = Mustard! (for burgers & hot dogs) This way, no one accidentally puts hot sauce on their ice cream. Of course, no one is forcing colored dots to be put on pumps, but if they do then the colors are the same for everyone. How This Helps Smart Contracts * No More Guessing! Contracts don’t have to check off-chain sources to understand return data. * Faster & Simpler! No need to rely on source code or external ABIs—everything is on-chain. * Less Mess! Contracts won’t get unexpected return types, reducing errors. This EIP brings a clear, built-in labeling system for smart contracts, just like adding color-coded labels to sauce dispensers that match the right foods! --- **Examples** 1. **Encoding `uint256[]` (Dynamic Array)** ```solidity uint8[] tokens; tokens.push(0xE0); // Dynamic array marker tokens.push(0x2F); // uint256 bytes32 encoded = encode(tokens); // Decodes to: "uint256[]" ``` 2. Encoding address[5] (Fixed Array) ```solidity uint8; tokens[0] = 0xE1; // Fixed array marker tokens[1] = 5; // Array length tokens[2] = 0x02; // address bytes32 encoded = encode(tokens); // Decodes to: "address[5]" ``` 3. Encoding (uint256, bool[]) (Tuple with Dynamic Array) ```solidity uint8; tokens[0] = 0xF0; // Tuple start tokens[1] = 0x4F; // int256 tokens[2] = 0xE0; // Dynamic array marker tokens[3] = 0x01; // bool tokens[4] = 0xF1; // Tuple end bytes32 encoded = encode(tokens); // Decodes to: "(int256, bool[])" ``` ## Rationale On-chain it is currently not possible to determine the return type(s) of a given contract's functions. This EIP is an optional, standardized way to provide such information. ### Function Selector From soliditylang documentation >The first four bytes of the call data for a function call specifies the function to be called. **The return type of a function is not part of this signature**. The JSON description of the ABI contains both inputs and outputs. This documentation means * The on-chain binary data does not encode the outputs. * The off-chain human readable *description* of the ABI does contain output information, but it is inaccessible to on-chain contracts. * Neither inputs nor outputs are decoded by the ABI. ### Why Not Use keccak256(ABI)? **EIP-712** encodes types and creates a TYPEHASH from the return string. A simple hash-based approach (e.g., keccak256(abi.encodePacked(returnTypes))) does not allow decoding on-chain without an extensive mapping. Additionally, a hash is not an efficient store of data. This solution fully preserves type structure, allowing contracts to interpret data without off-chain or on-chain lookups. ### Gas Efficiency This encoding fits in variable bytes array, making it cheap to store and transmit while still supporting complex type structures. Dynamic arrays and tuples are algorithmically parsed rather than using a hash lookup table. For gas efficency the same token encoding could be sent in a byte32 word with trailing bytes set to 0x00, but this standard will assume a bytes array. Not all byte values are used, so future return types can be incorporated. Assuming no changes to prior codes are made, existing contracts implementing the standard will not need to be altered. ### Off chain tools Off-chain wrappers decoding function output may or may not be implemented. Their existence would be welcome but is not essential. The target user for this functionality is on-chain contracts that need to know the shape of the data they are receiving. ### Limitations Fixed array lengths must fit in 1 byte (0-255). An additional code could be assigned for longer fixed arrays. A Bytes array is chosen to allow an extendable number of output types. A cheaper bytes32 encoding could be adopted but then at most 32 tokens can be encoded (since each takes 1 byte) meaning deeply nested structures may hit size limits. ## Backwards Compatibility No backward compatibility issues found. ## Test Cases Library instancing ```solidity interface ITypeReturn { //Return the encoded return type for the given function signature. function funcReturn(bytes4 funcSig) external view returns (bytes memory); } contract TypeSignatureExample is ITypeReturn{ using TypeEncoding for uint8[]; /** * @notice Encodes a type signature. * @return The bytes32 encoded signature. */ function exampleEncode() public pure returns (bytes32) { uint8[] memory tokens = new uint8[](5); tokens[0] = TypeEncoding.TOKEN_TUPLE_START; tokens[1] = TypeEncoding.TOKEN_UINT_256; // 0x1F means uint256. tokens[2] = TypeEncoding.TOKEN_ADDRESS; tokens[3] = TypeEncoding.TOKEN_BOOL; tokens[4] = TypeEncoding.TOKEN_TUPLE_END; return tokens.encode(); } function funcReturn(bytes4 funcSig) public pure override returns (bytes32 ans) { if(funcSig == bytes4(keccak256("exampleEncode()"))){ //"exampleEncode()": "a8bc58f4", uint8[] memory tokens = new uint8[](1); tokens[0] = TypeEncoding.TOKEN_BYTES_FIXED_32; ans = tokens.decode(); } } } ``` Unit Tests in Hardhat (Chai + Ethers.js) ```javascript const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("TypeEncoding", function () { let contract; before(async function () { const TypeEncodingLib = await ethers.deployContract("TypeEncoding"); const TypeContract = await ethers.getContractFactory("TypeSignatureExample", { libraries: { TypeEncoding: TypeEncodingLib.target } }); contract = await TypeContract.deploy(); }); it("Should encode a function return type signature correctly", async function () { const expectedEncoding = "0xf02f0201f1"; const encoded = await contract.exampleEncode(); expect(encoded).to.equal(expectedEncoding); }); it("should return 32 bytes when the function signature is a8bc58f4", async function () { // This is the keccak256 hash of "exampleEncode()" which should be a8bc58f4 const exampleEncodeSignature = "0xa8bc58f4"; const expectedEncoding = "0x6f"; // Call the function with the correct signature const encoded = await contract.funcReturn(exampleEncodeSignature); expect(encoded).to.equal(expectedEncoding); }); }); ``` ## Reference Implementation ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; library TypeEncoding { uint8 constant TOKEN_END = 0x00; // End-of-stream marker (unused) uint8 constant TOKEN_BOOL = 0x01; uint8 constant TOKEN_ADDRESS = 0x02; uint8 constant TOKEN_COMPLEX = 0x0C; uint8 constant TOKEN_UINT_8 = 0x10; // 0x0F == uint8, …, 0x1F == uint256 uint8 constant TOKEN_UINT_256 = 0x2F; uint8 constant TOKEN_INT_8 = 0x30; // 0x30 == int8, …, 0x4F == int256 uint8 constant TOKEN_INT_256 = 0x4F; uint8 constant TOKEN_BYTES_FIXED_1 = 0x50; // 0x50 == bytes1, …, 0x6F == bytes32 uint8 constant TOKEN_BYTES_FIXED_32 = 0x6f; uint8 constant TOKEN_BYTES_DYNAMIC = 0xB0; // dynamic bytes uint8 constant TOKEN_STRING = 0xB1; uint8 constant TOKEN_TUPLE_START = 0xF0; uint8 constant TOKEN_TUPLE_END = 0xF1; uint8 constant TOKEN_ARRAY_DYNAMIC = 0xE0; // dynamic array marker uint8 constant TOKEN_ARRAY_FIXED = 0xE1; // fixed array marker /** * @notice Encodes an array of uint8[] tokens into a bytes array. * @param tokens Array of uint8 tokens. * @return result The encoded bytes stream. */ function encode(uint8[] memory tokens) internal pure returns (bytes memory result) { result = new bytes(tokens.length); for (uint256 i = 0; i < tokens.length; i++) { result[i] = bytes1(tokens[i]); } } /** * @notice Decodes an encoded bytes stream into an array of tokens. * @param encoded The bytes token stream. * @return tokens An array of tokens. */ function decode(bytes memory encoded) internal pure returns (uint8[] memory) { uint8[] memory tokens = new uint8[](encoded.length); for (uint256 i = 0; i < encoded.length; i++) { tokens[i] = uint8(encoded[i]); if (tokens[i] == TOKEN_END) { break; } } return tokens; } // --- Parsing Functions --- struct ParseResult { string str; // Human–readable type string uint pos; // Next token position after parsing } /** * @dev Parses a comma–separated list of types until a terminator token is reached. * Used for tuple parsing. * @param tokens The token array. * @param start The starting index. * @param terminator The token that terminates the list. * @return pr A ParseResult with the comma-separated types and updated position. */ function _parseTypeList(uint8[] memory tokens, uint start, uint8 terminator) internal pure returns (ParseResult memory pr) { string memory output = ""; uint pos = start; bool first = true; while (pos < tokens.length && tokens[pos] != terminator) { ParseResult memory sub = _parseType(tokens, pos); if (!first) { output = string(abi.encodePacked(output, ",", sub.str)); } else { output = sub.str; first = false; } pos = sub.pos; } require(pos < tokens.length, "Terminator not found"); // Consume the terminator token. pos++; return ParseResult(output, pos); } /** * @dev Parses a single type starting at position `pos`. * Supports elementary types, tuples, and arrays. * @param tokens Array of tokens. * @param pos The starting index. * @return pr A ParseResult with the type string and updated position. */ function _parseType(uint8[] memory tokens, uint pos) internal pure returns (ParseResult memory pr) { require(pos < tokens.length, "Unexpected end of tokens"); uint8 token = tokens[pos]; if (token == TOKEN_TUPLE_START) { // Tuple: parse the comma-separated list until TOKEN_TUPLE_END. ParseResult memory inner = _parseTypeList(tokens, pos + 1, TOKEN_TUPLE_END); return ParseResult(string(abi.encodePacked("(", inner.str, ")")), inner.pos); } else if (token == TOKEN_ARRAY_DYNAMIC) { // Dynamic array: token then element type. ParseResult memory elem = _parseType(tokens, pos + 1); return ParseResult(string(abi.encodePacked(elem.str, "[]")), elem.pos); } else if (token == TOKEN_ARRAY_FIXED) { // Fixed array: token, then one token for the length, then element type. require(pos + 1 < tokens.length, "Missing fixed array length"); uint8 lenToken = tokens[pos + 1]; ParseResult memory elem = _parseType(tokens, pos + 2); string memory lenStr = ToString(lenToken); return ParseResult(string(abi.encodePacked(elem.str, "[", lenStr, "]")), elem.pos); } else { // Elementary type: convert token to its type name. return ParseResult(_tokenToTypeName(token), pos + 1); } } /** * @notice Returns a human–readable string for an encoded type signature. * @param encoded The bytes array encoding. * @return A string representation (for example, "uint256[]", "address[5]", or "(uint256,address,bool)"). */ function typeSignatureToString(bytes memory encoded) internal pure returns (string memory) { uint8[] memory tokens = decode(encoded); ParseResult memory pr = _parseType(tokens, 0); require(pr.pos == tokens.length, "Extra tokens remaining"); return pr.str; } /** * @notice Returns a human–readable string for an encoded type signature. * @param encoded The bytes32 encoding. * @return A string representation (for example, "uint256[]", "address[5]", or "(uint256,address,bool)"). */ function typeSignatureToString(bytes32 encoded) internal pure returns (string memory) { uint8[] memory tokens = decode(encoded); ParseResult memory pr = _parseType(tokens, 0); require(pr.pos == tokens.length, "Extra tokens remaining"); return pr.str; } /** * @dev Converts a single token into its type name. */ function _tokenToTypeName(uint8 token) internal pure returns (string memory) { if (token == TOKEN_BOOL) { return "bool"; } if (token == TOKEN_ADDRESS) { return "address"; } if (token == TOKEN_COMPLEX) { return "complex"; } if (token >= TOKEN_UINT_8 && token <= TOKEN_UINT_256) { //0x1f - 0x10 + 1 = 31 - 16 + 1 = 16 uint bits = uint(token - TOKEN_UINT_8 + 1) * 8; return string(abi.encodePacked("uint", ToString(bits))); } if (token >= TOKEN_INT_8 && token <= TOKEN_INT_256) { uint bits = uint(token - TOKEN_INT_8 + 1) * 8; return string(abi.encodePacked("int", ToString(bits))); } if (token >= TOKEN_BYTES_FIXED_1 && token <= TOKEN_BYTES_FIXED_32) { uint size = token - TOKEN_BYTES_FIXED_1 + 1; return string(abi.encodePacked("bytes", ToString(size))); } if (token == TOKEN_BYTES_DYNAMIC) { return "bytes"; } if (token == TOKEN_STRING) { return "string"; } return "unknown"; } /** * @notice Turns a uint number into a string * @dev No decimal places * @param _i value to convert. * @return _uintAsString string */ function ToString(uint _i) public pure returns (string memory _uintAsString) { if (_i == 0) { return "0"; } uint j = _i; uint len; while (j != 0) { len++; j /= 10; } bytes memory bstr = new bytes(len); uint k = len; while (_i != 0) { k = k-1; uint8 temp = (48 + uint8(_i - _i / 10 * 10)); bytes1 b1 = bytes1(temp); bstr[k] = b1; _i /= 10; } return string(bstr); } } ``` ## Security Considerations Encoding errors can cause misinterpretation of return values. Tools to generate encoded returns with validation and error handling should be implemented. Contract developers must validate the structure before processing encoded types. It is essential that there is no misalignment in decoding logic across different contracts. This is why a common encoding of return values needs to be adopted as an ecosystem-wide standard. A contract could promise one type of return data and deliver another. However, it should be noted that calling any unknown contract code is inherently risky, regardless of the return type. Open Question: Is it worth sacrificing some return data space to implement a checksum or similar validation mechanism? ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).