diff --git a/CHANGELOG.md b/CHANGELOG.md index 78aa012..8e46857 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 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.** + ### 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..756220d 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(user), mint) internally for an + // arbitrary mint; mirror of account(address) with an explicit mint. + function account(address user, bytes32 mint) external view returns(Account memory); } interface IAssociatedSplCached {