Skip to content

feat(l1+evm+precompiles): add L1-backed zone db#698

Open
0xrusowsky wants to merge 25 commits into
mainfrom
rus/l1-aware-db
Open

feat(l1+evm+precompiles): add L1-backed zone db#698
0xrusowsky wants to merge 25 commits into
mainfrom
rus/l1-aware-db

Conversation

@0xrusowsky

@0xrusowsky 0xrusowsky commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

replaces #626

High-level goal

before this PR stack, Zones had two representations of Tempo L1 state:

  1. A semantic PolicyCache / PolicyProvider, populated from typed TIP-403 and TIP-20 events with RPC fallback. Custom ZoneTip20Token and ZoneTip403ProxyRegistry implementations used it to reproduce upstream policy behavior.
  2. A generic block-versioned raw storage cache keyed by (address, slot), used by TempoState and Zone system contracts for portal storage reads.

this duplicated policy semantics and left two L1-state paths that had to remain aligned in block anchoring, event decoding, invalidation, RPC fallback, and upstream behavior. The goal is to remove the semantic policy implementation from the EVM path and execute upstream Tempo code against one factory-installed L1-aware database. The database wraps the normal caller-provided EVM database and:

  • lazily reads the Tempo L1 anchor from local TempoState
  • serves TIP-403 registry storage from L1 at that anchor
  • overlays only the L1-owned transferPolicyId field in TIP-20’s packed slot, preserving Zone-local fields
  • delegates ordinary account, code, block-hash, and storage reads to local Zone state
  • rejects TIP-403 writes and strips mirrored TIP-20 fields from canonical state transitions

from upstream Tempo's perspective these remain ordinary EVM storage operations, but selected slots are virtualized to finalized L1 state. This makes upstream Tempo code the source of execution semantics rather than maintaining Zone-specific policy implementations or installing a separate storage provider around each precompile call.

EVM Database

database.rs adds AnchoredZoneDb, a database adapter installed by ZoneEvmFactory for every EVM created for payload building, canonical import, RPC calls, tracing, transaction-pool validation, and maintenance execution. The factory uses the alloy-evm database-context extension so the internal Tempo context can execute over AnchoredZoneDb<DB> while ZoneEvm continues to expose and return the original caller-provided DB.

this keeps Reth’s state ownership and commit path unchanged. Reads pass through the adapter during execution, while successful state transitions are validated and sanitized before Reth commits them through the original database. Inner database failures preserve their original error type; anchor, L1-read, and mirrored-write failures are propagated as typed custom EVM errors.

the adapter and native TempoState precompile share one execution-local anchor controller. It tracks whether execution is still at the parent Tempo anchor or has advanced to its direct child, rejects reads at an inconsistent anchor, rejects parent-anchor reads before advancement, and rejects duplicate or non-contiguous advancement. Noncommitting payload simulations restore the controller snapshot so they cannot leak anchor state into included transaction execution.

until TIP-20 policy storage moves fully into TIP-403, the adapter retains compatibility with the current packed layout. It reads the local packed word first, replaces only the L1-owned transferPolicyId field for execution, and restores the local field before canonical transitions are committed. TIP-403 account or storage mutations fail closed.

Precompile Execution

execution.rs now has one execution path for Zone-native and upstream Tempo precompiles. Every wrapper receives the same EVM configuration, storage-action recorder, and non-creditable-slot set. Whether a storage read is local or L1-backed is entirely a database concern rather than a choice between local and L1-aware precompile providers.

CallRules contains only Zone-specific admission behavior:

  • delegate calls are denied by default and explicitly enabled only for precompiles that support them
  • fixed-gas selectors are checked before execution
  • pure selector/caller admission returns either Continue or ABI-encoded revert data
  • the common wrapper applies calldata gas, reservoir handling, and fixed-price output consistently
  • admitted calls preserve the original calldata and caller when forwarding upstream

