Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
81 changes: 81 additions & 0 deletions crates/forge/tests/cli/verify_bytecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,56 @@ fn test_verify_bytecode_with_ignore(
);
}
}

#[expect(clippy::too_many_arguments)]
fn test_verify_bytecode_mismatch(
prj: TestProject,
mut cmd: TestCommand,
addr: &str,
contract_name: &str,
source_code: &str,
config: Config,
verifier: &str,
verifier_url: &str,
) {
let etherscan_key = next_etherscan_api_key();
let rpc_url = next_http_archive_rpc_url();

// Fetch real source code
let real_source = cmd
.cast_fuse()
.args(["source", addr, "--flatten", "--etherscan-api-key", &etherscan_key])
.assert_success()
.get_output()
.stdout_lossy();

prj.add_source(contract_name, &real_source);
prj.write_config(config);
// Build once with correct source (creates cache)
cmd.forge_fuse().arg("build").assert_success();

// Now replace with different incorrect source code
prj.add_source(contract_name, source_code);
let args = vec![
"verify-bytecode",
addr,
contract_name,
"--etherscan-api-key",
&etherscan_key,
"--verifier",
verifier,
"--verifier-url",
verifier_url,
"--rpc-url",
&rpc_url,
];
let output = cmd.forge_fuse().args(args).assert_success().get_output().stderr_lossy();

// Verify that bytecode does NOT match (recompiled with incorrect source)
assert!(output.contains("Error: Creation code did not match".to_string().as_str()));
assert!(output.contains("Error: Runtime code did not match".to_string().as_str()));
}

forgetest_async!(can_verify_bytecode_no_metadata, |prj, cmd| {
test_verify_bytecode(
prj,
Expand Down Expand Up @@ -296,6 +346,37 @@ forgetest_async!(can_ignore_runtime, |prj, cmd| {
);
});

// Test that verification fails when source code doesn't match deployed bytecode
forgetest_async!(can_verify_bytecode_fails_on_source_mismatch, |prj, cmd| {
let modified_source = r#"
contract SystemConfig {
uint256 public constant MODIFIED_VALUE = 999;

function someFunction() public pure returns (uint256) {
return MODIFIED_VALUE;
}
}
"#;

test_verify_bytecode_mismatch(
prj,
cmd,
"0xba2492e52F45651B60B8B38d4Ea5E2390C64Ffb1",
"SystemConfig",
modified_source,
Config {
evm_version: EvmVersion::London,
optimizer_runs: Some(999999),
optimizer: Some(true),
cbor_metadata: false,
bytecode_hash: BytecodeHash::None,
..Default::default()
},
"etherscan",
"https://api.etherscan.io/v2/api?chainid=1",
);
});

// Test predeploy contracts
// TODO: Add test utils for base such as basescan keys and alchemy keys.
// WETH9 Predeploy
Expand Down
9 changes: 1 addition & 8 deletions crates/verify/src/bytecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,7 @@ impl VerifyBytecodeArgs {
let etherscan_metadata = source_code.items.first().unwrap();

// Obtain local artifact
let artifact = if let Ok(local_bytecode) =
crate::utils::build_using_cache(&self, etherscan_metadata, &config)
{
trace!("using cache");
local_bytecode
} else {
crate::utils::build_project(&self, &config)?
};
let artifact = crate::utils::build_project(&self, &config)?;

// Get local bytecode (creation code)
let local_bytecode = artifact
Expand Down
45 changes: 0 additions & 45 deletions crates/verify/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,51 +97,6 @@ pub fn build_project(
Ok(artifact.into_contract_bytecode())
}

pub fn build_using_cache(
args: &VerifyBytecodeArgs,
etherscan_settings: &Metadata,
config: &Config,
) -> Result<CompactContractBytecode> {
let project = config.project()?;
let cache = project.read_cache_file()?;
let cached_artifacts = cache.read_artifacts::<CompactContractBytecode>()?;

for (key, value) in cached_artifacts {
let name = args.contract.name.to_owned() + ".sol";
let version = etherscan_settings.compiler_version.to_owned();
// Ignores vyper
if version.starts_with("vyper:") {
eyre::bail!("Vyper contracts are not supported")
}
// Parse etherscan version string
let version = version.split('+').next().unwrap_or("").trim_start_matches('v').to_string();

// Check if `out/directory` name matches the contract name
if key.ends_with(name.as_str()) {
let name = name.replace(".sol", ".json");
for artifact in value.into_values().flatten() {
// Check if ABI file matches the name
if !artifact.file.ends_with(&name) {
continue;
}

// Check if Solidity version matches
if let Ok(version) = Version::parse(&version)
&& !(artifact.version.major == version.major
&& artifact.version.minor == version.minor
&& artifact.version.patch == version.patch)
{
continue;
}

return Ok(artifact.artifact);
}
}
}

eyre::bail!("couldn't find cached artifact for contract {}", args.contract.name)
}

pub fn print_result(
res: Option<VerificationType>,
bytecode_type: BytecodeType,
Expand Down
Loading