From 01d36e4fb7f43d37d9274ec9353cff3ebb207e95 Mon Sep 17 00:00:00 2001 From: Anil Kumar Date: Mon, 15 Jun 2026 12:23:44 +0300 Subject: [PATCH 1/2] perf(erc20spl-cached): collapse ATA-existence gate to one SplCached.account(address,mint) read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit balanceOf/allowance/approve/_transfer derived the target ATA via legacy HelperProgram.ata then read it via SplCached.account(ata) — two EVM->precompile round-trips per gate. The new ISplCached.account(address,bytes32) overload derives external_auth(owner)->ATA internally, so each gate is a single read. Behaviour unchanged. Requires the paired upstream Rome EVM SplCached read selector. --- CHANGELOG.md | 8 ++++++++ contracts/erc20spl/erc20spl_cached.sol | 12 ++++-------- contracts/examples/cached.sol | 11 +++++++++++ contracts/interface.sol | 3 +++ 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78aa012..384d3d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased +### Changed — `SPL_ERC20_cached` ATA-existence gate collapses two precompile round-trips into one + +[`contracts/erc20spl/erc20spl_cached.sol`](contracts/erc20spl/erc20spl_cached.sol) — `balanceOf`, `allowance`, `approve`, and `_transfer` previously derived the target ATA with a legacy `HelperProgram.ata(x, mint_id)` call (`0xff..09`) then read it with `SplCached.account(ata)`. Both now collapse into a single `SplCached.account(x, mint_id)` (selector `0xf9827227`) — the precompile derives `ATA(external_auth(x), mint)` internally, dropping one EVM→precompile round-trip per gate. Behaviour is unchanged: same overlay-aware existence check, same auto-create on the catch branch. + +[`contracts/interface.sol`](contracts/interface.sol) — adds `ISplCached.account(address owner, bytes32 mint)` (mirrors `account(address)` with an explicit mint). Worked example: [`contracts/examples/cached.sol#spl_account_mint`](contracts/examples/cached.sol). Requires the paired upstream Rome EVM `SplCached` read selector. + +Estimated saving ~23K CU per gated op (one round-trip ≈ 23,088 CU measured, Hadrian 2026-06-15) across the near-universal wrapper. **Per-op A/B + derivation-parity (`account(address,mint)` == `HelperProgram.ata`) pending Phase-0 on a rebuilt wrapper before merge.** + ### Changed — `SPL_ERC20_cached._transfer` skips the redundant recipient-ATA create when it already exists [`contracts/erc20spl/erc20spl_cached.sol#_transfer`](contracts/erc20spl/erc20spl_cached.sol) — gates `ensure_token_account(to)` behind an overlay-aware `try SplCached.account` existence check, mirroring the `approve` pattern from #216/#217. On the common transfer-to-existing-holder path the redundant idempotent `AssociatedSplCached.create_ata` round-trip is skipped; transfer to a fresh recipient still auto-creates the ATA via the catch branch (the #63 auto-create behaviour is preserved). Scoped to `_transfer` only — `mint_to` has the same redundancy but is left for a follow-up. diff --git a/contracts/erc20spl/erc20spl_cached.sol b/contracts/erc20spl/erc20spl_cached.sol index 8de1597..63d29ef 100644 --- a/contracts/erc20spl/erc20spl_cached.sol +++ b/contracts/erc20spl/erc20spl_cached.sol @@ -110,8 +110,7 @@ contract SPL_ERC20_cached is IERC20, IERC20Metadata { /// smoke (rome-ui PR #402). Cross-ref: /// rome-uniswap-v3/contracts/UniswapV3Pool.sol:486-490. function balanceOf(address account) external view returns (uint256) { - bytes32 ata = HelperProgram.ata(account, mint_id); - try SplCached.account(ata) returns (ISplCached.Account memory acc) { + try SplCached.account(account, mint_id) returns (ISplCached.Account memory acc) { return uint256(acc.amount); } catch { return 0; @@ -127,8 +126,7 @@ contract SPL_ERC20_cached is IERC20, IERC20Metadata { /// to overlay but the legacy lamports read is on-chain-only. /// Saturates uint64::max → uint256::max for wallet "infinite approval". function allowance(address owner, address spender) external view returns (uint256) { - bytes32 ownerAta = HelperProgram.ata(owner, mint_id); - try SplCached.account(ownerAta) returns (ISplCached.Account memory acc) { + try SplCached.account(owner, mint_id) returns (ISplCached.Account memory acc) { bytes32 spenderPda = HelperProgram.pda(spender); if (acc.delegate != spenderPda) return 0; return acc.delegated_amount == type(uint64).max @@ -205,8 +203,7 @@ contract SPL_ERC20_cached is IERC20, IERC20Metadata { // common transfer-to-existing-holder path, skip the idempotent // AssociatedSplCached.create_ata round-trip. Overlay-aware via // try SplCached.account so an ATA created earlier this tx counts. - bytes32 toAta = HelperProgram.ata(to, mint_id); - try SplCached.account(toAta) returns (ISplCached.Account memory) { + try SplCached.account(to, mint_id) returns (ISplCached.Account memory) { // recipient ATA exists (overlay or on-chain) — skip create } catch { ensure_token_account(to); @@ -259,8 +256,7 @@ contract SPL_ERC20_cached is IERC20, IERC20Metadata { /// Saturates uint64::max → uint256::max for wallet "infinite approval". function approve(address spender, uint256 value) external returns (bool) { _users.ensure_user(msg.sender); - bytes32 ownerAta = HelperProgram.ata(msg.sender, mint_id); - try SplCached.account(ownerAta) returns (ISplCached.Account memory) { + try SplCached.account(msg.sender, mint_id) returns (ISplCached.Account memory) { // ATA exists in cache (overlay or on-chain) — skip create. } catch { ensure_token_account(msg.sender); diff --git a/contracts/examples/cached.sol b/contracts/examples/cached.sol index 4f8b641..2947758 100644 --- a/contracts/examples/cached.sol +++ b/contracts/examples/cached.sol @@ -239,6 +239,17 @@ contract cached_example { return SplCached.account(ata); } + // Read the SPL token-account state for `user`'s ATA of an arbitrary + // `mint` via the (address,bytes32) overload — the precompile derives + // ATA(external_auth(user), mint) internally, collapsing the manual + // HelperProgram.ata + account(bytes32) pair above into one in-silo read. + function spl_account_mint(address user, bytes32 mint) + external view + returns (ISplCached.Account memory) + { + return SplCached.account(user, mint); + } + // ─── AssociatedSplCached (0xff..06) ────────────────────────────── function create_ata_self() external { diff --git a/contracts/interface.sol b/contracts/interface.sol index 94bc1c3..4767b9f 100644 --- a/contracts/interface.sol +++ b/contracts/interface.sol @@ -115,6 +115,9 @@ interface ISplCached { function account(address user) external view returns(Account memory); // 0x882358ae — raw 32-byte ATA pubkey function account(bytes32 ata) external view returns(Account memory); + // 0xf9827227 — derives ATA(external_auth(owner), mint) internally for an + // arbitrary mint; mirror of account(address) with an explicit mint. + function account(address owner, bytes32 mint) external view returns(Account memory); } interface IAssociatedSplCached { From cea869a9f52a51b1231bef0de57b6b0eb6b273fc Mon Sep 17 00:00:00 2001 From: Anil Kumar Date: Mon, 15 Jun 2026 12:41:57 +0300 Subject: [PATCH 2/2] refactor(erc20spl-cached): rename account(address,bytes32) param owner -> user Parity with the existing account(address user) overload. --- CHANGELOG.md | 2 +- contracts/interface.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 384d3d0..8e46857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). [`contracts/erc20spl/erc20spl_cached.sol`](contracts/erc20spl/erc20spl_cached.sol) — `balanceOf`, `allowance`, `approve`, and `_transfer` previously derived the target ATA with a legacy `HelperProgram.ata(x, mint_id)` call (`0xff..09`) then read it with `SplCached.account(ata)`. Both now collapse into a single `SplCached.account(x, mint_id)` (selector `0xf9827227`) — the precompile derives `ATA(external_auth(x), mint)` internally, dropping one EVM→precompile round-trip per gate. Behaviour is unchanged: same overlay-aware existence check, same auto-create on the catch branch. -[`contracts/interface.sol`](contracts/interface.sol) — adds `ISplCached.account(address owner, bytes32 mint)` (mirrors `account(address)` with an explicit mint). Worked example: [`contracts/examples/cached.sol#spl_account_mint`](contracts/examples/cached.sol). Requires the paired upstream Rome EVM `SplCached` read selector. +[`contracts/interface.sol`](contracts/interface.sol) — adds `ISplCached.account(address user, bytes32 mint)` (mirrors `account(address)` with an explicit mint). Worked example: [`contracts/examples/cached.sol#spl_account_mint`](contracts/examples/cached.sol). Requires the paired upstream Rome EVM `SplCached` read selector. Estimated saving ~23K CU per gated op (one round-trip ≈ 23,088 CU measured, Hadrian 2026-06-15) across the near-universal wrapper. **Per-op A/B + derivation-parity (`account(address,mint)` == `HelperProgram.ata`) pending Phase-0 on a rebuilt wrapper before merge.** diff --git a/contracts/interface.sol b/contracts/interface.sol index 4767b9f..756220d 100644 --- a/contracts/interface.sol +++ b/contracts/interface.sol @@ -115,9 +115,9 @@ interface ISplCached { function account(address user) external view returns(Account memory); // 0x882358ae — raw 32-byte ATA pubkey function account(bytes32 ata) external view returns(Account memory); - // 0xf9827227 — derives ATA(external_auth(owner), mint) internally for an + // 0xf9827227 — derives ATA(external_auth(user), mint) internally for an // arbitrary mint; mirror of account(address) with an explicit mint. - function account(address owner, bytes32 mint) external view returns(Account memory); + function account(address user, bytes32 mint) external view returns(Account memory); } interface IAssociatedSplCached {