Skip to content

Commit

Permalink
feat(forge build): add initcode size check (#9116)
Browse files Browse the repository at this point in the history
* Adds init code size limit check & column to table.

* Adds option to ignore init code size check during --size output.

* Updates tests with new column for --sizes table.

* Adds test helpers for forge CLI.

* Implements test for init code size limit as per EIP-3860

* Adds test for --ignore-eip-3860

* Fixes for Cargo +nightly fmt warnings.

* Refactors both contract size functions into one with a boolean arg.

* Adds alias for --ignore-eip-3860 to --ignore-initcode-size.

* Brings back the original comments.

* Update compile.rs

* Changes --ignore-eip-3860 to be a boolean field.

* Fixes ranges in table display code and comment punctuation.

* Moves testing helper to existing utils module.

* Improve ranges in table display code.

* Adds output assertions to initcode size check tests.

* Minor change to ranges in display logic for sizes table.

---------

Co-authored-by: mgiagante <251503-mgiagante@users.noreply.gitlab.com>
Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
  • Loading branch information
3 people authored Oct 19, 2024
1 parent a8c3e9c commit 8bdcbfa
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 42 deletions.
132 changes: 98 additions & 34 deletions crates/common/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ pub struct ProjectCompiler {
/// Whether to bail on compiler errors.
bail: Option<bool>,

/// Whether to ignore the contract initcode size limit introduced by EIP-3860.
ignore_eip_3860: bool,

/// Extra files to include, that are not necessarily in the project's source dir.
files: Vec<PathBuf>,
}
Expand All @@ -65,6 +68,7 @@ impl ProjectCompiler {
print_sizes: None,
quiet: Some(crate::shell::verbosity().is_silent()),
bail: None,
ignore_eip_3860: false,
files: Vec::new(),
}
}
Expand Down Expand Up @@ -114,6 +118,13 @@ impl ProjectCompiler {
self
}

/// Sets whether to ignore EIP-3860 initcode size limits.
#[inline]
pub fn ignore_eip_3860(mut self, yes: bool) -> Self {
self.ignore_eip_3860 = yes;
self
}

/// Sets extra files to include, that are not necessarily in the project's source dir.
#[inline]
pub fn files(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
Expand Down Expand Up @@ -232,7 +243,8 @@ impl ProjectCompiler {
.collect();

for (name, artifact) in artifacts {
let size = deployed_contract_size(artifact).unwrap_or_default();
let runtime_size = contract_size(artifact, false).unwrap_or_default();
let init_size = contract_size(artifact, true).unwrap_or_default();

let is_dev_contract = artifact
.abi
Expand All @@ -244,22 +256,32 @@ impl ProjectCompiler {
})
})
.unwrap_or(false);
size_report.contracts.insert(name, ContractInfo { size, is_dev_contract });
size_report
.contracts
.insert(name, ContractInfo { runtime_size, init_size, is_dev_contract });
}

println!("{size_report}");

// TODO: avoid process::exit
// exit with error if any contract exceeds the size limit, excluding test contracts.
if size_report.exceeds_size_limit() {
if size_report.exceeds_runtime_size_limit() {
std::process::exit(1);
}

// Check size limits only if not ignoring EIP-3860
if !self.ignore_eip_3860 && size_report.exceeds_initcode_size_limit() {
std::process::exit(1);
}
}
}
}

// https://eips.ethereum.org/EIPS/eip-170
const CONTRACT_SIZE_LIMIT: usize = 24576;
const CONTRACT_RUNTIME_SIZE_LIMIT: usize = 24576;

// https://eips.ethereum.org/EIPS/eip-3860
const CONTRACT_INITCODE_SIZE_LIMIT: usize = 49152;

