Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 11 additions & 10 deletions bin/katana/src/cli/init/deployment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ use katana_chain_spec::settlement_check::{
use katana_chain_spec::tee::compute_katana_tee_config_hash;
use katana_chain_spec::SettlementProofKind;
use katana_contracts::piltover::Appchain;
use katana_genesis::constant::DEFAULT_STRK_FEE_TOKEN_ADDRESS;
use katana_primitives::block::{BlockHash, BlockIdOrTag, BlockNumber};
use katana_primitives::class::ContractClass;
use katana_primitives::version::StarknetVersion;
use katana_primitives::{felt, ContractAddress, Felt};
use katana_primitives::{ContractAddress, Felt};
use katana_rpc_types::block::GetBlockWithTxHashesResponse;
use katana_rpc_types::class::Class;
use katana_starknet::rpc::StarknetRpcClient as StarknetClient;
Expand All @@ -33,15 +34,11 @@ use tracing::trace;

type SettlementInitializerAccount = SingleOwnerAccount<SettlementChainProvider, LocalWallet>;

/// Default STRK fee-token address for chains generated by `katana init`.
///
/// Both the StarknetOS config-hash (`StarknetOsConfig3`-tagged) and the Katana
/// TEE config-hash (`KatanaTeeConfig1`-tagged) bind this address, so the
/// deployment-time hash and the runtime hash agree.
///
/// TODO: thread the fee token through the rollup config rather than hard-coding it here.
const DEFAULT_FEE_TOKEN_ADDRESS: Felt =
felt!("0x2e7442625bab778683501c0eadbc1ea17b3535da040a12ac7d281066e915eea");
/// STRK fee-token address bound into the SNOS and Katana-TEE config hashes committed to the
/// settlement contract. Must match the address the chain's executor uses for fee charging — for
/// `katana init`-generated chains that's [`DEFAULT_STRK_FEE_TOKEN_ADDRESS`] (pre-allocated by
/// [`katana_chain_spec::rollup::ChainSpec::state_updates`]).
const DEFAULT_FEE_TOKEN_ADDRESS: Felt = DEFAULT_STRK_FEE_TOKEN_ADDRESS.0;

#[derive(Debug)]
pub struct DeploymentOutcome {
Expand Down Expand Up @@ -148,6 +145,10 @@ pub async fn deploy_settlement_contract(
let salt = Felt::from(rand::random::<u64>());
let factory = ContractFactory::new_with_udc(class_hash, &account, UdcSelector::Legacy);

// Left at zero for now — the rollup chain spec pre-allocates the STRK fee token before
// executing the genesis block, so the actual post-genesis state root is non-zero. Updating
// this to publish the real genesis state root is a follow-up that needs SNOS to accept a
// non-empty starting state.
const INITIAL_STATE_ROOT: Felt = Felt::ZERO;
/// When updating the piltover contract with the genesis block (ie block number 0), in the
/// attached StarknetOsOutput, the [previous block number] is expected to be
Expand Down
21 changes: 12 additions & 9 deletions bin/katana/src/cli/init/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ use std::str::FromStr;
use anyhow::Context;
use clap::{Args, Subcommand};
use deployment::DeploymentOutcome;
use katana_chain_spec::rollup::{ChainConfigDir, DEFAULT_APPCHAIN_FEE_TOKEN_ADDRESS};
use katana_chain_spec::rollup::ChainConfigDir;
use katana_chain_spec::settlement_check::SettlementChainProvider;
use katana_chain_spec::{rollup, FeeContracts, SettlementLayer, SettlementProofKind};
use katana_cli::utils::ShortStringValueParser;
use katana_contracts::piltover::Appchain;
use katana_genesis::allocation::DevAllocationsGenerator;
use katana_genesis::constant::DEFAULT_PREFUNDED_ACCOUNT_BALANCE;
use katana_genesis::constant::{DEFAULT_PREFUNDED_ACCOUNT_BALANCE, DEFAULT_STRK_FEE_TOKEN_ADDRESS};
use katana_genesis::Genesis;
use katana_primitives::block::BlockNumber;
use katana_primitives::cairo::ShortString;
Expand Down Expand Up @@ -266,10 +266,12 @@ impl RollupArgs {
#[cfg(feature = "init-slot")]
slot::add_paymasters_to_genesis(&mut genesis, &output.slot_paymasters.unwrap_or_default());

// At the moment, the fee token is limited to a predefined token.
// STRK is pre-allocated by rollup::ChainSpec::state_updates at the canonical Starknet
// mainnet address. ETH mirrors STRK on rollup — the on-disk config keeps only one address
// (see FileFeeContract).
let fee_contracts = FeeContracts {
eth: DEFAULT_APPCHAIN_FEE_TOKEN_ADDRESS,
strk: DEFAULT_APPCHAIN_FEE_TOKEN_ADDRESS,
eth: DEFAULT_STRK_FEE_TOKEN_ADDRESS,
strk: DEFAULT_STRK_FEE_TOKEN_ADDRESS,
};

let chain_spec = rollup::ChainSpec { id, genesis, settlement, fee_contracts };
Expand Down Expand Up @@ -479,11 +481,12 @@ impl SovereignArgs {
#[cfg(feature = "init-slot")]
slot::add_paymasters_to_genesis(&mut genesis, &output.slot_paymasters.unwrap_or_default());

// At the moment, the fee token is limited to a predefined token.
// At the moment, the fee token is limited to a predefined token.
// STRK is pre-allocated by rollup::ChainSpec::state_updates at the canonical Starknet
// mainnet address. ETH mirrors STRK on rollup — the on-disk config keeps only one address
// (see FileFeeContract).
let fee_contracts = FeeContracts {
eth: DEFAULT_APPCHAIN_FEE_TOKEN_ADDRESS,
strk: DEFAULT_APPCHAIN_FEE_TOKEN_ADDRESS,
eth: DEFAULT_STRK_FEE_TOKEN_ADDRESS,
strk: DEFAULT_STRK_FEE_TOKEN_ADDRESS,
};

let chain_spec = rollup::ChainSpec { id, genesis, settlement, fee_contracts };
Expand Down
85 changes: 19 additions & 66 deletions crates/chain-spec/src/dev.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
use std::collections::BTreeMap;
use std::str::FromStr;

use alloy_primitives::U256;
use katana_contracts::contracts;
use katana_genesis::allocation::{DevAllocationsGenerator, GenesisAllocation};
use katana_genesis::allocation::DevAllocationsGenerator;
use katana_genesis::constant::{
get_fee_token_balance_base_storage_address, DEFAULT_ACCOUNT_CLASS_PUBKEY_STORAGE_SLOT,
DEFAULT_ETH_FEE_TOKEN_ADDRESS, DEFAULT_FROZEN_DEV_ACCOUNT_ADDRESS_CLASS_HASH,
DEFAULT_LEGACY_UDC_ADDRESS, DEFAULT_PREFUNDED_ACCOUNT_BALANCE, DEFAULT_STRK_FEE_TOKEN_ADDRESS,
DEFAULT_UDC_ADDRESS, ERC20_DECIMAL_STORAGE_SLOT, ERC20_NAME_STORAGE_SLOT,
ERC20_SYMBOL_STORAGE_SLOT, ERC20_TOTAL_SUPPLY_STORAGE_SLOT,
DEFAULT_ACCOUNT_CLASS_PUBKEY_STORAGE_SLOT, DEFAULT_ETH_FEE_TOKEN_ADDRESS,
DEFAULT_FROZEN_DEV_ACCOUNT_ADDRESS_CLASS_HASH, DEFAULT_LEGACY_UDC_ADDRESS,
DEFAULT_PREFUNDED_ACCOUNT_BALANCE, DEFAULT_STRK_FEE_TOKEN_ADDRESS, DEFAULT_UDC_ADDRESS,
};
use katana_genesis::Genesis;
use katana_primitives::block::{ExecutableBlock, GasPrices, PartialHeader};
use katana_primitives::cairo::ShortString;
use katana_primitives::chain::ChainId;
use katana_primitives::class::ClassHash;
use katana_primitives::contract::ContractAddress;
use katana_primitives::da::L1DataAvailabilityMode;
use katana_primitives::state::StateUpdatesWithClasses;
use katana_primitives::utils::split_u256;
use katana_primitives::version::CURRENT_STARKNET_VERSION;
use katana_primitives::Felt;
use lazy_static::lazy_static;

use crate::fee_token::add_fee_token;
use crate::{FeeContracts, SettlementLayer};

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -160,6 +151,7 @@ fn add_default_fee_tokens(states: &mut StateUpdatesWithClasses, genesis: &Genesi
DEFAULT_ETH_FEE_TOKEN_ADDRESS,
contracts::LegacyERC20::HASH,
&genesis.allocations,
&[],
);

// -- STRK
Expand All @@ -171,58 +163,10 @@ fn add_default_fee_tokens(states: &mut StateUpdatesWithClasses, genesis: &Genesi
DEFAULT_STRK_FEE_TOKEN_ADDRESS,
contracts::LegacyERC20::HASH,
&genesis.allocations,
&[],
);
}

fn add_fee_token(
states: &mut StateUpdatesWithClasses,
name: &str,
symbol: &str,
decimals: u8,
address: ContractAddress,
class_hash: ClassHash,
allocations: &BTreeMap<ContractAddress, GenesisAllocation>,
) {
let mut storage = BTreeMap::new();
let mut total_supply = U256::ZERO;

// --- set the ERC20 balances for each allocations that have a balance

for (address, alloc) in allocations {
if let Some(balance) = alloc.balance() {
total_supply += balance;
let (low, high) = split_u256(balance);

// the base storage address for a standard ERC20 contract balance
let bal_base_storage_var = get_fee_token_balance_base_storage_address(*address);

// the storage address of low u128 of the balance
let low_bal_storage_var = bal_base_storage_var;
// the storage address of high u128 of the balance
let high_bal_storage_var = bal_base_storage_var + Felt::ONE;

storage.insert(low_bal_storage_var, low);
storage.insert(high_bal_storage_var, high);
}
}

// --- ERC20 metadata

let name = ShortString::from_str(name).expect("valid ERC20 name");
let symbol = ShortString::from_str(symbol).expect("valid ERC20 symbol");
let decimals = decimals.into();
let (total_supply_low, total_supply_high) = split_u256(total_supply);

storage.insert(ERC20_NAME_STORAGE_SLOT, name.into());
storage.insert(ERC20_SYMBOL_STORAGE_SLOT, symbol.into());
storage.insert(ERC20_DECIMAL_STORAGE_SLOT, decimals);
storage.insert(ERC20_TOTAL_SUPPLY_STORAGE_SLOT, total_supply_low);
storage.insert(ERC20_TOTAL_SUPPLY_STORAGE_SLOT + Felt::ONE, total_supply_high);

states.state_updates.deployed_contracts.insert(address, class_hash);
states.state_updates.storage_updates.insert(address, storage);
}

fn add_default_udc(states: &mut StateUpdatesWithClasses) {
// Default UDC: the current OpenZeppelin Sierra class on Starknet mainnet/sepolia.
states
Expand Down Expand Up @@ -261,14 +205,23 @@ fn add_default_udc(states: &mut StateUpdatesWithClasses) {
#[cfg(test)]
mod tests {

use std::collections::BTreeMap;
use std::str::FromStr;

use alloy_primitives::U256;
use katana_genesis::allocation::{GenesisAccount, GenesisAccountAlloc, GenesisContractAlloc};
use katana_genesis::constant::DEFAULT_ACCOUNT_CLASS_PUBKEY_STORAGE_SLOT;
use katana_primitives::address;
use katana_genesis::allocation::{
GenesisAccount, GenesisAccountAlloc, GenesisAllocation, GenesisContractAlloc,
};
use katana_genesis::constant::{
get_fee_token_balance_base_storage_address, DEFAULT_ACCOUNT_CLASS_PUBKEY_STORAGE_SLOT,
ERC20_DECIMAL_STORAGE_SLOT, ERC20_NAME_STORAGE_SLOT, ERC20_SYMBOL_STORAGE_SLOT,
ERC20_TOTAL_SUPPLY_STORAGE_SLOT,
};
use katana_primitives::block::GasPrices;
use katana_primitives::cairo::ShortString;
use katana_primitives::da::L1DataAvailabilityMode;
use katana_primitives::utils::split_u256;
use katana_primitives::{address, Felt};
use starknet::macros::felt;

use super::*;
Expand Down
96 changes: 96 additions & 0 deletions crates/chain-spec/src/fee_token.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//! Helpers for pre-allocating an ERC20 fee-token contract directly into genesis state.
//!
//! Bypasses on-chain UDC deployment so the contract can land at an arbitrary address (e.g. the
//! canonical Starknet mainnet STRK address), which is impossible with UDC alone because the
//! deployed address is mathematically derived from `(class_hash, salt, ctor_args, deployer)`.
//!
//! Used by both [`crate::dev::ChainSpec::state_updates`] (for ETH + STRK fee tokens) and
//! [`crate::rollup::ChainSpec::state_updates`] (for the STRK fee token).

use std::collections::BTreeMap;
use std::str::FromStr;

use alloy_primitives::U256;
use katana_genesis::allocation::GenesisAllocation;
use katana_genesis::constant::{
get_fee_token_balance_base_storage_address, ERC20_DECIMAL_STORAGE_SLOT,
ERC20_NAME_STORAGE_SLOT, ERC20_SYMBOL_STORAGE_SLOT, ERC20_TOTAL_SUPPLY_STORAGE_SLOT,
};
use katana_primitives::cairo::ShortString;
use katana_primitives::class::ClassHash;
use katana_primitives::contract::ContractAddress;
use katana_primitives::state::StateUpdatesWithClasses;
use katana_primitives::utils::split_u256;
use katana_primitives::Felt;

/// Writes a fee-token ERC20 contract into `states` at `address`.
///
/// Seeds balance storage for every entry in `allocations` that carries a balance, plus any extra
/// `(address, balance)` pairs in `extra_balances` — used by the rollup builder to credit its
/// transaction-genesis master account, which is not a genesis allocation.
///
/// The caller is responsible for inserting the contract class into `states.classes` and into the
/// appropriate declared-classes set; this helper only handles per-token deployment + storage.
#[allow(clippy::too_many_arguments)] // builder-style genesis helper; each arg is a distinct token field
pub(crate) fn add_fee_token(
states: &mut StateUpdatesWithClasses,
name: &str,
symbol: &str,
decimals: u8,
address: ContractAddress,
class_hash: ClassHash,
allocations: &BTreeMap<ContractAddress, GenesisAllocation>,
extra_balances: &[(ContractAddress, U256)],
) {
let mut storage = BTreeMap::new();
let mut total_supply = U256::ZERO;

// --- set the ERC20 balances for each allocations that have a balance

for (address, alloc) in allocations {
if let Some(balance) = alloc.balance() {
total_supply += balance;
write_balance(&mut storage, *address, balance);
}
}

for (address, balance) in extra_balances {
total_supply += *balance;
write_balance(&mut storage, *address, *balance);
}

// --- ERC20 metadata

let name = ShortString::from_str(name).expect("valid ERC20 name");
let symbol = ShortString::from_str(symbol).expect("valid ERC20 symbol");
let decimals = decimals.into();
let (total_supply_low, total_supply_high) = split_u256(total_supply);

storage.insert(ERC20_NAME_STORAGE_SLOT, name.into());
storage.insert(ERC20_SYMBOL_STORAGE_SLOT, symbol.into());
storage.insert(ERC20_DECIMAL_STORAGE_SLOT, decimals);
storage.insert(ERC20_TOTAL_SUPPLY_STORAGE_SLOT, total_supply_low);
storage.insert(ERC20_TOTAL_SUPPLY_STORAGE_SLOT + Felt::ONE, total_supply_high);

states.state_updates.deployed_contracts.insert(address, class_hash);
states.state_updates.storage_updates.insert(address, storage);
}

fn write_balance(
storage: &mut BTreeMap<katana_primitives::contract::StorageKey, Felt>,
holder: ContractAddress,
balance: U256,
) {
let (low, high) = split_u256(balance);

// the base storage address for a standard ERC20 contract balance
let bal_base_storage_var = get_fee_token_balance_base_storage_address(holder);

// the storage address of low u128 of the balance
let low_bal_storage_var = bal_base_storage_var;
// the storage address of high u128 of the balance
let high_bal_storage_var = bal_base_storage_var + Felt::ONE;

storage.insert(low_bal_storage_var, low);
storage.insert(high_bal_storage_var, high);
}
1 change: 1 addition & 0 deletions crates/chain-spec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
use url::Url;

pub mod dev;
mod fee_token;
pub mod full_node;
pub mod rollup;
pub mod settlement_check;
Expand Down
47 changes: 46 additions & 1 deletion crates/chain-spec/src/rollup/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
use katana_contracts::contracts;
use katana_genesis::constant::DEFAULT_STRK_FEE_TOKEN_ADDRESS;
use katana_genesis::Genesis;
use katana_primitives::block::{ExecutableBlock, GasPrices, PartialHeader};
use katana_primitives::chain::ChainId;
use katana_primitives::da::L1DataAvailabilityMode;
use katana_primitives::state::StateUpdatesWithClasses;
use katana_primitives::version::CURRENT_STARKNET_VERSION;

pub mod file;
pub mod utils;

pub use file::*;
pub use utils::DEFAULT_APPCHAIN_FEE_TOKEN_ADDRESS;

use crate::fee_token::add_fee_token;
use crate::{FeeContracts, SettlementLayer};

/// The rollup chain specification.
Expand Down Expand Up @@ -50,4 +53,46 @@ impl ChainSpec {

ExecutableBlock { header, body: transactions }
}

/// Pre-allocated genesis state applied before the genesis block executes.
///
/// Currently this holds the STRK fee token: declared and deployed at the canonical Starknet
/// mainnet address (`DEFAULT_STRK_FEE_TOKEN_ADDRESS`) with the full initial supply credited to
/// the genesis master account. This bypasses UDC because UDC-derived addresses can't land at
/// the canonical mainnet address.
///
/// The executor must see this state when processing the genesis transactions (the
/// `transfer_balance` invokes from [`utils::GenesisTransactionsBuilder`] target this exact
/// contract). Callers that drive genesis execution should overlay these state updates onto an
/// empty state provider before running the block.
pub fn state_updates(&self) -> StateUpdatesWithClasses {
let mut states = StateUpdatesWithClasses::default();

// Declare the legacy ERC20 class used by the fee token. It would otherwise be declared by
// a genesis transaction; the pre-allocation pulls it forward into initial state.
states
.classes
.entry(contracts::LegacyERC20::HASH)
.or_insert_with(|| contracts::LegacyERC20::CLASS.clone());
states.state_updates.deprecated_declared_classes.insert(contracts::LegacyERC20::HASH);

// The genesis master account starts with the full ERC20 supply (matches the constructor
// mint that the old UDC-deploy used). `GenesisTransactionsBuilder` then drains this into
// the dev accounts via `transfer` invokes during block execution.
let master = utils::master_account_address();
let extra_balances = [(master, utils::ROLLUP_FEE_TOKEN_INITIAL_SUPPLY)];

add_fee_token(
&mut states,
"Starknet Token",
"STRK",
18,
DEFAULT_STRK_FEE_TOKEN_ADDRESS,
contracts::LegacyERC20::HASH,
&self.genesis.allocations,
&extra_balances,
);

states
}
}
Loading
Loading