Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions crates/contracts/src/precompiles/zone_factory.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
//! `ZoneFactory` — deployed on Tempo L1.

use alloy_primitives::{Address, address};
use alloy_primitives::{Address, FixedBytes, address, fixed_bytes};

pub use ZoneFactory::ZoneInfo;

/// Protocol-managed ZoneFactory address defined by TIP-1091.
pub const ZONE_FACTORY_ADDRESS: Address = address!("0x5aF2000000000000000000000000000000000000");
pub const ZONE_PORTAL_PREFIX: FixedBytes<12> = fixed_bytes!("5AD000000000000000000000");
pub const ZONE_PORTAL_IMPL_ADDRESS: Address =
address!("0x5AD1000000000000000000000000000000000000");
pub const ZONE_VERIFIER_ADDRESS: Address = address!("0x5a56000000000000000000000000000000000000");
pub const ZONE_MESSENGER_ADDRESS: Address = address!("0x5A4d000000000000000000000000000000000000");
Comment on lines +9 to +13

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Will this break existing flows on Testnet before these are live?


crate::sol! {
#[derive(Debug)]
Expand All @@ -16,7 +21,6 @@ crate::sol! {
address initialToken;
address admin;
address sequencer;
address verifier;
bytes32 genesisBlockHash;
bytes32 genesisTempoBlockHash;
uint64 genesisTempoBlockNumber;
Expand All @@ -31,7 +35,6 @@ crate::sol! {
address initialToken;
address admin;
address sequencer;
address verifier;
ZoneParams zoneParams;
string rpcUrl;
}
Expand All @@ -47,10 +50,8 @@ crate::sol! {
uint64 genesisTempoBlockNumber
);
function createZone(CreateZoneParams calldata params) external returns (uint32 zoneId, address portal);
function verifier() external view returns (address);
function messenger() external view returns (address);
function zones(uint32 zoneId) external view returns (ZoneInfo memory);
function zoneCount() external view returns (uint32);
function nextZoneId() external view returns (uint32);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The existing skills in this repo still call the removed zoneCount() and verifier() methods. Can we update the skills as well?

function isZonePortal(address portal) external view returns (bool);
}
}
8 changes: 4 additions & 4 deletions crates/node/assets/zone-dev-genesis.json

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions crates/node/src/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,6 @@ pub async fn provision_zone(config: ProvisionConfig) -> eyre::Result<Provisioned
let factory_address = deploy_zone_factory(&l1_rpc_url, wallet).await?;

let factory = ZoneFactory::new(factory_address, &provider);
let verifier = factory.verifier().call().await?;

// Anchor before createZone so the L1 subscriber replays the creation block,
// including the initial TokenEnabled event emitted by the portal constructor.
let anchor_block_number = provider.get_block_number().await?;
Expand All @@ -103,7 +101,6 @@ pub async fn provision_zone(config: ProvisionConfig) -> eyre::Result<Provisioned
initialToken: initial_token,
admin: dev_address,
sequencer: dev_address,
verifier,
zoneParams: ZoneFactory::ZoneParams {
genesisBlockHash: B256::ZERO,
genesisTempoBlockHash: anchor_block_hash,
Expand Down
81 changes: 14 additions & 67 deletions crates/node/tests/it/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,94 +189,43 @@ fn forge_deployed_bytecode(contract: &str) -> eyre::Result<alloy_primitives::Byt
))
}

fn forge_deployed_bytecode_with_address_immutable(
contract: &str,
address: Address,
) -> eyre::Result<alloy_primitives::Bytes> {
let specs_dir =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../specs/ref-impls/out");
let path = specs_dir.join(format!("{contract}.sol/{contract}.json"));
let json = std::fs::read_to_string(&path).wrap_err_with(|| {
format!("{contract} artifact not found – run `forge build` in specs/ref-impls")
})?;
let artifact: serde_json::Value = serde_json::from_str(&json)?;
let mut bytecode = alloy_primitives::hex::decode(
artifact["deployedBytecode"]["object"]
.as_str()
.ok_or_else(|| eyre::eyre!("missing deployed bytecode in {contract} artifact"))?,
)?;
let references = artifact["deployedBytecode"]["immutableReferences"]
.as_object()
.ok_or_else(|| eyre::eyre!("missing immutable references in {contract} artifact"))?;
let mut patched = 0;
for reference in references.values().flat_map(|value| {
value
.as_array()
.into_iter()
.flat_map(|references| references.iter())
}) {
let start = reference["start"]
.as_u64()
.ok_or_else(|| eyre::eyre!("invalid immutable start in {contract} artifact"))?
as usize;
let length = reference["length"]
.as_u64()
.ok_or_else(|| eyre::eyre!("invalid immutable length in {contract} artifact"))?
as usize;
eyre::ensure!(length == 32, "unexpected immutable size in {contract}");
bytecode[start + 12..start + length].copy_from_slice(address.as_slice());
patched += 1;
}
eyre::ensure!(patched > 0, "no immutable references found in {contract}");
Ok(bytecode.into())
}

