Skip to content

tip: temporary storage precompile spec#3631

Open
legion2002 wants to merge 5 commits into
mainfrom
tip/1048
Open

tip: temporary storage precompile spec#3631
legion2002 wants to merge 5 commits into
mainfrom
tip/1048

Conversation

@legion2002
Copy link
Copy Markdown
Contributor

@legion2002 legion2002 commented Apr 16, 2026

Abstract

This TIP introduces a new precompile that stores expiring key-value slots for an address.

The precompile exposes three entrypoints:

  • XSTORE(slot, value, expiry)
  • XLOAD(slot)
  • XCLEAR(addr, slot)

XSTORE and XLOAD are scoped to msg.sender, so each caller gets an isolated slot namespace. XCLEAR is permissionless, but it may only succeed once an entry has expired.

This allows us to introduce an out-of-protocol solution for clearing expired state, without having to make involved changes to the state trie or introduce new system transactions.

Note: The precompile logic can be built completely out-of-protocol, but we propose this as a TIP so that we can also introduce a discounted gas schedule that encourages the use of temporary storage.

@legion2002 legion2002 marked this pull request as ready for review April 16, 2026 13:56
@legion2002 legion2002 changed the title docs(tip-1048): draft X storage precompile spec tip: temporary storage precompile spec Apr 16, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff3b6f37cd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tips/tip-1048.md Outdated
@legion2002 legion2002 added the cyclops Trigger Cyclops PR audit label Apr 16, 2026
Copy link
Copy Markdown

@tempoxyz-bot tempoxyz-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👁️ Cyclops Review — TIP-1048: X Storage Precompile

This is a docs-only PR adding a draft TIP specification for an expiring key-value storage precompile (XSTORE, XLOAD, XCLEAR). The specification has 8 actionable findings (5 high, 2 medium, 1 defense-in-depth) across gas accounting, economic safety, and consensus determinism.

Key themes:

  1. The "free to execute" successful XCLEAR bypasses EIP-2929 cold-access metering, enabling disk I/O DoS.
  2. Permissionless XCLEAR destroys state that integrations need for refunds — directly conflicting with the TIP's own DEX-orders use case.
  3. The bespoke XSTORE pricing conflicts with active TIP-1000/TIP-1016 invariants and underprices state creation.
  4. Two independent consensus-split risks: implementation-defined storage layout makes warm/cold gas non-canonical, and scalar bucket prices don't define the T1C regular/state gas split.
  5. The refund plumbing required by XCLEAR does not exist in the current precompile stack, and inherited SSTORE clear refunds defeat the stated anti-farming floor.

See inline comments for details.

Reviewer Callouts
  • Economic Incentives vs DoS Limits: Subsidizing state cleanup is desirable, but execution discounts that bypass base EVM disk-access costs (EIP-2929 cold reads) are fundamentally unsafe. Refunds apply at the end of the transaction and respect the 1/5 gas cap, which safely incentivizes cleanup without bypassing I/O accounting.
  • T1C Gas Model Integration: tips/tip-1048.md:189-200 must be reviewed together with tips/tip-1016.md:93-105 and L173-L183. A scalar bucket price is insufficient on Tempo because block.gas_used is derived from regular gas only.
  • Precompile Refund Plumbing: Any implementation needs an end-to-end test proving that a precompile-originated storage clear actually changes frame_result.gas().refunded() and final tx_gas_used_after_refund.
  • XLOAD Expired-Path Gas Ambiguity: The TIP simultaneously says XLOAD reverts once expiry is known and that it always charges two SLOADs, which is another source of gas divergence unless stated unambiguously.
  • Expiring Nonce Migration Note: Line 222 says "just abandon the ring buffer," but the current design uses exclusive expiry windows and a sponsor-invariant replay key. This guidance should be removed or rewritten before it becomes implementation folklore.
  • XStorage as Escrow State: The fundamental assumption that state cleanup can be crowdsourced to searchers via gas refunds breaks down when that state represents financial liabilities in higher-level contracts.

Comment thread tips/tip-1048.md

Let `ttl = expiry - block.timestamp` as measured at execution time.

`XSTORE` MUST charge gas according to the following lifetime buckets:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 [SECURITY] Free Execution of Successful XCLEAR Enables Disk I/O DoS

The TIP mandates that a successful XCLEAR is "free to execute," but each clear requires at least one cold state read plus state-clearing writes. By making the execution cost zero, the spec bypasses EIP-2929 cold-access gas accounting, allowing an attacker to batch hundreds of thousands of expired-slot clears into a single block with only loop-overhead gas, saturating disk I/O and stalling block processing.

Recommended Fix: Successful XCLEAR MUST still charge the normal SLOAD cost for reading the expiry slot (respecting warm/cold access). The cleanup incentive should rely solely on the 15,000 gas refund (subject to the existing 20% refund cap), not on zeroing the upfront execution cost.


⚠️ [ISSUE] XCLEAR Refund Cannot Reach Transaction Gas Accounting

The TIP promises 15,000 gas added to the transaction refund counter, but the current precompile stack has no path to forward precompile-originated storage refunds into the Gas object used for final transaction accounting. PrecompileOutput has no refund field, and the dispatch wrapper only propagates gas_used and reservoir. Even with future plumbing, active TIP-1016 caps realized refunds at 20% of gas used.

Recommended Fix: Either add end-to-end refund propagation from precompile storage through PrecompileOutput into frame gas accounting, or remove the refund claim until plumbing exists. Restate in capped-refund terms consistent with TIP-1016.

Comment thread tips/tip-1048.md

`XCLEAR` MUST first read the target entry's expiry to determine whether the entry is clearable.

