CHANGELOG.md

# Unreleased

# 1.4.0 - 2026-05-01

## Atom-creation hardening on `decode_structs: true` path

* **Security hardening — `String.to_atom/1` removed from both atom-creation call sites in `lib/abi/type_decoder.ex` (`tuple_value/3`) and `lib/abi/type_encoder.ex` (`fetch_by_name/2`).** Both call sites previously created atoms from contract-supplied field names (gated by `sobelow_skip ["DOS.StringToAtom"]` annotations on the trust assumption that "field names come from trusted ABI metadata"). The assumption breaks the moment a consumer ingests ABIs from arbitrary sources (block explorers, contract registries, user-submitted JSON, indexer feeds) — the atom table is a non-reclaimable VM resource, so an attacker who controls field names can DoS the BEAM. Both call sites now route through `String.to_existing_atom/1`. The `sobelow_skip` annotations are removed.
* **Decoder — observable contract change on the opt-in path.** `ABI.TypeDecoder.tuple_value/3` (called whenever `decode/3` / `decode_call/3` / `decode_event/4` runs with `decode_structs: true`) now requires every snake_case field atom to already exist in the VM atom table. New private helper `atom_key_for!/1` calls `String.to_existing_atom/1` on `Macro.underscore(name)` and re-raises `ArgumentError` with a migration hint naming both the underscored atom (`":#{underscored}"`) and the original ABI field (`"#{name}"`) when the atom hasn't been interned. Code that already references the decoded map's atoms (typical: `%{from: from, to: to, value: v} = decoded` or a `@field_atoms [:from, :to, :value]` module attribute) interns them at compile time and is unaffected. Code that decodes ABIs ingested at runtime without ever referencing the resulting atoms must intern them once before the first decode call. The `decode_structs: true` doctest in `ABI.decode/3` continues to pass because the assertion `%{a: 10, b: true}` interns `:a` / `:b` at module load.
* **Encoder — silent safety upgrade, no contract change.** `ABI.TypeEncoder.fetch_by_name/2`'s atom is only used as a `Map` lookup key, never returned as data, so atom *creation* was never necessary — a consumer's input map can only contain atom keys that already exist in the VM. New private helper `existing_atom/1` returns the existing atom or `nil`; the cond branch falls through to the next clause when the atom doesn't exist, with no observable behavior difference for any caller (string-key match, atom-key match, and miss-both-keys all behave identically to before). The miss-both raise message now reports the underscored snake_case form as a string (`:my_field`) rather than calling `inspect/1` on a freshly-created atom — strictly clearer.
* **Tests.** Added a `decode_structs: true atom safety` describe block in `test/abi/type_decoder_test.exs`: raise message names both the atom and the ABI field, success path with pre-interned atoms, fall-through to tuple when `decode_structs` is false, fall-through to tuple when any field name is empty. Added two encoder tests in `test/abi/type_encoder_test.exs` exercising the new `existing_atom/1 → nil` branch (string-key match still succeeds when the snake_case atom doesn't exist; missing-key raise reports the snake_case form correctly). All field-name strings in the new failure tests use guaranteed-uninterned suffixes (`Z47Q`-style) verified absent across `lib/`, `test/`, and `src/`. Test count 339 → 345; coverage on `ABI.TypeDecoder` 98.21% → 98.39% and `ABI.TypeEncoder` 99.28% → 99.30%.
* **Manifest.** `api(:decode, ...)`'s `opts` description now documents the pre-intern contract; `returns.type` corrected from `:list` (was inaccurate) to `:union`. `tuple_value/3`'s `@doc` rewritten to lead with the atom-table contract. Manifest entry count unchanged (32). Content diffs in the regenerated `mix hieroglyph.manifest` artifact: those two `api(:decode, ...)` description fields, the `tuple_value/3` `@doc` body, and the `generated_at` timestamp; in addition, the JSON output reflects a global `signature`/`spec` key-order swap (descripex emission ordering — content unchanged, key set unchanged).
* **No benchee dep added.** `String.to_existing_atom/1` is the same hash-table lookup as `String.to_atom/1` on the hit path; the miss path is configuration-time (a one-time module-load cost in the consumer), not a hot user-request path. Speculative benchmarking would have added a dev-only dep without a workload to measure against. If a regression surfaces in cartouche / onchain CI post-merge, add benchee then.
* **Why minor bump (1.3.0 → 1.4.0) not patch.** Behavior change on a documented opt-in path; not breaking the wire format, not breaking the default decoder, but observable for callers passing `decode_structs: true` against ABIs whose field names were never referenced in their code. Conservative minor matches the 1.1.0 / 1.2.0 precedent (1.1.0 added new public APIs alongside fixes; 1.2.0 narrowed `decode_event/4`'s error contract).
* **Upstream filing.** Per project policy ("No upstream gating … file issues/PRs upstream, then ship the fix here immediately"), the local change ships in this release; an upstream issue on `exthereum/abi` will be filed describing the DoS surface and proposing either the same `to_existing_atom` switch or a `decode_structs: :existing_atoms` opt-in for back-compat — maintainers' choice.

# 1.3.0 - 2026-05-01

## `function` ABI type — encode + decode + packed

* **New type support — Solidity `function` ABI type.** Lifts the parse-time rejection that previously raised `ArgumentError` on any signature mentioning `function` (per [upstream issue #54](https://github.com/exthereum/abi/issues/54)). Solidity's `function` type is a 24-byte external function pointer: 20-byte address ++ 4-byte selector, encoded as a 24-byte payload right-padded to 32 bytes in standard mode (wire-format identical to `bytes24`), or 24 bytes tight in packed mode. Concretely: `ABI.encode("foo(function)", [<<addr::binary-20, sel::binary-4>>])` produces a 36-byte calldata (4-byte selector + 32-byte slot); `ABI.decode("foo(function)", payload)` returns the original 24-byte binary. Composed types (`function[]`, `function[N]`, `(uint256, function)`, structs containing `function`) work via the existing recursive composition — no new array/tuple plumbing needed.
* **Production code touched.** Removed the `:function` clause from `ABI.Parser.reject_unsupported!/1` (parse-time gate); added `:function` to `ABI.FunctionSelector.@type type` and a `dynamic?(:function), do: false` clause; added `encode_type(:function, _)` clauses to `ABI.TypeEncoder` (binary-only input — wrong-size and non-binary inputs raise `ArgumentError` with a payload-shape hint); added a matching `packed_top(:function, _)` pair so `encode_packed/2` accepts `function` (24 bytes tight per spec) instead of falling into the unsupported-type catch-all; added `decode_type(:function, _, _)` to `ABI.TypeDecoder` delegating to `decode_bytes/3` with right-pad. `encode_bytes/1` and `decode_bytes/3` already do the 24→32 byte rounding via `Math.pad/4` and `Math.unpad/3` — reused, not duplicated.
* **Input shape — 24-byte binary only.** No `{addr, sel}` tuple form, no integer form. Parallel to how `{:bytes, N}` accepts a binary of exact size. Avoids confusion with the Solidity tuple type `(bytes20, bytes4)`. Caller can always `addr <> sel` if they have parts separately. Decode returns the raw 24-byte binary (symmetric with encode); caller pattern-matches `<<addr::binary-20, sel::binary-4>> = decoded` if needed.
* **`fixed<M>x<N>` / `ufixed<M>x<N>` stay deferred.** Solidity itself does not fully support fixed-point types — quoting the [language docs](https://docs.soliditylang.org/en/latest/types.html): *"Fixed point numbers are not fully supported by Solidity yet. They can be declared, but cannot be assigned to or from."* No real contracts emit them, so there's nothing to encode/decode in the wild. Parse-time rejection narrowed to fixed/ufixed only; the rejection-clause comment now cites the Solidity-side reason rather than treating all three types as a single class. README's Support table notes the deferral inline.
* **Tests.** Converted 5 `:function` parse-rejection tests in `test/abi/function_selector_test.exs` to positive-acceptance assertions in a new "`function` type acceptance" describe block; renamed/split the canonical-signature describe block since `:function` is no longer dead-via-parse. Added `:function` to the `@leaf_types` list and `value_for/1` dispatcher in `test/abi/roundtrip_property_test.exs`, plus a dedicated property — the recursive composite property at depth ≤ 5 now exercises `function[]`, `function[N]`, and `(uint256, function)` automatically. Added focused unit tests in `test/abi/type_encoder_test.exs` (slot layout, wrong-size raise, non-binary raise) and `test/abi/type_decoder_test.exs` (right-pad strip, round-trips inside `(uint256, function, bool)`, `function[3]`, and `function[]`). Added a `function: 24 bytes tight` golden vector + size-mismatch raise to `test/abi/encode_packed_test.exs`.
* **Manifest.** No public-API surface change — `dynamic?/1` arity is unchanged and the new `function`-handling lives entirely in private clauses. `mix hieroglyph.manifest` re-emits an `api_manifest.json` with identical content vs. 1.2.0 (the only diffs are the regenerated `generated_at` timestamp and JSON key ordering — `jq -S 'del(.generated_at)'` produces a byte-identical file).
* **Upstream PR.** Independent feature PR against `exthereum/abi` (parser-clause-deletion + encoder/decoder/packed clauses + tests) — not bundled with #53/#54/#55 follow-ups; small and self-contained.

# 1.2.0 - 2026-05-01

## Public Surface Pass

* **Public Surface Pass bundle — `decode_event/4` error contract narrowed.** The `decode_event/4` `@spec` previously declared `{:ok, ...} | {:error, term()}`, but the runtime path raised on malformed payloads instead of returning `{:error, _}` — so the typespec was a lie wherever a topic-matched log carried truncated/garbage data. Wrapped the `decode_raw`-driven payload-decode path in a `try/rescue` and converted raised exceptions into `{:error, {:malformed_data, message}}`. `verify_event_signature/2` already returned `{:error, ...}` for signature mismatches but used a string format; tightened to the atom-tagged shape `{:error, {:event_signature_mismatch, %{expected: ..., got: ...}}}` so callers can pattern-match the failure mode without parsing strings. Added `@type ABI.Event.decode_error/0` enumerating the closed error set (`{:event_signature_mismatch, _}`, `{:topics_length_mismatch, _}`, `{:malformed_data, _}`); narrowed `ABI.decode_event/4`'s `@spec` from `{:error, term()}` to `{:error, Event.decode_error()}`. The `api()` declaration now carries an `errors:` block mirroring `decode_call/3`'s pattern, so the manifest exposes the closed error set to agent consumers. Bugfix-honoring-typespec — not a breaking change in any sensible sense (the prior contract was unreachable for malformed payloads), but downstream callers that previously caught raises around `decode_event/4` should switch to matching `{:error, {:malformed_data, _}}`.
* **Public Surface Pass bundle — `encode_bytes/1` flipped from `def` to `defp`.** Hygiene-only flip. Already `@doc false` (lib/abi/type_encoder.ex), zero callers outside `type_encoder.ex` itself across the local monorepo (`cartouche`, `onchain`, `onchain_{aave,evm,js,tempo}`, `mpp`), and the agent-economy hint-rot test had already excluded it as a deliberate internal helper. The `def` was a leftover from before `encode_raw/2` became the canonical raw-encoding entrypoint. Removed the now-redundant `@doc false` and the explicit exclusion entry from `test/abi/agent_economy_test.exs` (the function is no longer in `module_info(:exports)`, so the hint-rot cross-check skips it naturally). Manifest user-declared count unchanged (already excluded). ROADMAP's "(breaking, if changed)" wording was overcautious — the codebase already treated this as internal.
* **New API — `ABI.decode_error/2`.** Decodes Solidity 0.8.4+ custom-error revert data: matches the first-4-byte selector in `revert_data` against a list of known error definitions (signature strings or pre-parsed `FunctionSelector` structs, mixed accepted) and decodes the payload of whichever matches. Returns `{:ok, %{error: name, args: [...]}}` on a hit, `{:error, :no_match}` when no definition's selector matches (or the list is empty), and `{:error, :calldata_too_short}` on `<4` bytes. Mirrors `decode_call/3`'s contract: malformed payload after a selector match still raises (same behavior as `decode/3`). The first definition with a matching selector wins — definition order is the disambiguation lever. Reuses `ABI.method_id/1` (selector computation), `ABI.decode/3` (payload decode), `ABI.Parser.parse!/2` (signature normalization). `api()` declaration carries the closed error set under namespace `/abi`. Solidity 0.8.4+ custom-error revert data is selector-prefixed exactly like calldata, so the implementation is structurally identical to `decode_call/3` modulo the multi-definition list.
* **New API — `ABI.encode_packed/2`.** Solidity's [non-standard packed encoding](https://docs.soliditylang.org/en/stable/abi-spec.html#non-standard-packed-mode) — used for Merkle airdrop leaves and `keccak256(abi.encodePacked(...))` signature schemes. Per the spec: types <32 bytes concatenate tight (no padding); dynamic types (`bytes`, `string`) inline as raw payload (no length prefix); array elements are padded to 32 bytes (or 32-byte multiples for `string`/`bytes`) so element boundaries are recoverable; tuples/structs and nested arrays are explicitly unsupported and raise `ArgumentError` with a spec link. The wrapper accepts the same polymorphic first arg as `encode/2` (`binary() | FunctionSelector.t()`); paren-only signatures like `"(uint256,address)"` parse as a single-tuple parameter (same shape as `"foo((uint256,address))"`) and therefore raise — pass a struct-arg-free signature like `"foo(uint256,address)"` for a comma-separated arg list. Cross-checked against the canonical spec example (`int16(-1), bytes1(0x42), uint16(0x03), string("Hello, world!")` → `0xffff42000348656c6c6f2c20776f726c6421`) and locked as a golden vector; Merkle-leaf golden vector (`address ++ uint256` → 52 bytes pre-hash) included for airdrop consumers. Implementation lives next to `encode_type/2` in `lib/abi/type_encoder.ex` with private `packed_top/2`, `packed_array/2`, `pack_uint/2`, `pack_int/2` helpers — does NOT thread through `Math.pad/4` (which always rounds to 32-byte multiples) since packed mode is the inverse of standard ABI. Inside an array, however, scalar elements DO route through `encode_type/2` to reuse the 32-byte rounding correctly per spec.

* **Property Suite Expansion bundle (5 members — original 4 plus a production fix the suite surfaced).** Single PR expanding `test/abi/roundtrip_property_test.exs` plus the resulting bug fix in `lib/abi/type_decoder.ex`.
  * **`tuple[]` (dynamic array of tuples) round-trip coverage** — the existing dispatcher already composes `{:array, inner}` with `{:tuple, ...}`, so no new generator clause was needed; what was missing was explicit pin-down. Three new properties: a static-only-element `tuple[]` (exercises the array-of-static-tuples layout), a mixed-element `tuple[]` where each element is itself dynamic (the most stress-testing shape — head/tail offsets are computed both per-array AND per-element), and an empty `tuple[]` unit test. The mixed-element property is what surfaced the string-NUL bug below on its first run.
  * **Empty dynamic fields inside structs** — explicit fixtures (not properties) for the four pinned shapes that mining surfaced from real protocol calldata: `(bytes, string)` with empty bytes and non-empty string, `(bytes, bytes)` both empty, empty `tuple[]` as the only dynamic field in a struct, and empty `tuple[]` followed by non-empty bytes. Inline `test "..." do` blocks rather than the `@fixtures` pattern from `defi_calldata_test.exs` — round-trip equality is enough; no need to lock against synthetic byte strings when the property tests already exercise the layout invariants.
  * **Multiple top-level struct args** — added a `roundtrip_args/2` helper alongside the single-arg `roundtrip/2`. New property mirrors the Balancer V2 `swap(SingleSwap, FundManagement, uint256, uint256)` shape: two sibling structs of differing dynamic-rate (one mixed static+dynamic, one static-only) plus two scalar args at the ends. Exercises sibling-tuple offset arithmetic when adjacent top-level tuples are dynamic at different rates — `roundtrip/2` always wrapped a single arg as `[%{type: type}]`, so this layout was never exercised by the property suite.
  * **Deep struct nesting (depth ≥ 4) round-trip** — the recursive `composite` property's `type_and_value_gen(3)` cap was bumped to depth 5 (Pendle `swapExactTokenForPt` exercises depth 5 in real calldata). `@tag timeout: 120_000 → 300_000` to absorb the larger generation surface; `max_runs: 50` (down from the default 100) keeps CI bounded — depth-5 trees can balloon (lists of length 4 of strings up to 64 chars at multiple nesting levels), and 50 deep samples have higher information-density than 100 shallow ones.
  * **Bug fix surfaced by the bundle:** `ABI.TypeDecoder` previously called `nul_terminate_string/1` on every decoded `:string`, splitting the binary at the first `<<0>>` byte and returning only the prefix. This treated Solidity strings as C strings — wrong. Per the Solidity ABI spec, strings are length-prefixed UTF-8 and may legally contain NUL codepoints (`U+0000`); the length is exact, so right-padded zeros after the data are the only NULs that need stripping (and `decode_bytes/3 → Math.unpad/3` already handled that). Removed the `nul_terminate_string/1` helper entirely; the `:string` decode clause now delegates straight to `decode_bytes(rest, length, :right)`. Pre-existing in upstream `exthereum/abi` since 2018 (commit `bdceb719` by Levi Aul) — undetected because random `StreamData.string(:utf8, ...)` rarely starts with NUL, and most real Solidity strings (function names, error messages, event signatures) don't either. The mixed-element `tuple[]` property happened to generate a NUL-prefixed string and surfaced it. Three regression unit tests added: leading-NUL string, embedded-NUL string, and all-NULs string. Production paths affected: `ABI.decode/3`, `ABI.decode_call/3`, `ABI.decode_event/4`, `ABI.TypeDecoder.decode/3` — anywhere a `:string` is decoded. **Should be filed upstream** alongside #53/#54/#55 (or batched with the lexer `x` sub-bug) — affects every `exthereum/abi` consumer that decodes user-supplied strings.

* **DeFi Real-World Fixtures bundle.** Tests-only addition; no production code touched. Closes both members of the bundle in a single commit.
  * `test/abi/defi_calldata_test.exs` — 10 round-trip golden vectors captured from `defi-skills build --action <name> --json` (defi-skills v0.3.0). Fixtures live inline as `@fixtures` (the originally-proposed `test/fixtures/defi_calldata.exs` shape was discarded — no `.exs` data-loading idiom exists in the repo and inline matches the existing test convention). Each fixture asserts both directions: `ABI.encode(sig, args)` reproduces the locked calldata byte-for-byte and `ABI.decode_call(sig, calldata)` recovers the original args. Coverage: Aave V3 supply/borrow/setCollateral, Compound V3 supply/claim, Lido stake/unstake (the only fixture exercising `uint256[]` head/tail layout), EigenLayer deposit, ERC-20 transfer, WETH unwrap. The aave_supply fixture reproduces the calldata locked in the ROADMAP planting note byte-for-byte.
  * `test/abi/function_selector_real_world_test.exs` — 12 explicit `ABI.method_id/1` golden-vector assertions: ERC-20 (`transfer`, `transferFrom`), Aave V3 (`supply`, `borrow`), WETH/Curve gauge (`withdraw(uint256)` — duplicate selector by design), WETH/Rocket Pool (`deposit()` — duplicate by design), Lido (`requestWithdrawals(uint256[],address)`), Compound V3 (`claim`), Curve 3pool (`add_liquidity(uint256[3],uint256)` — fixed-size-array path), Uniswap V3 (`exactInputSingle(tuple)` — single tuple arg), Balancer V2 (`swap` — multiple top-level tuples), and EigenLayer (`queueWithdrawals(tuple[])` — dynamic `tuple[]`). A second `describe` block round-trips the four tuple/`tuple[]`/fixed-array signatures through `FunctionSelector.decode/1 ∘ encode/1` and re-asserts the selector — the 4 corner-case canonical-signature shapes were the entire reason the selector vectors are scoped this widely.
  * **Why ship this.** Before this bundle, `hieroglyph` had a single mainnet-style fixture in the entire test suite (the ERC-20 `Transfer` event doctest in `lib/abi.ex`). Every encoder/decoder change was verified only against synthetic property-suite shapes. Real-world contract evidence now lives next to the synthetic suite, giving cartouche / onchain CI a stable contract-stability surface across hieroglyph version bumps. The 4 tuple-bearing selectors specifically prove the canonical-signature serialization in `function_selector.ex` matches what real chains expect.

## Agent Economy

* **Agent Economy — Phase 1: Descripex on `ABI` top-level.** Annotated the seven public functions (`encode/2`, `method_id/1`, `decode/3`, `decode_call/3`, `decode_event/4`, `event_signature/1`, `parse_specification/1`) with `api()` declarations under namespace `/abi`. Six of the seven take a polymorphic first arg (`binary() | FunctionSelector.t()`); the union is described in prose per the established mpp/mcp `payment_required_error/1` precedent (one `api()` block per function name; the formal union lives on `@spec`). `ABI` now `use`s `Descripex.Discoverable` with a single-module list — Phase 2 expands the list to all six annotated modules. Existing `@doc` blocks (with their doctests) preserved by ordering: `api()` first emits its generated `@doc`, then the manual `@doc """..."""` overrides slot 4 prose while `@doc hints:` survives via descripex's `__before_compile__` ETS injection. `@spec` and runtime behaviour unchanged. Manifest emission via `mix descripex.manifest --app hieroglyph` already works (returns 7 ABI entries plus the four Discoverable bookkeeping exports); the dedicated `mix hieroglyph.manifest` task lands in Phase 3. Doctor docs/specs coverage held at 100/100, dialyzer 0 warnings, credo `--strict` 0 issues. New runtime dep: `{:descripex, "~> 0.6"}` (transitively pulls `:json_spec`). Version bumped from `1.1.x` → **`1.2.0`** because adding a runtime dep changes downstream consumers' (cartouche, onchain) dependency closure.
* **Agent Economy — Phase 2: Descripex on the remaining five modules.** Annotated all 18 documented public functions across `ABI.Event` (`/selector` — 3 fns), `ABI.FunctionSelector` (`/selector` — 5 fns; the three `@doc false` internals `dynamic?/1`, `get_function_type/1`, `get_state_mutability/1` deliberately excluded), `ABI.TypeEncoder` (`/codec` — 2 fns; `encode_bytes/1` `@doc false` excluded), `ABI.TypeDecoder` (`/codec` — 4 fns), and `ABI.Math` (`/math` — 4 fns). None of these 18 functions are polymorphic, so each `api()` block follows the standard shape — name, one-sentence description, `params:` keyword list, `returns:` map. `composes_with:` links wired across the natural pairings: `Event.decode_event ↔ event_signature`, `FunctionSelector.decode ↔ encode`, `TypeEncoder.encode ↔ encode_raw`, `TypeDecoder.decode ↔ decode_raw`. Extended `use Descripex.Discoverable, modules: [...]` in `lib/abi.ex` from `[ABI]` to all six annotated modules. Manifest now emits the full **25 user-declared `api()` entries** (7 + 3 + 5 + 2 + 4 + 4) plus the 4 framework `Discoverable` exports — verified via `mix descripex.manifest --app hieroglyph` and per-module entry counts. `@doc`/doctest preservation pattern from Phase 1 carries through: existing `@doc """..."""` blocks override slot 4 prose; `@doc hints:` survives. No behaviour change, no `@spec` change.
* **Agent Economy — Phase 3: dedicated manifest task + hint-rot validation test.** New mix task `mix hieroglyph.manifest [path]` (defaults to `api_manifest.json`) emits the JSON manifest using `ABI.__descripex_modules__/0` as the single source of truth — direct port of the established `mix mpp.manifest` shape. Manifest is suitable for downstream cartouche/onchain CI to diff across hieroglyph version bumps as a contract-stability artifact. New test file `test/abi/agent_economy_test.exs` enforces the agent-discovery surface in three describe blocks plus a load-bearing cross-check: (1) every entry in each module's `__api__/0` carries `:hints.description`; (2) `ABI.describe/0..2` returns the expected modules / function lists / per-function detail; (3) namespaces (`/abi`, `/selector`, `/codec`, `/math`) match per-module assignments via `Code.fetch_docs/1`. The cross-check walks `module_info(:exports)`, strips Elixir/Descripex framework exports, plus an explicit allow-list of the four `@doc false` internal helpers (`FunctionSelector.dynamic?/1`, `get_function_type/1`, `get_state_mutability/1`, `TypeEncoder.encode_bytes/1`), then asserts every remaining export is declared with `api()` — without this gate, hints rot silently when new `def`s land without `api()`, and silent rot here propagates as silent contract drift through cartouche-generated bindings into every onchain_<protocol> package.
* **Bug fix (sub-bug of upstream [#54](https://github.com/exthereum/abi/issues/54), upstream filing deferred — will be batched into a future combined-bugs issue with PR offer):** `ABI.FunctionSelector.decode_type("fixed128x18")` and `decode_type("ufixed256x80")` (and the same-shape forms inside arrays/tuples/function signatures) raised a leaky `FunctionClauseError` instead of the friendly `ArgumentError` that bare `fixed`/`ufixed` already produced. Root cause was lexer rule ordering in `src/ethereum_abi_lexer.xrl`: the LETTERS rule (`[a-zA-Z_]+`) was listed before the standalone `'x'` terminal, so leex picked LETTERS on equal-length matches and the single `x` between the two integers tokenized as `letters`. The grammar rule `type -> typename digits 'x' digits` never fired, the parser fell through to `juxt_type(fixed, 128)`, and `juxt_type/2` had no `fixed` clause. Fix: introduced dedicated `fixed_typename` / `ufixed_typename` terminals (so the `'x'` separator is only valid in `fixed`/`ufixed` contexts), moved the `'x'` rule above `{LETTERS}`, and extended the parser's `identifier_part` to also accept `'x'`, `fixed_typename`, and `ufixed_typename` so single-char `x` and the keyword forms still work as function/argument names. The explicit-M/N forms now route through `ABI.Parser.reject_unsupported!/1` and raise the same friendly `ArgumentError` (with the upstream-#54 link) that the bare forms already produced. Yecc reports 3 shift/reduce conflicts (was 1) — the 2 new ones come from `fixed_typename`/`ufixed_typename` being able to start either a `type` or an `identifier_part`; yecc's default shift resolution is the desired behavior (`fixed128` is a type prefix, not an identifier), and is documented inline in the `.yrl` `Expect 3.` comment. Regression tests added: explicit-M/N rejection (`fixed128x18`, `ufixed256x80`) plus `x`-keyword identifier handling (function named `x`, argument named `x`, function named `fixed`/`ufixed`, and a function name containing `x` mid-string).

# 1.1.0 - 2026-05-01

* **Bug fix (upstream [#55](https://github.com/exthereum/abi/issues/55)):** `ABI.TypeEncoder.encode_int/2` rejected ALL `int<N>` values (including `0`) for small bit widths, because the overflow guard mixed up bytes and bits — it compared `byte_size(significant_bytes)` against `desired_size_bytes - 1`, which evaluates to `0` for `int8`, raising on every input. Replaced with a numeric range check against `2^(N-1)` performed up-front, so the encoder accepts the full signed range `-2^(N-1)..2^(N-1)-1` for every `int<N>`. The pre-existing `"int overflow raises data overflow"` test passed only because the encoder was broken for any value; tightened the test to assert specific in-range values encode AND specific boundary cases (`128`, `-129`) raise.
* **Bug fix:** `ABI.FunctionSelector.dynamic?/1` raised `FunctionClauseError` on `{:array, T, 0}` (zero-length fixed array). The grammar accepts `T[0]` (yrl rule allows `N >= 0`), so the type is parseable, but the existing clauses required `len > 0` and no clause matched the zero case. Added `def dynamic?({:array, _type, 0}), do: false` — a zero-length fixed array has no head/tail layout and no payload, so it is static by any sensible definition. Encoder and decoder paths already handle zero-length arrays (`encode_type({:array, _, 0})` produces an empty repeated-type tuple; `decode_type({:array, _, 0}, data, _opts) -> {[], data}`); verified by extending `roundtrip_property_test.exs`'s fixed-array length domain from `1..3` to `0..3`. Pre-existing in upstream `exthereum/abi`; not yet filed.
* `ABI.FunctionSelector.@type type` now carries a `@typedoc` clarifying that `address payable` collapses to `:address`. Solidity's ABI JSON only emits `"address"` for both forms, the on-the-wire encoding is identical (20-byte left-padded), and payability is a property of `state_mutability` rather than a separate type variant — so consumers shouldn't expect a distinct atom.
* Added `test/abi/roundtrip_property_test.exs` — property-based `decode(encode(x)) == x` coverage using `stream_data` for every type in `ABI.FunctionSelector.@type type/0`: `uint`, `int`, `address`, `bool`, `string`, `bytes`, `bytesN`, fixed and dynamic arrays, and recursively nested tuples (depth ≤ 3). Per-type properties localize failures to a single clause; the recursive composite property exercises nested `{:tuple, [{:array, ...}]}` shapes where head/tail offsets matter. Surfaced the `encode_int` bug above on its first run. Test-only dep `{:stream_data, "~> 1.1"}` added.
* Test coverage for the empty-args calldata path (`f()` shape — 4-byte selector with zero ABI-encoded args). `weth.deposit()` (selector `0xd0e30db0`), `rocket_pool.deposit()`, and similar zero-arg calls were untested by the round-trip property suite (every generator produced at least one value). Pinned both directions: `ABI.encode("deposit()", []) == <<0xD0, 0xE3, 0x0D, 0xB0>>` and `ABI.decode("deposit()", <<>>) == []`, plus the `function: nil`/`types: []` empty-bytes shape.
* Test coverage for `FunctionSelector` selector-rendering and parser edge cases: `encode/1` canonical-signature rendering of `{:int, N}`, `{:struct, _, _, _}`, dead-via-parse types (`:function`, `{:fixed, M, N}`, `{:ufixed, M, N}`) and `nil`-typed slots (defensive against partially-built typeinfo maps); plus `parse_specification/1`'s `%{"indexed" => _}`-without-`"name"` branch (older Solidity versions and hand-written ABIs may omit names on indexed event params).
* **New API:** `ABI.method_id/1` returns the 4-byte function selector (`keccak256(canonical_signature)[0..3]`) for a signature string or `FunctionSelector` struct. Returns `<<>>` for selectors with `function: nil`. Previously the same logic was private to `ABI.TypeEncoder`; exposing it is a useful primitive for callers that need to compute selectors without encoding args (selector-table routing, log-topic matching, calldata pre-validation).
* **New API:** `ABI.decode_call/3` is the symmetric counterpart to `ABI.encode/2` for selector-prefixed calldata. Splits the 4-byte prefix, verifies it matches the expected selector, and decodes the payload via the existing `decode/3` machinery. Returns `{:ok, decoded}` on match or `{:error, reason}` for `:calldata_too_short` (< 4 bytes), `:selector_mismatch` (prefix wrong), or `:no_function_name` (selector has `function: nil`, so there's nothing to verify against — caller should use `decode/3` with the payload). `ABI.decode/3` semantics are unchanged: it remains payload-only, matching `eth-abi` / `ethers` / `viem` / `alloy` conventions.

# 1.0.0 - 2026-04-24

First hex.pm release as `hieroglyph`. This is a maintained fork of [exthereum/abi](https://github.com/exthereum/abi); the module namespace is unchanged (`ABI.encode/2`, `ABI.decode/2`, etc. — consumers just swap the hex dep name). Version resets to `1.0.0` under the new package name; the `1.0.0-alpha*` / `1.0.0-bravo1` lines below this entry are the upstream's pre-release history, carried forward for context but never published to hex under `hieroglyph`.

* **Published as `hieroglyph`** — hex package renamed from the internal `abi` app name. Top-level module `ABI` preserved (the Solidity term is the correct module name). Repo renamed to `ZenHive/hieroglyph`; upstream `exthereum/abi` tracked in the package's "Upstream (fork-of)" link.
* **Bug fix (upstream [#53](https://github.com/exthereum/abi/issues/53)):** `ABI.Event.decode_event/4` now returns `{:indexed_hash, <<32 bytes>>}` for indexed parameters of *reference* type (`string`, `bytes`, all arrays — fixed-size or dynamic — and tuples/structs) instead of silently misdecoding the keccak topic as if it were a raw ABI-encoded value. Per the Solidity ABI spec, indexed reference-type values are stored in topics as `keccak256(value)` and the original is unrecoverable — the tagged tuple preserves the hash (useful for log filtering and equality checks) and makes the "unrecoverable" signal pattern-matchable. This is broader than the ABI head/tail "dynamic" rule: `uint256[2]` and tuples of all-static members are *static* for regular ABI encoding but are still hashed in event topics, and this fix handles both. Breaking for callers that consumed the previous garbage bytes directly; static value-type indexed params (`uint`/`int`/`address`/`bool`/`bytesN`/`function`/`fixed`/`ufixed`) are unchanged. Regression tests added for indexed `string`, indexed `bytes`, indexed dynamic array, indexed fixed-size static array (`uint256[2]`), indexed tuple of static members, and mixed static+dynamic+non-indexed cases.
* **Bug fix (upstream [#54](https://github.com/exthereum/abi/issues/54)):** `fixed`, `ufixed`, and `function` types now raise `ArgumentError` at parse time (in `ABI.Parser.parse!/2`, walking nested arrays and tuples) with a link to the tracking issue, instead of parsing silently into unsupported internal terms and later raising the cryptic `"Unsupported encoding type"` inside `TypeEncoder` / `TypeDecoder`. Also filled the `{:bytes, pos_integer()}` gap in `ABI.FunctionSelector.@type type` — previously omitted even though fully supported by the encoder and decoder. Note: the explicit `fixed<M>x<N>` / `ufixed<M>x<N>` forms still raise a `FunctionClauseError` upstream of this walker due to a separate pre-existing lexer-rule-ordering bug (single `x` tokenizes as `letters` instead of the `'x'` terminal) — tracked as a follow-up task. Also aligned the grammar's bare-`fixed` / bare-`ufixed` canonical expansion to the Solidity spec (`fixed128x18` / `ufixed128x18`; previously `x19`) so the rejection error message reports the correct form.
* Simplified `ABI.Parser.parse!/2`'s unsupported-type walker to drop a dead `is_list(returns)` branch — the yecc grammar only emits `nil` or a single bare type for `returns`, never a list. No behavior change.
* Extracted the 32-byte padding logic into `ABI.Math.pad/4` and `ABI.Math.unpad/3`. `ABI.TypeEncoder.encode_bytes/1`, `encode_int/2`, and `encode_uint/2` now delegate to `ABI.Math.pad/4`; `ABI.TypeDecoder.decode_bytes/3` is a thin wrapper around `ABI.Math.unpad/3`. No behavior change; resolves the long-standing `TODO: add to ABI.Math` comments in both modules.
* Renamed `ABI.FunctionSelector.is_dynamic?/1` to `ABI.FunctionSelector.dynamic?/1` to satisfy `Credo.Check.Readability.PredicateFunctionNames`. The function remains `@doc false` (internal). No deprecation shim — the old name had `@doc false` since 2017 and zero in-repo references outside three private call-sites, which were updated.
* Drove `mix credo --strict` to zero violations (was 51). Covers `Design.AliasUsage` (top-of-module aliases added across `ABI`, `ABI.Event`, `ABI.FunctionSelector`, `ABI.Parser`, `ABI.TypeDecoder`, `ABI.TypeEncoder`, and `ABI.Hex`), `Readability.MaxLineLength` (spec / docstring wraps + the big `Enum.reduce` tuple-encoder broken into an `encode_tuple_element/2` helper), `Consistency.ParameterPatternMatching` (flipped three `record = %{…}` heads to `%{…} = record`), and `Refactor.Nesting` (extracted `ABI.Event.verify_event_signature/2` and `ABI.TypeEncoder.fetch_named_field/2` + `fetch_by_name/2` helpers to drop nesting below 3).
* Added regression tests for the map-input encoder path (`ABI.TypeEncoder.data_to_list/2`): atom-keyed maps, string-keyed maps, camelCase→snake_case name resolution, string-over-atom key priority, integer values inside nested named-struct maps, and the missing-field / unnamed-type error raises. The map branch previously had zero test coverage; the string-key path was added in commit `46accc8`, and this suite also exercises integer encoding (`a43e9d5`) through the map branch.
* Added `@spec` typespecs and `@doc` strings for every previously-undeclared public function across `ABI`, `ABI.Event`, `ABI.TypeDecoder`, `ABI.TypeEncoder`, and `ABI.FunctionSelector`: `ABI.event_signature/1`, `ABI.parse_specification/1`, `ABI.TypeDecoder.decode/3`, `ABI.TypeDecoder.tuple_value/3`, `ABI.TypeEncoder.encode_raw/2`, `ABI.Event.decode_event/4` / `event_signature/1` / `canonical/2`, and `ABI.FunctionSelector.decode/1` / `decode_raw/1` / `parse_specification_item/1` / `decode_type/1` / `encode/3`; also added docs for `TypeDecoder.tuple_value/3` and `TypeDecoder.decode_bytes/3`. Matches the style widened in PR #52. Doctor spec coverage 42% → 88%, doc coverage 88% → 96%.
* Added regression tests for eleven previously-uncovered error paths: `bool` with non-boolean values, `bytes<N>` size mismatches and wrong-datatype values, unsupported type atoms across encoder / decoder / function-selector, int/uint overflow, trailing decode data, and `decode_event/4` returns for mismatched event signatures and invalid topic counts.
* README refreshed: dropped the stale "tuples with multiple elements don't parse" caveat (false since JSON-ABI support), corrected `ABI.encode/2` arity and flipped `bytes<M>` to supported in the Support checklist, migrated dead `solidity.readthedocs.io` links to `docs.soliditylang.org`, and added runnable examples for `ABI.parse_specification/1`, `ABI.Event.decode_event/4`, and map/struct input to `encode/2`.

# 1.0.0-bravo1
* Fix ABI tuple encoding for nested inlined tuples
# 1.0.0-alpha9
* Add Names to Event Signatures
# 1.0.0-alpha8
* Add Event Signature check to ABI.Event.decode_event
* Change `decode_event` to return an {:ok, event_name, event_params} tuple.
* Add ability to add `"indexed"` keyword to ABI canonicals
# 1.0.0-alpha7
* Bugfix for event decoding with dynamic parameters
# 1.0.0-alpha6
* Bugfix for is_dynamic
# 0.1.15
* Properly treat all function encodes as tuple encodings
# 0.1.14
* Fix 0-length `type[]` encoding
# 0.1.13
* Drop dependency on exth crypto and move in functionality
# 0.1.12
* Fix `string` decoding to truncate on encountering NUL
* Fix some edge-cases in `tuple` encoding/decoding
# 0.1.11
* Add support for method ID calculation of all standard types
# 0.1.10
* Fix parsing of function names containing uppercase letters/digits/underscores
* Add support for `bytes<M>`
# 0.1.9
* Add support for parsing ABI specification documents (`.abi.json` files)
* Reimplement function signature parsing using a BNF grammar
* Fix potential stack overflow during encoding/decoding
# 0.1.8
* Fix ordering of elements in tuples
# 0.1.7
* Fix support for arrays of uint types
# 0.1.6
* Add public interface to raw function versions.
# 0.1.5
* Bugfix so that addresses are still left padded.
# 0.1.4
* Bugfix for tuples to properly handle tail pointer poisition.
# 0.1.3
* Bugfix for tuples to properly handle head/tail encoding
# 0.1.2
* Add support for tuples, fixed-length and variable length arrays