diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index f5534871..311b6b9c 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -138,6 +138,9 @@ pub struct UnstableFeatures { #[cfg(test)] #[arg(long)] test_value: Option, + + #[arg(long)] + pub storage_sz_ub: bool, } /// How errors and other messages are produced. diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index f9ad5a96..6ce0cf9e 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -138,6 +138,7 @@ fn run_compiler_with(args: Args, f: impl FnOnce(&Compiler) -> Result + Send) -> sess.stop_after = args.stop_after; sess.dump = args.unstable.dump.clone(); sess.ast_stats = args.unstable.ast_stats; + sess.storage_sz_ub = args.unstable.storage_sz_ub; sess.jobs = NonZeroUsize::new(args.threads) .unwrap_or_else(|| std::thread::available_parallelism().unwrap_or(NonZeroUsize::MIN)); if !args.input.is_empty() diff --git a/crates/interface/src/session.rs b/crates/interface/src/session.rs index faa2576c..b5ab5529 100644 --- a/crates/interface/src/session.rs +++ b/crates/interface/src/session.rs @@ -45,6 +45,9 @@ pub struct Session { /// Whether to emit AST stats. #[builder(default)] pub ast_stats: bool, + /// Whether to emit contract's storage size upper bound + #[builder(default)] + pub storage_sz_ub: bool, } impl SessionBuilder { diff --git a/crates/sema/src/typeck/mod.rs b/crates/sema/src/typeck/mod.rs index c2bde199..a33f8201 100644 --- a/crates/sema/src/typeck/mod.rs +++ b/crates/sema/src/typeck/mod.rs @@ -1,8 +1,9 @@ use crate::{ ast_lowering::resolve::{Declaration, Declarations}, hir::{self, Res}, - ty::{Gcx, Ty}, + ty::{Gcx, Ty, TyKind}, }; +use alloy_primitives::U256; use rayon::prelude::*; use solar_data_structures::{map::FxHashSet, parallel}; @@ -11,6 +12,7 @@ pub(crate) fn check(gcx: Gcx<'_>) { gcx.sess, gcx.hir.par_contract_ids().for_each(|id| { check_duplicate_definitions(gcx, &gcx.symbol_resolver.contract_scopes[id]); + check_storage_size_upper_bound(gcx, id); }), gcx.hir.par_source_ids().for_each(|id| { check_duplicate_definitions(gcx, &gcx.symbol_resolver.source_scopes[id]); @@ -18,6 +20,83 @@ pub(crate) fn check(gcx: Gcx<'_>) { ); } +/// Checks for violation of maximum storage size to ensure slot allocation algorithms works. +/// Reference: https://github.com/ethereum/solidity/blob/03e2739809769ae0c8d236a883aadc900da60536/libsolidity/analysis/ContractLevelChecker.cpp#L556C1-L570C2 +fn check_storage_size_upper_bound(gcx: Gcx<'_>, contract_id: hir::ContractId) { + let contract_span = gcx.hir.contract(contract_id).span; + let contract_items = gcx.hir.contract_items(contract_id); + let mut total_size = U256::ZERO; + for item in contract_items { + if let hir::Item::Variable(variable) = item { + // Skip constant and immutable variables + if variable.mutability.is_none() { + let t = gcx.type_of_hir_ty(&variable.ty); + match ty_upper_bound_storage_var_size(t, gcx) + .and_then(|size_contribution| total_size.checked_add(size_contribution)) + { + Some(sz) => { + total_size = sz; + } + None => { + gcx.dcx() + .err("contract requires too much storage") + .span(contract_span) + .emit(); + return; + } + } + } + } + } + + if gcx.sess.storage_sz_ub { + let full_contract_name = format!("{}", gcx.contract_fully_qualified_name(contract_id)); + eprintln!("{full_contract_name} requires {total_size} maximum storage"); + } +} + +fn ty_upper_bound_storage_var_size(ty: Ty<'_>, gcx: Gcx<'_>) -> Option { + match ty.kind { + TyKind::Elementary(..) + | TyKind::StringLiteral(..) + | TyKind::IntLiteral(..) + | TyKind::Mapping(..) + | TyKind::Contract(..) + | TyKind::Udvt(..) + | TyKind::Enum(..) + | TyKind::DynArray(..) => Some(U256::from(1)), + TyKind::Ref(..) + | TyKind::Tuple(..) + | TyKind::FnPtr(..) + | TyKind::Module(..) + | TyKind::BuiltinModule(..) + | TyKind::Event(..) + | TyKind::Meta(..) + | TyKind::Err(..) + | TyKind::Error(..) => { + unreachable!() + } + TyKind::Array(ty, uint) => { + // Reference: https://github.com/ethereum/solidity/blob/03e2739809769ae0c8d236a883aadc900da60536/libsolidity/ast/Types.cpp#L1800C1-L1806C2 + let elem_size = ty_upper_bound_storage_var_size(ty, gcx)?; + uint.checked_mul(elem_size) + } + TyKind::Struct(struct_id) => { + let strukt = gcx.hir.strukt(struct_id); + // Reference https://github.com/ethereum/solidity/blob/03e2739809769ae0c8d236a883aadc900da60536/libsolidity/ast/Types.cpp#L2303C1-L2309C2 + let mut total_size = U256::from(1); + for field_id in strukt.fields { + let variable = gcx.hir.variable(*field_id); + let t = gcx.type_of_hir_ty(&variable.ty); + let size_contribution = ty_upper_bound_storage_var_size(t, gcx)?; + total_size = total_size.checked_add(size_contribution)?; + } + Some(total_size) + } + TyKind::Type(ty) => ty_upper_bound_storage_var_size(ty, gcx), + } +} + /// Checks for definitions that have the same name and parameter types in the given scope. fn check_duplicate_definitions(gcx: Gcx<'_>, scope: &Declarations) { let is_duplicate = |a: Declaration, b: Declaration| -> bool { diff --git a/tests/ui/typeck/contract_storage_size_check.sol b/tests/ui/typeck/contract_storage_size_check.sol new file mode 100644 index 00000000..9a3a6d58 --- /dev/null +++ b/tests/ui/typeck/contract_storage_size_check.sol @@ -0,0 +1,41 @@ +//@ compile-flags: -Zstorage-sz-ub + +struct Person { + string name; + uint age; +} + +contract B { + uint c; + bool x; +} + +// Total = 1 + 1 = 2 + +contract A { + uint256 a; // 1 + bool b; // 1 + B c; // 1 + Person e; // 1 + 2 fields = 3 + int128 f; // 1 + Person[] g; // 1 + Person[23] h; // 23 * 3 = 69 +} + +// Total = 1 + 1 + 1 + 3 + 1 + 1 + 69 = 77 + +contract M { + struct P1 { + string first; + string middle; + string last; + } + + P1 my; // 4 + mapping(string => uint256) public a; // 1 + P1[] public b; // 1 + bool c; // 1 + B d; // 1 +} + +// Total = 4 + 1 + 1 + 1 + 1 = 8 diff --git a/tests/ui/typeck/contract_storage_size_check.stderr b/tests/ui/typeck/contract_storage_size_check.stderr new file mode 100644 index 00000000..4a6e2dd3 --- /dev/null +++ b/tests/ui/typeck/contract_storage_size_check.stderr @@ -0,0 +1,3 @@ +ROOT/tests/ui/typeck/contract_storage_size_check.sol:B requires 2 maximum storage +ROOT/tests/ui/typeck/contract_storage_size_check.sol:A requires 77 maximum storage +ROOT/tests/ui/typeck/contract_storage_size_check.sol:M requires 8 maximum storage