Skip to content

Commit

Permalink
Bugfix struct member read access (#1563)
Browse files Browse the repository at this point in the history
Fixes a bug: `StorageLoad` after a struct member access is a storage
read, this case was not considered in the mutability check.

The regression in the EVM test is fine; it's caused by a [problem in
solc](ethereum/solidity#11573)

Signed-off-by: Cyrill Leutwiler <[email protected]>
Signed-off-by: Sean Young <[email protected]>
  • Loading branch information
seanyoung authored Oct 18, 2023
1 parent 8e23aed commit a9e20d3
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 92 deletions.
140 changes: 55 additions & 85 deletions src/sema/mutability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use super::{
Builtin, CallTy, DestructureField, Diagnostic, Expression, Function, Mutability, Namespace,
RetrieveType, Statement, Type,
},
diagnostics::Diagnostics,
yul::ast::{YulExpression, YulStatement},
Recurse,
};
Expand All @@ -16,7 +17,7 @@ use bitflags::bitflags;
use solang_parser::pt::Loc;
use solang_parser::{helpers::CodeLocation, pt};

#[derive(PartialEq, PartialOrd)]
#[derive(Clone, Copy, Hash, Eq, PartialEq, PartialOrd)]
enum Access {
None,
Read,
Expand Down Expand Up @@ -49,16 +50,16 @@ pub fn mutability(file_no: usize, ns: &mut Namespace) {
continue;
}

let mut diagnostics = check_mutability(func, ns);
let diagnostics = check_mutability(func, ns);

ns.diagnostics.append(&mut diagnostics);
ns.diagnostics.extend(diagnostics);
}
}
}

/// While we recurse through the AST, maintain some state
struct StateCheck<'a> {
diagnostics: Vec<Diagnostic>,
diagnostic: Diagnostics,
declared_access: Access,
required_access: Access,
func: &'a Function,
Expand All @@ -69,91 +70,65 @@ struct StateCheck<'a> {

