tip: temporary storage precompile spec#3631
Conversation
There was a problem hiding this comment.
💡 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".
tempoxyz-bot
left a comment
There was a problem hiding this comment.
👁️ 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:
- The "free to execute" successful
XCLEARbypasses EIP-2929 cold-access metering, enabling disk I/O DoS. - Permissionless
XCLEARdestroys state that integrations need for refunds — directly conflicting with the TIP's own DEX-orders use case. - The bespoke
XSTOREpricing conflicts with active TIP-1000/TIP-1016 invariants and underprices state creation. - 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.
- The refund plumbing required by
XCLEARdoes not exist in the current precompile stack, and inheritedSSTOREclear 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-200must be reviewed together withtips/tip-1016.md:93-105andL173-L183. A scalar bucket price is insufficient on Tempo becauseblock.gas_usedis 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 finaltx_gas_used_after_refund. - ⚡ XLOAD Expired-Path Gas Ambiguity: The TIP simultaneously says
XLOADreverts once expiry is known and that it always charges twoSLOADs, 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.
|
|
||
| Let `ttl = expiry - block.timestamp` as measured at execution time. | ||
|
|
||
| `XSTORE` MUST charge gas according to the following lifetime buckets: |
There was a problem hiding this comment.
🚨 [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.
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.
|
|
||
| `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. |
There was a problem hiding this comment.
🚨 [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.
|
|
||
| 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. |
There was a problem hiding this comment.
🚨 [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.
|
|
||
| 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 |
There was a problem hiding this comment.
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.
|
|
||
| The second storage slot of each entry uses the full 256-bit word with the following layout: | ||
|
|
||
| | Bits | Meaning | |
There was a problem hiding this comment.
🚨 [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.
|
|
||
| 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)` |
There was a problem hiding this comment.
🛡️ [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.
|
This PR has been marked stale due to 7 days of inactivity. |
|
Closed due to inactivity. Reopen if still needed. |
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)XSTOREandXLOADare scoped tomsg.sender, so each caller gets an isolated slot namespace.XCLEARis 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.