TIP-20, TIP-403, and TipFeeManager now execute the upstream Tempo implementations directly over ordinary EvmPrecompileStorageProvider storage. The remaining Zone-specific behavior is limited to privacy checks, bridge mint/burn authorization, read-only TIP-403 selectors, fixed gas, and delegate-call policy.

this removes ZonePrecompileStorageProvider, the separate local/L1-backed execution orchestration, and the custom L1-aware ZoneFeeManager. Tempo’s ordinary protocol fee manager now observes the same anchored policy state through the EVM database adapter.

Follow-up work

@socket-security

socket-security Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedalloy-primitives@​1.6.0 ⏵ 1.6.110010093100100
Updatedalloy-sol-types@​1.6.0 ⏵ 1.6.110010093100100

View full report

@0xrusowsky

Copy link
Copy Markdown
Contributor Author

cyclops audit

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

cc @0xrusowsky

Cyclops audit event published. View workflow run

Config: config: default, iterations: default, hours: default

@tempoxyz-bot

tempoxyz-bot commented Jul 17, 2026

Copy link
Copy Markdown

👁️ Cyclops Security Review

604908a

🧭 Finalizing · mode=normal · workers 3/3 done (0 left) · verify pending 0 · ending soon

Worker Engine Latest Status Status
pr-698-w1 claude-opus-4-8 🚨 Iteration 3 · Verify Done
pr-698-w2 gpt-5.5 🚨 Iteration 3 · Verify Done
pr-698-w3 gpt-5.6-sol 🚨 Iteration 3 · Verify Done

Findings

# Finding Severity Status
1 Zone TIP-20 privacy bypass: userRewardInfo/getPendingRewards reads are not access-gated, leaking any account's private balance High Iteration 1 · Verify
2 Zone TIP-20 balance/allowance privacy bypass via non-canonical (dirty-padded) address calldata high Iteration 2 · Verify
3 Enabling a token with a non-default L1 policy permanently stalls Tempo advancement High Iteration 1 · Verify
4 P2P followers can execute leader blocks with stale L1 policy storage High Iteration 1 · Verify
5 AnchoredZoneDb::sanitize_state rejects enableToken policy-id init, halting the zone for any bridged token with a non-default L1 transfer policy high Iteration 3 · Verify
6 Zone TIP-20 receive-policy blocked funds become unclaimable High Iteration 2 · Verify
7 Followers never receive the mutation barriers required by their consensus-critical L1 cache High Iteration 2 · Verify
8 ZoneBlockExecutor fee-token override reads the previous transaction, breaking non-default fee-token execution Medium Iteration 3 · Verify
9 Forgotten enabled tokens retain stale L1 transfer policy IDs indefinitely High Iteration 3 · Verify
⚙️ Controls
  • 🚀 Keep only 1 remaining iteration per worker after the current work finishes.
  • 👀 Keep only 2 remaining iterations per worker after the current work finishes.
  • ❤️ Let only worker 1 continue; other workers skip queued iterations.
  • 😄 Let only worker 2 continue; other workers skip queued iterations.
  • 🎉 End faster by skipping queued iterations and moving toward consolidation.
  • 😕 Stop active workers/verifiers now and start consolidation immediately.

📜 48 events

