Skip to content

Commit

Permalink
simplify calls & condition reentrancy
Browse files Browse the repository at this point in the history
  • Loading branch information
rachel-bousfield committed Sep 11, 2023
1 parent 04afecc commit b0a27c1
Show file tree
Hide file tree
Showing 12 changed files with 308 additions and 220 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ rust-version = "1.71.0"
[workspace.dependencies]
alloy-primitives = { version = "0.3.1", default-features = false , features = ["native-keccak"] }
alloy-sol-types = { version = "0.3.1", default-features = false }
cfg-if = "1.0.0"
derivative = { version = "2.2.0", features = ["use_core"] }
hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
keccak-const = "0.2.0"
Expand Down
2 changes: 2 additions & 0 deletions stylus-proc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ version.workspace = true
[dependencies]
alloy-primitives.workspace = true
alloy-sol-types.workspace = true
cfg-if.workspace = true
convert_case.workspace = true
lazy_static.workspace = true
proc-macro2.workspace = true
Expand All @@ -25,6 +26,7 @@ quote.workspace = true
[features]
export-abi = []
storage-cache = []
reentrant = []

[package.metadata.docs.rs]
all-features = true
Expand Down
3 changes: 1 addition & 2 deletions stylus-proc/src/calls/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2023, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md

use crate::types::solidity_type_info;
use convert_case::{Case, Casing};
use proc_macro::TokenStream;
use proc_macro2::Ident;
Expand All @@ -9,8 +10,6 @@ use sha3::{Digest, Keccak256};
use std::borrow::Cow;
use syn_solidity::{FunctionAttribute, Item, Mutability, SolIdent, Visibility};

use crate::types::solidity_type_info;

pub fn sol_interface(input: TokenStream) -> TokenStream {
let input = match syn_solidity::parse(input) {
Ok(f) => f,
Expand Down
71 changes: 28 additions & 43 deletions stylus-proc/src/methods/entrypoint.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
// Copyright 2023, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md

use cfg_if::cfg_if;
use proc_macro::TokenStream;
use proc_macro2::Ident;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, Item, LitBool, Result, Token,
};
use syn::{parse_macro_input, Item};

pub fn entrypoint(attr: TokenStream, input: TokenStream) -> TokenStream {
let args: EntrypointArgs = parse_macro_input!(attr);
let input: Item = parse_macro_input!(input);
let allow_reentrancy = args.allow_reentrancy;

if !attr.is_empty() {
error!(Span::mixed_site(), "this macro is not configurable");
}

let mut output = quote! { #input };

Expand Down Expand Up @@ -59,13 +59,27 @@ pub fn entrypoint(attr: TokenStream, input: TokenStream) -> TokenStream {
_ => error!(input, "not a struct or fn"),
};

#[cfg(feature = "storage-cache")]
let flush_cache = quote! {
stylus_sdk::storage::StorageCache::flush();
};
// revert on reentrancy unless explicitly enabled
cfg_if! {
if #[cfg(feature = "reentrant")] {
let deny_reentrant = quote! {};
} else {
let deny_reentrant = quote! {
if stylus_sdk::msg::reentrant() {
return 1; // revert
}
};
}
}

#[cfg(not(feature = "storage-cache"))]
let flush_cache = quote! {};
// flush the cache before program exit
cfg_if! {
if #[cfg(feature = "storage-cache")] {
let flush_cache = quote! { stylus_sdk::storage::StorageCache::flush(); };
} else {
let flush_cache = quote! {};
}
}

output.extend(quote! {
#[no_mangle]
Expand All @@ -76,12 +90,7 @@ pub fn entrypoint(attr: TokenStream, input: TokenStream) -> TokenStream {

#[no_mangle]
pub extern "C" fn user_entrypoint(len: usize) -> usize {
if !#allow_reentrancy && stylus_sdk::msg::reentrant() {
return 1; // revert on reentrancy
}
if #allow_reentrancy {
unsafe { stylus_sdk::call::opt_into_reentrancy() };
}
#deny_reentrant

let input = stylus_sdk::contract::args(len);
let (data, status) = match #user(input) {
Expand All @@ -96,27 +105,3 @@ pub fn entrypoint(attr: TokenStream, input: TokenStream) -> TokenStream {

output.into()
}

struct EntrypointArgs {
allow_reentrancy: bool,
}

impl Parse for EntrypointArgs {
fn parse(input: ParseStream) -> Result<Self> {
let mut allow_reentrancy = false;

while !input.is_empty() {
let ident: Ident = input.parse()?;
let _: Token![=] = input.parse()?;

match ident.to_string().as_str() {
"allow_reentrancy" => {
let lit: LitBool = input.parse()?;
allow_reentrancy = lit.value;
}
_ => error!(@ident, "Unknown entrypoint attribute"),
}
}
Ok(Self { allow_reentrancy })
}
}
2 changes: 2 additions & 0 deletions stylus-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ version.workspace = true
[dependencies]
alloy-primitives.workspace = true
alloy-sol-types.workspace = true
cfg-if.workspace = true
derivative.workspace = true
hex = { workspace = true, default-features = false, features = ["alloc"] }
keccak-const.workspace = true
Expand Down Expand Up @@ -41,3 +42,4 @@ debug = []
docs = []
hostio = []
storage-cache = ["fnv", "stylus-proc/storage-cache"]
reentrant = ["stylus-proc/reentrant"]
140 changes: 96 additions & 44 deletions stylus-sdk/src/call/context.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
// Copyright 2022-2023, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md

use super::{CallContext, MutatingCallContext, NonPayableCallContext, StaticCallContext};
use crate::storage::TopLevelStorage;

use super::{CallContext, MutatingCallContext, NonPayableCallContext, StaticCallContext};
use alloy_primitives::U256;
use cfg_if::cfg_if;

/// Enables configurable calls to other contracts.
#[derive(Debug, Clone)]
pub struct Context<S, const HAS_VALUE: bool = false> {
pub struct Call<S, const HAS_VALUE: bool = false> {
gas: u64,
value: U256,
_storage: S,
value: Option<U256>,
storage: S,
}

impl Context<(), false> {
/// Begin configuring a call, similar to how [`RawCall`](super::RawCall) and [`std::fs::OpenOptions`] work.
impl<'a, S: TopLevelStorage> Call<&'a mut S, false>
where
S: TopLevelStorage + 'a,
{
/// Similar to [`new`], but intended for projects and libraries using reentrant patterns.
///
/// [`new_in`] safeguards persistent storage by requiring a reference to a [`TopLevelStorage`] `struct`.
///
/// Recall that [`TopLevelStorage`] is special in that a reference to it represents access to the entire
/// contract's state. So that it's sound to [`flush`] or [`clear`] the [`StorageCache`] when calling out
/// to other contracts, calls that may induce reentrancy require an `&` or `&mut` to one.
///
/// ```no_run
/// use stylus_sdk::call::{Context, Error};
/// use stylus_sdk::call::{Call, Error};
/// use stylus_sdk::{prelude::*, evm, msg, alloy_primitives::Address};
/// extern crate alloc;
///
Expand All @@ -29,47 +40,70 @@ impl Context<(), false> {
///
/// pub fn do_call(
/// storage: &mut impl TopLevelStorage, // can be generic, but often just &mut self
/// account: IService,
/// account: IService, // serializes as an Address
/// user: Address,
/// ) -> Result<String, Error> {
///
/// let context = Context::new()
/// .mutate(storage) // make the call mutable (usually one passes self)
/// .gas(evm::gas_left() / 2) // limit to half the gas left
/// .value(msg::value()); // set the callvalue
/// let context = Call::new_in(storage)
/// .gas(evm::gas_left() / 2) // limit to half the gas left
/// .value(msg::value()); // set the callvalue
///
/// account.make_payment(context, user) // note the snake case
/// }
/// ```
pub fn new() -> Self {
///
/// Projects that opt out of the [`StorageCache`] by disabling the `storage-cache` feature
/// may ignore this method.
///
/// [`flush`]: StorageCache::flush
/// [`clear`]: StorageCache::clear
pub fn new_in(storage: &'a mut S) -> Self {
Self {
gas: u64::MAX,
value: U256::ZERO,
_storage: (),
value: None,
storage,
}
}
}

impl Default for Context<(), false> {
impl Default for Call<(), false> {
fn default() -> Self {
Self::new()
}
}

impl<S, const HAS_VALUE: bool> Context<S, HAS_VALUE> {
/// Assigns a [`TopLevelStorage`] so that mutable methods can be called.
/// Note: enabling mutation will prevent calls to `pure` and `view` methods.
pub fn mutate<NewS: TopLevelStorage>(
self,
storage: &mut NewS,
) -> Context<&mut NewS, HAS_VALUE> {
Context {
gas: self.gas,
value: self.value,
_storage: storage,
impl Call<(), false> {
/// Begin configuring a call, similar to how [`RawCall`](super::RawCall) and [`std::fs::OpenOptions`] work.
///
/// ```ignore
/// use stylus_sdk::call::{Call, Error};
/// use stylus_sdk::{prelude::*, evm, msg, alloy_primitives::Address};
/// extern crate alloc;
///
/// sol_interface! {
/// interface IService {
/// function makePayment(address user) payable returns (string);
/// }
/// }
///
/// pub fn do_call(account: IService, user: Address) -> Result<String, Error> {
/// let context = Call::new()
/// .gas(evm::gas_left() / 2) // limit to half the gas left
/// .value(msg::value()); // set the callvalue
///
/// account.make_payment(context, user) // note the snake case
/// }
/// ```
pub fn new() -> Self {
Self {
gas: u64::MAX,
value: None,
storage: (),
}
}
}

impl<S, const HAS_VALUE: bool> Call<S, HAS_VALUE> {
/// Amount of gas to supply the call.
/// Values greater than the amount provided will be clipped to all gas left.
pub fn gas(self, gas: u64) -> Self {
Expand All @@ -78,33 +112,21 @@ impl<S, const HAS_VALUE: bool> Context<S, HAS_VALUE> {

/// Amount of ETH in wei to give the other contract.
/// Note: adding value will prevent calls to non-payable methods.
pub fn value(self, value: U256) -> Context<S, true> {
Context {
value,
pub fn value(self, value: U256) -> Call<S, true> {
Call {
value: Some(value),
gas: self.gas,
_storage: self._storage,
storage: self.storage,
}
}
}

impl<S, const HAS_VALUE: bool> CallContext for Context<S, HAS_VALUE> {
impl<S, const HAS_VALUE: bool> CallContext for Call<S, HAS_VALUE> {
fn gas(&self) -> u64 {
self.gas
}
}

impl StaticCallContext for Context<(), false> {}

unsafe impl<S: TopLevelStorage, const HAS_VALUE: bool> MutatingCallContext
for Context<&mut S, HAS_VALUE>
{
fn value(&self) -> U256 {
self.value
}
}

impl<S: TopLevelStorage> NonPayableCallContext for Context<&mut S, false> {}

// allow &self to be a `pure` and `static` call context
impl<'a, T> CallContext for &'a T
where
Expand Down Expand Up @@ -137,3 +159,33 @@ where
}

impl<T> NonPayableCallContext for &mut T where T: TopLevelStorage {}

cfg_if! {
if #[cfg(all(feature = "storage-cache", feature = "reentrant"))] {
// The following impls safeguard state during reentrancy scenarios

impl<S: TopLevelStorage> StaticCallContext for Call<S, false> {}

impl<S: TopLevelStorage> NonPayableCallContext for Call<&mut S, false> {}

unsafe impl<S: TopLevelStorage, const HAS_VALUE: bool> MutatingCallContext
for Call<&mut S, HAS_VALUE>
{
fn value(&self) -> U256 {
self.value.unwrap_or_default()
}
}
} else {
// If there's no reentrancy, all calls are storage safe

impl<S> StaticCallContext for Call<S, false> {}

impl<S> NonPayableCallContext for Call<S, false> {}

unsafe impl<S, const HAS_VALUE: bool> MutatingCallContext for Call<S, HAS_VALUE> {
fn value(&self) -> U256 {
self.value.unwrap_or_default()
}
}
}
}
Loading

0 comments on commit b0a27c1

Please sign in to comment.