From f4fdcec3b98b2e0058cc8129881449d5a4a70f8d Mon Sep 17 00:00:00 2001 From: Sattvik Kansal <150642653+skansal-rome@users.noreply.github.com> Date: Mon, 15 Jun 2026 02:20:17 -0700 Subject: [PATCH] Update CLAUDE.md --- CLAUDE.md | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1f3bc7e..d1ec158 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -133,12 +133,13 @@ The core abstraction layer. Rome-EVM exposes Solana programs as EVM precompiles | **AssociatedSplCached** | `0xff..06` | `IAssociatedSplCached` — idempotent ATA-create variants, revertable | **cached** | `program/src/non_evm_cached/aspl_cached.rs` | | System Program | `0xff..07` | `ISystemProgram` — PDA derivation, base58, mint/operator/program-id getters | legacy | `program/src/non_evm/system.rs` | | CPI | `0xff..08` | `ICrossProgramInvocation` — arbitrary Solana CPI + 4 CU-shortcut selectors | legacy | `program/src/non_evm/cpi.rs` | -| Helper Program | `0xff..09` | `IHelperProgram` — 14 selectors for SPL / PDA / ATA / lamports / gas-token plumbing | legacy | `program/src/non_evm/helper.rs` | -| Ed25519 | `0xff..0a` | `IEd25519` — Ed25519 sysvar-verify allowlist (Pyth Lazer support) | neutral (read-only) | `program/src/non_evm/ed25519.rs` | +| Helper Program | `0xff..09` | `IHelperProgram` — selectors for SPL / PDA / ATA / lamports / gas-token plumbing | legacy | `program/src/non_evm/helper.rs` | | **WithdrawCached** | `0xff..0b` | `IWithdrawCached` — gas-token withdrawal legs, revertable. Cached counterpart to `Withdraw @ 0x42..16` | **cached** | `program/src/non_evm_cached/withdraw_cached.rs` | | Withdraw | `0x42..16` | `IWithdraw` — SOL `withdrawal` + the two gas-token bridge legs (`withdraw_to_pda` / `withdraw_to_ata`) | legacy | `program/src/non_evm/withdraw.rs` | -Global constants pre-bound by `interface.sol`: `SystemProgram`, `CpiProgram`, `HelperProgram`, `Withdraw`, plus the new cached-track addresses `system_cached_address`, `spl_cached_address`, `associated_spl_cached_address`, `withdraw_cached_address`, `ed25519_program_address` (per rome-solidity #204, merged 2026-05-23). +Global constants pre-bound by `interface.sol`: `SystemProgram`, `CpiProgram`, `HelperProgram`, `Withdraw`, plus the cached-track addresses `system_cached_address`, `spl_cached_address`, `associated_spl_cached_address`, `withdraw_cached_address` (per rome-solidity #204, merged 2026-05-23). + +**Removed surface — IEd25519 (`0xff..0a`)** dropped in #236 (paired with rome-evm-private `remove_lazer`). The `IEd25519` interface and `ed25519_program_address` constant are no longer declared. Pyth Lazer cache + adapter machinery (`PythLazerCache`, `PythLazerFeedAdapter`) was removed in #196 ahead of #236. ### Track selection — one track per contract (HARD RULE) @@ -150,7 +151,7 @@ Author statement (rome-evm-private PR #376, Valentin Konyakhin, 2026-05-22): *"C - A contract that calls `ISplCached.transfer(...)` MUST NOT also call `IHelperProgram.transfer_spl(...)` in any reachable code path - A contract using `IAssociatedSplCached.create_ata(...)` cannot fall back to `IHelperProgram.create_ata(...)` in `try/catch` — gate fires regardless of revert - Migration from legacy → cached is a **contract-redeploy event**: deploy a new contract (e.g., `SPL_ERC20_cached`), point consumers at the new address, do not hybrid-modify an existing contract -- CrossStateEthCall reads (`account_data_at`, `account_info`, `user_balance`, `lazer_price`, `ISplCached.account`, etc.) are track-neutral — pure reads, never lock a track +- CrossStateEthCall reads (`account_data_at`, `account_info`, `user_balance`, `ISplCached.account`, etc.) are track-neutral — pure reads, never lock a track **Why this rule:** - Auditability — one question per contract ("which track?"), not a code-path map @@ -227,6 +228,7 @@ The post-consolidation home for Rome-specific Solana plumbing that EVM contracts | `0x4f75e987` | `init_spl_mint(bytes32 mint, uint8 decimals, bytes32 mint_authority, bool has_freeze_authority, bytes32 freeze_authority)` | SPL Token InitializeMint2 against pre-allocated mint. No PDA signer (mint_authority + freeze_authority stored as account data). Consumed by `ERC20SPLFactory.init_token_mint`. Shipped in rome-evm-private PR #364. | | `0x20972d0f` | `create_and_init_mint(uint8 decimals, bytes32 mint_authority, bool has_freeze_authority, bytes32 freeze_authority, bytes32 salt)` | Composed: System CreateAccount + SPL InitializeMint2 in one dispatch. Saves one Rome DoTx envelope vs separate `create_mint_account` + `init_spl_mint`. Optional one-call mint creation for new callers; factory keeps the 2-step flow for back-compat. Shipped in rome-evm-private PR #364. | | `0xe479df56` | `transfer_spl(address from, address to, uint64 amount, bytes32 mint)` | **Addr-keyed delegate variant.** Derives both ATAs from EVM addresses internally; signs as `external_auth(caller)`. SPL Token accepts PDA as owner OR delegate. Replaces `SPL_ERC20.transferFrom`'s prior bytes32-ATA delegate variant (`0x766b362a`). | +| `0x46efa679` | `transfer_spl_to_signer(uint64 amount, bytes32 mint)` | **Solana-native return leg** (#223, paired with rome-evm-private `do_tx_unsigned` / `activate_ata`). Source = `ata(external_auth(caller), mint)`; destination = `ata(outer_solana_signer, mint)` — the real wallet, not a PDA. Signs as `external_auth(caller)`. Not used for MetaMask users (their outer signer is the proxy payer); reserved for Solana-native flows. | | `0xdd0119c8` | `user_balance(address account, bytes32 mint) view → uint64` | Read SPL TokenAccount.amount (u64 LE at offset 64) on user's PDA-owned ATA. Returns 0 if ATA doesn't exist (fresh-chain probe). Collapses 3 v1 dispatches (`ata` + `lamports` + `readU64At`) into one CrossStateEthCall. Backs `SPL_ERC20.balanceOf`. | | `0xed72dbc8` | `allowance_of(address owner, address spender, bytes32 mint) view → uint64` | Read owner-ATA's `delegated_amount` IFF on-chain delegate matches `external_auth(spender)` (HARD REQ in Rust to enforce ERC-20 semantics; otherwise routers probe non-zero allowance when actual delegate is someone else). Collapses 5 v1 dispatches into one. Backs `SPL_ERC20.allowance`. | | `0x4479b709` | `deposit_from_ata(uint256 wei_)` | Move wrapper SPL balance from caller's ATA into gas-token credit. Unwrap leg of the gas-wrapper bridge. | @@ -260,11 +262,11 @@ When writing new wrappers or adapters, prefer the `HelperProgram` selectors (sin - **`contracts/cpi/`** — Cardo CPI Foundation (library + templates). Shared Solidity helpers every Cardo app adapter builds on top of: `AccountMetaBuilder`, `AnchorInstruction`, `Cpi`, `PdaDeriver` (single PDA via `0xFF…07`), `PdasBatch` (multi-PDA via `0xFF…08` selector `0x944336f8` — use when deriving 2+ PDAs against the same Solana program), `SolanaConstants`, `UserPda`, `CostEstimate`, `CostEstimator`, `ICostView`, and the Pillar B cost-transparency trio. Also ships `templates/CpiAdapterBase.sol` (Ownable+Pausable+ReentrancyGuard+backend pointer scaffold) and `templates/CpiProgramWrapper.sol` (prose scaffold for golden-vector wrappers). See `contracts/cpi/README.md` for the adapter authoring guide, the three-layer pattern, and the `tx.origin`/`msg.sender` rule. Canonical spec: `rome-specs/active/technical/cardo-foundation.md`. - **`contracts/spl_token/`** — Low-level SPL token and associated token account libraries (`SplTokenLib`, `AssociatedSplTokenLib`). These use `CpiProgram.account_info()` to deserialize on-chain Solana account data (Borsh-encoded) from within Solidity. -- **`contracts/erc20spl/`** — Two wrappers live side-by-side: `SPL_ERC20` (legacy track, `erc20spl.sol`) and `SPL_ERC20_cached` (cached track, `erc20spl_cached.sol`, added #210). **`ERC20SPLFactory` deploys the cached variant since #211 (2026-05-22)**; the legacy contract is retained for back-compat but new chains get cached wrappers. Both use OpenZeppelin IERC20. Generic outbound-bridge surface (`bridgeOutToSolana`, `ensureRecipientAta`) lets any deployed wrapper serve as a Rome → Solana SPL bridge — consumed by **rome-ui's `useOutboundSplBridge` hook**. **Event emission:** standard `IERC20.Transfer` and `IERC20.Approval` fire on `_transfer` / `approve` / `mint_to` (#83). **Auto-ATA on writes:** `_transfer` and `mint_to` auto-create recipient ATAs (#63) — sender pays ~0.002 SOL rent. **Factory event:** `_register_contract` emits `TokenCreated` on every wrapper registration (#85), powering rome-ui's token-discovery indexer. **`ERC20Users.ensure_user` is mapping-only** — does NOT pre-fund the unified PDA; bootstrap happens via `SimpleActivator`. **Legacy track plumbing:** ATA derivation goes through `HelperProgram.ata(user, mint_id)`; transfers through `HelperProgram.transfer_spl(...)` (4-arg delegate variant when `from != msg.sender`) per #138 / #141 / #143. **Cached track defensive views (#216 + #217, 2026-05-24/25):** `balanceOf` / `allowance` return 0 (per ERC-20 spec) when the queried ATA doesn't exist instead of reverting; `approve` auto-creates the owner ATA if missing. All three use `try { SplCached.account(ata) }` for overlay-aware reads so cached wrappers compose with intra-tx balance-delta protocols (Uniswap V3 `Pool.mint`/`Pool.swap`). The previously-required deploy-time `wrapper.ensure_token_account()` warmup step was RETIRED by #216. **One legacy-read site remains in cached wrappers:** `totalSupply` reads the SPL Mint via `AccountReader.lamportsOf` + `readU64At` (no `mint_state` cached selector exists yet); stale after intra-tx `mint_to`. +- **`contracts/erc20spl/`** — Two wrappers live side-by-side: `SPL_ERC20` (legacy track, `erc20spl.sol`) and `SPL_ERC20_cached` (cached track, `erc20spl_cached.sol`, added #210). **`ERC20SPLFactory` deploys the cached variant since #211 (2026-05-22)**; the legacy contract is retained for back-compat but new chains get cached wrappers. Both use OpenZeppelin IERC20. Generic outbound-bridge surface (`bridgeOutToSolana`, `ensureRecipientAta`) lets any deployed wrapper serve as a Rome → Solana SPL bridge — consumed by **rome-ui's `useOutboundSplBridge` hook**. **Event emission:** standard `IERC20.Transfer` and `IERC20.Approval` fire on `_transfer` / `approve` / `mint_to` (#83). **Auto-ATA on writes:** `_transfer` and `mint_to` auto-create recipient ATAs (#63) — sender pays ~0.002 SOL rent (#238 gates the create when the ATA already exists, saving the CPI). **Factory event:** `_register_contract` emits `TokenCreated` on every wrapper registration (#85), powering rome-ui's token-discovery indexer. **`ERC20Users.ensure_user` is mapping-only** — does NOT pre-fund the unified PDA; bootstrap happens via `SimpleActivator`. **Legacy track plumbing:** ATA derivation goes through `HelperProgram.ata(user, mint_id)`; transfers through `HelperProgram.transfer_spl(...)` (4-arg delegate variant when `from != msg.sender`) per #138 / #141 / #143. **Cached track defensive views (#216 + #217, 2026-05-24/25):** `balanceOf` / `allowance` return 0 (per ERC-20 spec) when the queried ATA doesn't exist instead of reverting; `approve` auto-creates the owner ATA if missing. All three use `try { SplCached.account(ata) }` for overlay-aware reads so cached wrappers compose with intra-tx balance-delta protocols (Uniswap V3 `Pool.mint`/`Pool.swap`). The previously-required deploy-time `wrapper.ensure_token_account()` warmup step was RETIRED by #216. **One legacy-read site remains in cached wrappers:** `totalSupply` reads the SPL Mint via `AccountReader.lamportsOf` + `readU64At` (no `mint_state` cached selector exists yet); stale after intra-tx `mint_to`. - **`contracts/activation/`** — `SimpleActivator` is the user-paid first-time account-bootstrap entry point. Split into **three** `payable` functions because each ATA-create + activator-PDA-topup CPI pair consumes ~950k CU on Solana, and bundling two ATA creates in one tx (~1.65M CU emulated) exceeds the 1.4M-CU per-tx cap. **Step 1 — `activate()`** funds the user's unified PDA at the rent-exempt floor (890,880 lamports) and registers the EVM address in the `ERC20Users` mapping so wrapper writes (`transfer` / `approve` / `transferFrom`) and DEX swaps resolve `users.get_user(msg.sender)` correctly. **Step 2 — `createWusdcAta()`** tops up the activator's PDA + creates the user's WUSDC ATA. **Step 3 — `createWsolAta()`** same pattern for WSOL ATA. All three idempotent — re-running each one is a no-op on Solana. The UI fires the three txs sequentially on a single click. Sybil resistance: user pays all calls themselves (default `activationCost` = 1 USDC, `tokenAccountsCost` = 0.5 USDC × 2 = 2 USDC total); zero operator subsidy. Replaces the earlier `PdaActivator` (single-tx Meteora swap path) which exceeded CU budget on chains with the post-clean-slate wrapper stack. - **`contracts/meteora/`** — `MeteoraDAMMv1Factory` and `DAMMv1Pool` implement a Uniswap-style factory/pool pattern that delegates swaps to Meteora's on-chain Solana program via CPI. -- **`contracts/oracle/`** — Oracle Gateway V2: Chainlink-compatible adapters for Pyth Pull, Switchboard V3, and Pyth Lazer price feeds. `OracleAdapterFactory` deploys `PythPullAdapter`, `SwitchboardV3Adapter`, and `PythLazerFeedAdapter` instances via EIP-1167 minimal proxy clones. Each adapter reads Solana account data via CPI precompile, parses Borsh-encoded price data (`PythPullParser` / `SwitchboardParser`) or reads from the shared `PythLazerCache` singleton (Lazer), and normalizes to 8-decimal Chainlink format. `IExtendedOracleAdapter` extends `IAggregatorV3Interface` with confidence intervals, EMA data, and price status. `BatchReader` reads multiple feeds in one call. The factory includes owner-controlled pause/unpause emergency controls. Includes `examples/SampleLendingOracle.sol`. **Pyth Lazer surface** (`contracts/oracle/lazer/ILazerHelper.sol` + `PythLazerCache.sol` + `PythLazerFeedAdapter.sol`): a foundation-run keeper refreshes the singleton cache by calling `cache.refresh(envelope, ed25519_ix_idx, sig_idx)` (rome-sdk's `send_with_lazer*` constructs the Solana tx with the prepended Ed25519SigVerify ix); per-feed adapter clones expose Chainlink-compat `latestRoundData()` reads with cold-start `UninitializedPriceFeed` distinct from `StalePriceFeed`. Spec: [`rome-specs/active/technical/2026-05-20-og-v2-pyth-lazer-adapter.md`](../rome-specs/active/technical/2026-05-20-og-v2-pyth-lazer-adapter.md). Deploy via `scripts/oracle/deploy-lazer.ts` after the base OG-V2 stack is deployed. -- **`contracts/bridge/`** — Rome Bridge Phase 1 (Solana ↔ Ethereum cross-chain). `RomeBridgePaymaster` is an EIP-2771 trusted forwarder with per-user 3-tx sponsorship cap + (target, selector) allowlist. `RomeBridgeWithdraw` accepts ERC-20 input on Rome EVM and emits Wormhole Token Bridge or CCTP outbound messages via CPI signed as the user's PDA. Outbound Wormhole is split across two EVM txs (`approveBurnETH` then `burnETH`) because a single atomic Rome DoTx with two CPIs exceeds Solana's 1.4M compute-unit budget. `IWormholeTokenBridge.sol` and `ICCTP.sol` encode the native/Anchor Solana instructions. All Solana pubkeys are supplied via constructor params so the contract is network-agnostic. **See `contracts/bridge/README.md`** for architecture, flow diagrams, and a problems-and-fixes runbook covering the incidents from bring-up. Design spec: `rome-product/specs/rome-bridge-phase1.md`. +- **`contracts/oracle/`** — Oracle Gateway V2: Chainlink-compatible adapters for Pyth Pull and Switchboard V3 price feeds. `OracleAdapterFactory` deploys `PythPullAdapter` and `SwitchboardV3Adapter` instances via EIP-1167 minimal proxy clones. Each adapter reads Solana account data via the CPI precompile, parses Borsh-encoded price data (`PythPullParser` / `SwitchboardParser`), and normalizes to 8-decimal Chainlink format. `IExtendedOracleAdapter` extends `IAggregatorV3Interface` with confidence intervals, EMA data, and price status. `BatchReader` reads multiple feeds in one call. The factory includes owner-controlled pause/unpause emergency controls. Includes `examples/SampleLendingOracle.sol`. **Cached feed adapters (#224, #225 — 2026-06):** `CachedPythAdapter` is a Pyth-Pull adapter with an EVM-side cache — `refresh()` is a keeper-driven state-change that SSTOREs the normalized 8-decimal price; `latestRoundData()` is a pure SLOAD (~114K vs ~511K CU per feed on Hadrian). `CachedFeedAdapter` is a source-agnostic decorator that composes the same SLOAD shape over any `AggregatorV3Interface` feed. Keeper refresh runbook + script: `contracts/oracle/CACHED_FEEDS.md` + `scripts/oracle/refresh-cached-feeds.ts`. **Pyth Lazer surface removed in #196 + #236** (deferral decision; `PythLazerCache`, `PythLazerFeedAdapter`, the `lazer/` directory, and the `IEd25519` precompile are gone). +- **`contracts/bridge/`** — Rome Bridge Phase 1 (Solana ↔ Ethereum cross-chain). `RomeBridgeWithdraw` accepts ERC-20 input on Rome EVM and emits Wormhole Token Bridge or CCTP outbound messages via CPI signed as the user's PDA. Outbound Wormhole is split across two EVM txs (`approveBurnETH` then `burnETH`) because a single atomic Rome DoTx with two CPIs exceeds Solana's 1.4M compute-unit budget. **Rome → Solana SPL egress lives on `RomeBridgeWithdraw` as of #227** (2026-06): `bridgeOutToSolana(bytes32 recipient, uint256 amount, bytes32 mint)` + `ensureRecipientAta(bytes32 recipient, bytes32 mint)` — mint-agnostic, takes the wrapper's underlying SPL mint as a param so a single bridge contract serves every deployed wrapper. The legacy single-arg `SPL_ERC20.bridgeOutToSolana(bytes32, uint256)` + `ensureRecipientAta(bytes32)` (gas-mint-only) still exists on the wrapper for back-compat. `IWormholeTokenBridge.sol` and `ICCTP.sol` encode the native/Anchor Solana instructions. All Solana pubkeys are supplied via constructor params so the contract is network-agnostic. **`RomeBridgePaymaster` + `RomeBridgeInbound` were deleted in #227** — superseded long ago by the on-chain `settle_inbound_bridge` flow. **See `contracts/bridge/README.md`** for architecture + flow diagrams. Design spec: `contracts/bridge/SOLANA_EGRESS_DESIGN.md`. - **`contracts/system_program/`** — Solana System Program helpers. `instruction_data.sol` encodes System Program instructions (create account, transfer, assign, nonce operations, allocate) as little-endian bytes. `system_program.sol` wraps these as CPI calls. - **`contracts/mpl_token_metadata/`** — Deserializes Metaplex Token Metadata V2 accounts from Borsh-encoded binary. Parses creators, token standards, collection details, uses, and programmable config. Provides `find_metadata_pda()` and `load_metadata()`. - **`contracts/rome_evm_account.sol`** — PDA derivation helpers for Rome-EVM user accounts (maps `address` → Solana `bytes32` pubkey). @@ -287,7 +289,7 @@ If you're working on... | Cardo CPI Foundation | `contracts/cpi/` — `CpiAdapterBase`, `UserPda`, `PdaDeriver`, `PdasBatch`, `AnchorInstruction`, `AccountMetaBuilder`, cost-view trio | | Multi-PDA derivation (2+ PDAs against the same Solana program) | `contracts/cpi/PdasBatch.sol` — wraps the `pdas_batch_derive` selector (`0xFF…08` / `0x944336f8`). Worked example: `contracts/examples/pdas_batch.sol`. Use this over N×`PdaDeriver.derive` whenever you derive ≥2 PDAs back-to-back against the same program. | | Upstream precompile primitives (canonical) | [`rome-evm-private/CLAUDE.md`](../rome-evm-private/CLAUDE.md) — Rust dispatch + selector hex; treat as source of truth when adding methods | -| Bridge contracts | `contracts/bridge/RomeBridgeInbound.sol` + `RomeBridgeWithdraw.sol` + `contracts/bridge/README.md` | +| Bridge contracts | `contracts/bridge/RomeBridgeWithdraw.sol` + `contracts/bridge/README.md` (Paymaster + Inbound deleted in #227) | | Activator | `contracts/activation/SimpleActivator.sol` (3-tx user-paid bootstrap) | | ABI JSON for non-Solidity consumers | `npx hardhat compile` → `artifacts/contracts/interface.sol/*.json` | | Token nomenclature (wUSDC vs USDC, lowercase `w`) | This file, §"Token nomenclature" | @@ -320,7 +322,12 @@ When extending `IHelperProgram` / `ICrossProgramInvocation` / `IWithdraw` / `ISy Deployment metadata is tracked in `deployments/{network}.json`, written by the Hardhat scripts on each `npx hardhat run`. The local stack file `local.json` is generated by `scripts/setup-local.ts` and should not be committed (regenerated per local stack restart). Per-chain devnet receipts are committed alongside their hardhat network entry. -**Current devnet deployments:** none. All 2026-04 → 2026-05 devnet chains (rome, subura, esquiline, cassius, cassius-test, monti_spl, maximus) were retired as part of the clean-slate transition. Their per-chain hardhat network entries + `deployments/.json` artifacts were removed alongside the registry directory cleanup; CHANGELOG.md preserves the deploy history for archival reads. New chains add themselves back via `/bring-up-chain` Row 6 (`/deploy-solidity`). +**Current deployments** (live `deployments/.json` artifacts): +- `hadrian` (200010, testnet) — full stack: `ERC20SPLFactory`, OG-V2 (Pyth + Switchboard feeds, BatchReader), `RomeBridgeWithdraw`, `SPL_ERC20_*` wrappers +- `trajan` (121302, devnet) — wrappers + `RomeBridgeWithdraw` +- `nerva` (210000, testnet) — wrappers + `RomeBridgePaymaster` (legacy artifact, not actively used) + +**Retired** (network entry + deployment artifact both removed): `marcus` / `augustus` (#229, 2026-06), `aurelius` (#228, real-testnet decommissioned 2026-06-04); 2026-04→05 chains (rome, subura, esquiline, cassius, cassius-test, monti_spl, maximus) earlier in the clean-slate transition. CHANGELOG.md preserves deploy history. New chains add themselves back via `/bring-up-chain` Row 6 (`/deploy-solidity`). ### Networks @@ -328,9 +335,10 @@ Deployment metadata is tracked in `deployments/{network}.json`, written by the H - `sepolia` — Ethereum Sepolia testnet (key: `SEPOLIA_PRIVATE_KEY`) - `hardhatMainnet` — Hardhat EDR simulated L1 network (used for oracle parser unit tests) - `hardhatOp` — Hardhat EDR simulated OP Stack network +- `hadrian` (200010), `trajan` (121302), `nerva` (210000) — current Rome network entries in `hardhat.config.ts`. - Per-chain Rome networks (`: { chainId, url, accounts: [_PRIVATE_KEY] }`) are added when chains are brought up via `/bring-up-chain`. -**Decommissioned (removed from hardhat config in 2026-05):** `maximus` (121215, #90), `subura` (121222), `esquiline` (121225), `cassius` (121228), `cassius-test` (121298), `monti_spl` (legacy subura-proxy alias). See CHANGELOG.md for per-chain deploy history. +**Decommissioned (removed from hardhat config):** `marcus` / `augustus` (#229), `aurelius` (#228), and earlier batch — `maximus` (121215, #90), `subura` (121222), `esquiline` (121225), `cassius` (121228), `cassius-test` (121298), `monti_spl` (legacy subura-proxy alias). See CHANGELOG.md for per-chain deploy history. ### Solidity Version @@ -357,11 +365,11 @@ Target: `0.8.28`. Production profile enables optimizer with 200 runs. | `IHelperProgram` surface (`0xff..09`) | `contracts/erc20spl/erc20spl.sol` (4 ATA reads + `_transfer` + `ensure_token_account`), `contracts/cpi/UserPda.sol` (`.ata` delegates), `contracts/bridge/RomeBridgeWithdraw.sol` (3 ATA reads), `contracts/activation/SimpleActivator.sol`, `contracts/examples/helper.sol` (worked examples). | | `IWithdraw.withdraw_to_pda` / `withdraw_to_ata` | rome-ui `src/features/portfolio/hooks/useWrapUnwrap.ts` (wrap leg). | | Contract ABIs | `rome-ui/src/abis/*.json` + parseAbi() call sites, `tests/` Solidity test contracts, `CHANGELOG.md` | -| `SPL_ERC20.bridgeOutToSolana` / `ensureRecipientAta` / `balanceOf` | **rome-ui** `src/features/bridge/hooks/useOutboundSplBridge.ts`, `useBalances.ts`, `useRomeHoldings.ts`. ABI is parseAbi-encoded inline; no JSON to regenerate. | +| `RomeBridgeWithdraw.bridgeOutToSolana` / `ensureRecipientAta` (mint-agnostic 3-arg / 2-arg signatures since #227) and `SPL_ERC20.balanceOf` | **rome-ui** `src/features/bridge/hooks/useOutboundSplBridge.ts`, `useBalances.ts`, `useRomeHoldings.ts`. ABI is parseAbi-encoded inline; no JSON to regenerate. The legacy `SPL_ERC20.bridgeOutToSolana(bytes32, uint256)` 1-arg signature still exists on the wrapper but is not the active path. | | `ERC20SPLFactory.add_spl_token_no_metadata` / `TokenCreated` event | **rome-ui** backend's token-discovery indexer (watches `TokenCreated` to populate Redis token cache served at `/api/tokens`); `src/features/portfolio/hooks/useChainTokenBalances.ts` consumes the cache. `src/abis/ERC20SPLFactory.json` mirror only if the indexer's ABI parser uses it. | | `SimpleActivator.activate` / `createWusdcAta` / `createWsolAta` / `isActivated` / `activationCost` / `tokenAccountsCost` | **rome-ui** `src/features/portfolio/components/ActivateAccountButton.tsx` (primary CTA replacement on Swap/Bridge/Liquidity until activated; fires three txs sequentially on one click), `src/features/portfolio/hooks/useIsPdaActivated.ts` (visibility gate). Inline parseAbi; `chain.contracts.simpleActivator` field wires the address. | | `RomeBridgeWithdraw.burnUSDC` / `burnETH` / `approveBurnETH` | **rome-ui** `src/features/bridge/hooks/useOutboundCctpSend.ts`, `useOutboundWhSend.ts`. Inline parseAbi, no JSON regen. | -| `RomeBridgePaymaster` / `RomeBridgeInbound` | **Legacy** since 2026-04-26 inbound rewrite — superseded by `settle_inbound_bridge` on rome-evm-private. rome-ui keeps these in `chain.contracts` config for back-compat parsing only; no active call sites. | +| `RomeBridgePaymaster` / `RomeBridgeInbound` | **Deleted in #227** (2026-06). Were already legacy since 2026-04-26 — superseded by `settle_inbound_bridge` on rome-evm-private. rome-ui's `chain.contracts` config still parses optional addresses for back-compat; no active call sites. | | Oracle adapter interfaces | Consuming contracts in this repo that use the adapters | | SPL token wrapper logic (`SPL_ERC20` / `SPL_ERC20_cached`) | **Five canonical protocol forks** validated end-to-end against `SPL_ERC20_cached` on Hadrian: `rome-uniswap-v2/` (PR #59), `rome-uniswap-v3/` (PR #1 — `scripts/METRICS.md`), `rome-aave-v3/` (PR #1 — METRICS.md), `compound-on-rome-comet/` (PR #18 — [`scripts/hadrian-cached-test/METRICS.md`](https://github.com/rome-protocol/compound-on-rome-comet/blob/main/scripts/hadrian-cached-test/METRICS.md)), and `rome-uniswap-v4/` (PR #1, full canonical surface PoolManager + PositionManager + Permit2 + StateView + V4Quoter + UR — [`scripts/hadrian-cached-test/METRICS.md`](https://github.com/rome-protocol/rome-uniswap-v4/blob/main/scripts/hadrian-cached-test/METRICS.md)). The off-chain `wrapper.ensure_token_account()` deploy-time warmup is RETIRED as of #216 (2026-05-24) — `approve` auto-creates the owner ATA in the common case. #217 (2026-05-25) made `balanceOf` / `allowance` / `approve` overlay-aware via `try { SplCached.account(ata) }`, enabling composition with V3 `Pool.mint`/`Pool.swap` balance-delta checks. Existing Hadrian wrappers (wUSDC `0x7632…`, wETH `0xDaA3…`, wSOL `0x101a…`) need redeploy after #217; factory redeploy landed in #218. Plus `rome-ui/src/features/portfolio` (renders wrapper rows via `useRomeHoldings`). **Known overlay-blind site:** `totalSupply` still reads the SPL Mint via legacy `AccountReader`; needs a `mint_state(bytes32 mint)` cached selector upstream. | | Hardhat network config | `rome-solidity-sdk/` uses same network definitions | @@ -374,8 +382,9 @@ rome-ui consumes a small, stable surface from this repo. Changes to that surface | Contract | Method / event | rome-ui consumer | |---|---|---| -| `SPL_ERC20` | `bridgeOutToSolana(bytes32 recipient, uint256 value) → bool` | `src/features/bridge/hooks/useOutboundSplBridge.ts` (Rome → Solana outbound for any wrapper) | -| `SPL_ERC20` | `ensureRecipientAta(bytes32 recipient) → bytes32` | same hook (preflight before bridge tx — single CPI ATA-create paid by sender's unified user PDA) | +| `RomeBridgeWithdraw` | `bridgeOutToSolana(bytes32 recipient, uint256 amount, bytes32 mint)` (3-arg, mint-agnostic since #227) | `src/features/bridge/hooks/useOutboundSplBridge.ts` (Rome → Solana outbound for any wrapper) | +| `RomeBridgeWithdraw` | `ensureRecipientAta(bytes32 recipient, bytes32 mint)` (idempotent 2-arg ATA-create since #227) | same hook (preflight before bridge tx — single CPI ATA-create) | +| `SPL_ERC20` | `bridgeOutToSolana(bytes32 recipient, uint256 value) → bool` + `ensureRecipientAta(bytes32 recipient)` | Legacy gas-mint-only path kept on the wrapper for back-compat. rome-ui now uses the `RomeBridgeWithdraw` mint-agnostic variants. | | `SPL_ERC20` | `balanceOf(address) → uint256` (now reads AUTHORITY_PDA's ATA, not `_accounts` map) | wagmi multicall, `useChainTokenBalances`, every Portfolio row | | `SPL_ERC20` | `transfer` / `transferFrom` / `approve` / `symbol` / `decimals` (standard IERC20 + IERC20Metadata) | wagmi readContract, TokenList, swap/liquidity flows | | `SPL_ERC20` | `Transfer(from, to, value)` / `Approval(owner, spender, value)` events (emitted since #83) | rome-via-enrich/holders.rs (filters by topic0), eth_getLogs consumers, block explorers | @@ -398,7 +407,7 @@ rome-ui consumes a small, stable surface from this repo. Changes to that surface These are observable from the outside but not enforced by the type system. Breaking any silently breaks rome-ui: -- `bridgeOutToSolana` signs as `AUTHORITY_PDA` (`find_program_address([EXTERNAL_AUTHORITY, evmAddr])`), with **empty seeds** in the precompile `invoke_signed`. The source ATA = `getATA(AUTHORITY_PDA, mint)` — the canonical cross-chain location where bridged-in tokens live. rome-ui assumes the recipient ATA already exists; callers MUST run `ensureRecipientAta` first if uncertain (see `useOutboundSplBridge`). +- `RomeBridgeWithdraw.bridgeOutToSolana(recipient, amount, mint)` signs as `AUTHORITY_PDA` (`find_program_address([EXTERNAL_AUTHORITY, _msgSender()])`). The source ATA = `getATA(AUTHORITY_PDA, mint)` — the canonical cross-chain location where bridged-in tokens (any wrapper) live. Single CPI: `HelperProgram.transfer_spl(toAta, amount, mint)`. Recipient ATA derivation = `UserPda.ataForKey(recipient, mint)`. rome-ui assumes the recipient ATA already exists; callers MUST run `ensureRecipientAta` first if uncertain (see `useOutboundSplBridge`). - `ensureRecipientAta` is **idempotent** — returns the same ATA address whether it pre-existed or was created. rome-ui probes Solana directly first to skip the call when not needed. - `balanceOf` reads `getATA(AUTHORITY_PDA, mint)`, NOT the `_accounts` mapping. Bridged-in users (Wormhole complete_transfer_wrapped, useNativeDepositSend) only have balance in the AUTHORITY_PDA's ATA; the legacy mapping path returned 0 for them. - `SimpleActivator.activate()` + `SimpleActivator.createWusdcAta()` + `SimpleActivator.createWsolAta()` are the user-paid first-time bootstrap, split into **three** txs because each ATA-create + activator-PDA-topup CPI pair consumes ~950k CU on Solana, and bundling two ATA creates in one tx (~1.65M CU emulated) was rejected at the rome-sdk pre-flight as `TooManyComputeUnitsInAtomicTx`. Tx 1 funds the user's unified PDA at the rent-exempt floor (890,880 lamports) and registers in the `ERC20Users` mapping. Tx 2 tops up the activator's own PDA and creates the user's WUSDC ATA. Tx 3 same pattern for WSOL ATA. Each is `payable` and idempotent (activate refunds `msg.value` if PDA already has lamports; the ATA-create calls no-op via `create_payer` + `ensure_token_account` short-circuits when state is already met). Sybil resistance: user pays all calls themselves; no operator subsidy. The previously-distinct `PAYER_PDA` (salted at `[EXTERNAL_AUTHORITY, evmAddr, "PAYER"]`) collapsed into the unified PDA in rome-solidity 0acabea — the unified PDA now signs CPIs, owns ATAs, and holds rent funds for transient bridge accounts.