impl<'a> StateCheck<'a> {
fn value(&mut self, loc: &pt::Loc) {
if self.declared_access != Access::Value {
if let Some(modifier_loc) = &self.modifier {
self.diagnostics.push(Diagnostic::error_with_note(
*modifier_loc,
format!(
"function declared '{}' but modifier accesses value sent, which is only allowed for payable functions",
self.func.mutability
),
*loc,
"access of value sent".into()
));
} else {
self.diagnostics.push(Diagnostic::error(
*loc,
format!(
"function declared '{}' but this expression accesses value sent, which is only allowed for payable functions",
self.func.mutability
),
));
}
}

self.check_level(loc, Access::Value);
self.required_access.increase_to(Access::Value);
}

fn write(&mut self, loc: &pt::Loc) {
if self.declared_access < Access::Write {
if let Some(modifier_loc) = &self.modifier {
self.diagnostics.push(Diagnostic::error_with_note(
*modifier_loc,
format!(
"function declared '{}' but modifier writes to state",
self.func.mutability
),
*loc,
"write to state".into(),
));
} else {
self.diagnostics.push(Diagnostic::error(
*loc,
format!(
"function declared '{}' but this expression writes to state",
self.func.mutability
),
));
}
}

self.check_level(loc, Access::Write);
self.required_access.increase_to(Access::Write);
}

fn read(&mut self, loc: &pt::Loc) {
if self.declared_access < Access::Read {
if let Some(modifier_loc) = &self.modifier {
self.diagnostics.push(Diagnostic::error_with_note(
*modifier_loc,
format!(
"function declared '{}' but modifier reads from state",
self.func.mutability
),
*loc,
"read to state".into(),
));
} else {
self.diagnostics.push(Diagnostic::error(
*loc,
format!(
"function declared '{}' but this expression reads from state",
self.func.mutability
),
));
}
self.check_level(loc, Access::Read);
self.required_access.increase_to(Access::Read);
}

/// Compare the declared access level to the desired access level.
/// If there is an access violation, it'll be reported to the diagnostics.
fn check_level(&mut self, loc: &pt::Loc, desired: Access) {
if self.declared_access >= desired {
return;
}

self.required_access.increase_to(Access::Read);
let (message, note) = match desired {
Access::Read => ("reads from state", "read to state"),
Access::Write => ("writes to state", "write to state"),
Access::Value => (
"accesses value sent, which is only allowed for payable functions",
"access of value sent",
),
Access::None => unreachable!("desired access can't be None"),
};

let diagnostic = self
.modifier
.map(|modifier_loc| {
let message = format!(
"function declared '{}' but modifier {}",
self.func.mutability, message
);
Diagnostic::error_with_note(modifier_loc, message, *loc, note.into())
})
.unwrap_or_else(|| {
let message = format!(
"function declared '{}' but this expression {}",
self.func.mutability, message
);
Diagnostic::error(*loc, message)
});

self.diagnostic.push(diagnostic);
}
}

fn check_mutability(func: &Function, ns: &Namespace) -> Vec<Diagnostic> {
fn check_mutability(func: &Function, ns: &Namespace) -> Diagnostics {
if func.is_virtual {
return Vec::new();
return Default::default();
}

let mut state = StateCheck {
diagnostics: Vec::new(),
diagnostic: Default::default(),
declared_access: match func.mutability {
Mutability::Pure(_) => Access::None,
Mutability::View(_) => Access::Read,
Expand Down Expand Up @@ -213,13 +188,13 @@ fn check_mutability(func: &Function, ns: &Namespace) -> Vec<Diagnostic> {
match func.mutability {
Mutability::Payable(_) | Mutability::Pure(_) => (),
Mutability::Nonpayable(_) => {
state.diagnostics.push(Diagnostic::warning(
state.diagnostic.push(Diagnostic::warning(
func.loc,
"function can be declared 'pure'".to_string(),
));
}
_ => {
state.diagnostics.push(Diagnostic::warning(
state.diagnostic.push(Diagnostic::warning(
func.loc,
format!(
"function declared '{}' can be declared 'pure'",
Expand All @@ -232,7 +207,7 @@ fn check_mutability(func: &Function, ns: &Namespace) -> Vec<Diagnostic> {

// don't suggest marking payable as view (declared_access == Value)
if state.required_access == Access::Read && state.declared_access == Access::Write {
state.diagnostics.push(Diagnostic::warning(
state.diagnostic.push(Diagnostic::warning(
func.loc,
"function can be declared 'view'".to_string(),
));
Expand Down Expand Up @@ -271,7 +246,7 @@ fn check_mutability(func: &Function, ns: &Namespace) -> Vec<Diagnostic> {
);
}

state.diagnostics
state.diagnostic
}

fn recurse_statements(stmts: &[Statement], ns: &Namespace, state: &mut StateCheck) {
Expand Down Expand Up @@ -359,12 +334,15 @@ fn recurse_statements(stmts: &[Statement], ns: &Namespace, state: &mut StateChec

fn read_expression(expr: &Expression, state: &mut StateCheck) -> bool {
match expr {
Expression::StorageLoad { loc, .. } => {
state.data_account |= DataAccountUsage::READ;
state.read(loc)
}
Expression::PreIncrement { expr, .. }
| Expression::PreDecrement { expr, .. }
| Expression::PostIncrement { expr, .. }
| Expression::PostDecrement { expr, .. } => {
expr.recurse(state, write_expression);
return false;
}
Expression::Assign { left, right, .. } => {
right.recurse(state, read_expression);
Expand All @@ -374,14 +352,6 @@ fn read_expression(expr: &Expression, state: &mut StateCheck) -> bool {
Expression::StorageArrayLength { loc, .. } => {
state.data_account |= DataAccountUsage::READ;
state.read(loc);
return false;
}
Expression::Subscript { loc, array_ty, .. } if array_ty.is_contract_storage() => {
state.data_account |= DataAccountUsage::READ;
state.read(loc);
}
Expression::Variable { ty, .. } if ty.is_contract_storage() => {
state.data_account |= DataAccountUsage::READ;
}
Expression::StorageVariable { loc, .. } => {
state.data_account |= DataAccountUsage::READ;
Expand Down
22 changes: 19 additions & 3 deletions src/sema/statements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,14 +419,16 @@ fn statement(
pt::Statement::Block {
statements,
unchecked,
..
loc,
} => {
symtable.new_scope();
let mut reachable = true;

let mut context = context.clone();
context.unchecked |= *unchecked;

let mut resolved_stmts = Vec::new();

for stmt in statements {
if !reachable {
ns.diagnostics.push(Diagnostic::error(
Expand All @@ -435,11 +437,25 @@ fn statement(
));
return Err(());
}
reachable = statement(stmt, res, &mut context, symtable, loops, ns, diagnostics)?;
reachable = statement(
stmt,
&mut resolved_stmts,
&mut context,
symtable,
loops,
ns,
diagnostics,
)?;
}

symtable.leave_scope();

res.push(Statement::Block {
loc: *loc,
unchecked: *unchecked,
statements: resolved_stmts,
});

Ok(reachable)
}
pt::Statement::Break(loc) => {
Expand Down Expand Up @@ -2001,7 +2017,7 @@ fn return_with_values(
diagnostics,
ResolveTo::Type(&return_ty),
)?;
let expr = expr.cast(loc, &return_ty, true, ns, diagnostics)?;
let expr = expr.cast(&expr_return.loc(), &return_ty, true, ns, diagnostics)?;
used_variable(ns, &expr, symtable);
exprs.push(expr);
}
Expand Down
2 changes: 1 addition & 1 deletion tests/contract_testcases/evm/builtins/address_code_01.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ contract UpgradeableProxy {
}

// ---- Expect: diagnostics ----
// error: 5:9-38: conversion from bytes to uint256 not possible
// error: 5:16-38: conversion from bytes to uint256 not possible
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ contract test {
}
}
// ---- Expect: diagnostics ----
// error: 4:17-33: implicit conversion from enum test.state to uint8 not allowed
// error: 4:24-33: implicit conversion from enum test.state to uint8 not allowed
21 changes: 21 additions & 0 deletions tests/contract_testcases/polkadot/functions/mutability_13.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
library StorageSlot {
struct AddressSlot {
address value;
}

function getAddressSlot(
bytes32 slot
) internal pure returns (AddressSlot storage r) {
assembly {
r.slot := slot
}
}
}

contract StorageKeys {
function get(bytes32 key) public view returns (address a) {
a = StorageSlot.getAddressSlot(key).value;
}
}

// ---- Expect: diagnostics ----
15 changes: 15 additions & 0 deletions tests/contract_testcases/polkadot/functions/mutability_14.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
contract Foo {
address a;

function r() internal view returns (address) {
return msg.sender;
}

function foo() public pure {
a = r();
}
}

// ---- Expect: diagnostics ----
// error: 9:9-10: function declared 'pure' but this expression writes to state
// error: 9:13-16: function declared 'pure' but this expression reads from state
16 changes: 16 additions & 0 deletions tests/contract_testcases/polkadot/functions/mutability_15.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
contract C {
string s;

function test(string calldata x) public pure returns (string memory) {
s = "foo";
s = x;
print(s);
return s;
}
}

// ---- Expect: diagnostics ----
// error: 5:9-10: function declared 'pure' but this expression writes to state
// error: 6:9-10: function declared 'pure' but this expression writes to state
// error: 7:15-16: function declared 'pure' but this expression reads from state
// error: 8:16-17: function declared 'pure' but this expression reads from state
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
}
}
// ---- Expect: diagnostics ----
// error: 4:17-28: implicit conversion to address from contract b not allowed
// error: 4:24-28: implicit conversion to address from contract b not allowed
2 changes: 1 addition & 1 deletion tests/contract_testcases/solana/negative_exponent.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ contract c {

// ---- Expect: diagnostics ----
// warning: 6:16-26: ethereum currency unit used while targeting Solana
// error: 9:2-20: conversion to uint256 from rational not allowed
// error: 9:9-20: conversion to uint256 from rational not allowed
3 changes: 3 additions & 0 deletions tests/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ fn ethereum_solidity_tests() {
// FIXME: others listed explicitly cause panics and need fixing
if !file_name.ends_with("max_depth_reached_4.sol")
&& !file_name.ends_with("invalid_utf8_sequence.sol")
// Bug in solc: https://github.com/ethereum/solidity/issues/11573
&& !file_name
.ends_with("internal_library_function_attached_to_string_accepting_storage.sol")
&& file_name.ends_with(".sol")
{
Some(entry)
Expand Down

0 comments on commit a9e20d3

Please sign in to comment.