🔍 pr-698-w1 iter 1/3 [audit-general.md]
🔍 pr-698-w2 iter 1/3 [audit-ripple.md]
🔍 pr-698-w3 iter 1/3 [audit-invariants.md]
🚨 pr-698-w1 iter 1 — finding
🚨 Finding: Zone TIP-20 privacy bypass: userRewardInfo/getPendingRewards reads are not access-gated, leaking any account's private balance (High)
🔍 pr-698-w1 iter 2/3 [audit-invariants.md]
🔬 Verifying: Zone TIP-20 privacy bypass: userRewardInfo/getPendingRewards reads are not access-gated, leaking any account's private balance
📋 Verify: Zone TIP-20 privacy bypass: userRewardInfo/getPendingRewards reads are not access-gated, leaking any account's private balance → ✅ Verified
🚨 pr-698-w1 iter 2 — finding
🚨 Finding: Zone TIP-20 balance/allowance privacy bypass via non-canonical (dirty-padded) address calldata (high)
🔍 pr-698-w1 iter 3/3 [audit-invariants.md]
🔬 Verifying: Zone TIP-20 balance/allowance privacy bypass via non-canonical (dirty-padded) address calldata
📋 Verify: Zone TIP-20 balance/allowance privacy bypass via non-canonical (dirty-padded) address calldata → ✅ Verified
🚨 pr-698-w3 iter 1 — finding
🚨 Finding: Enabling a token with a non-default L1 policy permanently stalls Tempo advancement (High)
🔍 pr-698-w3 iter 2/3 [audit-ripple.md]
🔬 Verifying: Enabling a token with a non-default L1 policy permanently stalls Tempo advancement
🚨 pr-698-w2 iter 1 — finding
🚨 Finding: P2P followers can execute leader blocks with stale L1 policy storage (High)
🔍 pr-698-w2 iter 2/3 [audit-ripple.md]
🔬 Verifying: P2P followers can execute leader blocks with stale L1 policy storage
📋 Verify: P2P followers can execute leader blocks with stale L1 policy storage → ✅ Verified
🚨 pr-698-w1 iter 3 — finding
🚨 Finding: AnchoredZoneDb::sanitize_state rejects enableToken policy-id init, halting the zone for any bridged token with a non-default L1 transfer policy (high)
🔬 Verifying: AnchoredZoneDb::sanitize_state rejects enableToken policy-id init, halting the zone for any bridged token with a non-default L1 transfer policy
📋 Verify: Enabling a token with a non-default L1 policy permanently stalls Tempo advancement → ✅ Verified
🚨 pr-698-w2 iter 2 — finding
🚨 Finding: Zone TIP-20 receive-policy blocked funds become unclaimable (High)
🔍 pr-698-w2 iter 3/3 [audit-invariants.md]
🔬 Verifying: Zone TIP-20 receive-policy blocked funds become unclaimable
📋 Verify: AnchoredZoneDb::sanitize_state rejects enableToken policy-id init, halting the zone for any bridged token with a non-default L1 transfer policy → ✅ Verified
🏁 pr-698-w1 done
🚨 pr-698-w3 iter 2 — finding
🚨 Finding: Followers never receive the mutation barriers required by their consensus-critical L1 cache (High)
🔍 pr-698-w3 iter 3/3 [audit-ripple.md]
🔬 Verifying: Followers never receive the mutation barriers required by their consensus-critical L1 cache
📋 Verify: Zone TIP-20 receive-policy blocked funds become unclaimable → ✅ Verified
📋 Verify: Followers never receive the mutation barriers required by their consensus-critical L1 cache → ⏩ Dup
🚨 pr-698-w2 iter 3 — finding
🚨 Finding: ZoneBlockExecutor fee-token override reads the previous transaction, breaking non-default fee-token execution (Medium)
🔬 Verifying: ZoneBlockExecutor fee-token override reads the previous transaction, breaking non-default fee-token execution
🚨 pr-698-w3 iter 3 — finding
🚨 Finding: Forgotten enabled tokens retain stale L1 transfer policy IDs indefinitely (High)
🔬 Verifying: Forgotten enabled tokens retain stale L1 transfer policy IDs indefinitely
📋 Verify: ZoneBlockExecutor fee-token override reads the previous transaction, breaking non-default fee-token execution → ✅ Verified
🏁 pr-698-w2 done
📋 Verify: Forgotten enabled tokens retain stale L1 transfer policy IDs indefinitely → ✅ Verified
🏁 pr-698-w3 done

