Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions crates/resolc/src/cli_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ pub const SOLIDITY_LIBRARY_CALL_CONTRACT_PATH: &str = "src/tests/data/solidity/l
pub const SOLIDITY_COLLISION_NESTED_PATH: &str = "src/tests/data/solidity/collision/a/c.sol";
/// The paritytech/revive#622 reproducer contract at `a_c.sol`.
pub const SOLIDITY_COLLISION_UNDERSCORE_PATH: &str = "src/tests/data/solidity/collision/a_c.sol";
/// The verbatim reproducer from paritytech/revive#631: a contract creating another contract.
pub const SOLIDITY_FACTORY_DEPENDENCY_NESTED_OBJECT_PATH: &str =
"src/tests/data/solidity/factory_dependency_nested_object.sol";
/// The verbatim reproducer from paritytech/revive#632: two contracts calling the same library.
pub const SOLIDITY_LINK_INDEPENDENT_OBJECTS_PATH: &str =
"src/tests/data/solidity/link_independent_objects.sol";
Expand All @@ -74,10 +77,15 @@ pub const YUL_DUPLICATE_FUNCTIONS_DEEP_NESTING_PATH: &str =
pub const YUL_EMPTY_RUNTIME_OBJECT_PATH: &str = "src/tests/data/yul/empty_runtime_object.yul";
/// Yul contract carrying deploy code only, without the `_deployed` runtime sub-object.
pub const YUL_DEPLOY_ONLY_OBJECT_PATH: &str = "src/tests/data/yul/deploy_only_object.yul";
/// The solc IR of [`SOLIDITY_FACTORY_DEPENDENCY_NESTED_OBJECT_PATH`], with the dependency as a nested object.
pub const YUL_FACTORY_DEPENDENCY_NESTED_OBJECT_PATH: &str =
"src/tests/data/yul/factory_dependency_nested_object.yul";
/// Yul contract with a sibling object that is not a contract of its own.
pub const YUL_SIBLING_OBJECTS_PATH: &str = "src/tests/data/yul/sibling_objects.yul";
/// Yul contract addressing a nested object through the dotted notation.
pub const YUL_DOTTED_OBJECT_PATH: &str = "src/tests/data/yul/dotted_object_path.yul";
/// Yul contract with two different nested objects of the same name in different scopes.
pub const YUL_DUPLICATE_OBJECT_NAME_PATH: &str = "src/tests/data/yul/duplicate_object_name.yul";

/// The standard JSON contracts test fixture path.
pub const STANDARD_JSON_CONTRACTS_PATH: &str =
Expand All @@ -101,6 +109,15 @@ pub const STANDARD_JSON_NO_EVM_CODEGEN_COMPLEX_PATH: &str =
/// The verbatim reproducer from paritytech/revive#627: a library address used only in a switch expression.
pub const STANDARD_JSON_SWITCH_MISSING_LIBRARIES_PATH: &str =
"src/tests/data/standard_json/switch_missing_libraries.json";
/// The verbatim reproducer from paritytech/revive#631: a factory dependency outside the output selection.
pub const STANDARD_JSON_FACTORY_DEPENDENCY_OUTSIDE_OUTPUT_SELECTION_PATH: &str =
"src/tests/data/standard_json/factory_dependency_outside_output_selection.json";
/// A factory dependency outside the output selection calling a library.
pub const STANDARD_JSON_FACTORY_DEPENDENCY_OUTSIDE_OUTPUT_SELECTION_LIBRARIES_PATH: &str =
"src/tests/data/standard_json/factory_dependency_outside_output_selection_libraries.json";
/// A factory dependency outside the output selection whose Yul name is a contract name in the creator's file.
pub const STANDARD_JSON_FACTORY_DEPENDENCY_PATH_COLLISION_PATH: &str =
"src/tests/data/standard_json/factory_dependency_path_collision.json";
/// The standard JSON fixture remapping `target/` to `b_first/` and then to `a_second/`.
pub const STANDARD_JSON_REMAPPINGS_ORDER_PATH: &str =
"src/tests/data/standard_json/remappings_order.json";
Expand Down
38 changes: 34 additions & 4 deletions crates/resolc/src/project/contract/ir/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
//! The contract source code.

use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashSet;

use serde::Deserialize;
use serde::Serialize;

use revive_yul::parser::statement::object::Object;

use self::newyork::NewYork;
use self::yul::Yul;