If `XCLEAR` does not clear the slot, the call MUST cost exactly as much gas as an `SLOAD` of the expiry slot in the corresponding warm or cold access state. An unsuccessful `XCLEAR` MUST return `false` and MUST NOT add any gas refund.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 [SECURITY] Permissionless XCLEAR Destroys Application State Needed for Refunds

XCLEAR is permissionless and any actor can clear any expired slot. Because XLOAD reverts on expired slots and XCLEAR returns only a boolean, applications cannot recover stored values after expiry. For integrations using XStorage as authoritative proof of expiring orders/escrows (a use case the TIP itself cites in its Motivation), a third-party searcher can clear the slot first to claim the gas refund, destroying the only on-chain evidence needed for post-expiry refunds. The TIP's "Race Conditions" section claims XCLEAR has no correctness races, which is contradicted by this information-loss scenario.

Recommended Fix: Either (1) restrict XCLEAR to the slot owner (msg.sender == addr), or (2) add a grace period allowing XLOAD to return expired data with an is_expired flag so applications can read and act before clearing.

Comment thread tips/tip-1048.md

Short-lived entries are still cheaper than persistent storage, but they do not start below ordinary storage-write costs. The minimum `20,000` gas tier ensures that `XSTORE` plus a later successful `XCLEAR` can never create net-positive refund farming against the `15,000` gas cleanup refund.

A one-year entry is still cheaper than permanent storage at `200,000` gas. Anything beyond one year is charged `250,000` gas, matching the [TIP-1000](./tip-1000.md) benchmark for permanent state growth.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 [SECURITY] XSTORE Bespoke Gas Table Underprices State vs Active T1C Invariants

The TIP models each entry as two state elements (value and metadata) but replaces ordinary storage pricing with a custom table as low as 20,000 gas total. Active TIP-1000 requires 250,000 gas per new zero-to-nonzero storage slot, and TIP-1016 requires precompile storage to use the same SSTORE accounting path. A compliant implementation would create two new trie slots for 20,000 gas instead of the normal 500,000 creation charge. Entries can also be kept alive indefinitely by rewriting before expiry.

Recommended Fix: Remove the bespoke gas schedule unless TIP-1000/TIP-1016 are explicitly amended. If discounted pricing is desired, use a bounded structure (like the existing nonce ring buffer) that only performs reset-cost writes.


🚨 [SECURITY] Scalar Bucket Prices Do Not Define the T1C Regular/State Gas Split (Consensus Split)

XSTORE defines a single scalar gas number per TTL bucket, but T1C is a dual-counter gas model. Regular gas and state gas have different consensus effects: only regular gas counts toward block gas_used and the 30M block limit. Two honest implementations can satisfy the literal bucket table yet assign different regular/state splits, causing one to accept a block and another to reject it.

Recommended Fix: Define a canonical (regular_gas, state_gas) pair for every bucket, or explicitly inherit the normal SSTORE/T1C accounting split.

Comment thread tips/tip-1048.md

This keeps temporary storage useful for real application flows, while making sure that very long-lived entries do not underprice effectively persistent state.

## Empty And Cleared Slots
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ISSUE] Inherited SSTORE Clear Refunds Defeat the 20k Anti-Farming Floor

This line claims the 20,000 minimum tier prevents net-positive refund farming against the 15,000 cleanup refund. However, XCLEAR must zero two nonzero storage words, and Tempo's precompile sstore() independently accumulates ordinary SSTORE clear refunds (4,800 per slot under current Osaka tables). A successful XCLEAR therefore yields 15,000 + 4,800 + 4,800 = 24,600 total refund, exceeding the 20,000 floor. Currently masked by missing refund plumbing but becomes exploitable once that plumbing is added.

Recommended Fix: Explicitly state that XCLEAR's 15,000 refund replaces (not adds to) inherited SSTORE clear refunds for its internal zeroing writes, or raise the minimum tier to cover the full refund surface.

Comment thread tips/tip-1048.md

The second storage slot of each entry uses the full 256-bit word with the following layout:

| Bits | Meaning |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 [SECURITY] Hidden Storage Layout Makes Warm/Cold Gas Non-Canonical (Consensus Split)

"MAY use any internal layout" conflicts with the gas rules on lines 161-163 and 181 that charge based on warm/cold state of the corresponding storage slots. Tempo's access-list warming and precompile storage charging operate on concrete (address, key) pairs. Without a canonical slot derivation, two compliant implementations assign different physical keys to the same logical (owner, slot) entry. A transaction with an EIP-2930 access list warming one implementation's keys will be warm on that client and cold on another — a 4,200+ gas delta that can cause a consensus-visible split.

Recommended Fix: Canonicalize the physical storage key derivation. Define exactly which keys represent the value and metadata slots for each (owner, slot) pair so access-list warming is deterministic across implementations.

Comment thread tips/tip-1048.md

If the call succeeds, the precompile MUST store the expiry in the low 64 bits of the metadata slot, and MUST set the version bit and all reserved middle bits to `0`.

### `XLOAD(slot)`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ [DEFENSE-IN-DEPTH] Spec Does Not Address DELEGATECALL/CALLCODE/STATICCALL Semantics

The TIP scopes storage by msg.sender but never specifies behavior under alternate call schemes. Tempo's current precompile wrapper already rejects indirect calls and static-context mutations, so this is not currently exploitable. However, the spec should be self-contained — EIP-1153 (transient storage) explicitly defines ownership under all call schemes.

Recommended Fix: Add normative language specifying that the precompile MUST revert if invoked via DELEGATECALL/CALLCODE, and that XSTORE/XCLEAR MUST revert in STATICCALL context.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 2, 2026

This PR has been marked stale due to 7 days of inactivity.

@github-actions github-actions Bot added the stale label May 2, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Closed due to inactivity. Reopen if still needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cyclops Trigger Cyclops PR audit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants