mega-system-contracts 1.7.0

System contracts for the MegaETH EVM
Documentation
//! Build script that validates system contract bytecode.
//!
//! When building from the repository (detected by the presence of `scripts/`):
//! 1. Foundry (`forge`) is **required** — the build will fail if it is not installed.
//! 2. Compiles the Solidity contracts using Foundry and validates that the compiled bytecode
//!    matches `*-latest.json`.
//!
//! When building from a published crate (`scripts/` is excluded from the package):
//! - Foundry is not required.
//!
//! In both cases:
//! 3. Regenerates Rust constants from the artifact JSON files and verifies they match the
//!    pre-generated files in `src/generated/`.
//!
//! The pre-generated Rust constants in `src/generated/` are always used directly by `lib.rs`.

use std::{
    env,
    fmt::Write,
    fs,
    path::Path,
    process::{Command, Stdio},
};

use alloy_primitives::{hex, keccak256, Bytes, B256};
use semver::Version;
use serde::Deserialize;

/// Artifact format for system contract JSON files
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ContractArtifact {
    version: Version,
    code_hash: B256,
    deployed_bytecode: Bytes,
}

/// Configuration for a system contract to be validated
struct ContractConfig<'a> {
    /// Contract name (e.g., "Oracle")
    name: &'a str,
    /// Forge script path (e.g., "scripts/OracleBytecode.s.sol:SaveOracleBytecode")
    script_path: &'a str,
    /// Pre-generated Rust file name (e.g., `oracle_artifacts.rs`)
    generated_file: &'a str,
}

/// Runs a forge script and validates bytecode against expected artifact.
fn validate_contract_bytecode(crate_dir: &Path, config: &ContractConfig<'_>) {
    // Run the deploy script to generate bytecode with constructor args embedded
    let script_status = Command::new("forge")
        .args(["script", config.script_path, "--sig", "run()"])
        .current_dir(crate_dir)
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status()
        .unwrap_or_else(|_| panic!("Failed to execute {} forge script", config.name));

    assert!(script_status.success(), "{} forge script failed", config.name);

    // Read the generated artifact
    let generated_path = crate_dir.join(format!("artifacts/{}.json", config.name));
    let generated_content = fs::read_to_string(&generated_path)
        .unwrap_or_else(|_| panic!("Failed to read {} generated artifact", config.name));
    let generated: ContractArtifact = serde_json::from_str(&generated_content)
        .unwrap_or_else(|_| panic!("Failed to parse {} generated artifact", config.name));

    // Read the expected artifact
    let expected_path = crate_dir.join(format!("artifacts/{}-latest.json", config.name));
    let expected_content = fs::read_to_string(&expected_path)
        .unwrap_or_else(|_| panic!("Failed to read {}-latest.json", config.name));
    let expected: ContractArtifact = serde_json::from_str(&expected_content)
        .unwrap_or_else(|_| panic!("Failed to parse {}-latest.json", config.name));

    // Compare code hash
    assert!(
        generated.code_hash == expected.code_hash,
        r#"
ERROR: {name} contract bytecode mismatch!

The compiled {name}.sol bytecode does not match artifacts/{name}-latest.json.

If this change is intentional (new spec version):
  1. Create a new artifacts/{name}-X.Y.Z.json file
  2. Update {name}-latest.json symlink
  3. Commit all changes together

If this change is accidental:
  Revert your changes to contracts/{name}.sol

Expected:  {expected:x}
Generated: {generated:x}
"#,
        expected = expected.code_hash,
        generated = generated.code_hash,
        name = config.name,
    );

    // Clean up generated artifact
    let _ = fs::remove_file(&generated_path);
}

/// Collects all versioned artifacts for a contract, validates code hashes, and returns sorted list.
fn collect_versioned_artifacts(artifacts_dir: &Path, prefix: &str) -> Vec<ContractArtifact> {
    let mut versions = Vec::new();

    for entry in fs::read_dir(artifacts_dir).expect("Failed to read artifacts directory") {
        let entry = entry.expect("Failed to read directory entry");
        let path = entry.path();
        let filename = path.file_name().unwrap().to_str().unwrap();

        // Skip non-versioned files and *-latest.json aliases (symlinks in repo, regular files
        // in packaged crates).
        if !filename.starts_with(prefix) ||
            !filename.ends_with(".json") ||
            filename.contains("-latest")
        {
            continue;
        }

        let content = fs::read_to_string(&path).expect("Failed to read artifact");
        let artifact: ContractArtifact =
            serde_json::from_str(&content).expect("Failed to parse artifact");

        let computed_hash = keccak256(&artifact.deployed_bytecode);
        assert!(
            computed_hash == artifact.code_hash,
            "Code hash mismatch for artifact {}: expected {:x}, got {:x}",
            filename,
            artifact.code_hash,
            computed_hash
        );

        versions.push(artifact);
    }

    // Sort by semantic version
    versions.sort_by_key(|a| a.version.clone());

    versions
}

/// Attests the `*-latest.json` alias for a contract.
///
/// The versioned artifacts are self-hash-validated when collected, but the `*-latest.json` alias
/// is a separate file (a symlink in the repo, a regular copy in packaged crates). This verifies
/// that the alias:
/// 1. is internally consistent — `keccak256(deployed_bytecode) == code_hash`, and
/// 2. matches one of the already-attested versioned artifacts on BOTH `version` and `code_hash`.
fn attest_latest_alias(name: &str, latest: &ContractArtifact, versions: &[ContractArtifact]) {
    // (a) Self-hash: the alias's own bytecode must match its own recorded code hash.
    let computed_hash = keccak256(&latest.deployed_bytecode);
    assert!(
        computed_hash == latest.code_hash,
        "Code hash mismatch for {name}-latest.json: recorded {:x}, computed {:x}",
        latest.code_hash,
        computed_hash
    );

    // (b) Cross-check on BOTH version and code hash. Matching only the hash is insufficient:
    // `generate_rust_constants` derives `LATEST_CODE = V{latest.version}_CODE` from the `version`
    // field, so a latest.json whose bytecode/hash is e.g. v2.0.0 but whose `version` field says
    // "1.1.0" would pass a hash-only check yet alias the wrong versioned constant. Binding
    // (version, code_hash) keeps the generated alias consistent with the attested bytecode.
    assert!(
        versions.iter().any(|v| v.version == latest.version && v.code_hash == latest.code_hash),
        "{name}-latest.json (version {}, code hash {:x}) does not match any attested versioned \
         artifact on both version and code hash",
        latest.version,
        latest.code_hash
    );
}

/// Generates Rust source content with bytecode constants for a contract.
fn generate_rust_constants(
    config: &ContractConfig<'_>,
    versions: &[ContractArtifact],
    latest: &ContractArtifact,
) -> String {
    let mut content = String::new();

    writeln!(content, "// Pre-generated {} contract bytecode constants.", config.name).unwrap();
    writeln!(
        content,
        "// DO NOT EDIT MANUALLY - updated automatically by build.rs from artifacts/"
    )
    .unwrap();
    writeln!(content).unwrap();
    writeln!(content, "use alloy_primitives::{{bytes, b256, Bytes, B256}};").unwrap();
    writeln!(content).unwrap();

    for artifact in versions {
        let version_underscore = artifact.version.to_string().replace('.', "_");
        let const_name = format!("V{}", version_underscore);

        writeln!(content, "/// `{}` contract bytecode v{}", config.name, artifact.version).unwrap();
        writeln!(
            content,
            "pub const {}_CODE: Bytes = bytes!(\"{}\");",
            const_name,
            hex::encode(&artifact.deployed_bytecode)
        )
        .unwrap();
        writeln!(content, "/// `{}` contract code hash v{}", config.name, artifact.version)
            .unwrap();
        writeln!(
            content,
            "pub const {}_CODE_HASH: B256 = b256!(\"{}\");",
            const_name,
            hex::encode(artifact.code_hash)
        )
        .unwrap();
        writeln!(content).unwrap();
    }

    // Add latest alias
    let latest_version_underscore = latest.version.to_string().replace('.', "_");
    writeln!(content, "/// Latest `{}` contract bytecode", config.name).unwrap();
    writeln!(content, "pub const LATEST_CODE: Bytes = V{}_CODE;", latest_version_underscore)
        .unwrap();
    writeln!(content, "/// Latest `{}` contract code hash", config.name).unwrap();
    writeln!(
        content,
        "pub const LATEST_CODE_HASH: B256 = V{}_CODE_HASH;",
        latest_version_underscore
    )
    .unwrap();

    content
}

/// Verifies that the pre-generated Rust constants in `src/generated/` match what would be
/// generated from the current artifact JSON files. If they are out of sync, writes the updated
/// content and panics to prompt the developer to commit the change.
fn verify_generated_constants(
    crate_dir: &Path,
    config: &ContractConfig<'_>,
    versions: &[ContractArtifact],
    latest: &ContractArtifact,
) {
    let expected = generate_rust_constants(config, versions, latest);
    let generated_path = crate_dir.join("src/generated").join(config.generated_file);

    let actual = fs::read_to_string(&generated_path).unwrap_or_default();

    if expected != actual {
        // Write the correct content so the developer only needs to commit it.
        let generated_dir = crate_dir.join("src/generated");
        fs::create_dir_all(&generated_dir).expect("Failed to create src/generated/ directory");
        fs::write(&generated_path, &expected)
            .unwrap_or_else(|e| panic!("Failed to write {}: {e}", generated_path.display()));

        panic!(
            "\n\
             ERROR: Pre-generated constants in src/generated/{file} were out of sync \
             with artifacts!\n\n\
             The file has been automatically updated. Please review and commit the change:\n  \
             git add crates/system-contracts/src/generated/{file}\n",
            file = config.generated_file,
        );
    }
}