fn install_reference_zone_factory(genesis: &mut Genesis, owner: Address) -> eyre::Result<()> {
const VERIFIER_ADDRESS: Address = address!("0x5aF2000000000000000000000000000000000001");
const MESSENGER_ADDRESS: Address = address!("0x5aF2000000000000000000000000000000000002");
use tempo_zone_contracts::{
ZONE_MESSENGER_ADDRESS, ZONE_PORTAL_IMPL_ADDRESS, ZONE_VERIFIER_ADDRESS,
};

let one = B256::with_last_byte(1);
let mut factory_storage = BTreeMap::new();
factory_storage.insert(B256::ZERO, one);
factory_storage.insert(
keccak256((VERIFIER_ADDRESS, U256::from(3)).abi_encode()),
one,
);
factory_storage.insert(
B256::with_last_byte(4),
B256::left_padding_from(VERIFIER_ADDRESS.as_slice()),
);
factory_storage.insert(
B256::with_last_byte(5),
B256::left_padding_from(MESSENGER_ADDRESS.as_slice()),
);
factory_storage.insert(
B256::with_last_byte(6),
B256::with_last_byte(3),
B256::left_padding_from(owner.as_slice()),
);
factory_storage.insert(B256::with_last_byte(7), B256::with_last_byte(3));

genesis.alloc.insert(
ZONE_FACTORY_ADDRESS,
GenesisAccount::default()
.with_nonce(Some(3))
.with_nonce(Some(1))
.with_code(Some(forge_deployed_bytecode("ZoneFactory")?))
.with_storage(Some(factory_storage)),
);
genesis.alloc.insert(
VERIFIER_ADDRESS,
ZONE_VERIFIER_ADDRESS,
GenesisAccount::default()
.with_nonce(Some(1))
.with_code(Some(forge_deployed_bytecode("Verifier")?)),
);
genesis.alloc.insert(
MESSENGER_ADDRESS,
ZONE_PORTAL_IMPL_ADDRESS,
GenesisAccount::default()
.with_nonce(Some(1))
.with_code(Some(forge_deployed_bytecode("ZonePortal")?)),
);
genesis.alloc.insert(
ZONE_MESSENGER_ADDRESS,
GenesisAccount::default()
.with_nonce(Some(1))
.with_code(Some(forge_deployed_bytecode_with_address_immutable(
"ZoneMessenger",
ZONE_FACTORY_ADDRESS,
)?)),
.with_code(Some(forge_deployed_bytecode("ZoneMessenger")?)),
);
Ok(())
}
Expand Down Expand Up @@ -1271,13 +1220,11 @@ impl L1TestNode {
l1_header.encode(&mut rlp_buf);
let genesis_tempo_block_hash = keccak256(&rlp_buf);

let verifier_address = factory.verifier().call().await?;
let receipt = factory
.createZone(ZoneFactory::CreateZoneParams {
admin,
initialToken: PATH_USD_ADDRESS,
sequencer,
verifier: verifier_address,
zoneParams: ZoneFactory::ZoneParams {
genesisBlockHash: B256::ZERO,
genesisTempoBlockHash: genesis_tempo_block_hash,
Expand Down
46 changes: 13 additions & 33 deletions docs/ZONES.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ cargo run --release --bin tempo-zone -- dev \
--l1.rpc-url ws://127.0.0.1:8545
```

This provisions a new ZoneFactory and portal, writes the generated zone files to
This uses the protocol-managed ZoneFactory to create a portal, writes the generated zone files to
`/tmp/tempo-zone-dev`, and serves the zone HTTP RPC at `http://127.0.0.1:9545`.

Older Anvil builds are rejected because they mine Ethereum header hashes and only
Expand Down Expand Up @@ -141,7 +141,7 @@ View on explorer: `https://explore.moderato.tempo.xyz/address/<SEQUENCER_ADDR>`

### 4. Create the Zone on L1

This deploys a ZonePortal on L1, wired to the factory's shared ZoneMessenger, and generates the zone's genesis file:
This creates a ZonePortal through the protocol-managed factory, wired to the shared ZoneMessenger, and generates the zone's genesis file:

```bash
export PRIVATE_KEY="$SEQUENCER_KEY"
Expand Down Expand Up @@ -555,47 +555,27 @@ Zones inherit the Tempo L1 EVM but replace, disable, or pass through each precom
| Contract | Address |
|----------|---------|
| pathUSD (TIP-20) | `0x20C0000000000000000000000000000000000000` |
| ZoneFactory (moderato) | `0xd97052545B978cc79Dd083912C72CA62f889dFaF` |
| ZoneFactory | `0x5aF2000000000000000000000000000000000000` |
| ZonePortal implementation | `0x5AD1000000000000000000000000000000000000` |
| Zone verifier | `0x5a56000000000000000000000000000000000000` |
| ZoneMessenger | `0x5A4d000000000000000000000000000000000000` |

The xtasks use this Moderato `ZoneFactory` as their built-in default: `create-zone` and `zone-info` point at it automatically, and `deploy-router` uses `zoneFactory` from `zone.json` before falling back to this address. Pass `--zone-factory` or set `ZONE_FACTORY` to override it.

### Deploying a New ZoneFactory
### Verifying the ZoneFactory

Deploy a fresh shared factory when the Solidity `ZoneFactory`, `ZonePortal`, `ZoneMessenger`, or verifier ABI changes in a way that existing factory deployments cannot serve.
TIP-1091 makes the factory and its shared dependencies protocol-managed accounts. Verify them at their fixed addresses:

```bash
cd specs/ref-impls
export ETH_RPC_URL=https://rpc.moderato.tempo.xyz
export PRIVATE_KEY=<deployer_private_key>

forge build
forge create --broadcast --rpc-url "$ETH_RPC_URL" --private-key "$PRIVATE_KEY" src/tempo/ZoneFactory.sol:ZoneFactory
```

The `--private-key "$PRIVATE_KEY"` form is useful for controlled non-interactive deployments. For manual deployments, prefer replacing it with `--interactive` and paste the key at Foundry's prompt so the key is not written into shell history or process arguments.

After deployment, capture the `Deployed to` address and transaction hash, then verify the contract:

```bash
export ZONE_FACTORY=0x...
export ZONE_FACTORY=0x5aF2000000000000000000000000000000000000

cast code "$ZONE_FACTORY" --rpc-url "$ETH_RPC_URL"
cast call "$ZONE_FACTORY" "zoneCount()(uint32)" --rpc-url "$ETH_RPC_URL"
cast call "$ZONE_FACTORY" "verifier()(address)" --rpc-url "$ETH_RPC_URL"
cast call "$ZONE_FACTORY" "messenger()(address)" --rpc-url "$ETH_RPC_URL"
cast call "$ZONE_FACTORY" "nextZoneId()(uint32)" --rpc-url "$ETH_RPC_URL"
cast code 0x5AD1000000000000000000000000000000000000 --rpc-url "$ETH_RPC_URL"
cast code 0x5a56000000000000000000000000000000000000 --rpc-url "$ETH_RPC_URL"
cast code 0x5A4d000000000000000000000000000000000000 --rpc-url "$ETH_RPC_URL"
```

`zoneCount()` should be `0` on a fresh deployment, and `verifier()` and `messenger()` should return the contracts deployed by the factory constructor. Update `MODERATO_ZONE_FACTORY` in `xtask/src/zone_utils.rs`, the Key Addresses table above, and any other `rg` hits for the previous address.

Current deployment:

| Field | Value |
|-------|-------|
| Address | `0xd97052545B978cc79Dd083912C72CA62f889dFaF` |
| Transaction | `0xb99ae18e4223b4176fac475dfa6fdfe2c43da9e95777bd5ff0387b6b20b99b44` |
| Block | `26546762` |
| Deployed | `2026-07-14 18:26:32 UTC` |

### Zone Node CLI Options

| Flag | Default | Description |
Expand Down
28 changes: 8 additions & 20 deletions specs/ref-impls/src/interfaces/IZone.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ pragma solidity ^0.8.13;

// Protocol-managed ZoneFactory precompile defined by TIP-1091.
address constant ZONE_FACTORY_ADDRESS = 0x5aF2000000000000000000000000000000000000;
bytes12 constant ZONE_PORTAL_PREFIX = 0x5AD000000000000000000000;
address constant ZONE_PORTAL_IMPL_ADDRESS = 0x5AD1000000000000000000000000000000000000;
address constant ZONE_VERIFIER_ADDRESS = 0x5a56000000000000000000000000000000000000;
address constant ZONE_MESSENGER_ADDRESS = 0x5A4d000000000000000000000000000000000000;

/// @title IZoneToken
/// @notice Interface for the zone's zone token (TIP-20 with mint/burn for system)
Expand All @@ -27,7 +31,6 @@ struct ZoneInfo {
address initialToken; // first TIP-20 enabled at zone creation (additional tokens enabled via enableToken)
address admin;
address sequencer;
address verifier;
bytes32 genesisBlockHash;
bytes32 genesisTempoBlockHash;
uint64 genesisTempoBlockNumber;
Expand Down Expand Up @@ -420,7 +423,6 @@ interface IZoneFactory {
address initialToken; // first TIP-20 to enable (sequencer can enable more later)
address admin;
address sequencer;
address verifier;
ZoneParams zoneParams;
string rpcUrl;
}
Expand All @@ -442,7 +444,6 @@ interface IZoneFactory {
error NotOwner();
error InvalidAdmin();
error InvalidSequencer();
error InvalidVerifier();
error InsufficientGas();
error ZoneIdOverflow();

Expand All @@ -452,26 +453,16 @@ interface IZoneFactory {
/// @notice Transfers zone-creation authority to `newOwner`.
function transferOwnership(address newOwner) external;

/// @notice Returns whether a verifier contract is approved for zone creation.
/// @param verifier The verifier contract address to check.
/// @return valid True if `verifier` can be passed to `createZone`.
function isValidVerifier(address verifier) external view returns (bool);

/// @notice Returns the default verifier deployed by the factory.
/// @return verifier The default verifier contract address.
function verifier() external view returns (address);

/// @notice Creates a new zone and deploys its portal contract.
/// @param params The initial token, sequencer, verifier, and genesis parameters for the zone.
/// @param params The initial token, sequencer, and genesis parameters for the zone.
/// @return zoneId The newly assigned zone ID.
/// @return portal The deployed portal address for the new zone.
function createZone(CreateZoneParams calldata params)
external
returns (uint32 zoneId, address portal);

/// @notice Returns the number of zones created so far.
/// @return count The total number of created zones, excluding reserved zone ID 0.
function zoneCount() external view returns (uint32);
/// @notice Returns the next zone ID that will be assigned.
function nextZoneId() external view returns (uint32);

/// @notice Returns the stored metadata for a zone.
/// @param zoneId The zone ID to query.
Expand All @@ -483,10 +474,6 @@ interface IZoneFactory {
/// @return isPortal True if `portal` was created by this factory.
function isZonePortal(address portal) external view returns (bool);

/// @notice Returns the shared messenger used for withdrawal callbacks.
/// @return messenger The shared messenger contract address.
function messenger() external view returns (address);

}

/// @notice Per-token configuration in the portal's token registry
Expand Down Expand Up @@ -612,6 +599,7 @@ interface IZonePortal {
error NotAdmin();
error NotFactory();
error AlreadyInitialized();
error MustDelegateCall();
error NotPendingSequencer();
error NotPendingAdmin();
error InvalidProof();
Expand Down
3 changes: 2 additions & 1 deletion specs/ref-impls/src/tempo/SwapAndDepositRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
IWithdrawalReceiver,
IZoneFactory,
IZonePortal,
ZONE_MESSENGER_ADDRESS,
ZoneInfo
} from "../interfaces/IZone.sol";
import { IStablecoinDEX } from "tempo-std/interfaces/IStablecoinDEX.sol";
Expand Down Expand Up @@ -73,7 +74,7 @@ contract SwapAndDepositRouter is IWithdrawalReceiver {
external
returns (bytes4)
{
if (msg.sender != zoneFactory.messenger()) {
if (msg.sender != ZONE_MESSENGER_ADDRESS) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same thing here, this will break if we redeploy contracts on testnet until the precompiles are live. Can we update to remove this (and related changes) since we will need to redeploy on testnet before the precompiles are live?

revert UnauthorizedMessenger();
}

Expand Down
Loading
Loading