From 564684fcfa9027c292449b03a0713ee892822981 Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xKitsune@protonmail.com> Date: Fri, 19 Jun 2026 11:04:18 -0400 Subject: [PATCH 1/3] refactor: move bindings to contracts mod --- Cargo.lock | 25 +- Cargo.toml | 2 + crates/contracts/Cargo.toml | 45 + crates/contracts/src/bindings.rs | 525 ++++++++++ crates/contracts/src/lib.rs | 251 +++++ .../contracts/src/swap_and_deposit_router.rs | 78 ++ crates/contracts/src/types.rs | 63 ++ crates/contracts/src/zone_portal.rs | 71 ++ crates/precompiles/Cargo.toml | 2 + crates/precompiles/src/ztip20.rs | 2 +- crates/primitives/Cargo.toml | 13 +- crates/primitives/src/abi.rs | 954 ------------------ crates/primitives/src/lib.rs | 1 - crates/tempo-zone/Cargo.toml | 3 +- crates/tempo-zone/src/abi.rs | 4 +- 15 files changed, 1061 insertions(+), 978 deletions(-) create mode 100644 crates/contracts/Cargo.toml create mode 100644 crates/contracts/src/bindings.rs create mode 100644 crates/contracts/src/lib.rs create mode 100644 crates/contracts/src/swap_and_deposit_router.rs create mode 100644 crates/contracts/src/types.rs create mode 100644 crates/contracts/src/zone_portal.rs delete mode 100644 crates/primitives/src/abi.rs diff --git a/Cargo.lock b/Cargo.lock index ea884cc27..a5f7a0f07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11363,6 +11363,22 @@ dependencies = [ "zone", ] +[[package]] +name = "tempo-zone-contracts" +version = "0.1.0" +dependencies = [ + "alloy-contract", + "alloy-network", + "alloy-primitives", + "alloy-provider", + "alloy-sol-types", + "const-hex", + "futures", + "serde", + "tokio", + "zone-primitives", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -13380,6 +13396,7 @@ dependencies = [ "tempo-primitives", "tempo-revm", "tempo-transaction-pool", + "tempo-zone-contracts", "thiserror 2.0.18", "tokio", "tokio-tungstenite", @@ -13408,6 +13425,7 @@ dependencies = [ "tempo-contracts", "tempo-precompiles", "tempo-precompiles-macros", + "tempo-zone-contracts", "tracing", "zone-primitives", ] @@ -13416,16 +13434,9 @@ dependencies = [ name = "zone-primitives" version = "0.1.0" dependencies = [ - "alloy-contract", - "alloy-network", "alloy-primitives", - "alloy-provider", "alloy-rlp", - "alloy-sol-types", - "const-hex", - "futures", "serde", - "tokio", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 03dcac246..d621ad3f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ publish = false [workspace] members = [ "bin/tempo-zone", + "crates/contracts", "crates/precompiles", "crates/primitives", "crates/tempo-zone", @@ -99,6 +100,7 @@ tempo-revm = { git = "https://github.com/tempoxyz/tempo", rev = "19631ec55ccbb9e tempo-transaction-pool = { git = "https://github.com/tempoxyz/tempo", rev = "19631ec55ccbb9e8996b3d1481332d06d1ef5803", default-features = false } # zones +tempo-zone-contracts = { path = "crates/contracts", default-features = false } zone-precompiles = { path = "crates/precompiles", default-features = false } zone-primitives = { path = "crates/primitives", default-features = false } zone-rpc = { path = "crates/rpc" } diff --git a/crates/contracts/Cargo.toml b/crates/contracts/Cargo.toml new file mode 100644 index 000000000..35568f4d6 --- /dev/null +++ b/crates/contracts/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "tempo-zone-contracts" +description = "Tempo Zone contract bindings and ABI definitions" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[dependencies] +# zones +zone-primitives = { workspace = true, default-features = false } + +# alloy +alloy-primitives = { workspace = true, default-features = false } +alloy-sol-types = { workspace = true, default-features = false } +alloy-contract = { workspace = true, optional = true } +alloy-network = { workspace = true, optional = true } +alloy-provider = { workspace = true, optional = true } +futures = { workspace = true, optional = true } +tokio = { workspace = true, optional = true } +serde = { workspace = true, optional = true } + +[dev-dependencies] +const-hex.workspace = true + +[features] +default = ["std", "serde", "rpc"] +std = [ + "alloy-primitives/std", + "alloy-sol-types/std", + "serde?/std", + "zone-primitives/std", +] +serde = ["dep:serde", "alloy-primitives/serde"] +rpc = [ + "dep:alloy-contract", + "dep:alloy-network", + "dep:alloy-provider", + "dep:futures", + "dep:tokio", +] diff --git a/crates/contracts/src/bindings.rs b/crates/contracts/src/bindings.rs new file mode 100644 index 000000000..e526383cb --- /dev/null +++ b/crates/contracts/src/bindings.rs @@ -0,0 +1,525 @@ +//! Generated contract bindings for the Tempo Zone protocol. +//! +//! All contracts and the structs/enums they share are emitted from a single +//! [`alloy_sol_types::sol!`] invocation: the macro can only resolve user-defined types +//! (e.g. [`Withdrawal`], [`QueuedDeposit`]) that are declared within the same invocation, and +//! several zone contracts reference the same types. + +/// Internal macro that emits the full `sol!` block, placing `$($rpc_attr)*` +/// before every `contract` declaration. Called twice: once with `#[sol(rpc)]` +/// (when the `rpc` feature is active) and once with nothing. +macro_rules! define_abi { + ($($rpc_attr:tt)*) => { + alloy_sol_types::sol! { + // --------------------------------------------------------------- + // Shared types + // --------------------------------------------------------------- + + #[derive(Debug)] + struct Withdrawal { + address token; + bytes32 senderTag; + address to; + uint128 amount; + uint128 fee; + bytes32 memo; + uint64 gasLimit; + address fallbackRecipient; + bytes callbackData; + bytes encryptedSender; + } + + #[derive(Debug)] + struct Deposit { + address token; + address sender; + address to; + uint128 amount; + bytes32 memo; + } + + /// Encrypted deposit payload (ECIES encrypted recipient and memo) + #[derive(Debug)] + struct EncryptedDepositPayload { + bytes32 ephemeralPubkeyX; + uint8 ephemeralPubkeyYParity; + bytes ciphertext; + bytes12 nonce; + bytes16 tag; + } + + /// Encrypted deposit stored in the queue + #[derive(Debug)] + struct EncryptedDeposit { + address token; + address sender; + uint128 amount; + uint256 keyIndex; + EncryptedDepositPayload encrypted; + } + + #[derive(Debug)] + struct BlockTransition { + bytes32 prevBlockHash; + bytes32 nextBlockHash; + } + + #[derive(Debug)] + struct DepositQueueTransition { + bytes32 prevProcessedHash; + bytes32 nextProcessedHash; + uint64 prevDepositNumber; + uint64 nextDepositNumber; + } + + #[derive(Debug)] + struct LastBatch { + bytes32 withdrawalQueueHash; + uint64 withdrawalBatchIndex; + } + + /// A TIP-20 token enabled on L1 for bridging to the zone. + #[derive(Debug)] + struct EnabledToken { + address token; + string name; + string symbol; + string currency; + } + + /// Generic unauthorized access error used by zone wrapper logic. + error Unauthorized(); + + // --------------------------------------------------------------- + // ZonePortal — deployed on Tempo L1 + // --------------------------------------------------------------- + + $($rpc_attr)* + contract ZonePortal { + // -- Events -- + + #[derive(Debug)] + event DepositMade( + bytes32 indexed newCurrentDepositQueueHash, + address indexed sender, + address token, + address to, + uint128 netAmount, + uint128 fee, + bytes32 memo, + uint64 depositNumber + ); + + #[derive(Debug)] + event EncryptedDepositMade( + bytes32 indexed newCurrentDepositQueueHash, + address indexed sender, + address token, + uint128 netAmount, + uint128 fee, + uint256 keyIndex, + bytes32 ephemeralPubkeyX, + uint8 ephemeralPubkeyYParity, + bytes ciphertext, + bytes12 nonce, + bytes16 tag, + uint64 depositNumber + ); + + /// Event emitted when a new TIP-20 token is enabled for bridging. + /// Includes token metadata so the zone can create a matching TIP-20. + #[derive(Debug)] + event TokenEnabled(address indexed token, string name, string symbol, string currency); + + #[derive(Debug)] + event BatchSubmitted( + uint64 indexed withdrawalBatchIndex, + bytes32 nextProcessedDepositQueueHash, + bytes32 nextBlockHash, + bytes32 withdrawalQueueHash, + uint64 lastProcessedDepositNumber + ); + + #[derive(Debug)] + event WithdrawalProcessed(address indexed to, address token, uint128 amount, bool callbackSuccess); + + #[derive(Debug)] + event BounceBack( + bytes32 indexed newCurrentDepositQueueHash, + address indexed fallbackRecipient, + address token, + uint128 amount, + uint64 depositNumber + ); + + #[derive(Debug)] + event SequencerTransferStarted( + address indexed currentSequencer, + address indexed pendingSequencer + ); + + #[derive(Debug)] + event SequencerTransferred( + address indexed previousSequencer, + address indexed newSequencer + ); + + // -- Errors -- + + #[derive(Debug)] + error NotSequencer(); + #[derive(Debug)] + error InvalidProof(); + #[derive(Debug)] + error InvalidTempoBlockNumber(); + #[derive(Debug)] + error DepositPolicyForbids(); + + // -- View functions -- + + function zoneId() external view returns (uint32); + function sequencer() external view returns (address); + function verifier() external view returns (address); + function sequencerPubkey() external view returns (bytes32); + function withdrawalBatchIndex() external view returns (uint64); + function blockHash() external view returns (bytes32); + function currentDepositQueueHash() external view returns (bytes32); + function lastSyncedTempoBlockNumber() external view returns (uint64); + function withdrawalQueueHead() external view returns (uint256); + function withdrawalQueueTail() external view returns (uint256); + function withdrawalQueueMaxSize() external view returns (uint256); + function withdrawalQueueSlot(uint256 slot) external view returns (bytes32); + function genesisTempoBlockNumber() external view returns (uint64); + function calculateDepositFee() external view returns (uint128 fee); + function depositCount() external view returns (uint64); + function lastProcessedDepositNumber() external view returns (uint64); + function MAX_WITHDRAWAL_GAS_LIMIT() external view returns (uint64); + + // -- State-changing functions -- + + function deposit(address token, address to, uint128 amount, bytes32 memo) + external + returns (bytes32 newCurrentDepositQueueHash); + + function processWithdrawal(Withdrawal calldata withdrawal, bytes32 remainingQueue) external; + + function submitBatch( + uint64 tempoBlockNumber, + uint64 recentTempoBlockNumber, + BlockTransition calldata blockTransition, + DepositQueueTransition calldata depositQueueTransition, + bytes32 withdrawalQueueHash, + bytes calldata verifierConfig, + bytes calldata proof + ) external; + + function enableToken(address token) external; + + function rpcUrl() external view returns (string memory); + function setRpcUrl(string calldata rpcUrl) external; + + function depositEncrypted( + address token, + uint128 amount, + uint256 keyIndex, + EncryptedDepositPayload calldata encrypted + ) external returns (bytes32 newCurrentDepositQueueHash); + + function setSequencerEncryptionKey( + bytes32 x, + uint8 yParity, + uint8 popV, + bytes32 popR, + bytes32 popS + ) external; + + // -- View functions (token management) -- + + function isTokenEnabled(address token) external view returns (bool); + function enabledTokenCount() external view returns (uint256); + function enabledTokenAt(uint256 index) external view returns (address); + function zoneGasRate() external view returns (uint128); + function pendingSequencer() external view returns (address); + + function sequencerEncryptionKey() external view returns (bytes32 x, uint8 yParity); + + function encryptionKeyCount() external view returns (uint256); + } + + // --------------------------------------------------------------- + // ZoneOutbox — deployed on Zone L2 + // --------------------------------------------------------------- + + $($rpc_attr)* + contract ZoneOutbox { + // -- Events -- + + event WithdrawalRequested( + uint64 indexed withdrawalIndex, + address indexed sender, + address token, + address to, + uint128 amount, + uint128 fee, + bytes32 memo, + uint64 gasLimit, + address fallbackRecipient, + bytes data, + bytes revealTo + ); + + #[derive(Debug)] + event BatchFinalized(bytes32 indexed withdrawalQueueHash, uint64 withdrawalBatchIndex); + + // -- Errors -- + + error OnlySequencer(); + error GasLimitTooHigh(); + + // -- View functions -- + + function lastBatch() external view returns (LastBatch memory); + function withdrawalBatchIndex() external view returns (uint64); + function nextWithdrawalIndex() external view returns (uint64); + function pendingWithdrawalsCount() external view returns (uint256); + function calculateWithdrawalFee(uint64 gasLimit) external view returns (uint128 fee); + function MAX_WITHDRAWAL_GAS_LIMIT() external view returns (uint64); + + // -- State-changing functions -- + + function requestWithdrawal( + address token, + address to, + uint128 amount, + bytes32 memo, + uint64 gasLimit, + address fallbackRecipient, + bytes calldata data, + bytes calldata revealTo + ) external; + function finalizeWithdrawalBatch(uint256 count, uint64 blockNumber, bytes[] calldata encryptedSenders) external returns (bytes32 withdrawalQueueHash); + } + + // --------------------------------------------------------------- + // TempoState — Zone L2 predeploy (0x1c00...0000) + // --------------------------------------------------------------- + + $($rpc_attr)* + contract TempoState { + #[derive(Debug)] + event TempoBlockFinalized(bytes32 indexed blockHash, uint64 indexed blockNumber, bytes32 stateRoot); + + error InvalidParentHash(); + error InvalidBlockNumber(); + error InvalidRlpData(); + error OnlyZoneInbox(); + + function tempoBlockHash() external view returns (bytes32); + function tempoBlockNumber() external view returns (uint64); + function tempoStateRoot() external view returns (bytes32); + function tempoParentHash() external view returns (bytes32); + function tempoBeneficiary() external view returns (address); + function tempoTransactionsRoot() external view returns (bytes32); + function tempoReceiptsRoot() external view returns (bytes32); + function tempoGasLimit() external view returns (uint64); + function tempoGasUsed() external view returns (uint64); + function tempoTimestamp() external view returns (uint64); + function tempoTimestampMillis() external view returns (uint64); + function tempoPrevRandao() external view returns (bytes32); + function generalGasLimit() external view returns (uint64); + function sharedGasLimit() external view returns (uint64); + + function finalizeTempo(bytes calldata header) external; + } + + // --------------------------------------------------------------- + // TempoStateReader — Zone L2 standalone precompile + // Separate from TempoState; reads Tempo L1 storage at a caller-specified block. + // --------------------------------------------------------------- + + $($rpc_attr)* + contract TempoStateReader { + error DelegateCallNotAllowed(); + + function readStorageAt(address account, bytes32 slot, uint64 blockNumber) external view returns (bytes32); + function readStorageBatchAt(address account, bytes32[] calldata slots, uint64 blockNumber) external view returns (bytes32[] memory); + } + + $($rpc_attr)* + contract ZoneTxContext { + function currentTxHash() external returns (bytes32); + } + + // --------------------------------------------------------------- + // ZoneInbox shared types — Zone L2 system contract (0x1c00...0001) + // --------------------------------------------------------------- + + /// Deposit types for the unified deposit queue. + #[derive(Debug, PartialEq, Eq)] + enum DepositType { + Regular, + Encrypted, + } + + /// A queued deposit (regular or encrypted) passed to `advanceTempo`. + #[derive(Debug)] + struct QueuedDeposit { + DepositType depositType; + bytes depositData; + } + + /// Chaum-Pedersen proof for ECDH shared secret derivation. + #[derive(Debug)] + struct ChaumPedersenProof { + bytes32 s; + bytes32 c; + } + + /// Decryption data provided by the sequencer for encrypted deposits. + #[derive(Debug)] + struct DecryptionData { + bytes32 sharedSecret; + uint8 sharedSecretYParity; + ChaumPedersenProof cpProof; + } + + // --------------------------------------------------------------- + // ZoneFactory — deployed on Tempo L1 + // --------------------------------------------------------------- + + #[derive(Debug)] + struct ZoneInfo { + uint32 zoneId; + address portal; + address messenger; + address initialToken; + address sequencer; + address verifier; + bytes32 genesisBlockHash; + bytes32 genesisTempoBlockHash; + uint64 genesisTempoBlockNumber; + string rpcUrl; + } + + $($rpc_attr)* + contract ZoneFactory { + struct ZoneParams { + bytes32 genesisBlockHash; + bytes32 genesisTempoBlockHash; + uint64 genesisTempoBlockNumber; + } + struct CreateZoneParams { + address token; + address sequencer; + address verifier; + ZoneParams zoneParams; + string rpcUrl; + } + #[derive(Debug)] + event ZoneCreated( + uint32 indexed zoneId, + address indexed portal, + address indexed messenger, + address token, + address sequencer, + address verifier, + bytes32 genesisBlockHash, + bytes32 genesisTempoBlockHash, + uint64 genesisTempoBlockNumber + ); + function createZone(CreateZoneParams calldata params) external returns (uint32 zoneId, address portal); + function verifier() external view returns (address); + function zones(uint32 zoneId) external view returns (ZoneInfo memory); + function zoneCount() external view returns (uint32); + function isZonePortal(address portal) external view returns (bool); + function isZoneMessenger(address messenger) external view returns (bool); + } + + // --------------------------------------------------------------- + // ZoneInbox — Zone L2 system contract (0x1c00...0001) + // --------------------------------------------------------------- + + $($rpc_attr)* + contract ZoneInbox { + #[derive(Debug)] + event TempoAdvanced( + bytes32 indexed tempoBlockHash, + uint64 indexed tempoBlockNumber, + uint256 depositsProcessed, + bytes32 newProcessedDepositQueueHash, + uint64 lastProcessedDepositNumber + ); + + #[derive(Debug)] + event DepositProcessed( + bytes32 indexed depositHash, + address indexed sender, + address indexed to, + address token, + uint128 amount, + bytes32 memo + ); + + #[derive(Debug)] + event EncryptedDepositProcessed( + bytes32 indexed depositHash, + address indexed sender, + address indexed to, + address token, + uint128 amount, + bytes32 memo + ); + + #[derive(Debug)] + event EncryptedDepositFailed( + bytes32 indexed depositHash, + address indexed sender, + address token, + uint128 amount + ); + + /// Emitted when a TIP-20 token is enabled on the zone via advanceTempo. + #[derive(Debug)] + event TokenEnabled(address indexed token, string name, string symbol, string currency); + + error OnlySequencer(); + error InvalidDepositQueueHash(); + error MissingDecryptionData(); + error ExtraDecryptionData(); + error InvalidSharedSecretProof(); + function processedDepositQueueHash() external view returns (bytes32); + function processedDepositNumber() external view returns (uint64); + function tempoPortal() external view returns (address); + function tempoState() external view returns (address); + function config() external view returns (address); + + function advanceTempo( + bytes calldata header, + QueuedDeposit[] calldata deposits, + DecryptionData[] calldata decryptions, + EnabledToken[] calldata enabledTokens + ) external; + } + + // --------------------------------------------------------------- + // SwapAndDepositRouter — deployed on Tempo L1 + // --------------------------------------------------------------- + + $($rpc_attr)* + contract SwapAndDepositRouter { + function onWithdrawalReceived( + bytes32 senderTag, + address tokenIn, + uint128 amount, + bytes calldata data + ) external returns (bytes4); + } + } + }; +} + +#[cfg(feature = "rpc")] +define_abi!(#[sol(rpc)]); + +#[cfg(not(feature = "rpc"))] +define_abi!(); diff --git a/crates/contracts/src/lib.rs b/crates/contracts/src/lib.rs new file mode 100644 index 000000000..2c6c8438e --- /dev/null +++ b/crates/contracts/src/lib.rs @@ -0,0 +1,251 @@ +//! ABI bindings for the Tempo Zone protocol contracts. +//! +//! These bindings cover the contracts the sequencer interacts with across both layers: +//! +//! - **ZonePortal** — deployed on Tempo L1. Escrows gas tokens, manages the deposit queue, +//! accepts batch proofs, and processes withdrawals back to L1 recipients. +//! - **ZoneOutbox** — deployed on the Zone L2. Collects user withdrawal requests, builds +//! withdrawal hash chains, and exposes [`LastBatch`] state for proof generation. +//! - **ZoneInbox**, **TempoState**, **TempoStateReader**, **ZoneTxContext** — Zone L2 predeploys. +//! - **ZoneFactory**, **SwapAndDepositRouter** — deployed on Tempo L1. + +#![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(docsrs, feature(doc_cfg))] +// The `sol!` macro generates functions whose arity we don't control. +#![allow(clippy::too_many_arguments)] + +extern crate alloc; + +pub mod bindings; +pub mod swap_and_deposit_router; +mod types; +mod zone_portal; + +pub use bindings::*; +pub use swap_and_deposit_router::*; + +// Re-export the address and slot constants the bindings build on, so callers can reach them +// through the contracts crate (e.g. `tempo_zone_contracts::TEMPO_STATE_ADDRESS`). +pub use zone_primitives::constants::{ + EMPTY_SENTINEL, MAX_WITHDRAWAL_GAS_LIMIT, PORTAL_PENDING_SEQUENCER_SLOT, PORTAL_SEQUENCER_SLOT, + TEMPO_BLOCK_HASH_SLOT, TEMPO_PACKED_SLOT, TEMPO_STATE_ADDRESS, TEMPO_STATE_READER_ADDRESS, + ZONE_CONFIG_ADDRESS, ZONE_INBOX_ADDRESS, ZONE_OUTBOX_ADDRESS, ZONE_TOKEN_ADDRESS, + ZONE_TX_CONTEXT_ADDRESS, +}; + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + use alloy_primitives::{B256, Bytes, U256, address, keccak256}; + use alloy_sol_types::{SolCall, SolValue}; + + #[test] + fn test_deposit_abi_encode_vs_params() { + let d = Deposit { + token: address!("0x0000000000000000000000000000000000001000"), + sender: address!("0x0000000000000000000000000000000000000001"), + to: address!("0x0000000000000000000000000000000000000002"), + amount: 1000u128, + memo: B256::ZERO, + }; + + let encoded = d.abi_encode(); + let encoded_params = d.abi_encode_params(); + + println!("abi_encode length: {}", encoded.len()); + println!("abi_encode_params length: {}", encoded_params.len()); + println!("abi_encode hex:\n{}", const_hex::encode(&encoded)); + println!( + "abi_encode_params hex:\n{}", + const_hex::encode(&encoded_params) + ); + println!("Are they equal: {}", encoded == encoded_params); + } + + #[test] + fn test_queued_deposit_encoding() { + let deposit = Deposit { + token: address!("0x0000000000000000000000000000000000001000"), + sender: address!("0x0000000000000000000000000000000000000001"), + to: address!("0x0000000000000000000000000000000000000002"), + amount: 1000u128, + memo: B256::ZERO, + }; + + let deposit_data = Bytes::from(deposit.abi_encode()); + + let qd = QueuedDeposit { + depositType: DepositType::Regular, + depositData: deposit_data, + }; + + println!( + "DepositType::Regular abi_encode: {}", + const_hex::encode(DepositType::Regular.abi_encode()) + ); + println!( + "deposit.abi_encode() length: {}", + deposit.abi_encode().len() + ); + println!( + "deposit.abi_encode(): {}", + const_hex::encode(deposit.abi_encode()) + ); + println!( + "QueuedDeposit.abi_encode() length: {}", + qd.abi_encode().len() + ); + println!( + "QueuedDeposit.abi_encode(): {}", + const_hex::encode(qd.abi_encode()) + ); + + // Now test the full advanceTempo call encoding + let header_bytes = Bytes::from(vec![0xc0]); // minimal RLP empty list + let calldata = ZoneInbox::advanceTempoCall { + header: header_bytes, + deposits: vec![qd], + decryptions: vec![], + enabledTokens: vec![], + } + .abi_encode(); + + println!("\nadvanceTempo calldata length: {}", calldata.len()); + println!( + "advanceTempo selector: 0x{}", + const_hex::encode(&calldata[..4]) + ); + println!( + "advanceTempo full calldata:\n{}", + const_hex::encode(&calldata) + ); + } + + #[test] + fn test_deposit_hash_chain_matches_solidity() { + let deposit = Deposit { + token: address!("0x0000000000000000000000000000000000001000"), + sender: address!("0x0000000000000000000000000000000000000001"), + to: address!("0x0000000000000000000000000000000000000002"), + amount: 1000u128, + memo: B256::ZERO, + }; + let prev_hash = B256::ZERO; + + let solidity_encoding = (DepositType::Regular, deposit.clone(), prev_hash).abi_encode(); + let solidity_hash = keccak256(&solidity_encoding); + + let rust_encoding = (DepositType::Regular, deposit, prev_hash).abi_encode(); + let rust_hash = keccak256(&rust_encoding); + + assert_eq!(solidity_encoding, rust_encoding, "ABI encodings must match"); + assert_eq!(solidity_hash, rust_hash, "Deposit hash chains must match"); + } + + #[test] + fn test_decryption_data_encoding_uses_trimmed_layout() { + let shared_secret = B256::from([0x11; 32]); + let shared_secret_y_parity = 0x02; + let proof_s = B256::from([0x33; 32]); + let proof_c = B256::from([0x44; 32]); + + let decryption = DecryptionData { + sharedSecret: shared_secret, + sharedSecretYParity: shared_secret_y_parity, + cpProof: ChaumPedersenProof { + s: proof_s, + c: proof_c, + }, + }; + + let encoded = decryption.abi_encode(); + let mut expected_y_parity_word = [0u8; 32]; + expected_y_parity_word[31] = shared_secret_y_parity; + + assert_eq!( + encoded.len(), + 4 * 32, + "DecryptionData must encode as four ABI words" + ); + assert_eq!( + &encoded[0..32], + shared_secret.as_slice(), + "word 0 is sharedSecret" + ); + assert_eq!( + &encoded[32..64], + expected_y_parity_word, + "word 1 is sharedSecretYParity" + ); + assert_eq!(&encoded[64..96], proof_s.as_slice(), "word 2 is cpProof.s"); + assert_eq!(&encoded[96..128], proof_c.as_slice(), "word 3 is cpProof.c"); + } + + #[test] + fn test_router_plaintext_callback_encoding_matches_tuple() { + let callback = SwapAndDepositRouterPlaintextCallback { + token_out: address!("0x0000000000000000000000000000000000001001"), + target_portal: address!("0x0000000000000000000000000000000000002001"), + recipient: address!("0x0000000000000000000000000000000000003001"), + memo: B256::from([0x11; 32]), + min_amount_out: 1234, + }; + + let tuple_encoding = ( + false, + callback.token_out, + callback.target_portal, + callback.recipient, + callback.memo, + callback.min_amount_out, + ) + .abi_encode_params(); + + assert_eq!(callback.abi_encode(), tuple_encoding); + } + + #[test] + fn test_sender_tag_matches_plaintext_hash() { + let sender = address!("0x0000000000000000000000000000000000000001"); + let tx_hash = B256::repeat_byte(0x22); + let plaintext = Withdrawal::authenticated_sender_plaintext(sender, tx_hash); + + assert_eq!(&plaintext[..20], sender.as_slice()); + assert_eq!(&plaintext[20..], tx_hash.as_slice()); + assert_eq!( + Withdrawal::sender_tag(sender, tx_hash), + keccak256(plaintext) + ); + } + + #[test] + fn test_router_encrypted_callback_encoding_matches_tuple() { + let encrypted = EncryptedDepositPayload { + ephemeralPubkeyX: B256::from([0x22; 32]), + ephemeralPubkeyYParity: 0x02, + ciphertext: Bytes::from(vec![0xaa, 0xbb, 0xcc, 0xdd]), + nonce: [0x33; 12].into(), + tag: [0x44; 16].into(), + }; + let callback = SwapAndDepositRouterEncryptedCallback { + token_out: address!("0x0000000000000000000000000000000000001002"), + target_portal: address!("0x0000000000000000000000000000000000002002"), + key_index: U256::from(7), + encrypted: encrypted.clone(), + min_amount_out: 5678, + }; + + let tuple_encoding = ( + true, + callback.token_out, + callback.target_portal, + callback.key_index, + encrypted, + callback.min_amount_out, + ) + .abi_encode_params(); + + assert_eq!(callback.abi_encode(), tuple_encoding); + } +} diff --git a/crates/contracts/src/swap_and_deposit_router.rs b/crates/contracts/src/swap_and_deposit_router.rs new file mode 100644 index 000000000..f34f1b5e1 --- /dev/null +++ b/crates/contracts/src/swap_and_deposit_router.rs @@ -0,0 +1,78 @@ +//! Hand-written callback payloads for the +//! [`SwapAndDepositRouter`](crate::bindings::SwapAndDepositRouter) deployed on Tempo L1. + +use crate::bindings::EncryptedDepositPayload; +use alloc::vec::Vec; +use alloy_primitives::{Address, B256, U256}; +use alloy_sol_types::SolValue; + +/// Plaintext callback payload for `SwapAndDepositRouter.onWithdrawalReceived`. +/// +/// This payload tells the router to optionally swap the withdrawn token on L1 +/// and then perform a regular `ZonePortal.deposit(...)`. +#[derive(Debug, Clone)] +pub struct SwapAndDepositRouterPlaintextCallback { + /// Token that should be deposited after the optional L1 swap. + pub token_out: Address, + /// Target zone portal that receives the downstream deposit. + pub target_portal: Address, + /// Zone recipient for the downstream plaintext deposit. + pub recipient: Address, + /// Memo recorded on the downstream plaintext deposit. + pub memo: B256, + /// Minimum acceptable output from the optional swap. + /// + /// Ignored when `tokenIn == token_out` and the router can deposit directly. + pub min_amount_out: u128, +} + +impl SwapAndDepositRouterPlaintextCallback { + /// ABI-encode the router callback data expected by the Solidity router. + pub fn abi_encode(&self) -> Vec { + ( + false, + self.token_out, + self.target_portal, + self.recipient, + self.memo, + self.min_amount_out, + ) + .abi_encode_params() + } +} + +/// Encrypted callback payload for `SwapAndDepositRouter.onWithdrawalReceived`. +/// +/// This payload tells the router to optionally swap the withdrawn token on L1 +/// and then call `ZonePortal.depositEncrypted(...)` with an ECIES-encrypted +/// `(recipient, memo)` payload. +#[derive(Debug, Clone)] +pub struct SwapAndDepositRouterEncryptedCallback { + /// Token that should be deposited after the optional L1 swap. + pub token_out: Address, + /// Target zone portal that receives the downstream encrypted deposit. + pub target_portal: Address, + /// Portal encryption key index used to build [`Self::encrypted`]. + pub key_index: U256, + /// ECIES-encrypted `(recipient, memo)` payload for `depositEncrypted`. + pub encrypted: EncryptedDepositPayload, + /// Minimum acceptable output from the optional swap. + /// + /// Ignored when `tokenIn == token_out` and the router can deposit directly. + pub min_amount_out: u128, +} + +impl SwapAndDepositRouterEncryptedCallback { + /// ABI-encode the router callback data expected by the Solidity router. + pub fn abi_encode(&self) -> Vec { + ( + true, + self.token_out, + self.target_portal, + self.key_index, + self.encrypted.clone(), + self.min_amount_out, + ) + .abi_encode_params() + } +} diff --git a/crates/contracts/src/types.rs b/crates/contracts/src/types.rs new file mode 100644 index 000000000..0dfc2391d --- /dev/null +++ b/crates/contracts/src/types.rs @@ -0,0 +1,63 @@ +//! Hand-written helpers for the shared ABI types. + +use crate::bindings::{Withdrawal, ZoneOutbox}; +use alloy_primitives::{Address, B256, Bytes, keccak256}; +use alloy_sol_types::SolValue; +use zone_primitives::constants::EMPTY_SENTINEL; + +impl Withdrawal { + /// Build the authenticated-withdrawal sender plaintext `[sender(20) | tx_hash(32)]`. + pub fn authenticated_sender_plaintext(sender: Address, tx_hash: B256) -> [u8; 52] { + let mut plaintext = [0u8; 52]; + plaintext[..20].copy_from_slice(sender.as_slice()); + plaintext[20..].copy_from_slice(tx_hash.as_slice()); + plaintext + } + + /// Compute the authenticated sender tag `keccak256(sender || tx_hash)`. + pub fn sender_tag(sender: Address, tx_hash: B256) -> B256 { + keccak256(Self::authenticated_sender_plaintext(sender, tx_hash)) + } + + /// Reconstruct the public L1-facing withdrawal from a zone-side withdrawal request event. + pub fn from_requested_event( + event: &ZoneOutbox::WithdrawalRequested, + tx_hash: B256, + encrypted_sender: Bytes, + ) -> Self { + Self { + token: event.token, + senderTag: Self::sender_tag(event.sender, tx_hash), + to: event.to, + amount: event.amount, + fee: event.fee, + memo: event.memo, + gasLimit: event.gasLimit, + fallbackRecipient: event.fallbackRecipient, + callbackData: event.data.clone(), + encryptedSender: encrypted_sender, + } + } + + /// Compute the withdrawal queue hash for a slice of withdrawals. + /// + /// The hash chain has the oldest withdrawal at the outermost layer for efficient FIFO removal: + /// + /// ```text + /// hash = keccak256(encode(w[0], keccak256(encode(w[1], keccak256(encode(w[2], EMPTY_SENTINEL)))))) + /// ``` + /// + /// Building proceeds from the newest (innermost) to the oldest (outermost). + /// Returns `B256::ZERO` if `withdrawals` is empty. + pub fn queue_hash(withdrawals: &[Self]) -> B256 { + if withdrawals.is_empty() { + return B256::ZERO; + } + + let mut hash = EMPTY_SENTINEL; + for w in withdrawals.iter().rev() { + hash = keccak256((w.clone(), hash).abi_encode_params()); + } + hash + } +} diff --git a/crates/contracts/src/zone_portal.rs b/crates/contracts/src/zone_portal.rs new file mode 100644 index 000000000..dcb5b5925 --- /dev/null +++ b/crates/contracts/src/zone_portal.rs @@ -0,0 +1,71 @@ +//! Hand-written helpers for the [`ZonePortal`](crate::bindings::ZonePortal) bindings. + +use crate::bindings::ZonePortal; + +impl ZonePortal::sequencerEncryptionKeyReturn { + /// Normalize `yParity` to SEC1 compressed prefix (`0x02` or `0x03`). + /// + /// The contract may return `0`/`1` (parity bit) or `0x02`/`0x03` (SEC1 prefix). + pub fn normalized_y_parity(&self) -> Option { + match self.yParity { + 0x02 | 0x03 => Some(self.yParity), + 0 | 1 => Some(0x02 + self.yParity), + _ => None, + } + } +} + +impl core::fmt::Display for ZonePortal::ZonePortalErrors { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::NotSequencer(_) => f.write_str("NotSequencer"), + Self::InvalidProof(_) => f.write_str("InvalidProof"), + Self::InvalidTempoBlockNumber(_) => f.write_str("InvalidTempoBlockNumber"), + Self::DepositPolicyForbids(_) => f.write_str("DepositPolicyForbids"), + } + } +} + +#[cfg(feature = "rpc")] +impl, N: alloy_network::Network> + ZonePortal::ZonePortalInstance +{ + /// Returns all token addresses currently enabled for bridging on this [`ZonePortal`]. + /// + /// Calls [`enabledTokenCount`](ZonePortal::enabledTokenCountCall) followed by + /// [`enabledTokenAt`](ZonePortal::enabledTokenAtCall) for each index concurrently. + pub async fn enabled_tokens( + &self, + ) -> Result, alloy_contract::Error> { + let count = self.enabledTokenCount().call().await?; + let futs: alloc::vec::Vec<_> = (0..count.to::()) + .map(|i| async move { + self.enabledTokenAt(alloy_primitives::U256::from(i)) + .call() + .await + }) + .collect(); + futures::future::try_join_all(futs).await + } + + /// Fetches the active sequencer encryption key and its index. + /// + /// Returns `(key, key_index)` where `key` is the + /// [`sequencerEncryptionKeyReturn`](ZonePortal::sequencerEncryptionKeyReturn) and + /// `key_index` is the zero-based index of the current key. + pub async fn encryption_key( + &self, + ) -> Result< + ( + ZonePortal::sequencerEncryptionKeyReturn, + alloy_primitives::U256, + ), + alloy_contract::Error, + > { + let key_call = self.sequencerEncryptionKey(); + let count_call = self.encryptionKeyCount(); + let (key, count) = tokio::try_join!(key_call.call(), count_call.call())?; + let key_index = count.saturating_sub(alloy_primitives::U256::from(1)); + Ok((key, key_index)) + } +} diff --git a/crates/precompiles/Cargo.toml b/crates/precompiles/Cargo.toml index 113298646..d7a7c4e04 100644 --- a/crates/precompiles/Cargo.toml +++ b/crates/precompiles/Cargo.toml @@ -12,6 +12,7 @@ workspace = true [dependencies] # zones +tempo-zone-contracts = { workspace = true, default-features = false } zone-primitives = { workspace = true, default-features = false } # tempo (workspace deps already have default-features = false) @@ -46,6 +47,7 @@ tempo-precompiles = { workspace = true, features = ["test-utils"] } default = ["std"] test-utils = ["tempo-precompiles/test-utils"] std = [ + "tempo-zone-contracts/std", "zone-primitives/std", "alloy-evm/std", "alloy-primitives/std", diff --git a/crates/precompiles/src/ztip20.rs b/crates/precompiles/src/ztip20.rs index cff5dcdcd..b3c39bd41 100644 --- a/crates/precompiles/src/ztip20.rs +++ b/crates/precompiles/src/ztip20.rs @@ -24,8 +24,8 @@ use tempo_precompiles::{ tip20::{IRolesAuth, ITIP20, RolesAuthError, TIP20Token}, }; use tracing::{trace, warn}; +use tempo_zone_contracts::Unauthorized; use zone_primitives::{ - abi::Unauthorized, constants::{ZONE_INBOX_ADDRESS, ZONE_OUTBOX_ADDRESS}, policy::AuthRole, }; diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index b2517e666..84aaed1c9 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -13,24 +13,13 @@ workspace = true [dependencies] alloy-primitives = { workspace = true } alloy-rlp = { workspace = true } -alloy-sol-types = { workspace = true } -alloy-contract = { workspace = true, optional = true } -alloy-network = { workspace = true, optional = true } -alloy-provider = { workspace = true, optional = true } -futures = { workspace = true, optional = true } -tokio = { workspace = true, optional = true } serde = { workspace = true, optional = true } -[dev-dependencies] -const-hex.workspace = true - [features] -default = ["std", "serde", "rpc"] +default = ["std", "serde"] std = [ "alloy-primitives/std", "alloy-rlp/std", - "alloy-sol-types/std", "serde?/std", ] serde = ["dep:serde", "alloy-primitives/serde"] -rpc = ["dep:alloy-contract", "dep:alloy-network", "dep:alloy-provider", "dep:futures", "dep:tokio"] diff --git a/crates/primitives/src/abi.rs b/crates/primitives/src/abi.rs deleted file mode 100644 index 551776384..000000000 --- a/crates/primitives/src/abi.rs +++ /dev/null @@ -1,954 +0,0 @@ -//! ABI bindings for the Tempo Zone protocol contracts. -//! -//! These bindings cover the two main contracts the sequencer interacts with: -//! -//! - **ZonePortal** — deployed on Tempo L1. Escrows gas tokens, manages the deposit queue, -//! accepts batch proofs, and processes withdrawals back to L1 recipients. -//! -//! - **ZoneOutbox** — deployed on the Zone L2. Collects user withdrawal requests, builds -//! withdrawal hash chains, and exposes [`LastBatch`] state for proof generation. - -// The `sol!` macro generates functions whose arity we don't control. -#![allow(clippy::too_many_arguments)] - -use alloc::vec::Vec; -use alloy_primitives::{Address, B256, Bytes, U256, keccak256}; -use alloy_sol_types::SolValue; - -pub use crate::constants::{ - EMPTY_SENTINEL, MAX_WITHDRAWAL_GAS_LIMIT, PORTAL_PENDING_SEQUENCER_SLOT, PORTAL_SEQUENCER_SLOT, - TEMPO_BLOCK_HASH_SLOT, TEMPO_PACKED_SLOT, TEMPO_STATE_ADDRESS, TEMPO_STATE_READER_ADDRESS, - ZONE_CONFIG_ADDRESS, ZONE_INBOX_ADDRESS, ZONE_OUTBOX_ADDRESS, ZONE_TOKEN_ADDRESS, - ZONE_TX_CONTEXT_ADDRESS, -}; - -/// Internal macro that emits the full `sol!` block, placing `$($rpc_attr)*` -/// before every `contract` declaration. Called twice: once with `#[sol(rpc)]` -/// (when the `rpc` feature is active) and once with nothing. -macro_rules! define_abi { - ($($rpc_attr:tt)*) => { - alloy_sol_types::sol! { - // --------------------------------------------------------------- - // Shared types - // --------------------------------------------------------------- - - #[derive(Debug)] - struct Withdrawal { - address token; - bytes32 senderTag; - address to; - uint128 amount; - uint128 fee; - bytes32 memo; - uint64 gasLimit; - address fallbackRecipient; - bytes callbackData; - bytes encryptedSender; - } - - #[derive(Debug)] - struct Deposit { - address token; - address sender; - address to; - uint128 amount; - bytes32 memo; - } - - /// Encrypted deposit payload (ECIES encrypted recipient and memo) - #[derive(Debug)] - struct EncryptedDepositPayload { - bytes32 ephemeralPubkeyX; - uint8 ephemeralPubkeyYParity; - bytes ciphertext; - bytes12 nonce; - bytes16 tag; - } - - /// Encrypted deposit stored in the queue - #[derive(Debug)] - struct EncryptedDeposit { - address token; - address sender; - uint128 amount; - uint256 keyIndex; - EncryptedDepositPayload encrypted; - } - - #[derive(Debug)] - struct BlockTransition { - bytes32 prevBlockHash; - bytes32 nextBlockHash; - } - - #[derive(Debug)] - struct DepositQueueTransition { - bytes32 prevProcessedHash; - bytes32 nextProcessedHash; - uint64 prevDepositNumber; - uint64 nextDepositNumber; - } - - #[derive(Debug)] - struct LastBatch { - bytes32 withdrawalQueueHash; - uint64 withdrawalBatchIndex; - } - - /// A TIP-20 token enabled on L1 for bridging to the zone. - #[derive(Debug)] - struct EnabledToken { - address token; - string name; - string symbol; - string currency; - } - - /// Generic unauthorized access error used by zone wrapper logic. - error Unauthorized(); - - // --------------------------------------------------------------- - // ZonePortal — deployed on Tempo L1 - // --------------------------------------------------------------- - - $($rpc_attr)* - contract ZonePortal { - // -- Events -- - - #[derive(Debug)] - event DepositMade( - bytes32 indexed newCurrentDepositQueueHash, - address indexed sender, - address token, - address to, - uint128 netAmount, - uint128 fee, - bytes32 memo, - uint64 depositNumber - ); - - #[derive(Debug)] - event EncryptedDepositMade( - bytes32 indexed newCurrentDepositQueueHash, - address indexed sender, - address token, - uint128 netAmount, - uint128 fee, - uint256 keyIndex, - bytes32 ephemeralPubkeyX, - uint8 ephemeralPubkeyYParity, - bytes ciphertext, - bytes12 nonce, - bytes16 tag, - uint64 depositNumber - ); - - /// Event emitted when a new TIP-20 token is enabled for bridging. - /// Includes token metadata so the zone can create a matching TIP-20. - #[derive(Debug)] - event TokenEnabled(address indexed token, string name, string symbol, string currency); - - #[derive(Debug)] - event BatchSubmitted( - uint64 indexed withdrawalBatchIndex, - bytes32 nextProcessedDepositQueueHash, - bytes32 nextBlockHash, - bytes32 withdrawalQueueHash, - uint64 lastProcessedDepositNumber - ); - - #[derive(Debug)] - event WithdrawalProcessed(address indexed to, address token, uint128 amount, bool callbackSuccess); - - #[derive(Debug)] - event BounceBack( - bytes32 indexed newCurrentDepositQueueHash, - address indexed fallbackRecipient, - address token, - uint128 amount, - uint64 depositNumber - ); - - #[derive(Debug)] - event SequencerTransferStarted( - address indexed currentSequencer, - address indexed pendingSequencer - ); - - #[derive(Debug)] - event SequencerTransferred( - address indexed previousSequencer, - address indexed newSequencer - ); - - // -- Errors -- - - #[derive(Debug)] - error NotSequencer(); - #[derive(Debug)] - error InvalidProof(); - #[derive(Debug)] - error InvalidTempoBlockNumber(); - #[derive(Debug)] - error DepositPolicyForbids(); - - // -- View functions -- - - function zoneId() external view returns (uint32); - function sequencer() external view returns (address); - function verifier() external view returns (address); - function sequencerPubkey() external view returns (bytes32); - function withdrawalBatchIndex() external view returns (uint64); - function blockHash() external view returns (bytes32); - function currentDepositQueueHash() external view returns (bytes32); - function lastSyncedTempoBlockNumber() external view returns (uint64); - function withdrawalQueueHead() external view returns (uint256); - function withdrawalQueueTail() external view returns (uint256); - function withdrawalQueueMaxSize() external view returns (uint256); - function withdrawalQueueSlot(uint256 slot) external view returns (bytes32); - function genesisTempoBlockNumber() external view returns (uint64); - function calculateDepositFee() external view returns (uint128 fee); - function depositCount() external view returns (uint64); - function lastProcessedDepositNumber() external view returns (uint64); - function MAX_WITHDRAWAL_GAS_LIMIT() external view returns (uint64); - - // -- State-changing functions -- - - function deposit(address token, address to, uint128 amount, bytes32 memo) - external - returns (bytes32 newCurrentDepositQueueHash); - - function processWithdrawal(Withdrawal calldata withdrawal, bytes32 remainingQueue) external; - - function submitBatch( - uint64 tempoBlockNumber, - uint64 recentTempoBlockNumber, - BlockTransition calldata blockTransition, - DepositQueueTransition calldata depositQueueTransition, - bytes32 withdrawalQueueHash, - bytes calldata verifierConfig, - bytes calldata proof - ) external; - - function enableToken(address token) external; - - function rpcUrl() external view returns (string memory); - function setRpcUrl(string calldata rpcUrl) external; - - function depositEncrypted( - address token, - uint128 amount, - uint256 keyIndex, - EncryptedDepositPayload calldata encrypted - ) external returns (bytes32 newCurrentDepositQueueHash); - - function setSequencerEncryptionKey( - bytes32 x, - uint8 yParity, - uint8 popV, - bytes32 popR, - bytes32 popS - ) external; - - // -- View functions (token management) -- - - function isTokenEnabled(address token) external view returns (bool); - function enabledTokenCount() external view returns (uint256); - function enabledTokenAt(uint256 index) external view returns (address); - function zoneGasRate() external view returns (uint128); - function pendingSequencer() external view returns (address); - - function sequencerEncryptionKey() external view returns (bytes32 x, uint8 yParity); - - function encryptionKeyCount() external view returns (uint256); - } - - // --------------------------------------------------------------- - // ZoneOutbox — deployed on Zone L2 - // --------------------------------------------------------------- - - $($rpc_attr)* - contract ZoneOutbox { - // -- Events -- - - event WithdrawalRequested( - uint64 indexed withdrawalIndex, - address indexed sender, - address token, - address to, - uint128 amount, - uint128 fee, - bytes32 memo, - uint64 gasLimit, - address fallbackRecipient, - bytes data, - bytes revealTo - ); - - #[derive(Debug)] - event BatchFinalized(bytes32 indexed withdrawalQueueHash, uint64 withdrawalBatchIndex); - - // -- Errors -- - - error OnlySequencer(); - error GasLimitTooHigh(); - - // -- View functions -- - - function lastBatch() external view returns (LastBatch memory); - function withdrawalBatchIndex() external view returns (uint64); - function nextWithdrawalIndex() external view returns (uint64); - function pendingWithdrawalsCount() external view returns (uint256); - function calculateWithdrawalFee(uint64 gasLimit) external view returns (uint128 fee); - function MAX_WITHDRAWAL_GAS_LIMIT() external view returns (uint64); - - // -- State-changing functions -- - - function requestWithdrawal( - address token, - address to, - uint128 amount, - bytes32 memo, - uint64 gasLimit, - address fallbackRecipient, - bytes calldata data, - bytes calldata revealTo - ) external; - function finalizeWithdrawalBatch(uint256 count, uint64 blockNumber, bytes[] calldata encryptedSenders) external returns (bytes32 withdrawalQueueHash); - } - - // --------------------------------------------------------------- - // TempoState — Zone L2 predeploy (0x1c00...0000) - // --------------------------------------------------------------- - - $($rpc_attr)* - contract TempoState { - #[derive(Debug)] - event TempoBlockFinalized(bytes32 indexed blockHash, uint64 indexed blockNumber, bytes32 stateRoot); - - error InvalidParentHash(); - error InvalidBlockNumber(); - error InvalidRlpData(); - error OnlyZoneInbox(); - - function tempoBlockHash() external view returns (bytes32); - function tempoBlockNumber() external view returns (uint64); - function tempoStateRoot() external view returns (bytes32); - function tempoParentHash() external view returns (bytes32); - function tempoBeneficiary() external view returns (address); - function tempoTransactionsRoot() external view returns (bytes32); - function tempoReceiptsRoot() external view returns (bytes32); - function tempoGasLimit() external view returns (uint64); - function tempoGasUsed() external view returns (uint64); - function tempoTimestamp() external view returns (uint64); - function tempoTimestampMillis() external view returns (uint64); - function tempoPrevRandao() external view returns (bytes32); - function generalGasLimit() external view returns (uint64); - function sharedGasLimit() external view returns (uint64); - - function finalizeTempo(bytes calldata header) external; - } - - // --------------------------------------------------------------- - // TempoStateReader — Zone L2 standalone precompile - // Separate from TempoState; reads Tempo L1 storage at a caller-specified block. - // --------------------------------------------------------------- - - $($rpc_attr)* - contract TempoStateReader { - error DelegateCallNotAllowed(); - - function readStorageAt(address account, bytes32 slot, uint64 blockNumber) external view returns (bytes32); - function readStorageBatchAt(address account, bytes32[] calldata slots, uint64 blockNumber) external view returns (bytes32[] memory); - } - - $($rpc_attr)* - contract ZoneTxContext { - function currentTxHash() external returns (bytes32); - } - - // --------------------------------------------------------------- - // ZoneInbox — Zone L2 system contract (0x1c00...0001) - // --------------------------------------------------------------- - - /// Deposit types for the unified deposit queue. - #[derive(Debug, PartialEq, Eq)] - enum DepositType { - Regular, - Encrypted, - } - - /// A queued deposit (regular or encrypted) passed to `advanceTempo`. - #[derive(Debug)] - struct QueuedDeposit { - DepositType depositType; - bytes depositData; - } - - /// Chaum-Pedersen proof for ECDH shared secret derivation. - #[derive(Debug)] - struct ChaumPedersenProof { - bytes32 s; - bytes32 c; - } - - /// Decryption data provided by the sequencer for encrypted deposits. - #[derive(Debug)] - struct DecryptionData { - bytes32 sharedSecret; - uint8 sharedSecretYParity; - ChaumPedersenProof cpProof; - } - - // --------------------------------------------------------------- - // ZoneFactory — deployed on Tempo L1 - // --------------------------------------------------------------- - - #[derive(Debug)] - struct ZoneInfo { - uint32 zoneId; - address portal; - address messenger; - address initialToken; - address sequencer; - address verifier; - bytes32 genesisBlockHash; - bytes32 genesisTempoBlockHash; - uint64 genesisTempoBlockNumber; - string rpcUrl; - } - - $($rpc_attr)* - contract ZoneFactory { - struct ZoneParams { - bytes32 genesisBlockHash; - bytes32 genesisTempoBlockHash; - uint64 genesisTempoBlockNumber; - } - struct CreateZoneParams { - address token; - address sequencer; - address verifier; - ZoneParams zoneParams; - string rpcUrl; - } - #[derive(Debug)] - event ZoneCreated( - uint32 indexed zoneId, - address indexed portal, - address indexed messenger, - address token, - address sequencer, - address verifier, - bytes32 genesisBlockHash, - bytes32 genesisTempoBlockHash, - uint64 genesisTempoBlockNumber - ); - function createZone(CreateZoneParams calldata params) external returns (uint32 zoneId, address portal); - function verifier() external view returns (address); - function zones(uint32 zoneId) external view returns (ZoneInfo memory); - function zoneCount() external view returns (uint32); - function isZonePortal(address portal) external view returns (bool); - function isZoneMessenger(address messenger) external view returns (bool); - } - - // --------------------------------------------------------------- - // ZoneInbox — Zone L2 system contract (0x1c00...0001) - // --------------------------------------------------------------- - - $($rpc_attr)* - contract ZoneInbox { - #[derive(Debug)] - event TempoAdvanced( - bytes32 indexed tempoBlockHash, - uint64 indexed tempoBlockNumber, - uint256 depositsProcessed, - bytes32 newProcessedDepositQueueHash, - uint64 lastProcessedDepositNumber - ); - - #[derive(Debug)] - event DepositProcessed( - bytes32 indexed depositHash, - address indexed sender, - address indexed to, - address token, - uint128 amount, - bytes32 memo - ); - - #[derive(Debug)] - event EncryptedDepositProcessed( - bytes32 indexed depositHash, - address indexed sender, - address indexed to, - address token, - uint128 amount, - bytes32 memo - ); - - #[derive(Debug)] - event EncryptedDepositFailed( - bytes32 indexed depositHash, - address indexed sender, - address token, - uint128 amount - ); - - /// Emitted when a TIP-20 token is enabled on the zone via advanceTempo. - #[derive(Debug)] - event TokenEnabled(address indexed token, string name, string symbol, string currency); - - error OnlySequencer(); - error InvalidDepositQueueHash(); - error MissingDecryptionData(); - error ExtraDecryptionData(); - error InvalidSharedSecretProof(); - function processedDepositQueueHash() external view returns (bytes32); - function processedDepositNumber() external view returns (uint64); - function tempoPortal() external view returns (address); - function tempoState() external view returns (address); - function config() external view returns (address); - - function advanceTempo( - bytes calldata header, - QueuedDeposit[] calldata deposits, - DecryptionData[] calldata decryptions, - EnabledToken[] calldata enabledTokens - ) external; - } - - // --------------------------------------------------------------- - // SwapAndDepositRouter — deployed on Tempo L1 - // --------------------------------------------------------------- - - $($rpc_attr)* - contract SwapAndDepositRouter { - function onWithdrawalReceived( - bytes32 senderTag, - address tokenIn, - uint128 amount, - bytes calldata data - ) external returns (bytes4); - } - } - }; -} - -#[cfg(feature = "rpc")] -define_abi!(#[sol(rpc)]); - -#[cfg(not(feature = "rpc"))] -define_abi!(); - -impl ZonePortal::sequencerEncryptionKeyReturn { - /// Normalize `yParity` to SEC1 compressed prefix (`0x02` or `0x03`). - /// - /// The contract may return `0`/`1` (parity bit) or `0x02`/`0x03` (SEC1 prefix). - pub fn normalized_y_parity(&self) -> Option { - match self.yParity { - 0x02 | 0x03 => Some(self.yParity), - 0 | 1 => Some(0x02 + self.yParity), - _ => None, - } - } -} - -/// Plaintext callback payload for `SwapAndDepositRouter.onWithdrawalReceived`. -/// -/// This payload tells the router to optionally swap the withdrawn token on L1 -/// and then perform a regular `ZonePortal.deposit(...)`. -#[derive(Debug, Clone)] -pub struct SwapAndDepositRouterPlaintextCallback { - /// Token that should be deposited after the optional L1 swap. - pub token_out: Address, - /// Target zone portal that receives the downstream deposit. - pub target_portal: Address, - /// Zone recipient for the downstream plaintext deposit. - pub recipient: Address, - /// Memo recorded on the downstream plaintext deposit. - pub memo: B256, - /// Minimum acceptable output from the optional swap. - /// - /// Ignored when `tokenIn == token_out` and the router can deposit directly. - pub min_amount_out: u128, -} - -impl SwapAndDepositRouterPlaintextCallback { - /// ABI-encode the router callback data expected by the Solidity router. - pub fn abi_encode(&self) -> Vec { - ( - false, - self.token_out, - self.target_portal, - self.recipient, - self.memo, - self.min_amount_out, - ) - .abi_encode_params() - } -} - -/// Encrypted callback payload for `SwapAndDepositRouter.onWithdrawalReceived`. -/// -/// This payload tells the router to optionally swap the withdrawn token on L1 -/// and then call `ZonePortal.depositEncrypted(...)` with an ECIES-encrypted -/// `(recipient, memo)` payload. -#[derive(Debug, Clone)] -pub struct SwapAndDepositRouterEncryptedCallback { - /// Token that should be deposited after the optional L1 swap. - pub token_out: Address, - /// Target zone portal that receives the downstream encrypted deposit. - pub target_portal: Address, - /// Portal encryption key index used to build [`Self::encrypted`]. - pub key_index: U256, - /// ECIES-encrypted `(recipient, memo)` payload for `depositEncrypted`. - pub encrypted: EncryptedDepositPayload, - /// Minimum acceptable output from the optional swap. - /// - /// Ignored when `tokenIn == token_out` and the router can deposit directly. - pub min_amount_out: u128, -} - -impl SwapAndDepositRouterEncryptedCallback { - /// ABI-encode the router callback data expected by the Solidity router. - pub fn abi_encode(&self) -> Vec { - ( - true, - self.token_out, - self.target_portal, - self.key_index, - self.encrypted.clone(), - self.min_amount_out, - ) - .abi_encode_params() - } -} - -#[cfg(feature = "rpc")] -impl, N: alloy_network::Network> - ZonePortal::ZonePortalInstance -{ - /// Returns all token addresses currently enabled for bridging on this [`ZonePortal`]. - /// - /// Calls [`enabledTokenCount`](ZonePortal::enabledTokenCountCall) followed by - /// [`enabledTokenAt`](ZonePortal::enabledTokenAtCall) for each index concurrently. - pub async fn enabled_tokens( - &self, - ) -> Result, alloy_contract::Error> { - let count = self.enabledTokenCount().call().await?; - let futs: alloc::vec::Vec<_> = (0..count.to::()) - .map(|i| async move { - self.enabledTokenAt(alloy_primitives::U256::from(i)) - .call() - .await - }) - .collect(); - futures::future::try_join_all(futs).await - } - - /// Fetches the active sequencer encryption key and its index. - /// - /// Returns `(key, key_index)` where `key` is the - /// [`sequencerEncryptionKeyReturn`](ZonePortal::sequencerEncryptionKeyReturn) and - /// `key_index` is the zero-based index of the current key. - pub async fn encryption_key( - &self, - ) -> Result< - ( - ZonePortal::sequencerEncryptionKeyReturn, - alloy_primitives::U256, - ), - alloy_contract::Error, - > { - let key_call = self.sequencerEncryptionKey(); - let count_call = self.encryptionKeyCount(); - let (key, count) = tokio::try_join!(key_call.call(), count_call.call())?; - let key_index = count.saturating_sub(alloy_primitives::U256::from(1)); - Ok((key, key_index)) - } -} - -impl core::fmt::Display for ZonePortal::ZonePortalErrors { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::NotSequencer(_) => f.write_str("NotSequencer"), - Self::InvalidProof(_) => f.write_str("InvalidProof"), - Self::InvalidTempoBlockNumber(_) => f.write_str("InvalidTempoBlockNumber"), - Self::DepositPolicyForbids(_) => f.write_str("DepositPolicyForbids"), - } - } -} - -impl Withdrawal { - /// Build the authenticated-withdrawal sender plaintext `[sender(20) | tx_hash(32)]`. - pub fn authenticated_sender_plaintext(sender: Address, tx_hash: B256) -> [u8; 52] { - let mut plaintext = [0u8; 52]; - plaintext[..20].copy_from_slice(sender.as_slice()); - plaintext[20..].copy_from_slice(tx_hash.as_slice()); - plaintext - } - - /// Compute the authenticated sender tag `keccak256(sender || tx_hash)`. - pub fn sender_tag(sender: Address, tx_hash: B256) -> B256 { - keccak256(Self::authenticated_sender_plaintext(sender, tx_hash)) - } - - /// Reconstruct the public L1-facing withdrawal from a zone-side withdrawal request event. - pub fn from_requested_event( - event: &ZoneOutbox::WithdrawalRequested, - tx_hash: B256, - encrypted_sender: Bytes, - ) -> Self { - Self { - token: event.token, - senderTag: Self::sender_tag(event.sender, tx_hash), - to: event.to, - amount: event.amount, - fee: event.fee, - memo: event.memo, - gasLimit: event.gasLimit, - fallbackRecipient: event.fallbackRecipient, - callbackData: event.data.clone(), - encryptedSender: encrypted_sender, - } - } - - /// Compute the withdrawal queue hash for a slice of withdrawals. - /// - /// The hash chain has the oldest withdrawal at the outermost layer for efficient FIFO removal: - /// - /// ```text - /// hash = keccak256(encode(w[0], keccak256(encode(w[1], keccak256(encode(w[2], EMPTY_SENTINEL)))))) - /// ``` - /// - /// Building proceeds from the newest (innermost) to the oldest (outermost). - /// Returns `B256::ZERO` if `withdrawals` is empty. - pub fn queue_hash(withdrawals: &[Self]) -> B256 { - if withdrawals.is_empty() { - return B256::ZERO; - } - - let mut hash = EMPTY_SENTINEL; - for w in withdrawals.iter().rev() { - hash = keccak256((w.clone(), hash).abi_encode_params()); - } - hash - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{Bytes, address}; - use alloy_sol_types::SolCall; - - #[test] - fn test_deposit_abi_encode_vs_params() { - let d = Deposit { - token: address!("0x0000000000000000000000000000000000001000"), - sender: address!("0x0000000000000000000000000000000000000001"), - to: address!("0x0000000000000000000000000000000000000002"), - amount: 1000u128, - memo: B256::ZERO, - }; - - let encoded = d.abi_encode(); - let encoded_params = d.abi_encode_params(); - - println!("abi_encode length: {}", encoded.len()); - println!("abi_encode_params length: {}", encoded_params.len()); - println!("abi_encode hex:\n{}", const_hex::encode(&encoded)); - println!( - "abi_encode_params hex:\n{}", - const_hex::encode(&encoded_params) - ); - println!("Are they equal: {}", encoded == encoded_params); - } - - #[test] - fn test_queued_deposit_encoding() { - let deposit = Deposit { - token: address!("0x0000000000000000000000000000000000001000"), - sender: address!("0x0000000000000000000000000000000000000001"), - to: address!("0x0000000000000000000000000000000000000002"), - amount: 1000u128, - memo: B256::ZERO, - }; - - let deposit_data = Bytes::from(deposit.abi_encode()); - - let qd = QueuedDeposit { - depositType: DepositType::Regular, - depositData: deposit_data, - }; - - println!( - "DepositType::Regular abi_encode: {}", - const_hex::encode(DepositType::Regular.abi_encode()) - ); - println!( - "deposit.abi_encode() length: {}", - deposit.abi_encode().len() - ); - println!( - "deposit.abi_encode(): {}", - const_hex::encode(deposit.abi_encode()) - ); - println!( - "QueuedDeposit.abi_encode() length: {}", - qd.abi_encode().len() - ); - println!( - "QueuedDeposit.abi_encode(): {}", - const_hex::encode(qd.abi_encode()) - ); - - // Now test the full advanceTempo call encoding - let header_bytes = Bytes::from(vec![0xc0]); // minimal RLP empty list - let calldata = ZoneInbox::advanceTempoCall { - header: header_bytes, - deposits: vec![qd], - decryptions: vec![], - enabledTokens: vec![], - } - .abi_encode(); - - println!("\nadvanceTempo calldata length: {}", calldata.len()); - println!( - "advanceTempo selector: 0x{}", - const_hex::encode(&calldata[..4]) - ); - println!( - "advanceTempo full calldata:\n{}", - const_hex::encode(&calldata) - ); - } - - #[test] - fn test_deposit_hash_chain_matches_solidity() { - let deposit = Deposit { - token: address!("0x0000000000000000000000000000000000001000"), - sender: address!("0x0000000000000000000000000000000000000001"), - to: address!("0x0000000000000000000000000000000000000002"), - amount: 1000u128, - memo: B256::ZERO, - }; - let prev_hash = B256::ZERO; - - let solidity_encoding = (DepositType::Regular, deposit.clone(), prev_hash).abi_encode(); - let solidity_hash = keccak256(&solidity_encoding); - - let rust_encoding = (DepositType::Regular, deposit, prev_hash).abi_encode(); - let rust_hash = keccak256(&rust_encoding); - - assert_eq!(solidity_encoding, rust_encoding, "ABI encodings must match"); - assert_eq!(solidity_hash, rust_hash, "Deposit hash chains must match"); - } - - #[test] - fn test_decryption_data_encoding_uses_trimmed_layout() { - let shared_secret = B256::from([0x11; 32]); - let shared_secret_y_parity = 0x02; - let proof_s = B256::from([0x33; 32]); - let proof_c = B256::from([0x44; 32]); - - let decryption = DecryptionData { - sharedSecret: shared_secret, - sharedSecretYParity: shared_secret_y_parity, - cpProof: ChaumPedersenProof { - s: proof_s, - c: proof_c, - }, - }; - - let encoded = decryption.abi_encode(); - let mut expected_y_parity_word = [0u8; 32]; - expected_y_parity_word[31] = shared_secret_y_parity; - - assert_eq!( - encoded.len(), - 4 * 32, - "DecryptionData must encode as four ABI words" - ); - assert_eq!( - &encoded[0..32], - shared_secret.as_slice(), - "word 0 is sharedSecret" - ); - assert_eq!( - &encoded[32..64], - expected_y_parity_word, - "word 1 is sharedSecretYParity" - ); - assert_eq!(&encoded[64..96], proof_s.as_slice(), "word 2 is cpProof.s"); - assert_eq!(&encoded[96..128], proof_c.as_slice(), "word 3 is cpProof.c"); - } - - #[test] - fn test_router_plaintext_callback_encoding_matches_tuple() { - let callback = SwapAndDepositRouterPlaintextCallback { - token_out: address!("0x0000000000000000000000000000000000001001"), - target_portal: address!("0x0000000000000000000000000000000000002001"), - recipient: address!("0x0000000000000000000000000000000000003001"), - memo: B256::from([0x11; 32]), - min_amount_out: 1234, - }; - - let tuple_encoding = ( - false, - callback.token_out, - callback.target_portal, - callback.recipient, - callback.memo, - callback.min_amount_out, - ) - .abi_encode_params(); - - assert_eq!(callback.abi_encode(), tuple_encoding); - } - - #[test] - fn test_sender_tag_matches_plaintext_hash() { - let sender = address!("0x0000000000000000000000000000000000000001"); - let tx_hash = B256::repeat_byte(0x22); - let plaintext = Withdrawal::authenticated_sender_plaintext(sender, tx_hash); - - assert_eq!(&plaintext[..20], sender.as_slice()); - assert_eq!(&plaintext[20..], tx_hash.as_slice()); - assert_eq!( - Withdrawal::sender_tag(sender, tx_hash), - keccak256(plaintext) - ); - } - - #[test] - fn test_router_encrypted_callback_encoding_matches_tuple() { - let encrypted = EncryptedDepositPayload { - ephemeralPubkeyX: B256::from([0x22; 32]), - ephemeralPubkeyYParity: 0x02, - ciphertext: Bytes::from(vec![0xaa, 0xbb, 0xcc, 0xdd]), - nonce: [0x33; 12].into(), - tag: [0x44; 16].into(), - }; - let callback = SwapAndDepositRouterEncryptedCallback { - token_out: address!("0x0000000000000000000000000000000000001002"), - target_portal: address!("0x0000000000000000000000000000000000002002"), - key_index: U256::from(7), - encrypted: encrypted.clone(), - min_amount_out: 5678, - }; - - let tuple_encoding = ( - true, - callback.token_out, - callback.target_portal, - callback.key_index, - encrypted, - callback.min_amount_out, - ) - .abi_encode_params(); - - assert_eq!(callback.abi_encode(), tuple_encoding); - } -} diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 223d77c10..5f6a81b5b 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -7,7 +7,6 @@ extern crate alloc; -pub mod abi; pub mod constants; mod header; pub mod policy; diff --git a/crates/tempo-zone/Cargo.toml b/crates/tempo-zone/Cargo.toml index 848c24843..3997268d3 100644 --- a/crates/tempo-zone/Cargo.toml +++ b/crates/tempo-zone/Cargo.toml @@ -22,8 +22,9 @@ tempo-transaction-pool.workspace = true tempo-alloy.workspace = true tempo-contracts.workspace = true tempo-precompiles.workspace = true +tempo-zone-contracts = { workspace = true, features = ["std", "serde", "rpc"] } zone-precompiles = { workspace = true, features = ["std"] } -zone-primitives = { workspace = true, features = ["std", "serde", "rpc"] } +zone-primitives = { workspace = true, features = ["std", "serde"] } zone-rpc.workspace = true # reth diff --git a/crates/tempo-zone/src/abi.rs b/crates/tempo-zone/src/abi.rs index 125e6498e..e1c7e19dc 100644 --- a/crates/tempo-zone/src/abi.rs +++ b/crates/tempo-zone/src/abi.rs @@ -1,3 +1,3 @@ -//! ABI bindings — re-exported from [`zone_primitives::abi`]. +//! ABI bindings — re-exported from [`tempo_zone_contracts`]. -pub use zone_primitives::abi::*; +pub use tempo_zone_contracts::*; From 27881ae52d001d3391f37cafe290cb1d81c6d7cd Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xKitsune@protonmail.com> Date: Fri, 19 Jun 2026 12:34:20 -0400 Subject: [PATCH 2/3] chore: remove prev contract interfaces --- crates/contracts/src/bindings.rs | 525 ------------------ crates/contracts/src/lib.rs | 51 +- crates/contracts/src/precompiles/common.rs | 5 + crates/contracts/src/precompiles/mod.rs | 29 + .../swap_and_deposit_router.rs | 17 +- .../contracts/src/precompiles/tempo_state.rs | 30 + .../src/precompiles/tempo_state_reader.rs | 14 + .../contracts/src/precompiles/zone_factory.rs | 50 ++ .../contracts/src/precompiles/zone_inbox.rs | 110 ++++ .../contracts/src/precompiles/zone_outbox.rs | 61 ++ .../contracts/src/precompiles/zone_portal.rs | 322 +++++++++++ .../src/precompiles/zone_tx_context.rs | 8 + crates/contracts/src/types.rs | 63 --- crates/contracts/src/zone_portal.rs | 71 --- 14 files changed, 676 insertions(+), 680 deletions(-) delete mode 100644 crates/contracts/src/bindings.rs create mode 100644 crates/contracts/src/precompiles/common.rs create mode 100644 crates/contracts/src/precompiles/mod.rs rename crates/contracts/src/{ => precompiles}/swap_and_deposit_router.rs (87%) create mode 100644 crates/contracts/src/precompiles/tempo_state.rs create mode 100644 crates/contracts/src/precompiles/tempo_state_reader.rs create mode 100644 crates/contracts/src/precompiles/zone_factory.rs create mode 100644 crates/contracts/src/precompiles/zone_inbox.rs create mode 100644 crates/contracts/src/precompiles/zone_outbox.rs create mode 100644 crates/contracts/src/precompiles/zone_portal.rs create mode 100644 crates/contracts/src/precompiles/zone_tx_context.rs delete mode 100644 crates/contracts/src/types.rs delete mode 100644 crates/contracts/src/zone_portal.rs diff --git a/crates/contracts/src/bindings.rs b/crates/contracts/src/bindings.rs deleted file mode 100644 index e526383cb..000000000 --- a/crates/contracts/src/bindings.rs +++ /dev/null @@ -1,525 +0,0 @@ -//! Generated contract bindings for the Tempo Zone protocol. -//! -//! All contracts and the structs/enums they share are emitted from a single -//! [`alloy_sol_types::sol!`] invocation: the macro can only resolve user-defined types -//! (e.g. [`Withdrawal`], [`QueuedDeposit`]) that are declared within the same invocation, and -//! several zone contracts reference the same types. - -/// Internal macro that emits the full `sol!` block, placing `$($rpc_attr)*` -/// before every `contract` declaration. Called twice: once with `#[sol(rpc)]` -/// (when the `rpc` feature is active) and once with nothing. -macro_rules! define_abi { - ($($rpc_attr:tt)*) => { - alloy_sol_types::sol! { - // --------------------------------------------------------------- - // Shared types - // --------------------------------------------------------------- - - #[derive(Debug)] - struct Withdrawal { - address token; - bytes32 senderTag; - address to; - uint128 amount; - uint128 fee; - bytes32 memo; - uint64 gasLimit; - address fallbackRecipient; - bytes callbackData; - bytes encryptedSender; - } - - #[derive(Debug)] - struct Deposit { - address token; - address sender; - address to; - uint128 amount; - bytes32 memo; - } - - /// Encrypted deposit payload (ECIES encrypted recipient and memo) - #[derive(Debug)] - struct EncryptedDepositPayload { - bytes32 ephemeralPubkeyX; - uint8 ephemeralPubkeyYParity; - bytes ciphertext; - bytes12 nonce; - bytes16 tag; - } - - /// Encrypted deposit stored in the queue - #[derive(Debug)] - struct EncryptedDeposit { - address token; - address sender; - uint128 amount; - uint256 keyIndex; - EncryptedDepositPayload encrypted; - } - - #[derive(Debug)] - struct BlockTransition { - bytes32 prevBlockHash; - bytes32 nextBlockHash; - } - - #[derive(Debug)] - struct DepositQueueTransition { - bytes32 prevProcessedHash; - bytes32 nextProcessedHash; - uint64 prevDepositNumber; - uint64 nextDepositNumber; - } - - #[derive(Debug)] - struct LastBatch { - bytes32 withdrawalQueueHash; - uint64 withdrawalBatchIndex; - } - - /// A TIP-20 token enabled on L1 for bridging to the zone. - #[derive(Debug)] - struct EnabledToken { - address token; - string name; - string symbol; - string currency; - } - - /// Generic unauthorized access error used by zone wrapper logic. - error Unauthorized(); - - // --------------------------------------------------------------- - // ZonePortal — deployed on Tempo L1 - // --------------------------------------------------------------- - - $($rpc_attr)* - contract ZonePortal { - // -- Events -- - - #[derive(Debug)] - event DepositMade( - bytes32 indexed newCurrentDepositQueueHash, - address indexed sender, - address token, - address to, - uint128 netAmount, - uint128 fee, - bytes32 memo, - uint64 depositNumber - ); - - #[derive(Debug)] - event EncryptedDepositMade( - bytes32 indexed newCurrentDepositQueueHash, - address indexed sender, - address token, - uint128 netAmount, - uint128 fee, - uint256 keyIndex, - bytes32 ephemeralPubkeyX, - uint8 ephemeralPubkeyYParity, - bytes ciphertext, - bytes12 nonce, - bytes16 tag, - uint64 depositNumber - ); - - /// Event emitted when a new TIP-20 token is enabled for bridging. - /// Includes token metadata so the zone can create a matching TIP-20. - #[derive(Debug)] - event TokenEnabled(address indexed token, string name, string symbol, string currency); - - #[derive(Debug)] - event BatchSubmitted( - uint64 indexed withdrawalBatchIndex, - bytes32 nextProcessedDepositQueueHash, - bytes32 nextBlockHash, - bytes32 withdrawalQueueHash, - uint64 lastProcessedDepositNumber - ); - - #[derive(Debug)] - event WithdrawalProcessed(address indexed to, address token, uint128 amount, bool callbackSuccess); - - #[derive(Debug)] - event BounceBack( - bytes32 indexed newCurrentDepositQueueHash, - address indexed fallbackRecipient, - address token, - uint128 amount, - uint64 depositNumber - ); - - #[derive(Debug)] - event SequencerTransferStarted( - address indexed currentSequencer, - address indexed pendingSequencer - ); - - #[derive(Debug)] - event SequencerTransferred( - address indexed previousSequencer, - address indexed newSequencer - ); - - // -- Errors -- - - #[derive(Debug)] - error NotSequencer(); - #[derive(Debug)] - error InvalidProof(); - #[derive(Debug)] - error InvalidTempoBlockNumber(); - #[derive(Debug)] - error DepositPolicyForbids(); - - // -- View functions -- - - function zoneId() external view returns (uint32); - function sequencer() external view returns (address); - function verifier() external view returns (address); - function sequencerPubkey() external view returns (bytes32); - function withdrawalBatchIndex() external view returns (uint64); - function blockHash() external view returns (bytes32); - function currentDepositQueueHash() external view returns (bytes32); - function lastSyncedTempoBlockNumber() external view returns (uint64); - function withdrawalQueueHead() external view returns (uint256); - function withdrawalQueueTail() external view returns (uint256); - function withdrawalQueueMaxSize() external view returns (uint256); - function withdrawalQueueSlot(uint256 slot) external view returns (bytes32); - function genesisTempoBlockNumber() external view returns (uint64); - function calculateDepositFee() external view returns (uint128 fee); - function depositCount() external view returns (uint64); - function lastProcessedDepositNumber() external view returns (uint64); - function MAX_WITHDRAWAL_GAS_LIMIT() external view returns (uint64); - - // -- State-changing functions -- - - function deposit(address token, address to, uint128 amount, bytes32 memo) - external - returns (bytes32 newCurrentDepositQueueHash); - - function processWithdrawal(Withdrawal calldata withdrawal, bytes32 remainingQueue) external; - - function submitBatch( - uint64 tempoBlockNumber, - uint64 recentTempoBlockNumber, - BlockTransition calldata blockTransition, - DepositQueueTransition calldata depositQueueTransition, - bytes32 withdrawalQueueHash, - bytes calldata verifierConfig, - bytes calldata proof - ) external; - - function enableToken(address token) external; - - function rpcUrl() external view returns (string memory); - function setRpcUrl(string calldata rpcUrl) external; - - function depositEncrypted( - address token, - uint128 amount, - uint256 keyIndex, - EncryptedDepositPayload calldata encrypted - ) external returns (bytes32 newCurrentDepositQueueHash); - - function setSequencerEncryptionKey( - bytes32 x, - uint8 yParity, - uint8 popV, - bytes32 popR, - bytes32 popS - ) external; - - // -- View functions (token management) -- - - function isTokenEnabled(address token) external view returns (bool); - function enabledTokenCount() external view returns (uint256); - function enabledTokenAt(uint256 index) external view returns (address); - function zoneGasRate() external view returns (uint128); - function pendingSequencer() external view returns (address); - - function sequencerEncryptionKey() external view returns (bytes32 x, uint8 yParity); - - function encryptionKeyCount() external view returns (uint256); - } - - // --------------------------------------------------------------- - // ZoneOutbox — deployed on Zone L2 - // --------------------------------------------------------------- - - $($rpc_attr)* - contract ZoneOutbox { - // -- Events -- - - event WithdrawalRequested( - uint64 indexed withdrawalIndex, - address indexed sender, - address token, - address to, - uint128 amount, - uint128 fee, - bytes32 memo, - uint64 gasLimit, - address fallbackRecipient, - bytes data, - bytes revealTo - ); - - #[derive(Debug)] - event BatchFinalized(bytes32 indexed withdrawalQueueHash, uint64 withdrawalBatchIndex); - - // -- Errors -- - - error OnlySequencer(); - error GasLimitTooHigh(); - - // -- View functions -- - - function lastBatch() external view returns (LastBatch memory); - function withdrawalBatchIndex() external view returns (uint64); - function nextWithdrawalIndex() external view returns (uint64); - function pendingWithdrawalsCount() external view returns (uint256); - function calculateWithdrawalFee(uint64 gasLimit) external view returns (uint128 fee); - function MAX_WITHDRAWAL_GAS_LIMIT() external view returns (uint64); - - // -- State-changing functions -- - - function requestWithdrawal( - address token, - address to, - uint128 amount, - bytes32 memo, - uint64 gasLimit, - address fallbackRecipient, - bytes calldata data, - bytes calldata revealTo - ) external; - function finalizeWithdrawalBatch(uint256 count, uint64 blockNumber, bytes[] calldata encryptedSenders) external returns (bytes32 withdrawalQueueHash); - } - - // --------------------------------------------------------------- - // TempoState — Zone L2 predeploy (0x1c00...0000) - // --------------------------------------------------------------- - - $($rpc_attr)* - contract TempoState { - #[derive(Debug)] - event TempoBlockFinalized(bytes32 indexed blockHash, uint64 indexed blockNumber, bytes32 stateRoot); - - error InvalidParentHash(); - error InvalidBlockNumber(); - error InvalidRlpData(); - error OnlyZoneInbox(); - - function tempoBlockHash() external view returns (bytes32); - function tempoBlockNumber() external view returns (uint64); - function tempoStateRoot() external view returns (bytes32); - function tempoParentHash() external view returns (bytes32); - function tempoBeneficiary() external view returns (address); - function tempoTransactionsRoot() external view returns (bytes32); - function tempoReceiptsRoot() external view returns (bytes32); - function tempoGasLimit() external view returns (uint64); - function tempoGasUsed() external view returns (uint64); - function tempoTimestamp() external view returns (uint64); - function tempoTimestampMillis() external view returns (uint64); - function tempoPrevRandao() external view returns (bytes32); - function generalGasLimit() external view returns (uint64); - function sharedGasLimit() external view returns (uint64); - - function finalizeTempo(bytes calldata header) external; - } - - // --------------------------------------------------------------- - // TempoStateReader — Zone L2 standalone precompile - // Separate from TempoState; reads Tempo L1 storage at a caller-specified block. - // --------------------------------------------------------------- - - $($rpc_attr)* - contract TempoStateReader { - error DelegateCallNotAllowed(); - - function readStorageAt(address account, bytes32 slot, uint64 blockNumber) external view returns (bytes32); - function readStorageBatchAt(address account, bytes32[] calldata slots, uint64 blockNumber) external view returns (bytes32[] memory); - } - - $($rpc_attr)* - contract ZoneTxContext { - function currentTxHash() external returns (bytes32); - } - - // --------------------------------------------------------------- - // ZoneInbox shared types — Zone L2 system contract (0x1c00...0001) - // --------------------------------------------------------------- - - /// Deposit types for the unified deposit queue. - #[derive(Debug, PartialEq, Eq)] - enum DepositType { - Regular, - Encrypted, - } - - /// A queued deposit (regular or encrypted) passed to `advanceTempo`. - #[derive(Debug)] - struct QueuedDeposit { - DepositType depositType; - bytes depositData; - } - - /// Chaum-Pedersen proof for ECDH shared secret derivation. - #[derive(Debug)] - struct ChaumPedersenProof { - bytes32 s; - bytes32 c; - } - - /// Decryption data provided by the sequencer for encrypted deposits. - #[derive(Debug)] - struct DecryptionData { - bytes32 sharedSecret; - uint8 sharedSecretYParity; - ChaumPedersenProof cpProof; - } - - // --------------------------------------------------------------- - // ZoneFactory — deployed on Tempo L1 - // --------------------------------------------------------------- - - #[derive(Debug)] - struct ZoneInfo { - uint32 zoneId; - address portal; - address messenger; - address initialToken; - address sequencer; - address verifier; - bytes32 genesisBlockHash; - bytes32 genesisTempoBlockHash; - uint64 genesisTempoBlockNumber; - string rpcUrl; - } - - $($rpc_attr)* - contract ZoneFactory { - struct ZoneParams { - bytes32 genesisBlockHash; - bytes32 genesisTempoBlockHash; - uint64 genesisTempoBlockNumber; - } - struct CreateZoneParams { - address token; - address sequencer; - address verifier; - ZoneParams zoneParams; - string rpcUrl; - } - #[derive(Debug)] - event ZoneCreated( - uint32 indexed zoneId, - address indexed portal, - address indexed messenger, - address token, - address sequencer, - address verifier, - bytes32 genesisBlockHash, - bytes32 genesisTempoBlockHash, - uint64 genesisTempoBlockNumber - ); - function createZone(CreateZoneParams calldata params) external returns (uint32 zoneId, address portal); - function verifier() external view returns (address); - function zones(uint32 zoneId) external view returns (ZoneInfo memory); - function zoneCount() external view returns (uint32); - function isZonePortal(address portal) external view returns (bool); - function isZoneMessenger(address messenger) external view returns (bool); - } - - // --------------------------------------------------------------- - // ZoneInbox — Zone L2 system contract (0x1c00...0001) - // --------------------------------------------------------------- - - $($rpc_attr)* - contract ZoneInbox { - #[derive(Debug)] - event TempoAdvanced( - bytes32 indexed tempoBlockHash, - uint64 indexed tempoBlockNumber, - uint256 depositsProcessed, - bytes32 newProcessedDepositQueueHash, - uint64 lastProcessedDepositNumber - ); - - #[derive(Debug)] - event DepositProcessed( - bytes32 indexed depositHash, - address indexed sender, - address indexed to, - address token, - uint128 amount, - bytes32 memo - ); - - #[derive(Debug)] - event EncryptedDepositProcessed( - bytes32 indexed depositHash, - address indexed sender, - address indexed to, - address token, - uint128 amount, - bytes32 memo - ); - - #[derive(Debug)] - event EncryptedDepositFailed( - bytes32 indexed depositHash, - address indexed sender, - address token, - uint128 amount - ); - - /// Emitted when a TIP-20 token is enabled on the zone via advanceTempo. - #[derive(Debug)] - event TokenEnabled(address indexed token, string name, string symbol, string currency); - - error OnlySequencer(); - error InvalidDepositQueueHash(); - error MissingDecryptionData(); - error ExtraDecryptionData(); - error InvalidSharedSecretProof(); - function processedDepositQueueHash() external view returns (bytes32); - function processedDepositNumber() external view returns (uint64); - function tempoPortal() external view returns (address); - function tempoState() external view returns (address); - function config() external view returns (address); - - function advanceTempo( - bytes calldata header, - QueuedDeposit[] calldata deposits, - DecryptionData[] calldata decryptions, - EnabledToken[] calldata enabledTokens - ) external; - } - - // --------------------------------------------------------------- - // SwapAndDepositRouter — deployed on Tempo L1 - // --------------------------------------------------------------- - - $($rpc_attr)* - contract SwapAndDepositRouter { - function onWithdrawalReceived( - bytes32 senderTag, - address tokenIn, - uint128 amount, - bytes calldata data - ) external returns (bytes4); - } - } - }; -} - -#[cfg(feature = "rpc")] -define_abi!(#[sol(rpc)]); - -#[cfg(not(feature = "rpc"))] -define_abi!(); diff --git a/crates/contracts/src/lib.rs b/crates/contracts/src/lib.rs index 2c6c8438e..9f0dbf2ed 100644 --- a/crates/contracts/src/lib.rs +++ b/crates/contracts/src/lib.rs @@ -5,33 +5,48 @@ //! - **ZonePortal** — deployed on Tempo L1. Escrows gas tokens, manages the deposit queue, //! accepts batch proofs, and processes withdrawals back to L1 recipients. //! - **ZoneOutbox** — deployed on the Zone L2. Collects user withdrawal requests, builds -//! withdrawal hash chains, and exposes [`LastBatch`] state for proof generation. +//! withdrawal hash chains, and exposes `LastBatch` state for proof generation. //! - **ZoneInbox**, **TempoState**, **TempoStateReader**, **ZoneTxContext** — Zone L2 predeploys. //! - **ZoneFactory**, **SwapAndDepositRouter** — deployed on Tempo L1. #![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(docsrs, feature(doc_cfg))] -// The `sol!` macro generates functions whose arity we don't control. +// auto-generated sol! builders for events/errors/functions with many fields trigger this #![allow(clippy::too_many_arguments)] extern crate alloc; -pub mod bindings; -pub mod swap_and_deposit_router; -mod types; -mod zone_portal; - -pub use bindings::*; -pub use swap_and_deposit_router::*; - -// Re-export the address and slot constants the bindings build on, so callers can reach them -// through the contracts crate (e.g. `tempo_zone_contracts::TEMPO_STATE_ADDRESS`). -pub use zone_primitives::constants::{ - EMPTY_SENTINEL, MAX_WITHDRAWAL_GAS_LIMIT, PORTAL_PENDING_SEQUENCER_SLOT, PORTAL_SEQUENCER_SLOT, - TEMPO_BLOCK_HASH_SLOT, TEMPO_PACKED_SLOT, TEMPO_STATE_ADDRESS, TEMPO_STATE_READER_ADDRESS, - ZONE_CONFIG_ADDRESS, ZONE_INBOX_ADDRESS, ZONE_OUTBOX_ADDRESS, ZONE_TOKEN_ADDRESS, - ZONE_TX_CONTEXT_ADDRESS, -}; +/// Helper macro to allow feature-gating rpc and serde implementations. +macro_rules! sol { + ($($input:tt)*) => { + #[cfg(all(feature = "rpc", feature = "serde"))] + alloy_sol_types::sol! { + #[sol(rpc)] + #[derive(serde::Serialize, serde::Deserialize)] + $($input)* + } + #[cfg(all(feature = "rpc", not(feature = "serde")))] + alloy_sol_types::sol! { + #[sol(rpc)] + $($input)* + } + #[cfg(all(not(feature = "rpc"), feature = "serde"))] + alloy_sol_types::sol! { + #[derive(serde::Serialize, serde::Deserialize)] + $($input)* + } + #[cfg(all(not(feature = "rpc"), not(feature = "serde")))] + alloy_sol_types::sol! { + $($input)* + } + }; +} + +pub(crate) use sol; + +pub mod precompiles; + +pub use precompiles::*; #[cfg(test)] mod tests { diff --git a/crates/contracts/src/precompiles/common.rs b/crates/contracts/src/precompiles/common.rs new file mode 100644 index 000000000..f823fec63 --- /dev/null +++ b/crates/contracts/src/precompiles/common.rs @@ -0,0 +1,5 @@ +crate::sol! { + /// Generic unauthorized access error used by zone wrapper logic. + #[derive(Debug)] + error Unauthorized(); +} diff --git a/crates/contracts/src/precompiles/mod.rs b/crates/contracts/src/precompiles/mod.rs new file mode 100644 index 000000000..3898527d1 --- /dev/null +++ b/crates/contracts/src/precompiles/mod.rs @@ -0,0 +1,29 @@ +pub mod common; +pub mod swap_and_deposit_router; +pub mod tempo_state; +pub mod tempo_state_reader; +pub mod zone_factory; +pub mod zone_inbox; +pub mod zone_outbox; +pub mod zone_portal; +pub mod zone_tx_context; + +pub use common::*; +pub use swap_and_deposit_router::*; +pub use tempo_state::*; +pub use tempo_state_reader::*; +pub use zone_factory::*; +pub use zone_inbox::*; +pub use zone_outbox::*; +pub use zone_portal::*; +pub use zone_tx_context::*; + +// Address and storage-slot constants the bindings build on. These live in `zone-primitives` +// (shared with the proof system) and are re-exported here so callers can reach them through the +// contracts crate, e.g. `tempo_zone_contracts::TEMPO_STATE_ADDRESS`. +pub use zone_primitives::constants::{ + EMPTY_SENTINEL, MAX_WITHDRAWAL_GAS_LIMIT, PORTAL_PENDING_SEQUENCER_SLOT, PORTAL_SEQUENCER_SLOT, + TEMPO_BLOCK_HASH_SLOT, TEMPO_PACKED_SLOT, TEMPO_STATE_ADDRESS, TEMPO_STATE_READER_ADDRESS, + ZONE_CONFIG_ADDRESS, ZONE_INBOX_ADDRESS, ZONE_OUTBOX_ADDRESS, ZONE_TOKEN_ADDRESS, + ZONE_TX_CONTEXT_ADDRESS, +}; diff --git a/crates/contracts/src/swap_and_deposit_router.rs b/crates/contracts/src/precompiles/swap_and_deposit_router.rs similarity index 87% rename from crates/contracts/src/swap_and_deposit_router.rs rename to crates/contracts/src/precompiles/swap_and_deposit_router.rs index f34f1b5e1..8bc4311d7 100644 --- a/crates/contracts/src/swap_and_deposit_router.rs +++ b/crates/contracts/src/precompiles/swap_and_deposit_router.rs @@ -1,11 +1,22 @@ -//! Hand-written callback payloads for the -//! [`SwapAndDepositRouter`](crate::bindings::SwapAndDepositRouter) deployed on Tempo L1. +//! `SwapAndDepositRouter` — deployed on Tempo L1. -use crate::bindings::EncryptedDepositPayload; +use crate::EncryptedDepositPayload; use alloc::vec::Vec; use alloy_primitives::{Address, B256, U256}; use alloy_sol_types::SolValue; +crate::sol! { + #[derive(Debug)] + contract SwapAndDepositRouter { + function onWithdrawalReceived( + bytes32 senderTag, + address tokenIn, + uint128 amount, + bytes calldata data + ) external returns (bytes4); + } +} + /// Plaintext callback payload for `SwapAndDepositRouter.onWithdrawalReceived`. /// /// This payload tells the router to optionally swap the withdrawn token on L1 diff --git a/crates/contracts/src/precompiles/tempo_state.rs b/crates/contracts/src/precompiles/tempo_state.rs new file mode 100644 index 000000000..bd678e0da --- /dev/null +++ b/crates/contracts/src/precompiles/tempo_state.rs @@ -0,0 +1,30 @@ +//! `TempoState` — Zone L2 predeploy (0x1c00...0000). + +crate::sol! { + #[derive(Debug)] + contract TempoState { + event TempoBlockFinalized(bytes32 indexed blockHash, uint64 indexed blockNumber, bytes32 stateRoot); + + error InvalidParentHash(); + error InvalidBlockNumber(); + error InvalidRlpData(); + error OnlyZoneInbox(); + + function tempoBlockHash() external view returns (bytes32); + function tempoBlockNumber() external view returns (uint64); + function tempoStateRoot() external view returns (bytes32); + function tempoParentHash() external view returns (bytes32); + function tempoBeneficiary() external view returns (address); + function tempoTransactionsRoot() external view returns (bytes32); + function tempoReceiptsRoot() external view returns (bytes32); + function tempoGasLimit() external view returns (uint64); + function tempoGasUsed() external view returns (uint64); + function tempoTimestamp() external view returns (uint64); + function tempoTimestampMillis() external view returns (uint64); + function tempoPrevRandao() external view returns (bytes32); + function generalGasLimit() external view returns (uint64); + function sharedGasLimit() external view returns (uint64); + + function finalizeTempo(bytes calldata header) external; + } +} diff --git a/crates/contracts/src/precompiles/tempo_state_reader.rs b/crates/contracts/src/precompiles/tempo_state_reader.rs new file mode 100644 index 000000000..c2d944cce --- /dev/null +++ b/crates/contracts/src/precompiles/tempo_state_reader.rs @@ -0,0 +1,14 @@ +//! `TempoStateReader` — Zone L2 standalone precompile. +//! +//! Separate from [`TempoState`](crate::precompiles::tempo_state::TempoState); reads Tempo L1 +//! storage at a caller-specified block. + +crate::sol! { + #[derive(Debug)] + contract TempoStateReader { + error DelegateCallNotAllowed(); + + function readStorageAt(address account, bytes32 slot, uint64 blockNumber) external view returns (bytes32); + function readStorageBatchAt(address account, bytes32[] calldata slots, uint64 blockNumber) external view returns (bytes32[] memory); + } +} diff --git a/crates/contracts/src/precompiles/zone_factory.rs b/crates/contracts/src/precompiles/zone_factory.rs new file mode 100644 index 000000000..71264a23a --- /dev/null +++ b/crates/contracts/src/precompiles/zone_factory.rs @@ -0,0 +1,50 @@ +//! `ZoneFactory` — deployed on Tempo L1. + +pub use ZoneFactory::ZoneInfo; + +crate::sol! { + #[derive(Debug)] + contract ZoneFactory { + struct ZoneInfo { + uint32 zoneId; + address portal; + address messenger; + address initialToken; + address sequencer; + address verifier; + bytes32 genesisBlockHash; + bytes32 genesisTempoBlockHash; + uint64 genesisTempoBlockNumber; + string rpcUrl; + } + struct ZoneParams { + bytes32 genesisBlockHash; + bytes32 genesisTempoBlockHash; + uint64 genesisTempoBlockNumber; + } + struct CreateZoneParams { + address token; + address sequencer; + address verifier; + ZoneParams zoneParams; + string rpcUrl; + } + event ZoneCreated( + uint32 indexed zoneId, + address indexed portal, + address indexed messenger, + address token, + address sequencer, + address verifier, + bytes32 genesisBlockHash, + bytes32 genesisTempoBlockHash, + uint64 genesisTempoBlockNumber + ); + function createZone(CreateZoneParams calldata params) external returns (uint32 zoneId, address portal); + function verifier() external view returns (address); + function zones(uint32 zoneId) external view returns (ZoneInfo memory); + function zoneCount() external view returns (uint32); + function isZonePortal(address portal) external view returns (bool); + function isZoneMessenger(address messenger) external view returns (bool); + } +} diff --git a/crates/contracts/src/precompiles/zone_inbox.rs b/crates/contracts/src/precompiles/zone_inbox.rs new file mode 100644 index 000000000..62da0bbaa --- /dev/null +++ b/crates/contracts/src/precompiles/zone_inbox.rs @@ -0,0 +1,110 @@ +//! `ZoneInbox` — Zone L2 system contract (0x1c00...0001). + +pub use ZoneInbox::{ + ChaumPedersenProof, DecryptionData, Deposit, DepositType, EnabledToken, QueuedDeposit, +}; + +crate::sol! { + #[derive(Debug, PartialEq, Eq)] + contract ZoneInbox { + // -- Shared types -- + + struct Deposit { + address token; + address sender; + address to; + uint128 amount; + bytes32 memo; + } + + /// A TIP-20 token enabled on L1 for bridging to the zone. + struct EnabledToken { + address token; + string name; + string symbol; + string currency; + } + + /// Deposit types for the unified deposit queue. + enum DepositType { + Regular, + Encrypted, + } + + /// A queued deposit (regular or encrypted) passed to `advanceTempo`. + struct QueuedDeposit { + DepositType depositType; + bytes depositData; + } + + /// Chaum-Pedersen proof for ECDH shared secret derivation. + struct ChaumPedersenProof { + bytes32 s; + bytes32 c; + } + + /// Decryption data provided by the sequencer for encrypted deposits. + struct DecryptionData { + bytes32 sharedSecret; + uint8 sharedSecretYParity; + ChaumPedersenProof cpProof; + } + + // -- Events -- + + event TempoAdvanced( + bytes32 indexed tempoBlockHash, + uint64 indexed tempoBlockNumber, + uint256 depositsProcessed, + bytes32 newProcessedDepositQueueHash, + uint64 lastProcessedDepositNumber + ); + + event DepositProcessed( + bytes32 indexed depositHash, + address indexed sender, + address indexed to, + address token, + uint128 amount, + bytes32 memo + ); + + event EncryptedDepositProcessed( + bytes32 indexed depositHash, + address indexed sender, + address indexed to, + address token, + uint128 amount, + bytes32 memo + ); + + event EncryptedDepositFailed( + bytes32 indexed depositHash, + address indexed sender, + address token, + uint128 amount + ); + + /// Emitted when a TIP-20 token is enabled on the zone via advanceTempo. + event TokenEnabled(address indexed token, string name, string symbol, string currency); + + error OnlySequencer(); + error InvalidDepositQueueHash(); + error MissingDecryptionData(); + error ExtraDecryptionData(); + error InvalidSharedSecretProof(); + + function processedDepositQueueHash() external view returns (bytes32); + function processedDepositNumber() external view returns (uint64); + function tempoPortal() external view returns (address); + function tempoState() external view returns (address); + function config() external view returns (address); + + function advanceTempo( + bytes calldata header, + QueuedDeposit[] calldata deposits, + DecryptionData[] calldata decryptions, + EnabledToken[] calldata enabledTokens + ) external; + } +} diff --git a/crates/contracts/src/precompiles/zone_outbox.rs b/crates/contracts/src/precompiles/zone_outbox.rs new file mode 100644 index 000000000..dbb5105fb --- /dev/null +++ b/crates/contracts/src/precompiles/zone_outbox.rs @@ -0,0 +1,61 @@ +//! `ZoneOutbox` — deployed on the Zone L2. + +pub use ZoneOutbox::LastBatch; + +crate::sol! { + #[derive(Debug)] + contract ZoneOutbox { + // -- Shared types -- + + struct LastBatch { + bytes32 withdrawalQueueHash; + uint64 withdrawalBatchIndex; + } + + // -- Events -- + + event WithdrawalRequested( + uint64 indexed withdrawalIndex, + address indexed sender, + address token, + address to, + uint128 amount, + uint128 fee, + bytes32 memo, + uint64 gasLimit, + address fallbackRecipient, + bytes data, + bytes revealTo + ); + + event BatchFinalized(bytes32 indexed withdrawalQueueHash, uint64 withdrawalBatchIndex); + + // -- Errors -- + + error OnlySequencer(); + error GasLimitTooHigh(); + + // -- View functions -- + + function lastBatch() external view returns (LastBatch memory); + function withdrawalBatchIndex() external view returns (uint64); + function nextWithdrawalIndex() external view returns (uint64); + function pendingWithdrawalsCount() external view returns (uint256); + function calculateWithdrawalFee(uint64 gasLimit) external view returns (uint128 fee); + function MAX_WITHDRAWAL_GAS_LIMIT() external view returns (uint64); + + // -- State-changing functions -- + + function requestWithdrawal( + address token, + address to, + uint128 amount, + bytes32 memo, + uint64 gasLimit, + address fallbackRecipient, + bytes calldata data, + bytes calldata revealTo + ) external; + function finalizeWithdrawalBatch(uint256 count, uint64 blockNumber, bytes[] calldata encryptedSenders) external returns (bytes32 withdrawalQueueHash); + } +} diff --git a/crates/contracts/src/precompiles/zone_portal.rs b/crates/contracts/src/precompiles/zone_portal.rs new file mode 100644 index 000000000..a3b8bf98a --- /dev/null +++ b/crates/contracts/src/precompiles/zone_portal.rs @@ -0,0 +1,322 @@ +//! `ZonePortal` — deployed on Tempo L1. + +pub use ZonePortal::{ + BlockTransition, DepositQueueTransition, EncryptedDeposit, EncryptedDepositPayload, Withdrawal, +}; + +use crate::ZoneOutbox; +use alloy_primitives::{Address, B256, Bytes, keccak256}; +use alloy_sol_types::SolValue; +use zone_primitives::constants::EMPTY_SENTINEL; + +crate::sol! { + #[derive(Debug)] + contract ZonePortal { + // -- Shared types -- + + struct Withdrawal { + address token; + bytes32 senderTag; + address to; + uint128 amount; + uint128 fee; + bytes32 memo; + uint64 gasLimit; + address fallbackRecipient; + bytes callbackData; + bytes encryptedSender; + } + + /// Encrypted deposit payload (ECIES encrypted recipient and memo) + struct EncryptedDepositPayload { + bytes32 ephemeralPubkeyX; + uint8 ephemeralPubkeyYParity; + bytes ciphertext; + bytes12 nonce; + bytes16 tag; + } + + /// Encrypted deposit stored in the queue + struct EncryptedDeposit { + address token; + address sender; + uint128 amount; + uint256 keyIndex; + EncryptedDepositPayload encrypted; + } + + struct BlockTransition { + bytes32 prevBlockHash; + bytes32 nextBlockHash; + } + + struct DepositQueueTransition { + bytes32 prevProcessedHash; + bytes32 nextProcessedHash; + uint64 prevDepositNumber; + uint64 nextDepositNumber; + } + + // -- Events -- + + event DepositMade( + bytes32 indexed newCurrentDepositQueueHash, + address indexed sender, + address token, + address to, + uint128 netAmount, + uint128 fee, + bytes32 memo, + uint64 depositNumber + ); + + event EncryptedDepositMade( + bytes32 indexed newCurrentDepositQueueHash, + address indexed sender, + address token, + uint128 netAmount, + uint128 fee, + uint256 keyIndex, + bytes32 ephemeralPubkeyX, + uint8 ephemeralPubkeyYParity, + bytes ciphertext, + bytes12 nonce, + bytes16 tag, + uint64 depositNumber + ); + + /// Event emitted when a new TIP-20 token is enabled for bridging. + /// Includes token metadata so the zone can create a matching TIP-20. + event TokenEnabled(address indexed token, string name, string symbol, string currency); + + event BatchSubmitted( + uint64 indexed withdrawalBatchIndex, + bytes32 nextProcessedDepositQueueHash, + bytes32 nextBlockHash, + bytes32 withdrawalQueueHash, + uint64 lastProcessedDepositNumber + ); + + event WithdrawalProcessed(address indexed to, address token, uint128 amount, bool callbackSuccess); + + event BounceBack( + bytes32 indexed newCurrentDepositQueueHash, + address indexed fallbackRecipient, + address token, + uint128 amount, + uint64 depositNumber + ); + + event SequencerTransferStarted( + address indexed currentSequencer, + address indexed pendingSequencer + ); + + event SequencerTransferred( + address indexed previousSequencer, + address indexed newSequencer + ); + + // -- Errors -- + + error NotSequencer(); + error InvalidProof(); + error InvalidTempoBlockNumber(); + error DepositPolicyForbids(); + + // -- View functions -- + + function zoneId() external view returns (uint32); + function sequencer() external view returns (address); + function verifier() external view returns (address); + function sequencerPubkey() external view returns (bytes32); + function withdrawalBatchIndex() external view returns (uint64); + function blockHash() external view returns (bytes32); + function currentDepositQueueHash() external view returns (bytes32); + function lastSyncedTempoBlockNumber() external view returns (uint64); + function withdrawalQueueHead() external view returns (uint256); + function withdrawalQueueTail() external view returns (uint256); + function withdrawalQueueMaxSize() external view returns (uint256); + function withdrawalQueueSlot(uint256 slot) external view returns (bytes32); + function genesisTempoBlockNumber() external view returns (uint64); + function calculateDepositFee() external view returns (uint128 fee); + function depositCount() external view returns (uint64); + function lastProcessedDepositNumber() external view returns (uint64); + function MAX_WITHDRAWAL_GAS_LIMIT() external view returns (uint64); + + // -- State-changing functions -- + + function deposit(address token, address to, uint128 amount, bytes32 memo) + external + returns (bytes32 newCurrentDepositQueueHash); + + function processWithdrawal(Withdrawal calldata withdrawal, bytes32 remainingQueue) external; + + function submitBatch( + uint64 tempoBlockNumber, + uint64 recentTempoBlockNumber, + BlockTransition calldata blockTransition, + DepositQueueTransition calldata depositQueueTransition, + bytes32 withdrawalQueueHash, + bytes calldata verifierConfig, + bytes calldata proof + ) external; + + function enableToken(address token) external; + + function rpcUrl() external view returns (string memory); + function setRpcUrl(string calldata rpcUrl) external; + + function depositEncrypted( + address token, + uint128 amount, + uint256 keyIndex, + EncryptedDepositPayload calldata encrypted + ) external returns (bytes32 newCurrentDepositQueueHash); + + function setSequencerEncryptionKey( + bytes32 x, + uint8 yParity, + uint8 popV, + bytes32 popR, + bytes32 popS + ) external; + + // -- View functions (token management) -- + + function isTokenEnabled(address token) external view returns (bool); + function enabledTokenCount() external view returns (uint256); + function enabledTokenAt(uint256 index) external view returns (address); + function zoneGasRate() external view returns (uint128); + function pendingSequencer() external view returns (address); + + function sequencerEncryptionKey() external view returns (bytes32 x, uint8 yParity); + + function encryptionKeyCount() external view returns (uint256); + } +} + +#[cfg(feature = "rpc")] +impl, N: alloy_network::Network> + ZonePortal::ZonePortalInstance +{ + /// Returns all token addresses currently enabled for bridging on this [`ZonePortal`]. + /// + /// Calls [`enabledTokenCount`](ZonePortal::enabledTokenCountCall) followed by + /// [`enabledTokenAt`](ZonePortal::enabledTokenAtCall) for each index concurrently. + pub async fn enabled_tokens( + &self, + ) -> Result, alloy_contract::Error> { + let count = self.enabledTokenCount().call().await?; + let futs: alloc::vec::Vec<_> = (0..count.to::()) + .map(|i| async move { + self.enabledTokenAt(alloy_primitives::U256::from(i)) + .call() + .await + }) + .collect(); + futures::future::try_join_all(futs).await + } + + /// Fetches the active sequencer encryption key and its index. + /// + /// Returns `(key, key_index)` where `key` is the + /// [`sequencerEncryptionKeyReturn`](ZonePortal::sequencerEncryptionKeyReturn) and + /// `key_index` is the zero-based index of the current key. + pub async fn encryption_key( + &self, + ) -> Result< + ( + ZonePortal::sequencerEncryptionKeyReturn, + alloy_primitives::U256, + ), + alloy_contract::Error, + > { + let key_call = self.sequencerEncryptionKey(); + let count_call = self.encryptionKeyCount(); + let (key, count) = tokio::try_join!(key_call.call(), count_call.call())?; + let key_index = count.saturating_sub(alloy_primitives::U256::from(1)); + Ok((key, key_index)) + } +} + +impl ZonePortal::sequencerEncryptionKeyReturn { + /// Normalize `yParity` to SEC1 compressed prefix (`0x02` or `0x03`). + /// + /// The contract may return `0`/`1` (parity bit) or `0x02`/`0x03` (SEC1 prefix). + pub fn normalized_y_parity(&self) -> Option { + match self.yParity { + 0x02 | 0x03 => Some(self.yParity), + 0 | 1 => Some(0x02 + self.yParity), + _ => None, + } + } +} + +impl core::fmt::Display for ZonePortal::ZonePortalErrors { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::NotSequencer(_) => f.write_str("NotSequencer"), + Self::InvalidProof(_) => f.write_str("InvalidProof"), + Self::InvalidTempoBlockNumber(_) => f.write_str("InvalidTempoBlockNumber"), + Self::DepositPolicyForbids(_) => f.write_str("DepositPolicyForbids"), + } + } +} + +impl Withdrawal { + /// Build the authenticated-withdrawal sender plaintext `[sender(20) | tx_hash(32)]`. + pub fn authenticated_sender_plaintext(sender: Address, tx_hash: B256) -> [u8; 52] { + let mut plaintext = [0u8; 52]; + plaintext[..20].copy_from_slice(sender.as_slice()); + plaintext[20..].copy_from_slice(tx_hash.as_slice()); + plaintext + } + + /// Compute the authenticated sender tag `keccak256(sender || tx_hash)`. + pub fn sender_tag(sender: Address, tx_hash: B256) -> B256 { + keccak256(Self::authenticated_sender_plaintext(sender, tx_hash)) + } + + /// Reconstruct the public L1-facing withdrawal from a zone-side withdrawal request event. + pub fn from_requested_event( + event: &ZoneOutbox::WithdrawalRequested, + tx_hash: B256, + encrypted_sender: Bytes, + ) -> Self { + Self { + token: event.token, + senderTag: Self::sender_tag(event.sender, tx_hash), + to: event.to, + amount: event.amount, + fee: event.fee, + memo: event.memo, + gasLimit: event.gasLimit, + fallbackRecipient: event.fallbackRecipient, + callbackData: event.data.clone(), + encryptedSender: encrypted_sender, + } + } + + /// Compute the withdrawal queue hash for a slice of withdrawals. + /// + /// The hash chain has the oldest withdrawal at the outermost layer for efficient FIFO removal: + /// + /// ```text + /// hash = keccak256(encode(w[0], keccak256(encode(w[1], keccak256(encode(w[2], EMPTY_SENTINEL)))))) + /// ``` + /// + /// Building proceeds from the newest (innermost) to the oldest (outermost). + /// Returns `B256::ZERO` if `withdrawals` is empty. + pub fn queue_hash(withdrawals: &[Self]) -> B256 { + if withdrawals.is_empty() { + return B256::ZERO; + } + + let mut hash = EMPTY_SENTINEL; + for w in withdrawals.iter().rev() { + hash = keccak256((w.clone(), hash).abi_encode_params()); + } + hash + } +} diff --git a/crates/contracts/src/precompiles/zone_tx_context.rs b/crates/contracts/src/precompiles/zone_tx_context.rs new file mode 100644 index 000000000..11b1894ed --- /dev/null +++ b/crates/contracts/src/precompiles/zone_tx_context.rs @@ -0,0 +1,8 @@ +//! `ZoneTxContext` — Zone L2 precompile. + +crate::sol! { + #[derive(Debug)] + contract ZoneTxContext { + function currentTxHash() external returns (bytes32); + } +} diff --git a/crates/contracts/src/types.rs b/crates/contracts/src/types.rs deleted file mode 100644 index 0dfc2391d..000000000 --- a/crates/contracts/src/types.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Hand-written helpers for the shared ABI types. - -use crate::bindings::{Withdrawal, ZoneOutbox}; -use alloy_primitives::{Address, B256, Bytes, keccak256}; -use alloy_sol_types::SolValue; -use zone_primitives::constants::EMPTY_SENTINEL; - -impl Withdrawal { - /// Build the authenticated-withdrawal sender plaintext `[sender(20) | tx_hash(32)]`. - pub fn authenticated_sender_plaintext(sender: Address, tx_hash: B256) -> [u8; 52] { - let mut plaintext = [0u8; 52]; - plaintext[..20].copy_from_slice(sender.as_slice()); - plaintext[20..].copy_from_slice(tx_hash.as_slice()); - plaintext - } - - /// Compute the authenticated sender tag `keccak256(sender || tx_hash)`. - pub fn sender_tag(sender: Address, tx_hash: B256) -> B256 { - keccak256(Self::authenticated_sender_plaintext(sender, tx_hash)) - } - - /// Reconstruct the public L1-facing withdrawal from a zone-side withdrawal request event. - pub fn from_requested_event( - event: &ZoneOutbox::WithdrawalRequested, - tx_hash: B256, - encrypted_sender: Bytes, - ) -> Self { - Self { - token: event.token, - senderTag: Self::sender_tag(event.sender, tx_hash), - to: event.to, - amount: event.amount, - fee: event.fee, - memo: event.memo, - gasLimit: event.gasLimit, - fallbackRecipient: event.fallbackRecipient, - callbackData: event.data.clone(), - encryptedSender: encrypted_sender, - } - } - - /// Compute the withdrawal queue hash for a slice of withdrawals. - /// - /// The hash chain has the oldest withdrawal at the outermost layer for efficient FIFO removal: - /// - /// ```text - /// hash = keccak256(encode(w[0], keccak256(encode(w[1], keccak256(encode(w[2], EMPTY_SENTINEL)))))) - /// ``` - /// - /// Building proceeds from the newest (innermost) to the oldest (outermost). - /// Returns `B256::ZERO` if `withdrawals` is empty. - pub fn queue_hash(withdrawals: &[Self]) -> B256 { - if withdrawals.is_empty() { - return B256::ZERO; - } - - let mut hash = EMPTY_SENTINEL; - for w in withdrawals.iter().rev() { - hash = keccak256((w.clone(), hash).abi_encode_params()); - } - hash - } -} diff --git a/crates/contracts/src/zone_portal.rs b/crates/contracts/src/zone_portal.rs deleted file mode 100644 index dcb5b5925..000000000 --- a/crates/contracts/src/zone_portal.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Hand-written helpers for the [`ZonePortal`](crate::bindings::ZonePortal) bindings. - -use crate::bindings::ZonePortal; - -impl ZonePortal::sequencerEncryptionKeyReturn { - /// Normalize `yParity` to SEC1 compressed prefix (`0x02` or `0x03`). - /// - /// The contract may return `0`/`1` (parity bit) or `0x02`/`0x03` (SEC1 prefix). - pub fn normalized_y_parity(&self) -> Option { - match self.yParity { - 0x02 | 0x03 => Some(self.yParity), - 0 | 1 => Some(0x02 + self.yParity), - _ => None, - } - } -} - -impl core::fmt::Display for ZonePortal::ZonePortalErrors { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::NotSequencer(_) => f.write_str("NotSequencer"), - Self::InvalidProof(_) => f.write_str("InvalidProof"), - Self::InvalidTempoBlockNumber(_) => f.write_str("InvalidTempoBlockNumber"), - Self::DepositPolicyForbids(_) => f.write_str("DepositPolicyForbids"), - } - } -} - -#[cfg(feature = "rpc")] -impl, N: alloy_network::Network> - ZonePortal::ZonePortalInstance -{ - /// Returns all token addresses currently enabled for bridging on this [`ZonePortal`]. - /// - /// Calls [`enabledTokenCount`](ZonePortal::enabledTokenCountCall) followed by - /// [`enabledTokenAt`](ZonePortal::enabledTokenAtCall) for each index concurrently. - pub async fn enabled_tokens( - &self, - ) -> Result, alloy_contract::Error> { - let count = self.enabledTokenCount().call().await?; - let futs: alloc::vec::Vec<_> = (0..count.to::()) - .map(|i| async move { - self.enabledTokenAt(alloy_primitives::U256::from(i)) - .call() - .await - }) - .collect(); - futures::future::try_join_all(futs).await - } - - /// Fetches the active sequencer encryption key and its index. - /// - /// Returns `(key, key_index)` where `key` is the - /// [`sequencerEncryptionKeyReturn`](ZonePortal::sequencerEncryptionKeyReturn) and - /// `key_index` is the zero-based index of the current key. - pub async fn encryption_key( - &self, - ) -> Result< - ( - ZonePortal::sequencerEncryptionKeyReturn, - alloy_primitives::U256, - ), - alloy_contract::Error, - > { - let key_call = self.sequencerEncryptionKey(); - let count_call = self.encryptionKeyCount(); - let (key, count) = tokio::try_join!(key_call.call(), count_call.call())?; - let key_index = count.saturating_sub(alloy_primitives::U256::from(1)); - Ok((key, key_index)) - } -} From 3227dffa1731080a85aea7b25a2121f90c5a3c9d Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xKitsune@protonmail.com> Date: Fri, 19 Jun 2026 12:39:24 -0400 Subject: [PATCH 3/3] fmt: cargo fmt --- crates/precompiles/src/ztip20.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/precompiles/src/ztip20.rs b/crates/precompiles/src/ztip20.rs index b3c39bd41..76904bb95 100644 --- a/crates/precompiles/src/ztip20.rs +++ b/crates/precompiles/src/ztip20.rs @@ -23,8 +23,8 @@ use tempo_precompiles::{ storage::{StorageCtx, evm::EvmPrecompileStorageProvider}, tip20::{IRolesAuth, ITIP20, RolesAuthError, TIP20Token}, }; -use tracing::{trace, warn}; use tempo_zone_contracts::Unauthorized; +use tracing::{trace, warn}; use zone_primitives::{ constants::{ZONE_INBOX_ADDRESS, ZONE_OUTBOX_ADDRESS}, policy::AuthRole,