Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 4 additions & 8 deletions contracts/erc20spl/erc20spl_cached.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
11 changes: 11 additions & 0 deletions contracts/examples/cached.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions contracts/interface.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down