Comment on lines +63 to +87
fn execute_inner(
&mut self,
execute: impl FnOnce(
&mut TempoEvm<AnchoredZoneDb<DB, L1>, I>,
) -> Result<TempoResult, AdaptedEvmError<DB::Error>>,
) -> Result<TempoResult, ZoneEvmError<DB::Error>> {
let snapshot = self.anchor_controller().phase();
let mut result = match execute(&mut self.inner) {
Ok(result) => result,
Err(error) => {
self.anchor_controller().restore(snapshot);
return Err(map_adapter_error(error));
}
};
if !result.result.is_success() {
self.anchor_controller().restore(snapshot);
return Ok(result);
}
if let Err(error) = self.inner.db().sanitize_state(&mut result.state) {
self.anchor_controller().restore(snapshot);
return Err(error.into_evm_error());
}
Ok(result)
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can we not avoid this logic? we always have advanceTempo as first tx in the block so i think entire block's execution would use the same L1 state always?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

simplified state machine logic b62c49f (this PR)

Comment on lines +160 to +169
if let Some(account) = state.get(&TIP403_REGISTRY_ADDRESS) {
if account.info != account.original_info() {
return l1_write(TIP403_REGISTRY_ADDRESS, U256::ZERO);
}
for (slot, value) in &account.storage {
if value.is_changed() {
return l1_write(TIP403_REGISTRY_ADDRESS, *slot);
}
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is this actually reachable given that we ban all mutating selectors? it would be kinda unfortunate if txs can execute all the way to here and only then get rejected

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

should be unreachable yeah, i only added it cause it feels like an invariant worth enforcing?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah seems like a reasonable defense in depth.

Comment thread crates/evm/src/executor.rs Outdated
Comment on lines +118 to +131
/// Restores execution-local L1 anchor state when a simulated transaction is not committed.
fn execute_transaction_with_commit_condition(
&mut self,
tx: impl ExecutableTx<Self>,
f: impl FnOnce(&Self::Result) -> CommitChanges,
) -> Result<Option<GasOutput>, BlockExecutionError> {
let snapshot = self.evm().anchor_controller().phase();
let output = self.execute_transaction_without_commit(tx)?;
if !f(&output).should_commit() {
self.evm().anchor_controller().restore(snapshot);
return Ok(None);
}
Ok(Some(self.commit_transaction(output)))
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

execute_transaction_without_commit should not advance any state either. it is expected that execute_transaction_without_commit might be called multiple times in a row with different transactions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

addressed in b62c49f (this PR)

Comment thread crates/node/src/node.rs Outdated
@0xrusowsky
0xrusowsky requested a review from klkvr July 17, 2026 16:44
Comment on lines +80 to +87
impl<DB: fmt::Debug, L1> fmt::Debug for AnchoredZoneDb<DB, L1> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AnchoredZoneDb")
.field("inner", &self.inner)
.field("controller", &self.controller)
.finish_non_exhaustive()
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: can we move this to after the main AnchoredZoneDb impl block?

Comment on lines +26 to +54
/// Database error produced by [`AnchoredZoneDb`].
#[derive(Debug, Error)]
pub enum AnchoredZoneDbError<E> {
/// Error from the caller-provided database.
#[error("inner database error: {0}")]
Inner(#[source] E),
/// The selected Zone state contains an invalid Tempo anchor.
#[error("invalid Tempo anchor (does not fit in u64): {0}")]
AnchorOverflow(U256),
/// The execution-local anchor transition is inconsistent.
#[error(transparent)]
AnchorTransition(#[from] L1AnchorError),
/// Exact anchored L1 storage was unavailable.
#[error("Tempo L1 storage unavailable address={address} slot={slot} block={anchor}: {source}")]
L1Read {
address: Address,
slot: B256,
anchor: u64,
#[source]
source: PrecompileError,
},
/// A transaction attempted to persist mirrored Tempo-owned state.
#[error("write to mirrored Tempo storage address={address} slot={slot}")]
L1Write { address: Address, slot: U256 },
}

impl<E: DBErrorMarker> DBErrorMarker for AnchoredZoneDbError<E> {}

impl<E: DBErrorMarker> AnchoredZoneDbError<E> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: can we move this after the main AnchoredZoneDb logic? Same thing for PackedPolicySlot.


if let Some(account) = state.get(&TIP403_REGISTRY_ADDRESS) {
if account.info != account.original_info() {
return l1_write(TIP403_REGISTRY_ADDRESS, U256::ZERO);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: its a bit nicer IMO to use Err directly rather than a closure. Makes it a bit easier to read.

use AnchoredZoneDbError::L1Write;

// --snip--
return Err(L1Write { address, slot });

Comment on lines +160 to +169
if let Some(account) = state.get(&TIP403_REGISTRY_ADDRESS) {
if account.info != account.original_info() {
return l1_write(TIP403_REGISTRY_ADDRESS, U256::ZERO);
}
for (slot, value) in &account.storage {
if value.is_changed() {
return l1_write(TIP403_REGISTRY_ADDRESS, *slot);
}
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah seems like a reasonable defense in depth.

}

let anchor = self.anchor()?;
let l1 = self.l1_storage(address, slot, anchor)?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This can return stale policy state after the first read. L1StateProvider::get_storage treats the latest cached value at or before anchor as valid, but policy events still update only PolicyCache, they do not write or invalidate the corresponding raw L1StateCache slots.

For example, after a slot is cached at N and changed on L1 at N+1, this read at N+1 returns the N value without making an RPC request. Cache warmth can then make nodes execute the same block with different policy state.

Comment on lines +185 to +186
if storage::merge_transfer_policy_id(U256::ZERO, value.present_value)
!= storage::merge_transfer_policy_id(U256::ZERO, observed.l1)

@0xKitsune 0xKitsune Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This rejects the mandatory enableToken path whenever the token already has a non-default L1 policy.

ZoneTokenFactory::enable_token calls upstream TIP20Token::initialize, which writes transfer policy ID 1 into this packed slot. If the overlaid L1 field is anything other than 1, this comparison returns an error, causing the entire advanceTempo system transaction to fail and stalling block production.

Initialization needs an exception that discards/restores the mirrored field without treating the upstream default write as an attempted L1 mutation.

Comment on lines +139 to +141
} else {
None
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This replacement lookup drops the upstream RECEIVE_POLICY_GUARD_ADDRESS precompile. With L1 backed TIP403 execution, upstream TIP20 can enforce a receive policy, move blocked funds into the guard, and record a claim receipt internally.

However, users cannot call the guard's public claim or burnBlockedReceipt methods because this lookup returns None for its address, leaving those funds permanently locked. We should either explicitly reject receive policy resolution or find a different sollution.

Comment on lines +35 to +40
fn decode_and_check<C: SolCall>(args: &[u8], check: impl FnOnce(C) -> CallCheck) -> CallCheck {
match C::abi_decode_raw_validate(args) {
Ok(decoded) => check(decoded),
Err(_) => CallCheck::Continue,
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Protected selectors must not continue when strict ABI validation fails. Upstream dispatch accepts some encodings that abi_decode_raw_validate rejects.

For example, setting a nonzero byte in an address word's upper padding makes this helper return Continue, while upstream still decodes the address and successfully serves balanceOf.

Comment on lines +103 to +108
// System txs do not pay protocol fees. Resolving their validator token here could read L1
// policy state at N before `advanceTempo` moves the controller to N+1, causing the anchor
// advancement to fail. Thus, FeeAMM override is restricted to regular fee-paying txs.
if !tx_env.is_system_tx {
self.override_validator_token();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Lets add a TODO to remove this once the ZoneFeeManager lands.

block_number,
address_to_storage_value(pending_sequencer),
);
set_cache_slot(PORTAL_SEQUENCER_SLOT, current_sequencer.into_word());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we derive this slot (as well as other slots) from the sol! bindings rather than using a const?

Comment on lines +13 to 14
/// Canonical TIP-403 registry address, shared with Tempo L1.
pub const ZONE_TIP403_PROXY_ADDRESS: Address = TIP403_REGISTRY_ADDRESS;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we need a separate const for this or can we just use TIP403_REGISTRY_ADDRESS directly?


const FIXED_TRANSFER_GAS: u64 = 100_000;
/// Fixed gas charged for TIP20 transfer and approval selectors on the zone.
pub const TIP20_FIXED_TRANSFER_GAS: u64 = 100_000;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do we need this rather than using upstream pricing?

Some(Ok(Self::policy_forbids_output()))
}
Err(e) => Some(Err(e)),
impl TIP20Rules {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

All of these functions are essentially the same thing. We could write one check_auth function and pass in the authorized address. For example:

fn check_auth(&self, caller: Address, authorized: Address) -> CallCheck {
        if caller == authorized {
            CallCheck::Continue
        } else {
            unauthorized()
        }
}

// --snip--
            ITIP20::mintCall::SELECTOR | ITIP20::mintWithMemoCall::SELECTOR => {
                self.check_auth(caller, ZONE_INBOX_ADDRESS)
            }
            ITIP20::burnCall::SELECTOR | ITIP20::burnWithMemoCall::SELECTOR => {
                self.check_auth(caller, ZONE_OUTBOX_ADDRESS)
            }
// --snip--

) -> Result<TempoResult, ZoneEvmError<DB::Error>> {
let result = match execute(&mut self.inner) {
Ok(mut result) => {
if result.result.is_success()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reverts and halts still return state that the block executor commits, including the nonce and Tempo fee transition, so sanitization cannot be success only. B

Before execution, collect_fee_pre_tx authorizes the fee token transfer through TIP403 and mutates that same token's balances/reward state. Those fee effects remain when the user call later reverts.

The returned touched token account contains the L1 overlaid policy bits, but this branch skips sanitize_state, and commit_transaction persists them into canonical Zone storage.

Should we sanitize every Ok(result) regardless of ExecutionResult? The reverted user-frame writes have already been rolled back, while the persistent fee/nonce transition still needs its mirrored fields removed.

Comment on lines +171 to +173
if value.is_changed() {
return l1_write(TIP403_REGISTRY_ADDRESS, *slot);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Checking only is_changed() validates that the call did not modify the L1 value, but it does not remove that value from the returned transition. The adapter supplied the L1 value as both original_value and present_value, and revm commits every storage entry of a touched account.

For example, with canonical Zone slot 0 equal to 5 and the mirrored L1 value equal to 99, a policyIdCounter() transaction succeeds, sanitize_state returns Ok, and the touched registry account in result.state contains 99. Committing that result overwrites the Zone slot even though the transaction was read only.

cc @klkvr to double check if Im overlooking something here.

@@ -0,0 +1,3 @@
//! Shared test utilities for Zone EVM tests.

pub(crate) use zone_precompiles::test_utils::MockL1Reader as TestL1;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: do we need this?

Comment on lines 223 to 236
@@ -65,10 +230,11 @@ pub fn zone_rpc_error(msg: impl core::fmt::Display) -> PrecompileError {
PrecompileError::Fatal(alloc::format!("{ZONE_RPC_ERROR_PREFIX} {msg}"))
}

/// Returns `true` if the error string was produced by [`zone_rpc_error`].
/// Returns `true` if the error chain contains a failure produced by [`zone_rpc_error`].
pub fn is_zone_rpc_error(err: &str) -> bool {
err.starts_with(ZONE_RPC_ERROR_PREFIX)
err.contains(ZONE_RPC_ERROR_PREFIX)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What is the motivation for this? Should we have a proper error rather than [zone rpc] prefix?

Comment on lines +90 to +95
pub(crate) struct NoCallRules;
impl CallRules for NoCallRules {
fn is_delegate_call_allowed(&self) -> bool {
true
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can you remind me why/when we need to delegate call with precompiles? We explictly restrict this upstream in Tempo via the tempo_precompile macro.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants