|
| 1 | +--- |
| 2 | +hip: 0000 # Assigned by HIP editor. |
| 3 | +title: Ignore Trailing Calldata for System Contract |
| 4 | +author: Giuseppe Bertone (@neurone) |
| 5 | +working-group: Giuseppe Bertone (@neurone), Ian Holsman (@web3-nomad), Glib Kozyryatskyy (@gkozyryatskyy) |
| 6 | +requested-by: Squid <https://www.squidrouter.com>; Axelar <https://www.axelar.network>; Hedera Foundation <https://hedera.foundation> |
| 7 | +discussions-to: <URL of the GitHub Pull Request for this HIP> # This will be filled by the HIP editor upon PR creation. |
| 8 | +type: Standards Track |
| 9 | +category: Service |
| 10 | +needs-hiero-approval: Yes |
| 11 | +needs-hedera-review: Yes |
| 12 | +status: Draft |
| 13 | +created: 2025-11-14 |
| 14 | +updated: 2025-11-14 |
| 15 | +release: TBD |
| 16 | +--- |
| 17 | + |
| 18 | +## Abstract |
| 19 | + |
| 20 | +This HIP proposes enabling system contract to accept and ignore trailing calldata beyond the ABI-defined parameters of a function. This restores EVM-parity behavior where contracts typically ignore additional bytes in calldata, allowing applications to append small "memo" or order identifiers to standard ERC-20-like calls (e.g., `transfer`, `approve`) without causing transaction failure. The change is implemented by updating the parameter decoding used by system contract so that decoding succeeds when extra bytes remain after all expected parameters are parsed. The extra bytes are ignored by the system contract and preserved in the underlying Ethereum transaction/record for off-chain consumption. |
| 21 | + |
| 22 | +## Motivation |
| 23 | + |
| 24 | +Several cross-chain and attribution workflows append a compact memo (e.g., 32-byte order ID) to calldata so downstream systems can correlate external processes with on-chain actions. On most EVM chains, contracts either tolerate or ignore extra calldata beyond expected arguments, so these transactions succeed and the memo is retrievable from the transaction input data. |
| 25 | + |
| 26 | +System contracts currently fail when extra bytes are present in calldata, preventing ERC-20-style calls with a trailing memo from succeeding. This breaks compatibility for integrations relying on the memo pattern and requires workarounds that degrade developer experience. Enabling system contracts to accept and ignore trailing calldata resolves this incompatibility while preserving consensus integrity and transaction provenance. |
| 27 | + |
| 28 | +## Rationale |
| 29 | + |
| 30 | +- EVM compatibility: Typical Solidity contracts do not revert solely due to trailing calldata; the decoder consumes expected parameters and disregards the remainder. Bringing system contract behavior in line with this expectation reduces surprise and increases portability of EVM-based tooling. |
| 31 | +- Minimal scope: The change is localized to ABI parameter decoding within system contracts (and possibly shared libraries), with no changes to the system contract semantics or state transitions. |
| 32 | +- Data availability: The full calldata remains in the Ethereum transaction input; off-chain systems can continue to read memos via standard RPCs (e.g., `eth_getTransactionByHash`). HTS, account or transaction memo fields remain unrelated and are still only visible via native APIs. |
| 33 | +- Operational safety: Transaction size limits and fee/size checks already constrain worst-case payloads. |
| 34 | + |
| 35 | +## User stories |
| 36 | + |
| 37 | +- As a cross-chain router, I want to append an order ID to ERC-20 `transfer` calls routed through HTS system contract, so I can correlate off-chain orders with on-chain settlements. |
| 38 | +- As an analytics provider, I want to reliably extract app-defined identifiers from transaction calldata without modifying token contracts or system contracts. |
| 39 | +- As a dApp developer, I want my integration that works on other EVM networks to work on Hiero without special-case logic when using system contracts. |
| 40 | + |
| 41 | +## Specification |
| 42 | + |
| 43 | +This section defines the required behavior changes for system contracts. |
| 44 | + |
| 45 | +- Scope |
| 46 | + - HTS, HAS, HSS system contracts, including paths invoked via `redirectForToken` (see [HIP-218](https://hips.hedera.com/hip/hip-218)) that emulate ERC-20 functions (`transfer`, `transferFrom`, `approve`, etc.). |
| 47 | + - Apply the same decoding rule to all future system contracts to ensure consistent behavior across the precompile/system contract surface. |
| 48 | + |
| 49 | +- Decoding Rule |
| 50 | + - When decoding calldata for a system contract function, the decoder MUST: |
| 51 | + 1. Parse the function selector and decode the exact set of ABI-defined parameters for the matched function. |
| 52 | + 2. If additional bytes remain after successfully decoding all expected parameters, decoding MUST NOT fail solely due to the presence of these extra bytes. |
| 53 | + 3. The system contract MUST ignore (i.e., not read/use) any trailing bytes beyond the expected parameters. |
| 54 | + - If decoding of required parameters fails (e.g., insufficient bytes or invalid types), the call MUST revert as today. |
| 55 | + |
| 56 | +- Limits and Validation |
| 57 | + - Existing transaction size limits (including jumbo Ethereum transaction limits where applicable) remain in force. |
| 58 | + - No changes to fees or throttles are required. |
| 59 | + |
| 60 | +- Semantics |
| 61 | + - State transitions and events emitted by system contracts are unchanged. |
| 62 | + - The extra trailing bytes have no semantic effect and are not persisted in system contract-specific fields, but remain visible in the Ethereum transaction input data. |
| 63 | + |
| 64 | +- Observability |
| 65 | + - Mirror Node and RPC layers continue to return full transaction input data. No schema changes are required. |
| 66 | + - SDKs need no API changes; developers may continue using standard Ethereum JSON-RPC to read input data if desired. |
| 67 | + |
| 68 | +### Example Behavior |
| 69 | + |
| 70 | +- A call to an HTS-redirected ERC-20 `transfer(address,uint256)` with 20 extra bytes appended for a memo MUST succeed (assuming other checks pass) and perform an ordinary transfer, ignoring the memo bytes. The full calldata, including the memo trailer, is visible in the transaction input data via `eth_getTransactionByHash`. |
| 71 | + |
| 72 | +### Impact on Mirror Node |
| 73 | + |
| 74 | +- No schema changes. Transactions that previously failed due to strict decoding may now succeed; mirrors will observe corresponding status codes and logs. |
| 75 | +- The mirror node APIs are already compliant because they don't examine the calldata but just offer that information to clients. |
| 76 | + |
| 77 | +### Impact on the Mirror Node Explorer |
| 78 | + |
| 79 | +The mirror node explorer should show trailing data correctly: |
| 80 | + |
| 81 | +- In case the ABI is unknown, the raw input is visible as-is, including any trailing bytes. |
| 82 | +- In case the ABI is known, call arguments should be parsed and an extra field should be dedicated in the UI for the trailing data. |
| 83 | + |
| 84 | +### Impact on SDK |
| 85 | + |
| 86 | +- SDKs should adapt the Ethereum decoding functions so that they can safely ignore the additional info in the calldata. |
| 87 | +- No API changes required: the "memo-in-calldata" technique can be considered an EVM workaround, and there's no intention to offer this ability via SDK, where developers can leverage the dedicated memo field present in every Hiero transaction. |
| 88 | + |
| 89 | +## Backwards Compatibility |
| 90 | + |
| 91 | +This is a consensus rule change: transactions that previously failed when extra trailing calldata was present will now succeed. |
| 92 | + |
| 93 | +Mitigations: |
| 94 | + |
| 95 | +- Analyze all the mainnet and testnet Ethereum transactions calling the system contracts and failing because of malformed calldata. |
| 96 | + - In case transactions like that are present, put in place actions to contact the developers/users for those transactions: |
| 97 | + - Known actors: contact them directly via email, Slack, GitHub |
| 98 | + - Unknown actors: contact them over the network (void transfers with memo attached). |
| 99 | +- Document the change in release notes to alert any systems that may have (unusually) relied on reversion when extra bytes were present. |
| 100 | + |
| 101 | +Overall, the change aligns Hiero behavior with common EVM expectations and improves portability. |
| 102 | + |
| 103 | +## Security Implications |
| 104 | + |
| 105 | +- Misinterpretation risk: System contracts must explicitly ignore the trailing bytes and never act on them, avoiding semantic ambiguity. |
| 106 | +- Input size abuse: Extra bytes could be used to inflate input size. Existing transaction size limits and fees already constrain this. A configurable cap on trailing bytes further reduces risk. |
| 107 | +- Audit: Treat the decoder change as security-sensitive; apply standard code review and fuzz testing for malformed calldata. |
| 108 | + |
| 109 | +## How to Teach This |
| 110 | + |
| 111 | +- Concept: "Memo via calldata" is an EVM pattern where applications append memo or metadata to calls. System contracts will accept and ignore these bytes, while preserving them in the transaction input for off-chain reading. |
| 112 | +- Guidance: Continue using standard Ethereum JSON-RPC to obtain input data. Do not expect any `memo` field to reflect the appended calldata. |
| 113 | +- Examples: Provide docs showing `transfer(to, amount)` with an appended 32-byte order ID and how to retrieve it via RPC. |
| 114 | + |
| 115 | +### Examples |
| 116 | + |
| 117 | +Retrieve the transaction input via JSON-RPC and parse the trailing memo with Ethers.js. |
| 118 | + |
| 119 | +```js |
| 120 | +const { ethers } = require("ethers"); |
| 121 | + |
| 122 | +async function main() { |
| 123 | + const provider = new ethers.JsonRpcProvider("https://mainnet.hashio.io/api"); |
| 124 | + const txHash = "0x976965d65ee09f20fb152ff5e1de9490720a8e4e4a0707ae6093a6d0341f6bb1"; |
| 125 | + |
| 126 | + const input = (await provider.getTransaction(txHash)).data; |
| 127 | + const iface = new ethers.Interface(["function transfer(address to, uint256 amount)"]); |
| 128 | + const decoded = iface.decodeFunctionData("transfer", input); |
| 129 | + const trailingData = iface.encodeFunctionData("transfer", [decoded[0], decoded[1]]); |
| 130 | + const memoHex = "0x" + input.slice(trailingData.length); |
| 131 | + |
| 132 | + console.log("Calldata:", input); |
| 133 | + console.log("Decoded transfer:", decoded[1].toString(), "units to", decoded[0]); |
| 134 | + console.log("Unencoded memo:", memoHex); |
| 135 | +} |
| 136 | + |
| 137 | +main().catch(console.error); |
| 138 | + |
| 139 | +// Result |
| 140 | +// Calldata: 0xa9059cbb000000000000000000000000136c1cb3257dcf2615e25c914fd1767dcf08457700000000000000000000000000000000000000000000000000000000000000643372348f476a87cf942167a35e78e40f00c91c99072b0c515ba8118cbd927fb7 |
| 141 | +// Decoded transfer: 100 units to 0x136c1cb3257DCF2615E25c914fd1767dcf084577 |
| 142 | +// Unencoded memo: 0x3372348f476a87cf942167a35e78e40f00c91c99072b0c515ba8118cbd927fb7 |
| 143 | +``` |
| 144 | + |
| 145 | +## Reference Implementation |
| 146 | + |
| 147 | +The implementation depends on the current state of the code base, but here is an overall potential flow that satisfies the HIP: |
| 148 | + |
| 149 | +- Update the ABI decoding library used by system contracts (and shared system contract decoding utilities if present) to: |
| 150 | + - Succeed when extra bytes remain after expected parameters are decoded. |
| 151 | + - Ignore the trailing bytes without attempting to deserialize them. |
| 152 | +- Add unit and property/fuzz tests covering: |
| 153 | + - Exact-length calldata (baseline) |
| 154 | + - Calldata with small trailing bytes (success) |
| 155 | + - Malformed inputs (revert) |
| 156 | + |
| 157 | +## Rejected Ideas |
| 158 | + |
| 159 | +- Map trailing calldata to HTS `memo` automatically: Rejected to avoid conflating EVM calldata with native memo semantics and to keep behavior consistent with EVM norms. |
| 160 | +- Introduce new system contracts methods with explicit `bytes memo` parameters: Heavier API surface change with less compatibility benefit; ignores off-chain tools already reading calldata. |
| 161 | +- Limit to only `transfer`: Applying uniformly across HTS and all system contracts is simpler, less surprising for developers and users, and fully EVM-compatible. |
| 162 | +- Limit this HIP to HTS system contract only: Applying the change to all present and future system contracts ensures consistent behavior and avoids confusion. |
| 163 | + |
| 164 | +## Open Issues |
| 165 | + |
| 166 | +- Decide whether there should be a maximum tolerated trailing bytes cap and, if so, determine its value. |
| 167 | + |
| 168 | +## References |
| 169 | + |
| 170 | +- Ethereum StackExchange: "Is there a way to attach a memo to an ERC-20 approve transaction" (<https://ethereum.stackexchange.com/questions/138779/is-there-a-way-to-attach-a-memo-to-an-erc-20-approve-transaction>) |
| 171 | +- Example success on Arbitrum with 3rd argument ignored: <https://arbiscan.io/tx/0x4d17711d22420314c7da40df8a7262944fe41ac394d508e79e48bc71add39e51> |
| 172 | +- Example failure on Hedera (strict decoding): <https://hashscan.io/mainnet/transaction/1762783522.983266000/result> |
| 173 | + |
| 174 | +## Copyright/license |
| 175 | + |
| 176 | +This document is licensed under the Apache License, Version 2.0 — |
| 177 | +see [LICENSE](../LICENSE) or <https://www.apache.org/licenses/LICENSE-2.0>. |
0 commit comments