use std::{
env,
fmt::Write,
fs,
path::Path,
process::{Command, Stdio},
};
use alloy_primitives::{hex, keccak256, Bytes, B256};
use semver::Version;
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ContractArtifact {
version: Version,
code_hash: B256,
deployed_bytecode: Bytes,
}
struct ContractConfig<'a> {
name: &'a str,
script_path: &'a str,
generated_file: &'a str,
}
fn validate_contract_bytecode(crate_dir: &Path, config: &ContractConfig<'_>) {
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);
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));
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));
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,
);
let _ = fs::remove_file(&generated_path);
}
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();
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);
}
versions.sort_by_key(|a| a.version.clone());
versions
}
fn attest_latest_alias(name: &str, latest: &ContractArtifact, versions: &[ContractArtifact]) {
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
);
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
);
}
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();
}
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
}
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 {
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);
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",
},
];
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");
let is_repo_build = crate_dir.join("scripts").exists();
if !is_repo_build {
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);
}
}
if artifacts_dir.exists() {
for config in &contracts {
let prefix = format!("{}-", config.name);
let versions = collect_versioned_artifacts(&artifacts_dir, &prefix);
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_latest_alias(config.name, &latest, &versions);
verify_generated_constants(crate_dir, config, &versions, &latest);
}
}
}