Expand All @@ -31,15 +33,43 @@ impl IR {
/// Drains the list of factory dependencies.
pub fn drain_factory_dependencies(&mut self) -> BTreeSet<String> {
match self {
IR::Yul(ref mut yul) => yul.object.factory_dependencies.drain().collect(),
IR::Yul(ref mut yul) => std::mem::take(&mut yul.object.factory_dependencies),
IR::NewYork(ref mut newyork) => {
newyork.yul_object.factory_dependencies.drain().collect()
std::mem::take(&mut newyork.yul_object.factory_dependencies)
}
}
.into_keys()
.collect()
}

/// Returns the nested contract objects missing in `identifier_paths`, by identifier.
///
/// Only objects with their own `_deployed` runtime object are contracts, as solc emits them.
pub fn unresolved_factory_dependencies(
&self,
identifier_paths: &BTreeMap<String, String>,
) -> Vec<(String, Self)> {
self.factory_dependencies()
.iter()
.filter(|(identifier, object)| {
object.explicit_runtime_code && !identifier_paths.contains_key(identifier.as_str())
})
.map(|(identifier, object)| {
let ir = match self {
IR::Yul(_) => Self::Yul(Yul {
object: object.to_owned(),
}),
IR::NewYork(_) => Self::NewYork(NewYork {
yul_object: object.to_owned(),
}),
};
(identifier.to_owned(), ir)
})
.collect()
}

/// Returns the identifiers of the factory dependencies.
pub fn factory_dependencies(&self) -> &HashSet<String> {
pub fn factory_dependencies(&self) -> &BTreeMap<String, Object> {
match self {
IR::Yul(yul) => &yul.object.factory_dependencies,
IR::NewYork(newyork) => &newyork.yul_object.factory_dependencies,
Expand Down
16 changes: 16 additions & 0 deletions crates/resolc/src/project/contract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,22 @@ impl Contract {
}
}

/// Returns the nested factory dependencies missing in `identifier_paths` as contracts.
pub fn unresolved_factory_dependencies(
&self,
identifier_paths: &BTreeMap<String, String>,
) -> Vec<Self> {
self.ir
.unresolved_factory_dependencies(identifier_paths)
.into_iter()
.map(|(identifier, ir)| {
let identifier =
ContractIdentifier::new(self.identifier.path.to_owned(), Some(identifier));
Self::new(identifier, ir, serde_json::Value::Null)
})
.collect()
}

/// Compiles the specified contract, setting its build artifacts.
pub fn compile(
self,
Expand Down
47 changes: 36 additions & 11 deletions crates/resolc/src/project/mod.rs
Comment thread
elle-j marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,51 @@ pub struct Project {

impl Project {
/// A shortcut constructor.
///
/// Nested factory dependency objects not otherwise part of `contracts` are added as contracts.
/// A nested object whose path is already taken by another contract is an error.
pub fn new(
version: Option<SolcVersion>,
contracts: BTreeMap<String, Contract>,
mut contracts: BTreeMap<String, Contract>,
libraries: SolcStandardJsonInputSettingsLibraries,
) -> Self {
) -> anyhow::Result<Self> {
let mut identifier_paths = BTreeMap::new();
for (path, contract) in contracts.iter() {
identifier_paths.insert(contract.object_identifier().to_owned(), path.to_owned());
}

Self {
let mut unresolved_contracts: Vec<Contract> = contracts
.values()
.flat_map(|contract| contract.unresolved_factory_dependencies(&identifier_paths))
.collect();
while let Some(contract) = unresolved_contracts.pop() {
if identifier_paths.contains_key(contract.object_identifier()) {
continue;
}
if contracts.contains_key(contract.identifier.full_path.as_str()) {
anyhow::bail!(
"the nested object `{}` can not be emitted as `{}`, which is the path of another \
contract. Adding the source file of `{}` to the output selection avoids this.",
contract.object_identifier(),
contract.identifier.full_path,
contract.object_identifier(),
);
}
identifier_paths.insert(
contract.object_identifier().to_owned(),
contract.identifier.full_path.to_owned(),
);
unresolved_contracts
.extend(contract.unresolved_factory_dependencies(&identifier_paths));
contracts.insert(contract.identifier.full_path.to_owned(), contract);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A resolved contract will have the key <file>:<Solidity name>, while an unresolved factory dependency will have the key <parent file>:<dependency's Yul name> (the parent being the file that imports the dependency). So contract.identifier.full_path could collide here, and this line would then replace the first contract stored at the shared key, with the second contract stored at the same key.

This example shows how there can be 2 different contracts getting the key B.sol:K_4:

Example:

  • File A.sol:
    • Declares contract K.
      • Gets AST ID 4. Yul name: K_4.
  • File B.sol (imports A.sol):
    • Declares contract K_4.
      • K_4's full path: B.sol:K_4 (i.e. <file>:<Solidity name>)
    • Declares contract P which contains:
      • new K() (from the dependency)
        • K's full path: B.sol:K_4 (i.e. <parent file>:<dependency's Yul name>)

Output contracts["B.sol"]["K_4"] now contains contract K from A.sol.

{"language":"Solidity","sources":{"A.sol":{"content":"contract K { uint256 public v = 7; }"},"B.sol":{"content":"import \"A.sol\"; contract K_4 { function w() external pure returns (uint256) { return 42; } } contract P { function mk() external returns (address) { return address(new K()); } function mk4() external returns (address) { return address(new K_4()); } }"}},"settings":{"optimizer":{"enabled":true},"outputSelection":{"B.sol":{"*":["evm.bytecode"]}}}}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch thanks. Just errors out now

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still unresolved. For instance, you can compare the previous JSON repro (which selects only B.sol in the outputSelection) with the same file with the only change being selectiong both A.sol and B.sol:

repro.json (selects B.sol):

{"language":"Solidity","sources":{"A.sol":{"content":"contract K { uint256 public v = 7; }"},"B.sol":{"content":"import \"A.sol\"; contract K_4 { function w() external pure returns (uint256) { return 42; } } contract P { function mk() external returns (address) { return address(new K()); } function mk4() external returns (address) { return address(new K_4()); } }"}},"settings":{"optimizer":{"enabled":true},"outputSelection":{"B.sol":{"*":["evm.bytecode"]}}}}

repro-all.json (selects A.sol and B.sol):

{"language":"Solidity","sources":{"A.sol":{"content":"contract K { uint256 public v = 7; }"},"B.sol":{"content":"import \"A.sol\"; contract K_4 { function w() external pure returns (uint256) { return 42; } } contract P { function mk() external returns (address) { return address(new K()); } function mk4() external returns (address) { return address(new K_4()); } }"}},"settings":{"optimizer":{"enabled":true},"outputSelection":{"A.sol":{"*":["evm.bytecode"]},"B.sol":{"*":["evm.bytecode"]}}}}
for input in repro.json repro-all.json; do
  resolc --standard-json < $input | jq -c '.contracts | {"A.sol:K": .["A.sol"].K.hash[0:8], "B.sol:K_4": .["B.sol"].K_4.hash[0:8]}'
done

# Output (solc v0.8.36 used):
# {"A.sol:K":null,"B.sol:K_4":"55af49df"}
# {"A.sol:K":"55af49df","B.sol:K_4":"b96a268a"}

With both files selected (second output row), it shows that A.sol:K hash is 55af49df. For the repro with only B.sol selected (first output row), it shows that B.sol:K_4 hash is also 55af49df (i.e. B.sol's K_4 was replaced by A.sol's K, and the new K_4() in contract P would deploy the wrong contract).

Possible fix (reaches the error from #600):

crates/resolc/src/project/mod.rs:

 while let Some(contract) = unresolved_contracts.pop() {
-    if identifier_paths.contains_key(contract.object_identifier()) {
+    if identifier_paths.contains_key(contract.object_identifier())
+        || contracts.contains_key(&contract.identifier.full_path)
+    {
         continue;
     }

}

Ok(Self {
version,
contracts,
identifier_paths,
libraries,
}
})
}

/// Compiles all contracts, returning their build artifacts.
Expand Down Expand Up @@ -175,7 +204,7 @@ impl Project {
contract
.ir
.factory_dependencies()
.iter()
.keys()
.filter_map(|identifier| self.identifier_paths.get(identifier))
.map(String::as_str),
);
Expand Down Expand Up @@ -259,7 +288,7 @@ impl Project {
},
}
}
Ok(Self::new(None, contracts, libraries))
Self::new(None, contracts, libraries)
}

/// Converts the `solc` JSON output into a convenient project.
Expand Down Expand Up @@ -310,11 +339,7 @@ impl Project {
Err(error) => solc_output.push_error(Some(path), error),
}
}
Ok(Project::new(
Some(solc_version.clone()),
contracts,
libraries,
))
Project::new(Some(solc_version.clone()), contracts, libraries)
}
}

Expand Down
79 changes: 77 additions & 2 deletions crates/resolc/src/tests/cli/standard_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ use crate::cli_utils::{
execute_solc_with_stdin_input, STANDARD_JSON_ALL_OUTPUTS_PATH, STANDARD_JSON_CONTRACTS_PATH,
STANDARD_JSON_DEBUG_INFO_DEFAULT_PATH, STANDARD_JSON_DEBUG_INFO_EMPTY_PATH,
STANDARD_JSON_FACTORY_DEPENDENCY_LIBRARIES_DETECT_PATH,
STANDARD_JSON_FACTORY_DEPENDENCY_LIBRARIES_PATH, STANDARD_JSON_HEAP_SIZE_LINKER_FAILURE_PATH,
STANDARD_JSON_HEAP_SIZE_OUT_OF_RANGE_PATH, STANDARD_JSON_IMMUTABLES_OVER_LIMIT_PATH,
STANDARD_JSON_FACTORY_DEPENDENCY_LIBRARIES_PATH,
STANDARD_JSON_FACTORY_DEPENDENCY_OUTSIDE_OUTPUT_SELECTION_LIBRARIES_PATH,
STANDARD_JSON_FACTORY_DEPENDENCY_OUTSIDE_OUTPUT_SELECTION_PATH,
STANDARD_JSON_FACTORY_DEPENDENCY_PATH_COLLISION_PATH,
STANDARD_JSON_HEAP_SIZE_LINKER_FAILURE_PATH, STANDARD_JSON_HEAP_SIZE_OUT_OF_RANGE_PATH,
STANDARD_JSON_IMMUTABLES_OVER_LIMIT_PATH,
STANDARD_JSON_MIX_ALL_WILDCARD_AND_FILE_SELECTION_PATH, STANDARD_JSON_NEWYORK_DISABLED_PATH,
STANDARD_JSON_NEWYORK_ENABLED_PATH, STANDARD_JSON_NO_EVM_CODEGEN_COMPLEX_PATH,
STANDARD_JSON_NO_EVM_CODEGEN_PATH, STANDARD_JSON_NO_PVM_CODEGEN_PER_FILE_PATH,
Expand Down Expand Up @@ -846,6 +850,77 @@ fn switch_expression_missing_libraries() {
}
}

/// A factory dependency outside the output selection is compiled too.
#[test]
fn factory_dependency_outside_output_selection() {
let result = execute_resolc_with_stdin_input(
&[JSON_OPTION],
STANDARD_JSON_FACTORY_DEPENDENCY_OUTSIDE_OUTPUT_SELECTION_PATH,
);
assert_command_success(
&result,
"the factory dependency outside output selection fixture",
);
assert_no_errors(&to_solc_standard_json_output(&result.stdout));

let output: serde_json::Value = serde_json::from_str(&result.stdout).unwrap();
let factory_dependencies = output["contracts"]["A.sol"]["P"]["factoryDependencies"]
.as_object()
.expect("the contract `A.sol:P` should have factory dependencies");
assert_eq!(factory_dependencies.len(), 1);
for dependency in factory_dependencies.values() {
let (path, name) = dependency.as_str().unwrap().split_once(':').unwrap();
assert!(
output["contracts"][path][name]["evm"]["bytecode"]["object"]
.as_str()
.is_some_and(|bytecode| !bytecode.is_empty()),
"the dependency `{dependency}` should be compiled"
);
}
}

/// Missing libraries of a created contract outside the output selection are reported for its creator.
#[test]
fn factory_dependency_outside_output_selection_missing_libraries() {
for arguments in [
vec![JSON_OPTION],
vec![JSON_OPTION, "--detect-missing-libraries"],
] {
let result = execute_resolc_with_stdin_input(
&arguments,
STANDARD_JSON_FACTORY_DEPENDENCY_OUTSIDE_OUTPUT_SELECTION_LIBRARIES_PATH,
);
assert_command_success(
&result,
"the factory dependency outside output selection libraries fixture",
);
assert_no_errors(&to_solc_standard_json_output(&result.stdout));

let output: serde_json::Value = serde_json::from_str(&result.stdout).unwrap();
assert_eq!(
output["contracts"]["A.sol"]["P"]["missingLibraries"],
serde_json::json!(["B.sol:L"]),
"`A.sol:P` should miss the library `B.sol:L` with arguments {arguments:?}"
);
}
}

/// A factory dependency outside the output selection must not replace a contract of the same path.
#[test]
fn factory_dependency_path_collision() {
let result = execute_resolc_with_stdin_input(
&[JSON_OPTION],
STANDARD_JSON_FACTORY_DEPENDENCY_PATH_COLLISION_PATH,
);
assert_command_success(&result, "the factory dependency path collision fixture");

let output = to_solc_standard_json_output(&result.stdout);
assert_standard_json_errors_contain(
&output,
"`B.sol:K_4`, which is the path of another contract",
);
}

/// The last of two remappings for the same prefix wins, as in solc.
#[test]
fn remappings_last_one_wins() {
Expand Down
24 changes: 23 additions & 1 deletion crates/resolc/src/tests/cli/yul.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::cli_utils::{
assert_command_failure, assert_command_success, assert_equal_exit_codes, execute_resolc,
execute_solc, RESOLC_YUL_FLAG, SOLC_YUL_FLAG, YUL_CONTRACT_PATH, YUL_DEPLOY_ONLY_OBJECT_PATH,
YUL_DUPLICATE_FUNCTIONS_DEEP_NESTING_PATH, YUL_DUPLICATE_FUNCTIONS_SWITCH_PATH,
YUL_INVALID_HEX_NIBBLES_PATH,
YUL_FACTORY_DEPENDENCY_NESTED_OBJECT_PATH, YUL_INVALID_HEX_NIBBLES_PATH,
};

#[test]
Expand Down Expand Up @@ -75,3 +75,25 @@ fn compiles_object_without_a_runtime_sub_object() {
let solc_result = execute_solc(&[YUL_DEPLOY_ONLY_OBJECT_PATH, SOLC_YUL_FLAG]);
assert_equal_exit_codes(&solc_result, &resolc_result);
}

/// A factory dependency given only as a nested object is compiled too.
#[test]
fn compiles_factory_dependency_nested_object() {
let resolc_result = execute_resolc(&[
YUL_FACTORY_DEPENDENCY_NESTED_OBJECT_PATH,
RESOLC_YUL_FLAG,
"--bin",
]);
assert_command_success(&resolc_result, "A factory dependency as a nested object");
for object in ["K_4", "P_19"] {
assert!(
resolc_result.stdout.contains(&format!(
"{YUL_FACTORY_DEPENDENCY_NESTED_OBJECT_PATH}:{object}"
)),
"the object `{object}` should be compiled"
);
}

let solc_result = execute_solc(&[YUL_FACTORY_DEPENDENCY_NESTED_OBJECT_PATH, SOLC_YUL_FLAG]);
assert_equal_exit_codes(&solc_result, &resolc_result);
}
15 changes: 14 additions & 1 deletion crates/resolc/src/tests/cli/yul_objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

use crate::cli_utils::{
absolute_path, assert_command_failure, execute_resolc, CommandResult, RESOLC_YUL_FLAG,
YUL_DOTTED_OBJECT_PATH, YUL_SIBLING_OBJECTS_PATH,
YUL_DOTTED_OBJECT_PATH, YUL_DUPLICATE_OBJECT_NAME_PATH, YUL_SIBLING_OBJECTS_PATH,
};

/// Asserts the command failed with a diagnostic rather than by aborting.
Expand Down Expand Up @@ -47,3 +47,16 @@ fn reports_the_dotted_object_notation() {
result.stderr
);
}

#[test]
fn reports_a_duplicate_object_name() {
let path = absolute_path(YUL_DUPLICATE_OBJECT_NAME_PATH);
let result = execute_resolc(&[&path, RESOLC_YUL_FLAG, "--bin"]);

assert_reported_not_aborted(&result, "Two different objects of the same name");
assert!(
result.stderr.contains("DuplicateObjectName"),
"the object should be named in the diagnostic, got: {}",
result.stderr
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
contract K { uint256 public v = 7; }
contract P { function mk() external returns (address) { return address(new K()); } }
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"language":"Solidity","sources":{"A.sol":{"content":"import \"B.sol\"; contract P { function mk() external returns (address) { return address(new K()); } }"},"B.sol":{"content":"contract K { uint256 public v = 7; }"}},"settings":{"optimizer":{"enabled":true},"outputSelection":{"A.sol":{"*":["evm.bytecode"]}}}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"language":"Solidity","sources":{"A.sol":{"content":"import \"B.sol\"; contract P { function mk() external returns (address) { return address(new K()); } }"},"B.sol":{"content":"library L { function f(uint256 x) external pure returns (uint256) { return x * 3; } } contract K { function g(uint256 x) external pure returns (uint256) { return L.f(x); } }"}},"settings":{"optimizer":{"enabled":true},"outputSelection":{"A.sol":{"*":["evm.bytecode"]}}}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"language":"Solidity","sources":{"A.sol":{"content":"contract K { uint256 public v = 7; }"},"B.sol":{"content":"import \"A.sol\"; contract K_4 { function w() external pure returns (uint256) { return 42; } } contract P { function mk() external returns (address) { return address(new K()); } function mk4() external returns (address) { return address(new K_4()); } }"}},"settings":{"optimizer":{"enabled":true},"outputSelection":{"B.sol":{"*":["evm.bytecode"]}}}}
Loading
Loading