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: checks on upper bounds of contract storage sizes #169

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions crates/cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ pub struct UnstableFeatures {
#[cfg(test)]
#[arg(long)]
test_value: Option<usize>,

#[arg(long)]
pub storage_sz_ub: bool,
Copy link
Member

Choose a reason for hiding this comment

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

please move to above help

}

/// How errors and other messages are produced.
Expand Down
1 change: 1 addition & 0 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
TilakMaddy marked this conversation as resolved.
Show resolved Hide resolved
sess.jobs = NonZeroUsize::new(args.threads)
.unwrap_or_else(|| std::thread::available_parallelism().unwrap_or(NonZeroUsize::MIN));
if !args.input.is_empty()
Expand Down
3 changes: 3 additions & 0 deletions crates/interface/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
81 changes: 80 additions & 1 deletion crates/sema/src/typeck/mod.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -11,13 +12,91 @@ 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]);
}),
);
}

/// 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 {
Copy link
Member

Choose a reason for hiding this comment

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

let's call it like "print-contract-storage-sizes"

Copy link
Contributor Author

@TilakMaddy TilakMaddy Dec 15, 2024

Choose a reason for hiding this comment

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

We only check the upper bound of storage size, so I'm afraid that wouldn't be accurate depiction of what users are looking for.

for example:
If there's

contract C {
  int128 a;
  int128 b;
}

Currently, this totals to "2", which is less than 2^256 so passes the check. (this check is pre-condition to make sure the slot allocation algorithm will work properly later)

Whereas someone wanting to print storage size would expect 128 + 128 = 256 bits so 1 slot. (because of how solidity allocates the slots)

If we go with this, do we make another flag for the actual contract storage size then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@DaniPopes isn't this more like "print-contract-max-storage-sizes" ?

Copy link
Member

Choose a reason for hiding this comment

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

@DaniPopes isn't this more like "print-contract-max-storage-sizes" ?

sure

Copy link
Member

Choose a reason for hiding this comment

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

If we go with this, do we make another flag for the actual contract storage size then?

This probably will go with the actual storage implementation later on, the "storage-layout" output

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<U256> {
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 {
Expand Down
41 changes: 41 additions & 0 deletions tests/ui/typeck/contract_storage_size_check.sol
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions tests/ui/typeck/contract_storage_size_check.stderr
Original file line number Diff line number Diff line change
@@ -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
Loading