Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(forge build): add initcode size check #9116

Merged
merged 24 commits into from
Oct 19, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
18d52cd
Adds init code size limit check & column to table.
Oct 14, 2024
8a70f92
Adds option to ignore init code size check during --size output.
Oct 14, 2024
9c72b63
Updates tests with new column for --sizes table.
Oct 14, 2024
34d615c
Adds test helpers for forge CLI.
Oct 15, 2024
c423a95
Implements test for init code size limit as per EIP-3860
Oct 15, 2024
53b856d
Adds test for --ignore-eip-3860
Oct 15, 2024
b0b1ca4
Merge remote-tracking branch 'upstream/master' into feature/add-initc…
Oct 15, 2024
ab8af7e
Fixes for Cargo +nightly fmt warnings.
Oct 15, 2024
235de94
Merge branch 'master' into feature/add-initcode-size-check
mgiagante Oct 15, 2024
4d8b7b6
Merge branch 'master' into feature/add-initcode-size-check
mgiagante Oct 15, 2024
7ed3992
Refactors both contract size functions into one with a boolean arg.
Oct 15, 2024
dc15893
Adds alias for --ignore-eip-3860 to --ignore-initcode-size.
Oct 15, 2024
70d1c76
Brings back the original comments.
Oct 15, 2024
f41b08f
Update compile.rs
zerosnacks Oct 15, 2024
3c092a0
Merge remote-tracking branch 'upstream/master' into feature/add-initc…
Oct 16, 2024
eca4e1a
Changes --ignore-eip-3860 to be a boolean field.
Oct 16, 2024
ab4751d
Fixes ranges in table display code and comment punctuation.
Oct 16, 2024
d5b9928
Moves testing helper to existing utils module.
Oct 16, 2024
3682274
Improve ranges in table display code.
Oct 16, 2024
24133b6
Adds output assertions to initcode size check tests.
Oct 16, 2024
80212db
Merge branch 'master' into feature/add-initcode-size-check
mgiagante Oct 16, 2024
cae5767
Minor change to ranges in display logic for sizes table.
Oct 16, 2024
a247b69
Merge branch 'master' into feature/add-initcode-size-check
mgiagante Oct 17, 2024
7c91f9d
Merge branch 'master' into feature/add-initcode-size-check
mgiagante Oct 18, 2024
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
133 changes: 97 additions & 36 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: Option<bool>,
mgiagante marked this conversation as resolved.
Show resolved Hide resolved

/// 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: None,
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 = Some(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.unwrap_or(false) && 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
zerosnacks marked this conversation as resolved.
Show resolved Hide resolved
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 {
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 {
0..=17999 => Color::Reset,
mgiagante marked this conversation as resolved.
Show resolved Hide resolved
18000..=CONTRACT_SIZE_LIMIT => Color::Yellow,
18000..=CONTRACT_RUNTIME_SIZE_LIMIT => Color::Yellow,
mgiagante marked this conversation as resolved.
Show resolved Hide resolved
_ => Color::Red,
};

let init_color = match contract.init_size {
0..=35999 => Color::Reset,
36000..=CONTRACT_INITCODE_SIZE_LIMIT => Color::Yellow,
mgiagante marked this conversation as resolved.
Show resolved Hide resolved
_ => 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,30 +378,35 @@ 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()?
DaniPopes marked this conversation as resolved.
Show resolved Hide resolved
} else {
artifact.get_deployed_bytecode_object()?
};

let size = match bytecode.as_ref() {
BytecodeObject::Bytecode(bytes) => bytes.len(),
BytecodeObject::Unlinked(unlinked) => {
// we don't need to account for placeholders here, because library placeholders take up
mgiagante marked this conversation as resolved.
Show resolved Hide resolved
// 40 characters: `__$<library hash>$__` which is the same as a 20byte address in hex.
let mut size = unlinked.as_bytes().len();
if unlinked.starts_with("0x") {
size -= 2;
}
// hex -> bytes
mgiagante marked this conversation as resolved.
Show resolved Hide resolved
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
mgiagante marked this conversation as resolved.
Show resolved Hide resolved
#[arg(long, alias = "ignore-initcode-size")]
#[serde(skip)]
pub ignore_eip_3860: bool,
mgiagante marked this conversation as resolved.
Show resolved Hide resolved

#[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
18 changes: 15 additions & 3 deletions crates/forge/tests/cli/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::test_helpers::generate_large_contract;

mgiagante marked this conversation as resolved.
Show resolved Hide resolved
use foundry_config::Config;
use foundry_test_utils::{forgetest, snapbox::IntoData, str};
use globset::Glob;
Expand Down Expand Up @@ -42,6 +44,16 @@ 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();
});

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();
});
mgiagante marked this conversation as resolved.
Show resolved Hide resolved

// 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 +69,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
1 change: 1 addition & 0 deletions crates/forge/tests/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod script;
mod soldeer;
mod svm;
mod test_cmd;
mod test_helpers;
mod verify;
mod verify_bytecode;

Expand Down
26 changes: 26 additions & 0 deletions crates/forge/tests/cli/test_helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! Test helpers for Forge CLI tests.
mgiagante marked this conversation as resolved.
Show resolved Hide resolved

// This function 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
}
Loading