/// Contracts with info about their size
pub struct SizeReport {
Expand All @@ -268,20 +290,34 @@ pub struct SizeReport {
}

impl SizeReport {
/// Returns the size of the largest contract, excluding test contracts.
pub fn max_size(&self) -> usize {
let mut max_size = 0;
for contract in self.contracts.values() {
if !contract.is_dev_contract && contract.size > max_size {
max_size = contract.size;
}
}
max_size
/// Returns the maximum runtime code size, excluding dev contracts.
pub fn max_runtime_size(&self) -> usize {
self.contracts
.values()
.filter(|c| !c.is_dev_contract)
.map(|c| c.runtime_size)
.max()
.unwrap_or(0)
}

/// Returns the maximum initcode size, excluding dev contracts.
pub fn max_init_size(&self) -> usize {
self.contracts
.values()
.filter(|c| !c.is_dev_contract)
.map(|c| c.init_size)
.max()
.unwrap_or(0)
}

/// Returns true if any contract exceeds the size limit, excluding test contracts.
pub fn exceeds_size_limit(&self) -> bool {
self.max_size() > CONTRACT_SIZE_LIMIT
/// Returns true if any contract exceeds the runtime size limit, excluding dev contracts.
pub fn exceeds_runtime_size_limit(&self) -> bool {
self.max_runtime_size() > CONTRACT_RUNTIME_SIZE_LIMIT
}

/// Returns true if any contract exceeds the initcode size limit, excluding dev contracts.
pub fn exceeds_initcode_size_limit(&self) -> bool {
self.max_init_size() > CONTRACT_INITCODE_SIZE_LIMIT
}
}

Expand All @@ -291,29 +327,49 @@ impl Display for SizeReport {
table.load_preset(ASCII_MARKDOWN);
table.set_header([
Cell::new("Contract").add_attribute(Attribute::Bold).fg(Color::Blue),
Cell::new("Size (B)").add_attribute(Attribute::Bold).fg(Color::Blue),
Cell::new("Margin (B)").add_attribute(Attribute::Bold).fg(Color::Blue),
Cell::new("Runtime Size (B)").add_attribute(Attribute::Bold).fg(Color::Blue),
Cell::new("Initcode Size (B)").add_attribute(Attribute::Bold).fg(Color::Blue),
Cell::new("Runtime Margin (B)").add_attribute(Attribute::Bold).fg(Color::Blue),
Cell::new("Initcode Margin (B)").add_attribute(Attribute::Bold).fg(Color::Blue),
]);

// filters out non dev contracts (Test or Script)
let contracts = self.contracts.iter().filter(|(_, c)| !c.is_dev_contract && c.size > 0);
// Filters out dev contracts (Test or Script)
let contracts = self
.contracts
.iter()
.filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0));
for (name, contract) in contracts {
let margin = CONTRACT_SIZE_LIMIT as isize - contract.size as isize;
let color = match contract.size {
0..=17999 => Color::Reset,
18000..=CONTRACT_SIZE_LIMIT => Color::Yellow,
let runtime_margin =
CONTRACT_RUNTIME_SIZE_LIMIT as isize - contract.runtime_size as isize;
let init_margin = CONTRACT_INITCODE_SIZE_LIMIT as isize - contract.init_size as isize;

let runtime_color = match contract.runtime_size {
..18_000 => Color::Reset,
18_000..=CONTRACT_RUNTIME_SIZE_LIMIT => Color::Yellow,
_ => Color::Red,
};

let init_color = match contract.init_size {
..36_000 => Color::Reset,
36_000..=CONTRACT_INITCODE_SIZE_LIMIT => Color::Yellow,
_ => Color::Red,
};

let locale = &Locale::en;
table.add_row([
Cell::new(name).fg(color),
Cell::new(contract.size.to_formatted_string(locale))
Cell::new(name).fg(Color::Blue),
Cell::new(contract.runtime_size.to_formatted_string(locale))
.set_alignment(CellAlignment::Right)
.fg(color),
Cell::new(margin.to_formatted_string(locale))
.fg(runtime_color),
Cell::new(contract.init_size.to_formatted_string(locale))
.set_alignment(CellAlignment::Right)
.fg(color),
.fg(init_color),
Cell::new(runtime_margin.to_formatted_string(locale))
.set_alignment(CellAlignment::Right)
.fg(runtime_color),
Cell::new(init_margin.to_formatted_string(locale))
.set_alignment(CellAlignment::Right)
.fg(init_color),
]);
}

Expand All @@ -322,9 +378,14 @@ impl Display for SizeReport {
}
}

/// Returns the size of the deployed contract
pub fn deployed_contract_size<T: Artifact>(artifact: &T) -> Option<usize> {
let bytecode = artifact.get_deployed_bytecode_object()?;
/// Returns the deployed or init size of the contract.
fn contract_size<T: Artifact>(artifact: &T, initcode: bool) -> Option<usize> {
let bytecode = if initcode {
artifact.get_bytecode_object()?
} else {
artifact.get_deployed_bytecode_object()?
};

let size = match bytecode.as_ref() {
BytecodeObject::Bytecode(bytes) => bytes.len(),
BytecodeObject::Unlinked(unlinked) => {
Expand All @@ -338,14 +399,17 @@ pub fn deployed_contract_size<T: Artifact>(artifact: &T) -> Option<usize> {
size / 2
}
};

Some(size)
}

/// How big the contract is and whether it is a dev contract where size limits can be neglected
#[derive(Clone, Copy, Debug)]
pub struct ContractInfo {
/// size of the contract in bytes
pub size: usize,
/// Size of the runtime code in bytes
pub runtime_size: usize,
/// Size of the initcode in bytes
pub init_size: usize,
/// A development contract is either a Script or a Test contract.
pub is_dev_contract: bool,
}
Expand Down
10 changes: 10 additions & 0 deletions crates/forge/bin/cmd/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ pub struct BuildArgs {
#[serde(skip)]
pub sizes: bool,

/// Ignore initcode contract bytecode size limit introduced by EIP-3860.
#[arg(long, alias = "ignore-initcode-size")]
#[serde(skip)]
pub ignore_eip_3860: bool,

#[command(flatten)]
#[serde(flatten)]
pub args: CoreBuildArgs,
Expand Down Expand Up @@ -102,6 +107,7 @@ impl BuildArgs {
.files(files)
.print_names(self.names)
.print_sizes(self.sizes)
.ignore_eip_3860(self.ignore_eip_3860)
.quiet(self.format_json)
.bail(!self.format_json);

Expand Down Expand Up @@ -158,6 +164,10 @@ impl Provider for BuildArgs {
dict.insert("sizes".to_string(), true.into());
}

if self.ignore_eip_3860 {
dict.insert("ignore_eip_3860".to_string(), true.into());
}

Ok(Map::from([(Config::selected_profile(), dict)]))
}
}
Expand Down
33 changes: 30 additions & 3 deletions crates/forge/tests/cli/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::utils::generate_large_contract;
use foundry_config::Config;
use foundry_test_utils::{forgetest, snapbox::IntoData, str};
use globset::Glob;
Expand Down Expand Up @@ -42,6 +43,32 @@ contract Dummy {
"#]].is_json());
});

forgetest!(initcode_size_exceeds_limit, |prj, cmd| {
prj.add_source("LargeContract", generate_large_contract(5450).as_str()).unwrap();
cmd.args(["build", "--sizes"]).assert_failure().stdout_eq(str![
r#"
...
| Contract | Runtime Size (B) | Initcode Size (B) | Runtime Margin (B) | Initcode Margin (B) |
|--------------|------------------|-------------------|--------------------|---------------------|
| HugeContract | 202 | 49,359 | 24,374 | -207 |
...
"#
]);
});

forgetest!(initcode_size_limit_can_be_ignored, |prj, cmd| {
prj.add_source("LargeContract", generate_large_contract(5450).as_str()).unwrap();
cmd.args(["build", "--sizes", "--ignore-eip-3860"]).assert_success().stdout_eq(str![
r#"
...
| Contract | Runtime Size (B) | Initcode Size (B) | Runtime Margin (B) | Initcode Margin (B) |
|--------------|------------------|-------------------|--------------------|---------------------|
| HugeContract | 202 | 49,359 | 24,374 | -207 |
...
"#
]);
});

// tests build output is as expected
forgetest_init!(exact_build_output, |prj, cmd| {
cmd.args(["build", "--force"]).assert_success().stdout_eq(str![[r#"
Expand All @@ -57,9 +84,9 @@ forgetest_init!(build_sizes_no_forge_std, |prj, cmd| {
cmd.args(["build", "--sizes"]).assert_success().stdout_eq(str![
r#"
...
| Contract | Size (B) | Margin (B) |
|----------|----------|------------|
| Counter | 247 | 24,329 |
| Contract | Runtime Size (B) | Initcode Size (B) | Runtime Margin (B) | Initcode Margin (B) |
|----------|------------------|-------------------|--------------------|---------------------|
| Counter | 247 | 277 | 24,329 | 48,875 |
...
"#
]);
Expand Down
6 changes: 3 additions & 3 deletions crates/forge/tests/cli/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2186,9 +2186,9 @@ forgetest_init!(can_build_sizes_repeatedly, |prj, cmd| {
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!
| Contract | Size (B) | Margin (B) |
|----------|----------|------------|
| Counter | 247 | 24,329 |
| Contract | Runtime Size (B) | Initcode Size (B) | Runtime Margin (B) | Initcode Margin (B) |
|----------|------------------|-------------------|--------------------|---------------------|
| Counter | 247 | 277 | 24,329 | 48,875 |
"#]]);
Expand Down
29 changes: 27 additions & 2 deletions crates/forge/tests/cli/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ impl EnvExternalities {
pub fn parse_deployed_address(out: &str) -> Option<String> {
for line in out.lines() {
if line.starts_with("Deployed to") {
return Some(line.trim_start_matches("Deployed to: ").to_string())
return Some(line.trim_start_matches("Deployed to: ").to_string());
}
}
None
Expand All @@ -135,8 +135,33 @@ pub fn parse_deployed_address(out: &str) -> Option<String> {
pub fn parse_verification_guid(out: &str) -> Option<String> {
for line in out.lines() {
if line.contains("GUID") {
return Some(line.replace("GUID:", "").replace('`', "").trim().to_string())
return Some(line.replace("GUID:", "").replace('`', "").trim().to_string());
}
}
None
}

// Generates a string containing the code of a Solidity contract
// with a variable init code size.
pub fn generate_large_contract(num_elements: usize) -> String {
let mut contract_code = String::new();

contract_code.push_str(
"// Auto-generated Solidity contract to inflate initcode size\ncontract HugeContract {\n uint256 public number;\n"
);

contract_code.push_str(" uint256[] public largeArray;\n\n constructor() {\n");
contract_code.push_str(" largeArray = [");

for i in 0..num_elements {
if i != 0 {
contract_code.push_str(", ");
}
contract_code.push_str(&i.to_string());
}

contract_code.push_str("];\n");
contract_code.push_str(" }\n}");

contract_code
}

0 comments on commit 8bdcbfa

Please sign in to comment.