diff --git a/crates/cargo-pvm-contract-builder/src/lib.rs b/crates/cargo-pvm-contract-builder/src/lib.rs index 00d3dfaf..a0b2cbfe 100644 --- a/crates/cargo-pvm-contract-builder/src/lib.rs +++ b/crates/cargo-pvm-contract-builder/src/lib.rs @@ -298,10 +298,24 @@ fn process_elf_binaries( } let output_path = profile_dir.join(format!("{bin}.polkavm")); + let abi_path = profile_dir.join(format!("{bin}.abi.json")); + link_to_polkavm(&elf_path, &output_path)?; + // Clear any previous `.abi.json`. The re-emit below skips writing + // when `generate_abi == false` (`PvmBuilder::skip_abi(true)`) or + // the source has no `#[contract]` macro, so without this cleanup + // a stale ABI would survive a `.polkavm` overwrite. + // `NotFound` is the expected first-build case; everything else + // (permissions, IO) is a real error worth surfacing. + if let Err(e) = fs::remove_file(&abi_path) + && e.kind() != std::io::ErrorKind::NotFound + { + return Err(e) + .with_context(|| format!("Failed to remove stale ABI: {}", abi_path.display())); + } + if generate_abi { - let abi_path = profile_dir.join(format!("{bin}.abi.json")); generate_abi_file(manifest_dir, bin, &abi_path, abi_target_root, features)?; } } diff --git a/crates/cargo-pvm-contract/tests/fixtures/no_contract_macro.rs b/crates/cargo-pvm-contract/tests/fixtures/no_contract_macro.rs new file mode 100644 index 00000000..1c36b717 --- /dev/null +++ b/crates/cargo-pvm-contract/tests/fixtures/no_contract_macro.rs @@ -0,0 +1,32 @@ +// Test fixture for `rebuild_without_contract_macro_clears_stale_abi_json`. +// +// A minimal polkavm-only contract source that compiles against the standard +// macro-scaffolded Cargo.toml (`pvm-contract-sdk` + `polkavm-derive`) but +// does not invoke the contract attribute macro, so the builder's +// `has_contract_macro` check returns false and `generate_abi_file` hits +// the `Ok(None)` arm without writing. + +#![cfg_attr(not(test), no_main, no_std)] + +use pvm_contract_sdk::U256; + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { + core::arch::asm!("unimp"); + core::hint::unreachable_unchecked() + } +} + +#[cfg(not(test))] +#[unsafe(no_mangle)] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[cfg(not(test))] +#[unsafe(no_mangle)] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + let _u = U256::ZERO; +} diff --git a/crates/cargo-pvm-contract/tests/test_cmd.rs b/crates/cargo-pvm-contract/tests/test_cmd.rs index 88b3dcaf..9fce524e 100644 --- a/crates/cargo-pvm-contract/tests/test_cmd.rs +++ b/crates/cargo-pvm-contract/tests/test_cmd.rs @@ -652,3 +652,64 @@ fn build_forwards_features_with_package_flag() { "ws-feat-pkg.abi.json should exist — proves --features reached the abi-gen invocation too" ); } + +/// Regression test for paritytech/bugbounty_reports#78: stripping the +/// `#[contract]` macro from a previously-built project must not leave the +/// old `.abi.json` next to the freshly-linked `.polkavm`. +#[test] +fn rebuild_without_contract_macro_clears_stale_abi_json() { + let temp_dir = TempDir::new().expect("temp dir"); + let name = "stale-abi-test"; + + let project_dir = scaffold_new_contract(&temp_dir, name, "macro", None); + build_project(&project_dir, "release"); + verify_abi_json(&project_dir, name, "release"); + + let src_path = project_dir.join("src").join(format!("{name}.rs")); + let stripped = include_str!("fixtures/no_contract_macro.rs"); + std::fs::write(&src_path, stripped).expect("rewrite source"); + + build_project(&project_dir, "release"); + + let abi_path = project_dir + .join("target") + .join("release") + .join(format!("{name}.abi.json")); + assert!( + !abi_path.exists(), + "stale .abi.json at {}", + abi_path.display() + ); + verify_polkavm_binary(&project_dir, name, "release"); +} + +/// A no-op source edit must produce a byte-identical `.abi.json`. +/// Guards against a regression where cleanup runs but re-emission doesn't. +#[test] +fn rebuild_with_macro_keeps_abi_byte_stable() { + let temp_dir = TempDir::new().expect("temp dir"); + let name = "fresh-abi-test"; + + let project_dir = scaffold_new_contract(&temp_dir, name, "macro", None); + build_project(&project_dir, "release"); + verify_abi_json(&project_dir, name, "release"); + + let abi_path = project_dir + .join("target") + .join("release") + .join(format!("{name}.abi.json")); + let abi_v1 = std::fs::read(&abi_path).expect("read v1 abi"); + + let src_path = project_dir.join("src").join(format!("{name}.rs")); + let original = std::fs::read_to_string(&src_path).expect("read source"); + std::fs::write(&src_path, format!("{original}\n// rebuild trigger\n")).expect("write source"); + + build_project(&project_dir, "release"); + verify_abi_json(&project_dir, name, "release"); + + let abi_v2 = std::fs::read(&abi_path).expect("read v2 abi"); + assert_eq!( + abi_v1, abi_v2, + "ABI bytes should be stable across a no-op rebuild" + ); +}