From 09b2388ef8f0d31560e4e908500c6fbce5e1ad5c Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Mon, 6 Dec 2021 23:37:27 +0100 Subject: [PATCH 1/8] feat(solc): add support for library linking --- Cargo.lock | 1 + ethers-solc/Cargo.toml | 1 + ethers-solc/src/artifacts.rs | 273 +++++++++++++++++++++++++++++++++-- ethers-solc/src/config.rs | 2 +- ethers-solc/src/utils.rs | 29 ++++ 5 files changed, 290 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 150197b09..026f83a93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1203,6 +1203,7 @@ dependencies = [ "svm-rs", "tempdir", "thiserror", + "tiny-keccak", "tokio", "tracing", "walkdir", diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml index fbd7a5927..4ed4e4e74 100644 --- a/ethers-solc/Cargo.toml +++ b/ethers-solc/Cargo.toml @@ -30,6 +30,7 @@ colored = "2.0.0" svm = { package = "svm-rs", version = "0.2.0", optional = true } glob = "0.3.0" tracing = "0.1.29" +tiny-keccak = { version = "2.0.2", default-features = false } [target.'cfg(not(any(target_arch = "x86", target_arch = "x86_64")))'.dependencies] sha2 = { version = "0.9.8", default-features = false } diff --git a/ethers-solc/src/artifacts.rs b/ethers-solc/src/artifacts.rs index 63ab7ec7a..4f4fd3f57 100644 --- a/ethers-solc/src/artifacts.rs +++ b/ethers-solc/src/artifacts.rs @@ -12,6 +12,7 @@ use std::{ }; use crate::{compile::*, remappings::Remapping, utils}; +use ethers_core::abi::Address; use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; /// An ordered list of files and their source @@ -561,20 +562,20 @@ pub struct CompactContract { /// The Ethereum Contract ABI. If empty, it is represented as an empty /// array. See https://docs.soliditylang.org/en/develop/abi-spec.html pub abi: Option, - #[serde( - default, - deserialize_with = "deserialize_opt_bytes", - skip_serializing_if = "Option::is_none" - )] - pub bin: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bin: Option, #[serde(default, rename = "bin-runtime", skip_serializing_if = "Option::is_none")] - pub bin_runtime: Option, + pub bin_runtime: Option, } impl CompactContract { /// Returns the contents of this type as a single pub fn into_parts(self) -> (Option, Option, Option) { - (self.abi, self.bin, self.bin_runtime) + ( + self.abi, + self.bin.and_then(|bin| bin.into_bytes()), + self.bin_runtime.and_then(|bin| bin.into_bytes()), + ) } /// Returns the individual parts of this contract. @@ -583,8 +584,8 @@ impl CompactContract { pub fn into_parts_or_default(self) -> (Abi, Bytes, Bytes) { ( self.abi.unwrap_or_default(), - self.bin.unwrap_or_default(), - self.bin_runtime.unwrap_or_default(), + self.bin.and_then(|bin| bin.into_bytes()).unwrap_or_default(), + self.bin_runtime.and_then(|bin| bin.into_bytes()).unwrap_or_default(), ) } } @@ -612,9 +613,9 @@ impl<'a> From> for CompactContract { pub struct CompactContractRef<'a> { pub abi: Option<&'a Abi>, #[serde(default, skip_serializing_if = "Option::is_none")] - pub bin: Option<&'a Bytes>, + pub bin: Option<&'a BytecodeObject>, #[serde(default, rename = "bin-runtime", skip_serializing_if = "Option::is_none")] - pub bin_runtime: Option<&'a Bytes>, + pub bin_runtime: Option<&'a BytecodeObject>, } impl<'a> CompactContractRef<'a> { @@ -700,13 +701,12 @@ pub struct Bytecode { #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")] pub function_debug_data: BTreeMap, /// The bytecode as a hex string. - #[serde(deserialize_with = "deserialize_bytes")] - pub object: Bytes, + pub object: BytecodeObject, /// Opcodes list (string) pub opcodes: String, /// The source mapping as a string. See the source mapping definition. pub source_map: String, - /// Array of sources generated by the compiler. Currently only contains a + /// Array of sources generated by the compiler. Currently, only contains a /// single Yul file. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub generated_sources: Vec, @@ -715,6 +715,199 @@ pub struct Bytecode { pub link_references: BTreeMap>>, } +impl Bytecode { + /// Same as `Bytecode::link` but with fully qualified name (`file.sol:Math`) + pub fn link_fully_qualified(&mut self, name: impl AsRef, addr: Address) -> bool { + if let Some((file, lib)) = name.as_ref().split_once(':') { + self.link(file, lib, addr) + } else { + false + } + } + + /// Tries to link the bytecode object with the `file` and `library` name. + /// Replaces all library placeholders with the given address. + /// + /// Returns true if the bytecode object is fully linked, false otherwise + /// This is a noop if the bytecode object is already fully linked. + pub fn link( + &mut self, + file: impl AsRef, + library: impl AsRef, + address: Address, + ) -> bool { + if !self.object.is_unlinked() { + return true + } + + let file = file.as_ref(); + let library = library.as_ref(); + if let Some((key, mut contracts)) = self.link_references.remove_entry(file) { + if contracts.remove(library).is_some() { + self.object.link(file, library, address); + } + if !contracts.is_empty() { + self.link_references.insert(key, contracts); + } + if self.link_references.is_empty() { + return self.object.try_resolve().is_some() + } + } + false + } + + /// Links the bytecode object with all provided `(file, lib, addr)` + pub fn link_all(&mut self, libs: I) -> bool + where + I: IntoIterator, + S: AsRef, + T: AsRef, + { + for (file, lib, addr) in libs.into_iter() { + if self.link(file, lib, addr) { + return true + } + } + false + } + + /// Links the bytecode object with all provided `(fully_qualified, addr)` + pub fn link_all_fully_qualified(&mut self, libs: I) -> bool + where + I: IntoIterator, + S: AsRef, + { + for (name, addr) in libs.into_iter() { + if self.link_fully_qualified(name, addr) { + return true + } + } + false + } +} + +/// Represents the bytecode of a contracts that might be not fully linked yet. +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum BytecodeObject { + /// Fully linked bytecode object + #[serde(deserialize_with = "deserialize_bytes")] + Bytecode(Bytes), + /// Bytecode as hex string that's not fully linked yet and contains library placeholders + Unlinked(String), +} + +impl BytecodeObject { + pub fn into_bytes(self) -> Option { + match self { + BytecodeObject::Bytecode(bytes) => Some(bytes), + BytecodeObject::Unlinked(_) => None, + } + } + + pub fn as_bytes(&self) -> Option<&Bytes> { + match self { + BytecodeObject::Bytecode(bytes) => Some(bytes), + BytecodeObject::Unlinked(_) => None, + } + } + + pub fn into_unlinked(self) -> Option { + match self { + BytecodeObject::Bytecode(_) => None, + BytecodeObject::Unlinked(code) => Some(code), + } + } + + /// Tries to resolve the unlinked string object a valid bytecode object in place + /// + /// Returns the string if it is a valid + pub fn try_resolve(&mut self) -> Option { + let mut resolved = None; + + if let BytecodeObject::Unlinked(unlinked) = self { + if let Ok(linked) = hex::decode(unlinked) { + resolved = Some(BytecodeObject::Bytecode(linked.into())); + } + } + if let Some(resolved) = resolved { + std::mem::replace(self, resolved).into_unlinked().map(LinkedByteCode) + } else { + None + } + } + + /// Link using the fully qualified name of a library + /// The fully qualified library name is the path of its source file and the library name + /// separated by `:` like `file.sol:Math` + /// + /// This will replace all occurrences of the library placeholder with the given address. + /// + /// See also: https://docs.soliditylang.org/en/develop/using-the-compiler.html#library-linking + pub fn link_fully_qualified(&mut self, name: impl AsRef, addr: Address) { + if let BytecodeObject::Unlinked(ref mut unlinked) = self { + let name = name.as_ref(); + let place_holder = utils::library_hash_placeholder(name); + // the address as hex without prefix + let hex_addr = hex::encode(addr); + + // the library placeholder used to be the fully qualified name of the library instead of + // the hash. This is also still supported by `solc` so we handle this as well + let fully_qualified_placeholder = utils::library_fully_qualified_placeholder(name); + + *unlinked = unlinked + .replace(&format!("__{}__", fully_qualified_placeholder), &hex_addr) + .replace(&format!("__{}__", place_holder), &hex_addr) + } + } + + /// Link using the `file` and `library` names as fully qualified name `:` + /// See `BytecodeObject::link_fully_qualified` + pub fn link(&mut self, file: impl AsRef, library: impl AsRef, addr: Address) { + self.link_fully_qualified(format!("{}:{}", file.as_ref(), library.as_ref(),), addr) + } + + /// Whether this object is still unlinked + pub fn is_unlinked(&self) -> bool { + matches!(self, BytecodeObject::Unlinked(_)) + } + + /// Whether the bytecode contains a matching placeholder using the qualified name + pub fn contains_fully_qualified_placeholder(&self, name: impl AsRef) -> bool { + if let BytecodeObject::Unlinked(unlinked) = self { + let name = name.as_ref(); + unlinked.contains(&utils::library_hash_placeholder(name)) || + unlinked.contains(&utils::library_fully_qualified_placeholder(name)) + } else { + false + } + } + + /// Whether the bytecode contains a matching placeholder + pub fn contains_placeholder(&self, file: impl AsRef, library: impl AsRef) -> bool { + self.contains_fully_qualified_placeholder(format!("{}:{}", file.as_ref(), library.as_ref())) + } +} + +impl AsRef<[u8]> for BytecodeObject { + fn as_ref(&self) -> &[u8] { + match self { + BytecodeObject::Bytecode(code) => code.as_ref(), + BytecodeObject::Unlinked(code) => code.as_bytes(), + } + } +} + +/// A fully linked byte code that represents a valid hex string +#[derive(Debug, Clone)] +pub struct LinkedByteCode(String); + +impl From for Bytes { + fn from(inner: LinkedByteCode) -> Self { + hex::decode(&inner.0).expect("Is valid hex").into() + } +} + #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FunctionDebugData { @@ -1031,6 +1224,56 @@ mod tests { use super::*; use std::{fs, path::PathBuf}; + #[test] + fn can_link_bytecode() { + // test cases taken from https://github.com/ethereum/solc-js/blob/master/test/linker.js + + #[derive(Serialize, Deserialize)] + struct Mockject { + object: BytecodeObject, + } + fn parse_bytecode(bytecode: &str) -> BytecodeObject { + let object: Mockject = + serde_json::from_value(serde_json::json!({ "object": bytecode })).unwrap(); + object.object + } + + let bytecode = "6060604052341561000f57600080fd5b60f48061001d6000396000f300606060405260043610603e5763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166326121ff081146043575b600080fd5b3415604d57600080fd5b60536055565b005b73__lib2.sol:L____________________________6326121ff06040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160006040518083038186803b151560b357600080fd5b6102c65a03f4151560c357600080fd5b5050505600a165627a7a723058207979b30bd4a07c77b02774a511f2a1dd04d7e5d65b5c2735b5fc96ad61d43ae40029"; + + let mut object = parse_bytecode(bytecode); + assert!(object.is_unlinked()); + assert!(object.contains_placeholder("lib2.sol", "L")); + assert!(object.contains_fully_qualified_placeholder("lib2.sol:L")); + object.link("lib2.sol", "L", Address::random()); + assert!(object.try_resolve().is_some()); + assert!(!object.is_unlinked()); + + let mut code = Bytecode { + function_debug_data: Default::default(), + object: parse_bytecode(bytecode), + opcodes: "".to_string(), + source_map: "".to_string(), + generated_sources: vec![], + link_references: BTreeMap::from([( + "lib2.sol".to_string(), + BTreeMap::from([("L".to_string(), vec![])]), + )]), + }; + + assert!(!code.link("lib2.sol", "Y", Address::random())); + assert!(code.link("lib2.sol", "L", Address::random())); + assert!(code.link("lib2.sol", "L", Address::random())); + + let hashed_placeholder = "6060604052341561000f57600080fd5b60f48061001d6000396000f300606060405260043610603e5763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166326121ff081146043575b600080fd5b3415604d57600080fd5b60536055565b005b73__$cb901161e812ceb78cfe30ca65050c4337$__6326121ff06040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160006040518083038186803b151560b357600080fd5b6102c65a03f4151560c357600080fd5b5050505600a165627a7a723058207979b30bd4a07c77b02774a511f2a1dd04d7e5d65b5c2735b5fc96ad61d43ae40029"; + let mut object = parse_bytecode(hashed_placeholder); + assert!(object.is_unlinked()); + assert!(object.contains_placeholder("lib2.sol", "L")); + assert!(object.contains_fully_qualified_placeholder("lib2.sol:L")); + object.link("lib2.sol", "L", Address::default()); + assert!(object.try_resolve().is_some()); + assert!(!object.is_unlinked()); + } + #[test] fn can_parse_compiler_output() { let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs index 2be7a5412..4f97dd1e5 100644 --- a/ethers-solc/src/config.rs +++ b/ethers-solc/src/config.rs @@ -223,7 +223,7 @@ pub trait Artifact { impl Artifact for CompactContract { fn into_inner(self) -> (Option, Option) { - (self.abi, self.bin) + (self.abi, self.bin.and_then(|bin| bin.into_bytes())) } } diff --git a/ethers-solc/src/utils.rs b/ethers-solc/src/utils.rs index 697e6e1a5..40da5e3c4 100644 --- a/ethers-solc/src/utils.rs +++ b/ethers-solc/src/utils.rs @@ -6,6 +6,7 @@ use crate::error::SolcError; use once_cell::sync::Lazy; use regex::Regex; use semver::Version; +use tiny_keccak::{Hasher, Keccak}; use walkdir::WalkDir; /// A regex that matches the import path and identifier of a solidity import @@ -123,6 +124,34 @@ pub fn installed_versions(root: impl AsRef) -> Result, SolcEr Ok(versions) } +/// Returns the 36 char (deprecated) fully qualified name placeholder +/// +/// If the name is longer than 36 char, then the name gets truncated, +/// If the name is shorter than 36 char, then the name is filled with trailing `_` +pub fn library_fully_qualified_placeholder(name: impl AsRef) -> String { + name.as_ref().chars().chain(std::iter::repeat('_')).take(36).collect() +} + +/// Returns the library hash placeholder as `$hex(library_hash(name))$` +pub fn library_hash_placeholder(name: impl AsRef<[u8]>) -> String { + let hash = library_hash(name); + let placeholder = hex::encode(hash); + format!("${}$", placeholder) +} + +/// Returns the library placeholder for the given name +/// The placeholder is a 34 character prefix of the hex encoding of the keccak256 hash of the fully +/// qualified library name. +/// +/// See also https://docs.soliditylang.org/en/develop/using-the-compiler.html#library-linking +pub fn library_hash(name: impl AsRef<[u8]>) -> [u8; 17] { + let mut output = [0u8; 17]; + let mut hasher = Keccak::v256(); + hasher.update(name.as_ref()); + hasher.finalize(&mut output); + output +} + #[cfg(test)] mod tests { use super::*; From 6af91eb8d5fc4defd976f8c907b24cc41a7c3d7f Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Mon, 6 Dec 2021 23:40:19 +0100 Subject: [PATCH 2/8] chore: update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index baf8eee5b..b7d268b44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ - Return cached artifacts from project `compile` when the cache only contains some files +- Add support for library linking and make `Bytecode`'s `object` filed an `enum BytecodeObject` + [#656](https://github.com/gakonst/ethers-rs/pull/656). ### 0.6.0 From 4ce6e58be5618cb2053e91907ec4bdd384c41c14 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Tue, 7 Dec 2021 00:31:22 +0100 Subject: [PATCH 3/8] fixbreaking compactref api --- ethers-contract/src/factory.rs | 2 +- ethers-contract/tests/abigen.rs | 2 +- ethers-contract/tests/common/mod.rs | 3 ++- ethers-middleware/tests/signer.rs | 3 ++- ethers-middleware/tests/transformer.rs | 3 ++- ethers-solc/src/artifacts.rs | 8 ++++++++ examples/contract_human_readable.rs | 2 +- 7 files changed, 17 insertions(+), 6 deletions(-) diff --git a/ethers-contract/src/factory.rs b/ethers-contract/src/factory.rs index 5bc1a7612..621ca23f6 100644 --- a/ethers-contract/src/factory.rs +++ b/ethers-contract/src/factory.rs @@ -122,7 +122,7 @@ impl Deployer { /// let client = std::sync::Arc::new(client); /// /// // create a factory which will be used to deploy instances of the contract -/// let factory = ContractFactory::new(contract.abi.unwrap().clone(), contract.bin.unwrap().clone(), client); +/// let factory = ContractFactory::new(contract.abi.unwrap().clone(), contract.bytecode().unwrap().clone(), client); /// /// // The deployer created by the `deploy` call exposes a builder which gets consumed /// // by the async `send` call diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs index fcfa8b39f..b49c83bbf 100644 --- a/ethers-contract/tests/abigen.rs +++ b/ethers-contract/tests/abigen.rs @@ -293,7 +293,7 @@ async fn can_handle_underscore_functions() { let compiled = compiled.get(path, contract).unwrap(); let factory = ethers_contract::ContractFactory::new( compiled.abi.unwrap().clone(), - compiled.bin.unwrap().clone(), + compiled.bytecode().unwrap().clone(), client.clone(), ); let addr = factory.deploy("hi".to_string()).unwrap().legacy().send().await.unwrap().address(); diff --git a/ethers-contract/tests/common/mod.rs b/ethers-contract/tests/common/mod.rs index 5328b9eb9..2a1a29c45 100644 --- a/ethers-contract/tests/common/mod.rs +++ b/ethers-contract/tests/common/mod.rs @@ -33,7 +33,8 @@ pub fn compile_contract(name: &str, filename: &str) -> (Abi, Bytes) { let path = format!("./tests/solidity-contracts/{}", filename); let compiled = Solc::default().compile_source(&path).unwrap(); let contract = compiled.get(&path, name).expect("could not find contract"); - (contract.abi.unwrap().clone(), contract.bin.unwrap().clone()) + let (abi, bin, _) = contract.into_parts_or_default(); + (abi, bin) } /// connects the private key to http://localhost:8545 diff --git a/ethers-middleware/tests/signer.rs b/ethers-middleware/tests/signer.rs index de5d6bdff..5d4cd67b4 100644 --- a/ethers-middleware/tests/signer.rs +++ b/ethers-middleware/tests/signer.rs @@ -223,7 +223,8 @@ async fn deploy_and_call_contract() { let path = format!("./tests/solidity-contracts/{}", path); let compiled = Solc::default().compile_source(&path).unwrap(); let contract = compiled.get(&path, name).expect("could not find contract"); - (contract.abi.unwrap().clone(), contract.bin.unwrap().clone()) + let (abi, bin, _) = contract.into_parts_or_default(); + (abi, bin) } let (abi, bytecode) = compile_contract("SimpleStorage.sol", "SimpleStorage"); diff --git a/ethers-middleware/tests/transformer.rs b/ethers-middleware/tests/transformer.rs index 377de5037..26c2375db 100644 --- a/ethers-middleware/tests/transformer.rs +++ b/ethers-middleware/tests/transformer.rs @@ -19,7 +19,8 @@ fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) { let path = format!("./tests/solidity-contracts/{}", path); let compiled = Solc::default().compile_source(&path).unwrap(); let contract = compiled.get(&path, name).expect("could not find contract"); - (contract.abi.unwrap().clone(), contract.bin.unwrap().clone()) + let (abi, bin, _) = contract.into_parts_or_default(); + (abi, bin) } #[tokio::test] diff --git a/ethers-solc/src/artifacts.rs b/ethers-solc/src/artifacts.rs index 69df3ffec..f3c7d95e6 100644 --- a/ethers-solc/src/artifacts.rs +++ b/ethers-solc/src/artifacts.rs @@ -635,6 +635,14 @@ impl<'a> CompactContractRef<'a> { pub fn into_parts_or_default(self) -> (Abi, Bytes, Bytes) { CompactContract::from(self).into_parts_or_default() } + + pub fn bytecode(&self) -> Option<&Bytes> { + self.bin.as_ref().and_then(|bin| bin.as_bytes()) + } + + pub fn runtime_bytecode(&self) -> Option<&Bytes> { + self.bin_runtime.as_ref().and_then(|bin| bin.as_bytes()) + } } impl<'a> From<&'a Contract> for CompactContractRef<'a> { diff --git a/examples/contract_human_readable.rs b/examples/contract_human_readable.rs index 9829b0657..33b9724e4 100644 --- a/examples/contract_human_readable.rs +++ b/examples/contract_human_readable.rs @@ -54,7 +54,7 @@ async fn main() -> Result<()> { // 5. create a factory which will be used to deploy instances of the contract let factory = ContractFactory::new( contract.abi.unwrap().clone(), - contract.bin.unwrap().clone(), + contract.bytecode().unwrap().clone(), client.clone(), ); From 38dcba1aee651d1442696b73af6c4dd6e3705ea2 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Tue, 7 Dec 2021 01:00:33 +0100 Subject: [PATCH 4/8] rm check --- ethers-solc/benches/compile_many.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ethers-solc/benches/compile_many.rs b/ethers-solc/benches/compile_many.rs index ee2e9f5ad..b667a6b98 100644 --- a/ethers-solc/benches/compile_many.rs +++ b/ethers-solc/benches/compile_many.rs @@ -40,10 +40,6 @@ fn load_compiler_inputs() -> Vec { .take(5) { let file = file.unwrap(); - if file.path().to_string_lossy().as_ref().ends_with("20.json") { - // TODO needs support for parsing library placeholders first - continue - } let input = std::fs::read_to_string(file.path()).unwrap(); let input: CompilerInput = serde_json::from_str(&input).unwrap(); inputs.push(input); From 0c9db9d284f5795f1492f5cf8eddd793816e0541 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Tue, 7 Dec 2021 04:51:37 +0100 Subject: [PATCH 5/8] return Bytes instead --- ethers-solc/src/artifacts.rs | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/ethers-solc/src/artifacts.rs b/ethers-solc/src/artifacts.rs index f3c7d95e6..96979311b 100644 --- a/ethers-solc/src/artifacts.rs +++ b/ethers-solc/src/artifacts.rs @@ -840,19 +840,13 @@ impl BytecodeObject { /// Tries to resolve the unlinked string object a valid bytecode object in place /// /// Returns the string if it is a valid - pub fn resolve(&mut self) -> Option { - let mut resolved = None; - + pub fn resolve(&mut self) -> Option<&Bytes> { if let BytecodeObject::Unlinked(unlinked) = self { if let Ok(linked) = hex::decode(unlinked) { - resolved = Some(BytecodeObject::Bytecode(linked.into())); + *self = BytecodeObject::Bytecode(linked.into()); } } - if let Some(resolved) = resolved { - std::mem::replace(self, resolved).into_unlinked().map(LinkedByteCode) - } else { - None - } + self.as_bytes() } /// Link using the fully qualified name of a library @@ -916,16 +910,6 @@ impl AsRef<[u8]> for BytecodeObject { } } -/// A fully linked byte code that represents a valid hex string -#[derive(Debug, Clone)] -pub struct LinkedByteCode(String); - -impl From for Bytes { - fn from(inner: LinkedByteCode) -> Self { - hex::decode(&inner.0).expect("Is valid hex").into() - } -} - #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FunctionDebugData { From 47c081115154817d0d0f4260476a35cdb69b65bc Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Tue, 7 Dec 2021 04:54:32 +0100 Subject: [PATCH 6/8] revert changes --- ethers-solc/src/artifacts.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/ethers-solc/src/artifacts.rs b/ethers-solc/src/artifacts.rs index 96979311b..f3c7d95e6 100644 --- a/ethers-solc/src/artifacts.rs +++ b/ethers-solc/src/artifacts.rs @@ -840,13 +840,19 @@ impl BytecodeObject { /// Tries to resolve the unlinked string object a valid bytecode object in place /// /// Returns the string if it is a valid - pub fn resolve(&mut self) -> Option<&Bytes> { + pub fn resolve(&mut self) -> Option { + let mut resolved = None; + if let BytecodeObject::Unlinked(unlinked) = self { if let Ok(linked) = hex::decode(unlinked) { - *self = BytecodeObject::Bytecode(linked.into()); + resolved = Some(BytecodeObject::Bytecode(linked.into())); } } - self.as_bytes() + if let Some(resolved) = resolved { + std::mem::replace(self, resolved).into_unlinked().map(LinkedByteCode) + } else { + None + } } /// Link using the fully qualified name of a library @@ -910,6 +916,16 @@ impl AsRef<[u8]> for BytecodeObject { } } +/// A fully linked byte code that represents a valid hex string +#[derive(Debug, Clone)] +pub struct LinkedByteCode(String); + +impl From for Bytes { + fn from(inner: LinkedByteCode) -> Self { + hex::decode(&inner.0).expect("Is valid hex").into() + } +} + #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FunctionDebugData { From d063e3ddc01f3fc6ee0e7a8c9cb4c63bb25b7316 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Tue, 7 Dec 2021 04:56:01 +0100 Subject: [PATCH 7/8] simplify resolve --- ethers-solc/src/artifacts.rs | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/ethers-solc/src/artifacts.rs b/ethers-solc/src/artifacts.rs index f3c7d95e6..96979311b 100644 --- a/ethers-solc/src/artifacts.rs +++ b/ethers-solc/src/artifacts.rs @@ -840,19 +840,13 @@ impl BytecodeObject { /// Tries to resolve the unlinked string object a valid bytecode object in place /// /// Returns the string if it is a valid - pub fn resolve(&mut self) -> Option { - let mut resolved = None; - + pub fn resolve(&mut self) -> Option<&Bytes> { if let BytecodeObject::Unlinked(unlinked) = self { if let Ok(linked) = hex::decode(unlinked) { - resolved = Some(BytecodeObject::Bytecode(linked.into())); + *self = BytecodeObject::Bytecode(linked.into()); } } - if let Some(resolved) = resolved { - std::mem::replace(self, resolved).into_unlinked().map(LinkedByteCode) - } else { - None - } + self.as_bytes() } /// Link using the fully qualified name of a library @@ -916,16 +910,6 @@ impl AsRef<[u8]> for BytecodeObject { } } -/// A fully linked byte code that represents a valid hex string -#[derive(Debug, Clone)] -pub struct LinkedByteCode(String); - -impl From for Bytes { - fn from(inner: LinkedByteCode) -> Self { - hex::decode(&inner.0).expect("Is valid hex").into() - } -} - #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FunctionDebugData { From 2d48b355d098ab101c9bbdf37818c02480af4622 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Tue, 7 Dec 2021 20:51:24 +0100 Subject: [PATCH 8/8] test: add lost tests --- ethers-solc/src/artifacts.rs | 71 +++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/ethers-solc/src/artifacts.rs b/ethers-solc/src/artifacts.rs index 96979311b..2fb842387 100644 --- a/ethers-solc/src/artifacts.rs +++ b/ethers-solc/src/artifacts.rs @@ -856,7 +856,7 @@ impl BytecodeObject { /// This will replace all occurrences of the library placeholder with the given address. /// /// See also: https://docs.soliditylang.org/en/develop/using-the-compiler.html#library-linking - pub fn link_fully_qualified(&mut self, name: impl AsRef, addr: Address) { + pub fn link_fully_qualified(&mut self, name: impl AsRef, addr: Address) -> &mut Self { if let BytecodeObject::Unlinked(ref mut unlinked) = self { let name = name.as_ref(); let place_holder = utils::library_hash_placeholder(name); @@ -871,14 +871,33 @@ impl BytecodeObject { .replace(&format!("__{}__", fully_qualified_placeholder), &hex_addr) .replace(&format!("__{}__", place_holder), &hex_addr) } + self } /// Link using the `file` and `library` names as fully qualified name `:` /// See `BytecodeObject::link_fully_qualified` - pub fn link(&mut self, file: impl AsRef, library: impl AsRef, addr: Address) { + pub fn link( + &mut self, + file: impl AsRef, + library: impl AsRef, + addr: Address, + ) -> &mut Self { self.link_fully_qualified(format!("{}:{}", file.as_ref(), library.as_ref(),), addr) } + /// Links the bytecode object with all provided `(file, lib, addr)` + pub fn link_all(&mut self, libs: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + T: AsRef, + { + for (file, lib, addr) in libs.into_iter() { + self.link(file, lib, addr); + } + self + } + /// Whether this object is still unlinked pub fn is_unlinked(&self) -> bool { matches!(self, BytecodeObject::Unlinked(_)) @@ -1226,6 +1245,54 @@ mod tests { use super::*; use std::{fs, path::PathBuf}; + #[test] + fn can_link_bytecode() { + // test cases taken from https://github.com/ethereum/solc-js/blob/master/test/linker.js + + #[derive(Serialize, Deserialize)] + struct Mockject { + object: BytecodeObject, + } + fn parse_bytecode(bytecode: &str) -> BytecodeObject { + let object: Mockject = + serde_json::from_value(serde_json::json!({ "object": bytecode })).unwrap(); + object.object + } + + let bytecode = "6060604052341561000f57600080fd5b60f48061001d6000396000f300606060405260043610603e5763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166326121ff081146043575b600080fd5b3415604d57600080fd5b60536055565b005b73__lib2.sol:L____________________________6326121ff06040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160006040518083038186803b151560b357600080fd5b6102c65a03f4151560c357600080fd5b5050505600a165627a7a723058207979b30bd4a07c77b02774a511f2a1dd04d7e5d65b5c2735b5fc96ad61d43ae40029"; + + let mut object = parse_bytecode(bytecode); + assert!(object.is_unlinked()); + assert!(object.contains_placeholder("lib2.sol", "L")); + assert!(object.contains_fully_qualified_placeholder("lib2.sol:L")); + assert!(object.link("lib2.sol", "L", Address::random()).resolve().is_some()); + assert!(!object.is_unlinked()); + + let mut code = Bytecode { + function_debug_data: Default::default(), + object: parse_bytecode(bytecode), + opcodes: None, + source_map: None, + generated_sources: vec![], + link_references: BTreeMap::from([( + "lib2.sol".to_string(), + BTreeMap::from([("L".to_string(), vec![])]), + )]), + }; + + assert!(!code.link("lib2.sol", "Y", Address::random())); + assert!(code.link("lib2.sol", "L", Address::random())); + assert!(code.link("lib2.sol", "L", Address::random())); + + let hashed_placeholder = "6060604052341561000f57600080fd5b60f48061001d6000396000f300606060405260043610603e5763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166326121ff081146043575b600080fd5b3415604d57600080fd5b60536055565b005b73__$cb901161e812ceb78cfe30ca65050c4337$__6326121ff06040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160006040518083038186803b151560b357600080fd5b6102c65a03f4151560c357600080fd5b5050505600a165627a7a723058207979b30bd4a07c77b02774a511f2a1dd04d7e5d65b5c2735b5fc96ad61d43ae40029"; + let mut object = parse_bytecode(hashed_placeholder); + assert!(object.is_unlinked()); + assert!(object.contains_placeholder("lib2.sol", "L")); + assert!(object.contains_fully_qualified_placeholder("lib2.sol:L")); + assert!(object.link("lib2.sol", "L", Address::default()).resolve().is_some()); + assert!(!object.is_unlinked()); + } + #[test] fn can_parse_compiler_output() { let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));