fn main() {
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    let crate_dir = Path::new(&manifest_dir);

    // Define contract configurations
    let contracts = [
        ContractConfig {
            name: "Oracle",
            script_path: "scripts/OracleBytecode.s.sol:SaveOracleBytecode",
            generated_file: "oracle_artifacts.rs",
        },
        ContractConfig {
            name: "KeylessDeploy",
            script_path: "scripts/KeylessDeployBytecode.s.sol:SaveKeylessDeployBytecode",
            generated_file: "keyless_deploy_artifacts.rs",
        },
        ContractConfig {
            name: "MegaAccessControl",
            script_path: "scripts/MegaAccessControlBytecode.s.sol:SaveMegaAccessControlBytecode",
            generated_file: "access_control_artifacts.rs",
        },
        ContractConfig {
            name: "MegaLimitControl",
            script_path: "scripts/MegaLimitControlBytecode.s.sol:SaveMegaLimitControlBytecode",
            generated_file: "limit_control_artifacts.rs",
        },
        ContractConfig {
            name: "SequencerRegistry",
            script_path: "scripts/SequencerRegistryBytecode.s.sol:SaveSequencerRegistryBytecode",
            generated_file: "sequencer_registry_artifacts.rs",
        },
    ];

    // Set up rerun-if-changed triggers
    for config in &contracts {
        println!(
            "cargo::rerun-if-changed={}",
            crate_dir.join(format!("contracts/{}.sol", config.name)).display()
        );
        println!(
            "cargo::rerun-if-changed={}",
            crate_dir.join(format!("artifacts/{}-latest.json", config.name)).display()
        );
        println!(
            "cargo::rerun-if-changed={}",
            crate_dir.join("src/generated").join(config.generated_file).display()
        );
    }
    println!("cargo::rerun-if-changed={}", crate_dir.join("foundry.toml").display());

    let artifacts_dir = crate_dir.join("artifacts");

    // Detect whether we are building from the repository (scripts/ directory present) or from a
    // published crate (scripts/ excluded by Cargo.toml `exclude`).
    let is_repo_build = crate_dir.join("scripts").exists();

    // Phase 1: Forge validation — required when building from the repository.
    if !is_repo_build {
        // Published crate: `scripts/`, Solidity source, and `artifacts/` are excluded from the
        // package, so the bytecode cannot be re-derived from source here. Make this explicit
        // instead of silently trusting the checked-in constants. The `cargo test` self-consistency
        // check (tests/generated_self_consistency.rs) still runs in this context.
        println!(
            "cargo::warning=system-contract bytecode source-attestation was SKIPPED: forge / \
             Solidity source / scripts/ are unavailable (published crate). The build is trusting \
             the pre-generated constants in src/generated/ without re-deriving them from source."
        );
    }

    if is_repo_build {
        let forge_available = Command::new("forge")
            .arg("--version")
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status();

        match forge_available {
            Ok(status) if status.success() => {}
            _ => {
                panic!(
                    r#"
ERROR: `forge` command not found

Foundry is required to build system-contracts from the repository.
Install it from: https://getfoundry.sh

Quick install:
  curl -L https://foundry.paradigm.xyz | bash
  foundryup
"#
                );
            }
        }

        for config in &contracts {
            validate_contract_bytecode(crate_dir, config);
        }
    }

    // Phase 2: Verify pre-generated constants match artifacts (always runs if artifacts exist).
    if artifacts_dir.exists() {
        for config in &contracts {
            let prefix = format!("{}-", config.name);
            let versions = collect_versioned_artifacts(&artifacts_dir, &prefix);

            // Determine the latest version from the *-latest.json file.
            let latest_path = artifacts_dir.join(format!("{}-latest.json", config.name));
            let latest_content = fs::read_to_string(&latest_path)
                .unwrap_or_else(|_| panic!("Failed to read {}-latest.json", config.name));
            let latest: ContractArtifact = serde_json::from_str(&latest_content)
                .unwrap_or_else(|_| panic!("Failed to parse {}-latest.json", config.name));

            // Attest the latest alias against the self-validated versioned artifacts before
            // trusting it as the source for the generated constants.
            attest_latest_alias(config.name, &latest, &versions);

            verify_generated_constants(crate_dir, config, &versions, &latest);
        }
    }
}