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 10 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
88 changes: 88 additions & 0 deletions crates/sema/src/typeck/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::{
ast_lowering::resolve::{Declaration, Declarations},
eval::ConstantEvaluator,
hir::{self, Res},
ty::{Gcx, Ty},
};
use alloy_primitives::U256;
use rayon::prelude::*;
use solar_data_structures::{map::FxHashSet, parallel};

Expand All @@ -15,9 +17,95 @@ pub(crate) fn check(gcx: Gcx<'_>) {
gcx.hir.par_source_ids().for_each(|id| {
check_duplicate_definitions(gcx, &gcx.symbol_resolver.source_scopes[id]);
}),
gcx.hir.par_contract_ids().for_each(|id| {
check_storage_size_upper_bound(gcx, id);
TilakMaddy marked this conversation as resolved.
Show resolved Hide resolved
}),
);
}

/// 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 Some(size_contribution) = variable_ty_upper_bound_size(&variable.ty.kind, gcx)
TilakMaddy marked this conversation as resolved.
Show resolved Hide resolved
else {
gcx.dcx().err("contract requires too much storage").span(contract_span).emit();
return;
};
let Some(sz) = total_size.checked_add(size_contribution) else {
gcx.dcx().err("contract requires too much storage").span(contract_span).emit();
TilakMaddy marked this conversation as resolved.
Show resolved Hide resolved
return;
};
total_size = sz;
}
}
}
if cfg!(debug_assertions) {
DaniPopes marked this conversation as resolved.
Show resolved Hide resolved
let full_contract_name = format!("{}", gcx.contract_fully_qualified_name(contract_id));
if full_contract_name.contains("contract_storage_size_check") {
eprintln!("{full_contract_name} requires {total_size} maximum storage");
}
}
}

fn item_ty_upper_bound_size(item: &hir::Item<'_, '_>, gcx: Gcx<'_>) -> Option<U256> {
match item {
hir::Item::Function(_) | hir::Item::Contract(_) => Some(U256::from(1)),
hir::Item::Variable(variable) => variable_ty_upper_bound_size(&variable.ty.kind, gcx),
hir::Item::Udvt(udvt) => variable_ty_upper_bound_size(&udvt.ty.kind, gcx),
hir::Item::Struct(strukt) => {
// 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 size_contribution = variable_ty_upper_bound_size(&variable.ty.kind, gcx)?;
total_size = total_size.checked_add(size_contribution)?;
}
Some(total_size)
}
hir::Item::Enum(_) | hir::Item::Event(_) | hir::Item::Error(_) => {
// Enum and events cannot be types of storage variables
unreachable!("illegal values")
}
}
}

fn variable_ty_upper_bound_size(var_ty: &hir::TypeKind<'_>, gcx: Gcx<'_>) -> Option<U256> {
match &var_ty {
hir::TypeKind::Elementary(_) | hir::TypeKind::Function(_) | hir::TypeKind::Mapping(_) => {
Some(U256::from(1))
}
hir::TypeKind::Array(array) => {
// Reference: https://github.com/ethereum/solidity/blob/03e2739809769ae0c8d236a883aadc900da60536/libsolidity/ast/Types.cpp#L1800C1-L1806C2
if let Some(len_expr) = array.size {
// Evaluate the length expression in array declaration
let mut e = ConstantEvaluator::new(gcx);
let arr_len = e.eval(len_expr).unwrap().data; // `.eval()` emits errors beforehand

// Estimate the upper bound size of each individual element
let elem_size = variable_ty_upper_bound_size(&array.element.kind, gcx)?;
arr_len.checked_mul(elem_size)
} else {
// For dynamic size arrays
Some(U256::from(1))
}
}
hir::TypeKind::Custom(item_id) => {
let item = gcx.hir.item(*item_id);
item_ty_upper_bound_size(&item, gcx)
}
hir::TypeKind::Err(_) => {
unreachable!()
}
}
}

/// 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
39 changes: 39 additions & 0 deletions tests/ui/typeck/contract_storage_size_check.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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