diff --git a/README.md b/README.md index 1e363252..11eedc77 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Once a DVF is published, any user can choose to trust the signer of that DVF and - [Registry](#registry) - [Etherscan Verified Contracts](#etherscan-verified-contracts) - [Initialization by event topics](#initialization-by-event-topics) + - [Public libraries](#public-libraries) 7. [Common Problems](#common-problems) 8. [Getting Help](#getting-help) @@ -544,6 +545,18 @@ Alternatively, it is also possible to pass an empty list of event topics to sear dv init --project --address
--contractname --eventtopics "" new.dvf.json ``` +### Public libraries + +If your contracts use public libraries, they are not automatically linked during compilation. You have to explicitly pass the addresses of the libraries using the `--libraries` argument: + +``` +dv init --project --address
--contractname --libraries new.dvf.json +``` + +`` can be a comma-separated list of libraries. Each item must have the following format: `::
`. + +`` is the path to the library contract file, relative to your project root. `` is the name of the library contract. `
` is the public address of the already deployed library. + ## Common Problems Sometimes, it is possible that the `init` command cannot find a deployment transaction. In this case, you have the following options: @@ -571,7 +584,6 @@ This section will be updated soon. ## Known Limitations and Bugs -- Compilation with libraries is currently not supported. The best workaround is to compile using `forge build --libraries --build-info --build-info-path ` and then use `` using the `--buildcache` argument. - Currently only solidity is supported. - Only projects with `solc` version starting from `0.5.13` are supported due to the lack of generated storage layout in older versions (see [solc release 0.5.13](https://github.com/ethereum/solidity/releases/tag/v0.5.13)). - The RPC endpoints automatically parsed in `dv generate-config` are not guaranteed to be compatible. diff --git a/lib/bytecode_verification/parse_json.rs b/lib/bytecode_verification/parse_json.rs index 0da40602..ac300069 100644 --- a/lib/bytecode_verification/parse_json.rs +++ b/lib/bytecode_verification/parse_json.rs @@ -73,18 +73,36 @@ impl ProjectInfo { } // build it - fn forge_build(project: &Path, build_info_path: &Path) -> Result<(), ValidationError> { + fn forge_build( + project: &Path, + build_info_path: &Path, + libraries: Option>, + ) -> Result<(), ValidationError> { info!( "Starting . If you had previous builds, it is recommended to ." ); - let build = Command::new("forge") + + let mut command = Command::new("forge"); + command .current_dir(project) .arg("build") .arg("--build-info") .arg("--build-info-path") - .arg(build_info_path.to_str().unwrap()) - .output() - .expect("Could not build project"); + .arg(build_info_path.to_str().unwrap()); + + if let Some(libs) = libraries { + for lib in libs { + command.arg("--libraries").arg(lib); + } + } + + let program = command.get_program(); + let args: Vec<_> = command.get_args().collect(); + + println!("Command: {:?}", program); + println!("Args: {:?}", args); + + let build = command.output().expect("Could not build project"); if !build.status.success() { println!( @@ -1375,6 +1393,7 @@ impl ProjectInfo { project: &Path, env: Environment, artifacts_path: &Path, + libraries: Option>, ) -> Result { let build_info_path: PathBuf; let build_info_dir: TempDir; @@ -1385,7 +1404,7 @@ impl ProjectInfo { build_info_dir = Builder::new().prefix("dvf_bi").tempdir().unwrap(); // Persist for now build_info_path = build_info_dir.keep(); - Self::forge_build(project, &build_info_path)?; + Self::forge_build(project, &build_info_path, libraries)?; } Environment::Hardhat => { assert!(Self::check_hardhat(project)); @@ -1403,10 +1422,13 @@ impl ProjectInfo { env: Environment, artifacts_path: &Path, build_cache: Option<&String>, + libraries: Option>, ) -> Result { + println!("Libraries are {:?}", libraries); + let build_info_path: PathBuf = match build_cache { Some(s) => PathBuf::from(s), - None => Self::compile(project, env, artifacts_path)?, + None => Self::compile(project, env, artifacts_path, libraries)?, }; let command = match env { diff --git a/src/dvf.rs b/src/dvf.rs index 28ec65eb..e404b52c 100644 --- a/src/dvf.rs +++ b/src/dvf.rs @@ -470,6 +470,12 @@ fn main() { .action(clap::ArgAction::SetTrue), ) .arg(arg!(--buildcache ).help("Folder containing build-info files")) + .arg( + arg!(--libraries ...) + .help("Library specifiers in the form Path:Name:Address. Accepts comma-separated values or repeated flags") + .value_delimiter(',') + .action(clap::ArgAction::Append), + ) .arg( arg!(--implementationbuildcache ) .help("Folder containing the implementation contract's build-info files"), @@ -571,6 +577,12 @@ fn main() { arg!(--artifacts ) .help("Folder containing the artifacts") .default_value("artifacts"), + ) + .arg( + arg!(--libraries ...) + .help("Library specifiers in the form Path:Name:Address. Accepts comma-separated values or repeated flags") + .value_delimiter(',') + .action(clap::ArgAction::Append), ), ) .subcommand( @@ -620,6 +632,12 @@ fn main() { .help("Folder containing the artifacts") .default_value("artifacts"), ) + .arg( + arg!(--libraries ...) + .help("Library specifiers in the form Path:Name:Address. Accepts comma-separated values or repeated flags") + .value_delimiter(',') + .action(clap::ArgAction::Append), + ) .arg(arg!(--buildcache ).help("Folder containing build-info files")), ) .subcommand( @@ -783,6 +801,9 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { let artifacts = sub_m.get_one::("artifacts").unwrap(); let build_cache = sub_m.get_one::("buildcache"); let artifacts_path = get_project_paths(project, artifacts); + let libraries = sub_m + .get_many::("libraries") + .map(|vals| vals.cloned().collect()); let event_topics = sub_m .get_many::>("eventtopics") .map(|v| v.flat_map(|x| x.clone()).collect::>()); @@ -869,6 +890,7 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { env, &artifacts_path, build_cache, + libraries.clone(), )?; print_progress("Comparing bytecode.", &mut pc, &progress_mode); @@ -990,6 +1012,7 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { imp_env, &imp_artifacts_path, imp_build_cache, + libraries, )?; print_progress( @@ -1564,6 +1587,9 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { let project = sub_m.get_one::("project").unwrap(); let artifacts = sub_m.get_one::("artifacts").unwrap(); let artifacts_path = get_project_paths(project, artifacts); + let libraries = sub_m + .get_many::("libraries") + .map(|vals| vals.cloned().collect()); let mut pc = 1_u64; let progress_mode: ProgressMode = ProgressMode::GenerateBuildCache; @@ -1571,7 +1597,7 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { // Bytecode and Immutable check print_progress("Compiling local bytecode.", &mut pc, &progress_mode); - let build_cache_path = ProjectInfo::compile(project, env, &artifacts_path)?; + let build_cache_path = ProjectInfo::compile(project, env, &artifacts_path, libraries)?; println!("Build Cache: {}", build_cache_path.display()); exit(0); @@ -1583,6 +1609,9 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { let project = sub_m.get_one::("project").unwrap(); let artifacts = sub_m.get_one::("artifacts").unwrap(); let artifacts_path = get_project_paths(project, artifacts); + let libraries = sub_m + .get_many::("libraries") + .map(|vals| vals.cloned().collect()); let contract_name = sub_m.get_one::("contractname").unwrap().to_string(); let address = sub_m.get_one::
("address").unwrap(); @@ -1607,8 +1636,14 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { // Bytecode and Immutable check print_progress("Compiling local bytecode.", &mut pc, &progress_mode); - let mut project_info = - ProjectInfo::new(&contract_name, project, env, &artifacts_path, build_cache)?; + let mut project_info = ProjectInfo::new( + &contract_name, + project, + env, + &artifacts_path, + build_cache, + libraries, + )?; print_progress("Comparing bytecode.", &mut pc, &progress_mode); let factory_mode = sub_m.get_flag("factory"); @@ -1647,6 +1682,9 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { let project = sub_m.get_one::("project").unwrap(); let artifacts = sub_m.get_one::("artifacts").unwrap(); let artifacts_path = get_project_paths(project, artifacts); + let libraries = sub_m + .get_many::("libraries") + .map(|vals| vals.cloned().collect()); let contract_name = sub_m.get_one::("contractname").unwrap().to_string(); let build_cache = sub_m.get_one::("buildcache"); @@ -1655,8 +1693,14 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { let progress_mode: ProgressMode = ProgressMode::ListEvents; print_progress("Compiling local bytecode.", &mut pc, &progress_mode); - let project_info = - ProjectInfo::new(&contract_name, project, env, &artifacts_path, build_cache)?; + let project_info = ProjectInfo::new( + &contract_name, + project, + env, + &artifacts_path, + build_cache, + libraries, + )?; let mut event_table = Table::new(); for event in project_info.events { diff --git a/tests/Contracts/script/Deploy_LinkedLibraries.sh b/tests/Contracts/script/Deploy_LinkedLibraries.sh new file mode 100644 index 00000000..1af38069 --- /dev/null +++ b/tests/Contracts/script/Deploy_LinkedLibraries.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# deploy.sh + +set -e + +ANVIL_DEF_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +RPC_URL=$1 + +LIB_ADDR1=$(forge create src/linked_libraries/SimpleMath.sol:SimpleMath \ + --private-key $ANVIL_DEF_PRIVATE_KEY \ + --rpc-url $RPC_URL \ + --chain-id 31337 \ + --broadcast \ + --json | jq -r .deployedTo) + +echo "Deployed SimpleMath to: $LIB_ADDR1" + +LIB_ADDR2=$(forge create src/linked_libraries/SimpleNumber.sol:SimpleNumber \ + --private-key $ANVIL_DEF_PRIVATE_KEY \ + --rpc-url $RPC_URL \ + --chain-id 31337 \ + --broadcast \ + --json | jq -r .deployedTo) + +echo "Deployed SimpleNumber to: $LIB_ADDR2" + +# Step 3: Run the Solidity script to deploy Calculator +CALCULATOR_ADDR=$(forge create src/linked_libraries/Calculator.sol:Calculator \ + --private-key $ANVIL_DEF_PRIVATE_KEY \ + --rpc-url $RPC_URL \ + --chain-id 31337 \ + --libraries src/linked_libraries/SimpleMath.sol:SimpleMath:$LIB_ADDR1 \ + --libraries src/linked_libraries/SimpleNumber.sol:SimpleNumber:$LIB_ADDR2 \ + --broadcast \ + --json | jq -r .deployedTo) + +echo "Deployed Calculator to: $CALCULATOR_ADDR" + +# clean up because we want to test that the DV tool is able to generate the build files +forge clean + + diff --git a/tests/Contracts/src/linked_libraries/Calculator.sol b/tests/Contracts/src/linked_libraries/Calculator.sol new file mode 100644 index 00000000..8a188af8 --- /dev/null +++ b/tests/Contracts/src/linked_libraries/Calculator.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./SimpleMath.sol"; +import "./SimpleNumber.sol"; + +contract Calculator { + /** + * @dev Calculates the sum of two numbers using the SimpleMath library. + */ + function calculateSum(uint256 a, uint256 b) public pure returns (uint256) { + return SimpleMath.add(a, b); + } + + function getSimpleNumber() public pure returns (uint256) { + return SimpleNumber.number(); + } +} diff --git a/tests/Contracts/src/linked_libraries/SimpleMath.sol b/tests/Contracts/src/linked_libraries/SimpleMath.sol new file mode 100644 index 00000000..8e32f227 --- /dev/null +++ b/tests/Contracts/src/linked_libraries/SimpleMath.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +library SimpleMath { + /** + * @dev Returns the sum of two unsigned integers. + */ + function add(uint256 a, uint256 b) public pure returns (uint256) { + return a + b; + } +} diff --git a/tests/Contracts/src/linked_libraries/SimpleNumber.sol b/tests/Contracts/src/linked_libraries/SimpleNumber.sol new file mode 100644 index 00000000..0e2ce8c4 --- /dev/null +++ b/tests/Contracts/src/linked_libraries/SimpleNumber.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +library SimpleNumber { + /** + * @dev Returns simple number + */ + function number() public pure returns (uint256) { + return 42; + } +} diff --git a/tests/expected_dvfs/LinkedLibraries.dvf.json b/tests/expected_dvfs/LinkedLibraries.dvf.json new file mode 100644 index 00000000..0d677ec1 --- /dev/null +++ b/tests/expected_dvfs/LinkedLibraries.dvf.json @@ -0,0 +1,27 @@ +{ + "version": "0.9.1", + "id": "0x8cb6eba76dc9103ef653730f5c4431b0b8bcfa0f08166a677e38084ea6a23e2c", + "contract_name": "Calculator", + "address": "0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0", + "chain_id": 31337, + "deployment_block_num": 4, + "init_block_num": 4, + "deployment_tx": "0xfb42c08715bcd72f549378cb69613499ecabb8b9bce4054237aa82213ca0a15b", + "codehash": "0x1f193068755fd35acee5dd20113cc9f941ba09a7fd34e5f8e45163cfcacb97c5", + "insecure": false, + "immutables": [], + "constructor_args": [], + "critical_storage_variables": [], + "critical_events": [], + "unvalidated_metadata": { + "author_name": "Author", + "description": "System Description", + "hardfork": [ + "paris", + "shanghai" + ], + "audit_report": "https://example.org/report.pdf", + "source_url": "https://github.com/source/code", + "security_contact": "security@example.org" + } +} \ No newline at end of file diff --git a/tests/test_decoding.rs b/tests/test_decoding.rs index 3a94bd8a..eddc2293 100644 --- a/tests/test_decoding.rs +++ b/tests/test_decoding.rs @@ -26,6 +26,7 @@ mod tests { Environment::Foundry, PathBuf::from("").as_path(), None, + None, ) .unwrap(); let pretty_printer = PrettyPrinter::new(&empty_config, None); diff --git a/tests/test_end_to_end.rs b/tests/test_end_to_end.rs index b70bcbe1..e2ef266a 100644 --- a/tests/test_end_to_end.rs +++ b/tests/test_end_to_end.rs @@ -984,6 +984,70 @@ mod tests { } } + #[test] + fn test_e2e_init_linked_libraries() { + let port = 8545u16; + let config_file = match DVFConfig::test_config_file(Some(port)) { + Ok(config) => config, + Err(err) => { + println!("{}", err); + assert!(false); + return; + } + }; + + let script = String::from("script/Deploy_LinkedLibraries.sh"); + let contract = String::from("Calculator"); + let expected = String::from("tests/expected_dvfs/LinkedLibraries.dvf.json"); + let client_type = LocalClientType::Anvil; + let local_client = start_local_client(client_type.clone(), port); + let url = format!("http://localhost:{}", port).to_string(); + + // deploy the all contracts (incl. ext. libraries) with a bash script + // because external libraries cannot be deployed with a Foundry script + let mut bash_cmd = Command::new("sh"); + bash_cmd.current_dir("tests/Contracts"); + let bash_assert = bash_cmd.args(&[&script, &url]).assert().success(); + println!( + "{}", + &String::from_utf8_lossy(&bash_assert.get_output().stdout) + ); + + let outfile = NamedTempFile::new().unwrap(); + let mut dvf_cmd = Command::cargo_bin("dv").unwrap(); + let assert = dvf_cmd + .args(&[ + "--config", + &config_file.path().to_string_lossy(), + "--verbose", + "init", + "--address", + "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "--chainid", + &chain_id_str(client_type.clone()), + "--project", + "tests/Contracts/", + "--contractname", + &contract, + "--initblock", + "4", + "--libraries", + "src/linked_libraries/SimpleMath.sol:SimpleMath:0x5FbDB2315678afecb367f032d93F642f64180aa3", + "--libraries", + "src/linked_libraries/SimpleNumber.sol:SimpleNumber:0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + &outfile.path().to_string_lossy(), + ]) + .assert() + .success(); + println!("{}", &String::from_utf8_lossy(&assert.get_output().stdout)); + + // Uncomment to regenerate expected files + // std::fs::copy(outfile.path(), Path::new(&expected)).unwrap(); + + assert_eq_files(&outfile.path(), &Path::new(&expected)); + drop(local_client); + } + #[test] fn test_e2e_validate() { let port = 8551u16;