diff --git a/.gitignore b/.gitignore index 2f7896d..dd9c271 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ target/ +.config/spellcheck.toml +.config/stylus.dic diff --git a/Cargo.lock b/Cargo.lock index d44d4c4..2e26cc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -434,10 +434,11 @@ dependencies = [ [[package]] name = "stylus-proc" -version = "0.2.4" +version = "0.3.0" dependencies = [ "alloy-primitives", "alloy-sol-types", + "cfg-if", "convert_case 0.6.0", "lazy_static", "proc-macro2", @@ -450,10 +451,11 @@ dependencies = [ [[package]] name = "stylus-sdk" -version = "0.2.4" +version = "0.3.0" dependencies = [ "alloy-primitives", "alloy-sol-types", + "cfg-if", "derivative", "fnv", "hex", diff --git a/Cargo.toml b/Cargo.toml index ef965ff..f17970a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,17 +3,19 @@ members = ["stylus-sdk", "stylus-proc"] resolver = "2" [workspace.package] -version = "0.2.4" +version = "0.3.0" edition = "2021" authors = ["Offchain Labs"] license = "MIT OR Apache-2.0" homepage = "https://github.com/OffchainLabs/stylus-sdk-rs" repository = "https://github.com/OffchainLabs/stylus-sdk-rs" +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 } -derivative = { version = "2.2.0", use_core = true } +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" lazy_static = "1.4.0" @@ -33,4 +35,4 @@ convert_case = "0.6.0" # members stylus-sdk = { path = "stylus-sdk" } -stylus-proc = { path = "stylus-proc", version = "0.2.0" } +stylus-proc = { path = "stylus-proc", version = "0.3.0" } diff --git a/stylus-proc/Cargo.toml b/stylus-proc/Cargo.toml index b753d4c..fc85630 100644 --- a/stylus-proc/Cargo.toml +++ b/stylus-proc/Cargo.toml @@ -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 @@ -25,9 +26,10 @@ quote.workspace = true [features] export-abi = [] storage-cache = [] +reentrant = [] [package.metadata.docs.rs] -all-features = true +features = ["export-abi", "storage-cache"] [lib] proc-macro = true diff --git a/stylus-proc/src/calls/mod.rs b/stylus-proc/src/calls/mod.rs index e7aebae..844457b 100644 --- a/stylus-proc/src/calls/mod.rs +++ b/stylus-proc/src/calls/mod.rs @@ -1,6 +1,8 @@ // 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; use quote::quote; @@ -8,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, @@ -80,7 +80,7 @@ pub fn sol_interface(input: TokenStream) -> TokenStream { quote! { stylus_sdk::call::call }, ), Payable => ( - quote! { impl stylus_sdk::call::PayableCallContext }, + quote! { impl stylus_sdk::call::MutatingCallContext }, quote! { stylus_sdk::call::call }, ), }; @@ -135,8 +135,10 @@ pub fn sol_interface(input: TokenStream) -> TokenStream { let selector2 = selector[2]; let selector3 = selector[3]; + let rust_name = Ident::new(&name.to_string().to_case(Case::Snake), name.span()); + method_impls.extend(quote! { - pub fn #name(&self, context: #context #(, #rust_args)*) -> + pub fn #rust_name(&self, context: #context #(, #rust_args)*) -> Result<<#return_type as #sol_type>::RustType, stylus_sdk::call::Error> { use alloc::vec; diff --git a/stylus-proc/src/lib.rs b/stylus-proc/src/lib.rs index d5f9e86..2b9893b 100644 --- a/stylus-proc/src/lib.rs +++ b/stylus-proc/src/lib.rs @@ -1,6 +1,21 @@ // Copyright 2022-2023, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md +//! Procedural macros for [The Stylus SDK][sdk]. +//! +//! You can import these via +//! +//! ```ignore +//! use stylus_sdk::prelude::*; +//! ``` +//! +//! For a guided exploration of the features, please see the comprehensive [Feature Overview][overview]. +//! +//! [overview]: https://docs.arbitrum.io/stylus/reference/rust-sdk-guide#calls +//! [sdk]: https://docs.rs/stylus-sdk/latest/stylus_sdk/index.html + +#![warn(missing_docs)] + use proc_macro::TokenStream; /// Generates a pretty error message. @@ -20,36 +35,479 @@ mod methods; mod storage; mod types; +/// Allows a Rust `struct` to be used in persistent storage. +/// +/// ```ignore +/// #[solidity_storage] +/// pub struct Contract { +/// owner: StorageAddress, +/// active: StorageBool, +/// sub_struct: SubStruct, +///} +/// +///#[solidity_storage] +///pub struct SubStruct { +/// // types implementing the `StorageType` trait. +///} +/// ``` +/// +/// Each field must implement [`StorageType`]. This includes other structs, which will +/// implement the `trait` automatically when [`#[solidity_storage]`][solidity_storage] is applied. +/// +/// One may even implement [`StorageType`] to define custom storage entries, though this is rarely necessary +/// since the [Stylus SDK][sdk] intends to include all standard Solidity types out-of-the-box. +/// +/// Please refer to the [SDK Feature Overview][overview] for more information on defining storage. +/// +/// [solidity_storage]: macro@solidity_storage +/// [`StorageType`]: https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.StorageType.html +/// [overview]: https://docs.arbitrum.io/stylus/reference/rust-sdk-guide#storage +/// [sdk]: https://docs.rs/stylus-sdk/latest/stylus_sdk/index.html #[proc_macro_attribute] pub fn solidity_storage(attr: TokenStream, input: TokenStream) -> TokenStream { storage::solidity_storage(attr, input) } +/// The types in [`#[solidity_storage]`][solidity_storage] are laid out in the EVM state trie exactly +/// as they are in [Solidity][solidity]. This means that the fields of a `struct` definition will map +/// to the same storage slots as they would in EVM programming languages. Hence, it is often nice to +/// define types using Solidity syntax, which makes this guarantee easier to see. +/// +/// ```ignore +/// sol_storage! { +/// pub struct Contract { +/// address owner; // becomes a StorageAddress +/// bool active; // becomes a StorageBool +/// SubStruct sub_struct, +/// } +/// +/// pub struct SubStruct { +/// // other solidity fields, such as +/// mapping(address => uint) balances; // becomes a StorageMap +/// Delegate delegates[]; // becomes a StorageVec +/// } +/// } +/// ``` +/// +/// The above will expand to equivalent definitions in Rust, with each structure implementing the [`StorageType`] +/// `trait`. Many contracts, like [the ERC 20 example][erc20], do exactly this. +/// +/// Because the layout is identical to [Solidity's][solidity], existing Solidity smart contracts can +/// upgrade to Rust without fear of storage slots not lining up. You simply copy-paste your type definitions. +/// +/// Consequently, the order of fields will affect the JSON ABIs produced that explorers and tooling might use. +/// Most developers don't need to worry about this though and can freely order their types when working on a +/// Rust contract from scratch. +/// +/// Please refer to the [SDK Feature Overview][overview] for more information on defining storage. +/// +/// [solidity_storage]: macro@solidity_storage +/// [`StorageType`]: https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.StorageType.html +/// [solidity]: https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html +/// [overview]: https://docs.arbitrum.io/stylus/reference/rust-sdk-guide#erase-and-deriveerase +/// [erc20]: https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/examples/erc20/src/main.rs #[proc_macro] pub fn sol_storage(input: TokenStream) -> TokenStream { storage::sol_storage(input) } +/// Facilitates calls to other contracts. +/// +/// This macro defines a `struct` for each of the Solidity interfaces provided. +/// +/// ```ignore +/// sol_interface! { +/// interface IService { +/// function makePayment(address user) payable returns (string); +/// function getConstant() pure returns (bytes32) +/// } +/// +/// interface ITree { +/// // other interface methods +/// } +/// } +/// ``` +/// +/// The above will define `IService` and `ITree` for calling the methods of the two contracts. +/// +/// For example, `IService` will have a `make_payment` method that accepts an [`Address`] and returns a [`B256`]. +/// +/// ```ignore +/// pub fn do_call(account: IService, user: Address) -> Result { +/// let config = Call::new() +/// .gas(evm::gas_left() / 2) // limit to half the gas left +/// .value(msg::value()); // set the callvalue +/// +/// account.make_payment(config, user) // note the snake case +/// } +/// ``` +/// +/// Observe the casing change. [`sol_interface!`] computes the selector based on the exact name passed in, +/// which should almost always be `CamelCase`. For aesthetics, the rust functions will instead use `snake_case`. +/// +/// # Reentrant calls +/// +/// Contracts that opt into reentrancy via the `reentrant` feature flag require extra care. +/// When the `storage-cache` feature is enabled, cross-contract calls must [`flush`] or [`clear`] +/// the [`StorageCache`] to safeguard state. This happens automatically via the type system. +/// +/// ```ignore +/// sol_interface! { +/// interface IMethods { +/// function pureFoo() pure; +/// function viewFoo() view; +/// function writeFoo(); +/// function payableFoo() payable; +/// } +/// } +/// +/// #[external] +/// impl Contract { +/// pub fn call_pure(&self, methods: IMethods) -> Result<(), Vec> { +/// Ok(methods.pure_foo(self)?) // `pure` methods might lie about not being `view` +/// } +/// +/// pub fn call_view(&self, methods: IMethods) -> Result<(), Vec> { +/// Ok(methods.view_foo(self)?) +/// } +/// +/// pub fn call_write(&mut self, methods: IMethods) -> Result<(), Vec> { +/// methods.view_foo(self)?; // allows `pure` and `view` methods too +/// Ok(methods.write_foo(self)?) +/// } +/// +/// #[payable] +/// pub fn call_payable(&mut self, methods: IMethods) -> Result<(), Vec> { +/// methods.write_foo(Call::new_in(self))?; // these are the same +/// Ok(methods.payable_foo(self)?) // ------------------ +/// } +/// } +/// ``` +/// +/// In the above, we're able to pass `&self` and `&mut self` because `Contract` implements +/// [`TopLevelStorage`], which means that a reference to it entails access to the entirety of +/// the contract's state. This is the reason it is sound to make a call, since it ensures all +/// cached values are invalidated and/or persisted to state at the right time. +/// +/// When writing Stylus libraries, a type might not be [`TopLevelStorage`] and therefore +/// `&self` or `&mut self` won't work. Building a [`Call`] from a generic parameter is the usual solution. +/// +/// ```ignore +/// pub fn do_call( +/// storage: &mut impl TopLevelStorage, // can be generic, but often just &mut self +/// account: IService, // serializes as an Address +/// user: Address, +/// ) -> Result { +/// +/// let config = Call::new_in(storage) +/// .gas(evm::gas_left() / 2) // limit to half the gas left +/// .value(msg::value()); // set the callvalue +/// +/// account.make_payment(config, user) // note the snake case +/// } +/// ``` +/// +/// Note that in the context of an [`#[external]`][external] call, the `&mut impl` argument will correctly +/// distinguish the method as being `write` or `payable`. This means you can write library code that will +/// work regardless of whether the `reentrant` feature flag is enabled. +/// +/// [sol_interface]: macro@sol_interface +/// [external]: macro@external +/// [`TopLevelStorage`]: https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.TopLevelStorage.html +/// [`StorageCache`]: https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageCache.html +/// [`flush`]: https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageCache.html#method.flush +/// [`clear`]: https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageCache.html#method.clear +/// [`Address`]: https://docs.rs/alloy-primitives/latest/alloy_primitives/struct.Address.html +/// [`B256`]: https://docs.rs/alloy-primitives/latest/alloy_primitives/aliases/type.B256.html +/// [`Call`]: https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.Call.html #[proc_macro] pub fn sol_interface(input: TokenStream) -> TokenStream { calls::sol_interface(input) } +/// Some [`StorageType`] values implement [`Erase`], which provides an [`erase()`] method for clearing state. +/// [The Stylus SDK][sdk] implements [`Erase`] for all primitives, and for vectors of primitives, but not for maps. +/// This is because a Solidity mapping does not provide iteration, and so it's generally impossible to +/// know which slots to clear. +/// +/// Structs may also be [`Erase`] if all of the fields are. `#[derive(Erase)]` +/// lets you do this automatically. +/// +/// ```ignore +/// sol_storage! { +/// #[derive(Erase)] +/// pub struct Contract { +/// address owner; // can erase primitive +/// uint256[] hashes; // can erase vector of primitive +/// } +/// +/// pub struct NotErase { +/// mapping(address => uint) balances; // can't erase a map +/// mapping(uint => uint)[] roots; // can't erase vector of maps +/// } +/// } +/// ``` +/// +/// You can also implement [`Erase`] manually if desired. Note that the reason we care about [`Erase`] +/// at all is that you get storage refunds when clearing state, lowering fees. There's also +/// minor implications for storage patterns using `unsafe` Rust. +/// +/// Please refer to the [SDK Feature Overview][overview] for more information on defining storage. +/// +/// [`StorageType`]: https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.StorageType.html +/// [`Erase`]: https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.Erase.html +/// [`erase()`]: https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.Erase.html#tymethod.erase +/// [overview]: https://docs.arbitrum.io/stylus/reference/rust-sdk-guide#storage +/// [sdk]: https://docs.rs/stylus-sdk/latest/stylus_sdk/index.html #[proc_macro_derive(Erase)] pub fn derive_erase(input: TokenStream) -> TokenStream { storage::derive_erase(input) } -/// For structs, this macro generates a richly-typed entrypoint that parses incoming calldata. -/// For functions, this macro generates a simple, untyped entrypoint that's bytes-in, bytes-out. +/// Defines the entrypoint, which is where Stylus execution begins. +/// Without it the contract will fail to pass [`cargo stylus check`][check]. +/// Most commonly this macro is used to annotate the top level storage `struct`. +/// +/// ```ignore +/// sol_storage! { +/// #[entrypoint] +/// pub struct Contract { +/// ... +/// } +/// +/// // only one entrypoint is allowed +/// pub struct SubStruct { +/// ... +/// } +/// } +/// ``` +/// +/// The above will make the external methods of Contract the first to consider during invocation. +/// See [`#[external]`][external] for more information on method selection. /// -/// Reentrancy is disabled by default, which will cause the program to revert in cases of nested calls. -/// This behavior can be overridden by passing `#[entrypoint(allow_reentrancy = true)]` +/// # Bytes-in, bytes-out programming +/// +/// A less common usage of [`#[entrypoint]`][entrypoint] is for low-level, bytes-in bytes-out programming. +/// When applied to a free-standing function, a different way of writing smart contracts becomes possible, +/// wherein the Stylus SDK's macros and storage types are entirely optional. +/// +/// ```ignore +/// #[entrypoint] +/// fn entrypoint(calldata: Vec) -> ArbResult { +/// // bytes-in, bytes-out programming +/// } +/// ``` +/// +/// # Reentrancy +/// +/// If a contract calls another that then calls the first, it is said to be reentrant. By default, +/// all Stylus programs revert when this happens. However, you can opt out of this behavior by +/// recompiling with the `reentrant` flag. +/// +/// ```toml +/// stylus_sdk = { version = "0.3.0", features = ["reentrant"] } +/// ``` +/// +/// This is dangerous, and should be done only after careful review -- ideally by 3rd-party auditors. +/// Numerous exploits and hacks have in Web3 are attributable to developers misusing or not fully +/// understanding reentrant patterns. +/// +/// If enabled, the Stylus SDK will flush the storage cache in between reentrant calls, persisting values +/// to state that might be used by inner calls. Note that preventing storage invalidation is only part +/// of the battle in the fight against exploits. You can tell if a call is reentrant via +/// [`msg::reentrant`][reentrant], and condition your business logic accordingly. +/// +/// # [`TopLevelStorage`] +/// +/// The [`#[entrypoint]`][entrypoint] macro will automatically implement the [`TopLevelStorage`] `trait` +/// for the annotated `struct`. The single type implementing [`TopLevelStorage`] is special in that +/// mutable access to it represents mutable access to the entire program's state. +/// This has implications for calls via [`sol_interface`]. +/// +/// [`TopLevelStorage`]: https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.TopLevelStorage.html +/// [`sol_interface`]: macro@sol_interface +/// [entrypoint]: macro@entrypoint +/// [reentrant]: https://docs.rs/stylus-sdk/latest/stylus_sdk/msg/fn.reentrant.html +/// [external]: macro@external +/// [check]: https://github.com/OffchainLabs/cargo-stylus#developing-with-stylus #[proc_macro_attribute] pub fn entrypoint(attr: TokenStream, input: TokenStream) -> TokenStream { methods::entrypoint::entrypoint(attr, input) } +/// Just as with storage, Stylus SDK methods are Solidity ABI-equivalent. This means that contracts written +/// in different programming languages are fully interoperable. You can even automatically export your +/// Rust contract as a Solidity interface so that others can add it to their Solidity projects. +/// +/// This macro makes methods "external" so that other contracts can call them by implementing the [`Router`] trait. +/// +/// ```ignore +/// #[external] +/// impl Contract { +/// // our owner method is now callable by other contracts +/// pub fn owner(&self) -> Result> { +/// Ok(self.owner.get()) +/// } +/// } +/// +/// impl Contract { +/// // our set_owner method is not +/// pub fn set_owner(&mut self, new_owner: Address) -> Result<(), Vec> { +/// ... +/// } +/// } +/// ``` +/// +/// Note that, currently, all external methods must return a [`Result`] with the error type [`Vec`]. +/// We intend to change this very soon. In the current model, [`Vec`] becomes the program's revert data, +/// which we intend to both make optional and richly typed. +/// +/// # [`#[payable]`][payable] +/// +/// As in Solidity, methods may accept ETH as call value. +/// +/// ```ignore +/// #[external] +/// impl Contract { +/// #[payable] +/// pub fn credit(&mut self) -> Result<(), Vec { +/// self.erc20.add_balance(msg::sender(), msg::value()) +/// } +/// } +/// ``` +/// +/// In the above, [msg::value][value] is the amount of ETH passed to the contract in wei, which may be used +/// to pay for something depending on the contract's business logic. Note that you have to annotate the method +/// with [`#[payable]`][payable], or else calls to it will revert. This is required as a safety measure +/// to prevent users losing funds to methods that didn't intend to accept ether. +/// +/// # [`#[pure]`][pure] [`#[view]`][view], and #[write] +/// +/// For aesthetics, these additional purity attributes exist to clarify that a method is [`pure`][pure], +/// [`view`][view], or `write`. They aren't necessary though, since the [`#[external]`][external] macro +/// can figure purity out for you based on the types of the arguments. +/// +/// For example, if a method includes an `&self`, it's at least [`view`][view]. If you'd prefer it be write, +/// applying `#[write]` will make it so. Note however that the reverse is not allowed. An `&mut self` +/// method cannot be made [`#[view]`][view], since it might mutate state. +/// +/// Please refer to the [SDK Feature Overview][overview] for more information on defining methods. +/// +/// # Inheritance, `#[inherit]`, and `#[borrow]` +/// +/// Composition in Rust follows that of Solidity. Types that implement [`Router`], the trait that +/// [`#[external]`][external] provides, can be connected via inheritance. +/// +/// ```ignore +/// #[external] +/// #[inherit(Erc20)] +/// impl Token { +/// pub fn mint(&mut self, amount: U256) -> Result<(), Vec> { +/// ... +/// } +/// } +/// +/// #[external] +/// impl Erc20 { +/// pub fn balance_of() -> Result { +/// ... +/// } +/// } +/// ``` +/// +/// Because `Token` inherits `Erc20` in the above, if `Token` has the [`#[entrypoint]`][entrypoint], calls to the +/// contract will first check if the requested method exists within `Token`. If a matching function is not found, +/// it will then try the `Erc20`. Only after trying everything `Token` inherits will the call revert. +/// +/// Note that because methods are checked in that order, if both implement the same method, the one in `Token` +/// will override the one in `Erc20`, which won't be callable. This allows for patterns where the developer +/// imports a crate implementing a standard, like ERC 20, and then adds or overrides just the methods they +/// want to without modifying the imported `Erc20` type. +/// +/// Inheritance can also be chained. `#[inherit(Erc20, Erc721)]` will inherit both `Erc20` and `Erc721`, checking +/// for methods in that order. `Erc20` and `Erc721` may also inherit other types themselves. Method resolution +/// finds the first matching method by [`Depth First Search`][dfs]. +/// +/// Note that for the above to work, Token must implement [`Borrow`][Borrow] and +/// [`BorrowMut`][BorrowMut]. You can implement this yourself, but for simplicity, +/// [`#[solidity_storage]`][solidity_storage] and [`sol_storage!`][sol_storage] provide a +/// `#[borrow]` annotation. +/// +/// ```ignore +/// sol_storage! { +/// #[entrypoint] +/// pub struct Token { +/// #[borrow] +/// Erc20 erc20; +/// ... +/// } +/// +/// pub struct Erc20 { +/// ... +/// } +/// } +/// ``` +/// +/// In the future we plan to simplify the SDK so that [`Borrow`][Borrow] isn't needed and so that +/// [`Router`] composition is more configurable. The motivation for this becomes clearer in complex +/// cases of multi-level inheritance, which we intend to improve. +/// +/// # Exporting a Solidity interface +/// +/// Recall that Stylus contracts are fully interoperable across all languages, including Solidity. +/// The Stylus SDK provides tools for exporting a Solidity interface for your contract so that others +/// can call it. This is usually done with the cargo stylus [CLI tool][cli]. +/// +/// The SDK does this automatically via a feature flag called `export-abi` that causes the +/// [`#[external]`][external] and [`#[entrypoint]`][entrypoint] macros to generate a `main` function +/// that prints the Solidity ABI to the console. +/// +/// ```sh +/// cargo run --features export-abi --target +/// ``` +/// +/// Note that because the above actually generates a `main` function that you need to run, the target +/// can't be `wasm32-unknown-unknown` like normal. Instead you'll need to pass in your target triple, +/// which cargo stylus figures out for you. This `main` function is also why the following commonly +/// appears in the `main.rs` file of Stylus contracts. +/// +/// ```ignore +/// #![cfg_attr(not(feature = "export-abi"), no_main)] +/// ``` +/// +/// Here's an example output. Observe that the method names change from Rust's `snake_case` to Solidity's +/// `camelCase`. For compatibility reasons, onchain method selectors are always `camelCase`. We'll provide +/// the ability to customize selectors very soon. Note too that you can use argument names like "address" +/// without fear. The SDK will prepend an `_` when necessary. +/// +/// ```solidity +/// interface Erc20 { +/// function name() external pure returns (string memory); +/// +/// function balanceOf(address _address) external view returns (uint256); +/// } +/// +/// interface Weth is Erc20 { +/// function mint() external payable; +/// +/// function burn(uint256 amount) external; +/// } +/// ``` +/// +/// [solidity_storage]: macro@solidity_storage +/// [sol_storage]: macro@sol_storage +/// [entrypoint]: macro@entrypoint +/// [external]: macro@external +/// [overview]: https://docs.arbitrum.io/stylus/reference/rust-sdk-guide#methods +/// [`Router`]: https://docs.rs/stylus-sdk/latest/stylus_sdk/abi/trait.Router.html +/// [Borrow]: https://doc.rust-lang.org/std/borrow/trait.Borrow.html +/// [BorrowMut]: https://doc.rust-lang.org/std/borrow/trait.BorrowMut.html +/// [value]: https://docs.rs/stylus-sdk/latest/stylus_sdk/msg/fn.value.html +/// [payable]: https://docs.alchemy.com/docs/solidity-payable-functions +/// [view]: https://docs.soliditylang.org/en/develop/contracts.html#view-functions +/// [pure]: https://docs.soliditylang.org/en/develop/contracts.html#pure-functions +/// [cli]: https://github.com/OffchainLabs/cargo-stylus#exporting-solidity-abis +/// [dfs]: https://en.wikipedia.org/wiki/Depth-first_search #[proc_macro_attribute] pub fn external(attr: TokenStream, input: TokenStream) -> TokenStream { methods::external::external(attr, input) diff --git a/stylus-proc/src/methods/entrypoint.rs b/stylus-proc/src/methods/entrypoint.rs index 6cfc8c4..aecfdcd 100644 --- a/stylus-proc/src/methods/entrypoint.rs +++ b/stylus-proc/src/methods/entrypoint.rs @@ -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 }; @@ -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] @@ -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) { @@ -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 { - 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 }) - } -} diff --git a/stylus-sdk/Cargo.toml b/stylus-sdk/Cargo.toml index 1cf9f6d..007a2b6 100644 --- a/stylus-sdk/Cargo.toml +++ b/stylus-sdk/Cargo.toml @@ -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 @@ -32,7 +33,7 @@ paste.workspace = true sha3.workspace = true [package.metadata.docs.rs] -all-features = true +features = ["default", "docs", "debug", "export-abi"] [features] default = ["storage-cache"] @@ -41,3 +42,4 @@ debug = [] docs = [] hostio = [] storage-cache = ["fnv", "stylus-proc/storage-cache"] +reentrant = ["stylus-proc/reentrant"] diff --git a/stylus-sdk/src/abi/bytes.rs b/stylus-sdk/src/abi/bytes.rs index 5b475f8..923cacd 100644 --- a/stylus-sdk/src/abi/bytes.rs +++ b/stylus-sdk/src/abi/bytes.rs @@ -7,6 +7,7 @@ use crate::{ util::evm_padded_length, }; use alloc::borrow::Cow; +use alloc::vec::Vec; use alloy_sol_types::{token::PackedSeqToken, Encodable, SolType}; use core::ops::{Deref, DerefMut}; @@ -54,7 +55,9 @@ impl AsMut<[u8]> for Bytes { } } -/// Provides a corresponding [`SolType`] for an abi [`Bytes`]. +/// Provides a corresponding [`SolType`] for an [`abi`] [`Bytes`]. +/// +/// [`abi`]: crate::abi pub struct BytesSolType; impl SolType for BytesSolType { diff --git a/stylus-sdk/src/abi/export.rs b/stylus-sdk/src/abi/export.rs index 2aff3f8..c81c6c5 100644 --- a/stylus-sdk/src/abi/export.rs +++ b/stylus-sdk/src/abi/export.rs @@ -13,7 +13,7 @@ use lazy_static::lazy_static; use regex::Regex; /// Trait for storage types so that users can print a Solidity interface to the console. -/// This is auto-derived via the [`external`] marco when the `export-abi` feature is enabled. +/// This is auto-derived via the [`external`] macro when the `export-abi` feature is enabled. /// /// [`external`]: stylus-proc::external pub trait GenerateAbi { @@ -24,7 +24,8 @@ pub trait GenerateAbi { fn fmt_abi(f: &mut fmt::Formatter<'_>) -> fmt::Result; } -pub struct AbiPrinter(PhantomData); +/// Type that makes an ABI printable. +struct AbiPrinter(PhantomData); impl fmt::Display for AbiPrinter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -39,8 +40,7 @@ pub fn print_abi() { println!(" * For more information, please see [The Stylus SDK](https://github.com/OffchainLabs/stylus-sdk-rs)."); println!(" */"); println!(); - let abi = AbiPrinter::(PhantomData); - print!("{}", abi); + print!("{}", AbiPrinter::(PhantomData)); } lazy_static! { diff --git a/stylus-sdk/src/abi/fixed_bytes.rs b/stylus-sdk/src/abi/fixed_bytes.rs index 7b0e5ab..f500387 100644 --- a/stylus-sdk/src/abi/fixed_bytes.rs +++ b/stylus-sdk/src/abi/fixed_bytes.rs @@ -3,6 +3,7 @@ use super::{AbiType, ConstString}; use alloc::borrow::Cow; +use alloc::vec::Vec; use alloy_primitives::FixedBytes; use alloy_sol_types::{ sol_data::{ByteCount, SupportedFixedBytes}, diff --git a/stylus-sdk/src/abi/impls.rs b/stylus-sdk/src/abi/impls.rs index 9f1a903..368ac41 100644 --- a/stylus-sdk/src/abi/impls.rs +++ b/stylus-sdk/src/abi/impls.rs @@ -2,6 +2,7 @@ // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md use super::{AbiType, ConstString}; +use alloc::{string::String, vec::Vec}; use alloy_primitives::{Address, Signed, Uint}; use alloy_sol_types::sol_data::{self, IntBitCount, SupportedInt}; diff --git a/stylus-sdk/src/abi/internal.rs b/stylus-sdk/src/abi/internal.rs index 014725f..8ea4d53 100644 --- a/stylus-sdk/src/abi/internal.rs +++ b/stylus-sdk/src/abi/internal.rs @@ -5,6 +5,7 @@ //! Most users shouldn't call these. use crate::{abi::AbiType, console, msg}; +use alloc::{vec, vec::Vec}; use alloy_primitives::U256; use alloy_sol_types::SolType; use core::fmt; diff --git a/stylus-sdk/src/abi/mod.rs b/stylus-sdk/src/abi/mod.rs index 5074ad7..0545ff1 100644 --- a/stylus-sdk/src/abi/mod.rs +++ b/stylus-sdk/src/abi/mod.rs @@ -7,7 +7,7 @@ //! This module provides the inverse mapping, forming a bijective, 2-way relationship between Rust and Solidity. //! //! This allows the [`prelude`][prelude] macros to generate method selectors, export -//! Solidity interfaces, and otherwise facilitate interop between Rust and Solidity contracts. +//! Solidity interfaces, and otherwise facilitate inter-op between Rust and Solidity contracts. //! //! Notably, the SDK treats `Vec` as a Solidity `uint8[]`. //! For a Solidity `bytes`, see [`Bytes`]. @@ -43,6 +43,7 @@ pub trait Router where S: TopLevelStorage + BorrowMut, { + /// The type the [`TopLevelStorage`] borrows into. Usually just `Self`. type Storage; /// Tries to find and execute a method for the given selector, returning `None` if none is found. @@ -54,8 +55,9 @@ where /// Provides a mapping of Rust to Solidity types. /// When combined with alloy, which provides the reverse direction, a two-way relationship is formed. /// -/// Additionally, `AbiType` provides a const equivalent to alloy's [`SolType::sol_type_name`]. +/// Additionally, `AbiType` provides a `const` equivalent to alloy's [`SolType::sol_type_name`]. pub trait AbiType { + /// The associated Solidity type. type SolType: SolType; /// Equivalent to [`SolType::sol_type_name`], but `const`. diff --git a/stylus-sdk/src/call/context.rs b/stylus-sdk/src/call/context.rs index 18565a8..e7e09e0 100644 --- a/stylus-sdk/src/call/context.rs +++ b/stylus-sdk/src/call/context.rs @@ -1,49 +1,112 @@ // 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; -/// Type enabling configurable calls to other contracts. -/// Users should rarely implement this trait outside of proc macros. +/// Enables configurable calls to other contracts. #[derive(Debug, Clone)] -pub struct Context { +pub struct Call { gas: u64, - value: U256, - _storage: S, + value: Option, + storage: S, } -impl Context<(), false> { - pub fn new() -> Self { +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::{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( + /// storage: &mut impl TopLevelStorage, // can be generic, but often just &mut self + /// account: IService, // serializes as an Address + /// user: Address, + /// ) -> Result { + /// + /// let config = Call::new_in(storage) + /// .gas(evm::gas_left() / 2) // limit to half the gas left + /// .value(msg::value()); // set the callvalue + /// + /// account.make_payment(config, user) // note the snake case + /// } + /// ``` + /// + /// Projects that opt out of the [`StorageCache`] by disabling the `storage-cache` feature + /// may ignore this method. + /// + /// [`StorageCache`]: crate::storage::StorageCache + /// [`flush`]: crate::storage::StorageCache::flush + /// [`clear`]: crate::storage::StorageCache::clear + /// [`new_in`]: Call::new_in + /// [`new`]: Call::new + 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 Context { - /// Assigns a [`TopLevelStorage`] so that mutatable methods can be called. - /// Note: enabling mutation will prevent calls to `pure` and `view` methods. - pub fn mutate( - 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 { + /// let config = Call::new() + /// .gas(evm::gas_left() / 2) // limit to half the gas left + /// .value(msg::value()); // set the callvalue + /// + /// account.make_payment(config, user) // note the snake case + /// } + /// ``` + pub fn new() -> Self { + Self { + gas: u64::MAX, + value: None, + storage: (), } } +} +impl Call { /// 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 { @@ -52,34 +115,22 @@ impl Context { /// 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 { - Context { - value, + pub fn value(self, value: U256) -> Call { + Call { + value: Some(value), gas: self.gas, - _storage: self._storage, + storage: self.storage, } } } -impl CallContext for Context { +impl CallContext for Call { fn gas(&self) -> u64 { self.gas } } -impl StaticCallContext for Context<(), false> {} - -unsafe impl MutatingCallContext - for Context<&mut S, HAS_VALUE> -{ - fn value(&self) -> U256 { - self.value - } -} - -impl NonPayableCallContext for Context<&mut S, false> {} - -// allow &self to be a `pure` and `static` call context +// allow &self as a context impl<'a, T> CallContext for &'a T where T: TopLevelStorage, @@ -89,9 +140,7 @@ where } } -impl<'a, T> StaticCallContext for &'a T where T: TopLevelStorage {} - -// allow &mut self to be a non-static call context +// allow &mut self as a context impl CallContext for &mut T where T: TopLevelStorage, @@ -101,6 +150,13 @@ where } } +// allow &self to be a `pure` and `static` call context +impl<'a, T> StaticCallContext for &'a T where T: TopLevelStorage {} + +// allow &mut self to be a `pure` and `static` call context +impl<'a, T> StaticCallContext for &'a mut T where T: TopLevelStorage {} + +// allow &mut self to be a `write` and `payable` call context unsafe impl MutatingCallContext for &mut T where T: TopLevelStorage, @@ -110,4 +166,37 @@ where } } +// allow &mut self to be a `write`-only call context impl 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 StaticCallContext for Call<&S, false> {} + + impl StaticCallContext for Call<&mut S, false> {} + + impl NonPayableCallContext for Call<&mut S, false> {} + + unsafe impl 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 StaticCallContext for Call {} + + impl NonPayableCallContext for Call {} + + unsafe impl MutatingCallContext for Call { + fn value(&self) -> U256 { + self.value.unwrap_or_default() + } + } + } +} diff --git a/stylus-sdk/src/call/error.rs b/stylus-sdk/src/call/error.rs index aa88992..1956d0e 100644 --- a/stylus-sdk/src/call/error.rs +++ b/stylus-sdk/src/call/error.rs @@ -1,12 +1,15 @@ // Copyright 2022-2023, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md +use alloc::vec::Vec; use alloy_sol_types::{Panic, PanicKind, SolError}; /// Represents error data when a call fails. #[derive(Debug, PartialEq)] pub enum Error { + /// Revert data returned by the other contract. Revert(Vec), + /// Failure to decode the other contract's return data. AbiDecodingFailed(alloy_sol_types::Error), } @@ -16,8 +19,8 @@ impl From for Error { } } -#[allow(unused)] impl From for Vec { + #[allow(unused)] fn from(err: Error) -> Vec { match err { Error::Revert(data) => data, diff --git a/stylus-sdk/src/call/mod.rs b/stylus-sdk/src/call/mod.rs index 330cb3c..778f256 100644 --- a/stylus-sdk/src/call/mod.rs +++ b/stylus-sdk/src/call/mod.rs @@ -2,98 +2,88 @@ // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md //! Call other contracts. +//! +//! There are two primary ways to make calls to other contracts via the Stylus SDK. +//! - [`CallContext`] for richly-typed calls. +//! - The `unsafe` [`RawCall`] for `unsafe`, bytes-in bytes-out calls. +//! +//! Additional helpers exist for specific use-cases like [`transfer_eth`]. -use crate::storage::TopLevelStorage; -use alloy_primitives::{Address, U256}; -use core::sync::atomic::{AtomicBool, Ordering}; +use alloc::vec::Vec; +use alloy_primitives::Address; -pub use self::{context::Context, error::Error, raw::RawCall, traits::*}; +pub use self::{context::Call, error::Error, raw::RawCall, traits::*, transfer::transfer_eth}; pub(crate) use raw::CachePolicy; -#[cfg(feature = "storage-cache")] +#[cfg(all(feature = "storage-cache", feature = "reentrant"))] use crate::storage::Storage; mod context; mod error; mod raw; mod traits; +mod transfer; -/// Dangerous. Enables reentrancy. -/// -/// # Safety -/// -/// If a contract calls another that then calls the first, it is said to be reentrant. -/// By default, all Stylus programs revert when this happened. -/// This method overrides this behavior, allowing reentrant calls to proceed. -/// -/// This is extremely dangerous, and should be done only after careful review -- -/// ideally by 3rd party auditors. Numerous exploits and hacks have in Web3 are -/// attributable to developers misusing or not fully understanding reentrant patterns. -/// -/// If enabled, the Stylus SDK will flush the storage cache in between reentrant calls, -/// persisting values to state that might be used by inner calls. Note that preventing storage -/// invalidation is only part of the battle in the fight against exploits. -pub unsafe fn opt_into_reentrancy() { - ENABLE_REENTRANCY.store(true, Ordering::Relaxed) -} +macro_rules! unsafe_reentrant { + ($block:block) => { + #[cfg(all(feature = "storage-cache", feature = "reentrant"))] + unsafe { + $block + } -/// Whether the program has opted into reentrancy. -pub fn reentrancy_enabled() -> bool { - ENABLE_REENTRANCY.load(Ordering::Relaxed) + #[cfg(not(all(feature = "storage-cache", feature = "reentrant")))] + $block + }; } -/// Whether the program has opted in to reentrancy. -static ENABLE_REENTRANCY: AtomicBool = AtomicBool::new(false); - /// Static calls the contract at the given address. pub fn static_call( context: impl StaticCallContext, to: Address, data: &[u8], ) -> Result, Error> { - #[cfg(feature = "storage-cache")] - if reentrancy_enabled() { - // flush storage to persist changes, but don't invalidate the cache - Storage::flush(); - } - unsafe { + #[cfg(all(feature = "storage-cache", feature = "reentrant"))] + Storage::flush(); // flush storage to persist changes, but don't invalidate the cache + + unsafe_reentrant! {{ RawCall::new_static() .gas(context.gas()) .call(to, data) .map_err(Error::Revert) - } + }} +} + +/// Delegate calls the contract at the given address. +/// +/// # Safety +/// +/// A delegate call must trust the other contract to uphold safety requirements. +/// Though this function clears any cached values, the other contract may arbitrarily change storage, +/// spend ether, and do other things one should never blindly allow other contracts to do. +pub unsafe fn delegate_call( + context: impl MutatingCallContext, + to: Address, + data: &[u8], +) -> Result, Error> { + #[cfg(all(feature = "storage-cache", feature = "reentrant"))] + Storage::clear(); // clear the storage to persist changes, invalidating the cache + + RawCall::new_with_value(context.value()) + .gas(context.gas()) + .call(to, data) + .map_err(Error::Revert) } /// Calls the contract at the given address. pub fn call(context: impl MutatingCallContext, to: Address, data: &[u8]) -> Result, Error> { - #[cfg(feature = "storage-cache")] - if reentrancy_enabled() { - // clear the storage to persist changes, invalidating the cache - Storage::clear(); - } - unsafe { + #[cfg(all(feature = "storage-cache", feature = "reentrant"))] + Storage::clear(); // clear the storage to persist changes, invalidating the cache + + unsafe_reentrant! {{ RawCall::new_with_value(context.value()) .gas(context.gas()) .call(to, data) .map_err(Error::Revert) - } -} - -/// Transfers an amount of ETH in wei to the given account. -/// Note that this method will call the other contract, which may in turn call others. -/// -/// All gas is supplied, which the recipient may burn. -/// If this is not desired, the [`call`] method can be used directly. -pub fn transfer_eth( - _storage: &mut impl TopLevelStorage, - to: Address, - amount: U256, -) -> Result<(), Vec> { - unsafe { - RawCall::new_with_value(amount) - .skip_return_data() - .call(to, &[])?; - } - Ok(()) + }} } diff --git a/stylus-sdk/src/call/raw.rs b/stylus-sdk/src/call/raw.rs index 2379b21..119c774 100644 --- a/stylus-sdk/src/call/raw.rs +++ b/stylus-sdk/src/call/raw.rs @@ -6,11 +6,28 @@ use crate::{ hostio, tx, ArbResult, }; use alloy_primitives::{Address, B256, U256}; +use cfg_if::cfg_if; -#[cfg(feature = "storage-cache")] +#[cfg(all(feature = "storage-cache", feature = "reentrant"))] use crate::storage::StorageCache; +macro_rules! unsafe_reentrant { + ($(#[$meta:meta])* pub fn $name:ident $($rest:tt)*) => { + cfg_if! { + if #[cfg(all(feature = "storage-cache", feature = "reentrant"))] { + $(#[$meta])* + pub unsafe fn $name $($rest)* + } else { + $(#[$meta])* + pub fn $name $($rest)* + } + } + }; +} + /// Mechanism for performing raw calls to other contracts. +/// +/// For safe calls, see [`Call`](super::Call). #[derive(Clone, Default)] #[must_use] pub struct RawCall { @@ -61,7 +78,23 @@ impl Default for RustVec { } impl RawCall { - /// Begin configuring the raw call. + /// Begin configuring the raw call, similar to how [`std::fs::OpenOptions`] works. + /// + /// ```no_run + /// use stylus_sdk::call::RawCall; + /// use stylus_sdk::{alloy_primitives::address, hex}; + /// + /// let contract = address!("361594F5429D23ECE0A88E4fBE529E1c49D524d8"); + /// let calldata = &hex::decode("eddecf107b5740cef7f5a01e3ea7e287665c4e75").unwrap(); + /// + /// unsafe { + /// let result = RawCall::new() // configure a call + /// .gas(2100) // supply 2100 gas + /// .limit_return_data(0, 32) // only read the first 32 bytes back + /// // .flush_storage_cache() // flush the storage cache before the call (available in `reentrant`) + /// .call(contract, calldata); // do the call + /// } + /// ``` pub fn new() -> Self { Default::default() } @@ -126,72 +159,84 @@ impl RawCall { } /// Write all cached values to persistent storage before the call. - #[cfg(feature = "storage-cache")] + #[cfg(any( + all(feature = "storage-cache", feature = "reentrant"), + feature = "docs" + ))] pub fn flush_storage_cache(mut self) -> Self { self.cache_policy = self.cache_policy.max(CachePolicy::Flush); self } /// Flush and clear the storage cache before the call. - #[cfg(feature = "storage-cache")] + #[cfg(any( + all(feature = "storage-cache", feature = "reentrant"), + feature = "docs" + ))] pub fn clear_storage_cache(mut self) -> Self { self.cache_policy = CachePolicy::Clear; self } - /// Performs a raw call to another contract at the given address with the given `calldata`. - /// - /// # Safety - /// - /// Enables storage aliasing if used in the middle of a storage reference's lifetime and reentrancy is allowed. - /// - /// For extra flexibility, this method does not clear the global storage cache. - /// See [`StorageCache::flush`] and [`StorageCache::clear`] for more information. - pub unsafe fn call(self, contract: Address, calldata: &[u8]) -> ArbResult { - let mut outs_len = 0; - let gas = self.gas.unwrap_or(u64::MAX); // will be clamped by 63/64 rule - let value = B256::from(self.callvalue); - let status = unsafe { - #[cfg(feature = "storage-cache")] - match self.cache_policy { - CachePolicy::Clear => StorageCache::clear(), - CachePolicy::Flush => StorageCache::flush(), - CachePolicy::DoNothing => {} - } - match self.kind { - CallKind::Basic => hostio::call_contract( - contract.as_ptr(), - calldata.as_ptr(), - calldata.len(), - value.as_ptr(), - gas, - &mut outs_len, - ), - CallKind::Delegate => hostio::delegate_call_contract( - contract.as_ptr(), - calldata.as_ptr(), - calldata.len(), - gas, - &mut outs_len, - ), - CallKind::Static => hostio::static_call_contract( - contract.as_ptr(), - calldata.as_ptr(), - calldata.len(), - gas, - &mut outs_len, - ), + unsafe_reentrant! { + /// Performs a raw call to another contract at the given address with the given `calldata`. + /// + /// # Safety + /// + /// This function becomes `unsafe` when the `reentrant` and `storage-cache` features are enabled. + /// That's because raw calls might alias storage if used in the middle of a storage ref's lifetime. + /// + /// For extra flexibility, this method does not clear the global storage cache by default. + /// See [`flush_storage_cache`] and [`clear_storage_cache`] for more information. + /// + /// [`flush_storage_cache`]: RawCall::flush_storage_cache + /// [`clear_storage_cache`]: RawCall::clear_storage_cache + pub fn call(self, contract: Address, calldata: &[u8]) -> ArbResult { + let mut outs_len = 0; + let gas = self.gas.unwrap_or(u64::MAX); // will be clamped by 63/64 rule + let value = B256::from(self.callvalue); + let status = unsafe { + #[cfg(all(feature = "storage-cache", feature = "reentrant"))] + match self.cache_policy { + CachePolicy::Clear => StorageCache::clear(), + CachePolicy::Flush => StorageCache::flush(), + CachePolicy::DoNothing => {} + } + match self.kind { + CallKind::Basic => hostio::call_contract( + contract.as_ptr(), + calldata.as_ptr(), + calldata.len(), + value.as_ptr(), + gas, + &mut outs_len, + ), + CallKind::Delegate => hostio::delegate_call_contract( + contract.as_ptr(), + calldata.as_ptr(), + calldata.len(), + gas, + &mut outs_len, + ), + CallKind::Static => hostio::static_call_contract( + contract.as_ptr(), + calldata.as_ptr(), + calldata.len(), + gas, + &mut outs_len, + ), + } + }; + + unsafe { + RETURN_DATA_LEN.set(outs_len); } - }; - unsafe { - RETURN_DATA_LEN.set(outs_len); - } - - let outs = read_return_data(self.offset, self.size); - match status { - 0 => Ok(outs), - _ => Err(outs), + let outs = read_return_data(self.offset, self.size); + match status { + 0 => Ok(outs), + _ => Err(outs), + } } } } diff --git a/stylus-sdk/src/call/traits.rs b/stylus-sdk/src/call/traits.rs index a91c510..dd5c98e 100644 --- a/stylus-sdk/src/call/traits.rs +++ b/stylus-sdk/src/call/traits.rs @@ -30,13 +30,6 @@ pub unsafe trait MutatingCallContext: CallContext { /// Trait for calling the `write` methods of other contracts. /// Users should rarely implement this trait outside of proc macros. +/// /// Note: any implementations of this must return zero for [`MutatingCallContext::value`]. pub trait NonPayableCallContext: MutatingCallContext {} - -impl CallContext for () { - fn gas(&self) -> u64 { - u64::MAX // use everything - } -} - -impl StaticCallContext for () {} diff --git a/stylus-sdk/src/call/transfer.rs b/stylus-sdk/src/call/transfer.rs new file mode 100644 index 0000000..6db8bb5 --- /dev/null +++ b/stylus-sdk/src/call/transfer.rs @@ -0,0 +1,47 @@ +// Copyright 2022-2023, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md + +use crate::call::RawCall; +use alloc::vec::Vec; +use alloy_primitives::{Address, U256}; + +#[cfg(all(feature = "storage-cache", feature = "reentrant"))] +use crate::storage::TopLevelStorage; + +#[cfg(all(feature = "storage-cache", feature = "reentrant"))] +use crate::storage::Storage; + +/// Transfers an amount of ETH in wei to the given account. +/// Note that this method will call the other contract, which may in turn call others. +/// +/// All gas is supplied, which the recipient may burn. +/// If this is not desired, the [`call`] method can be used directly. +/// +/// [`call`]: super::call +#[cfg(all(feature = "storage-cache", feature = "reentrant"))] +pub fn transfer_eth( + _storage: &mut impl TopLevelStorage, + to: Address, + amount: U256, +) -> Result<(), Vec> { + Storage::clear(); // clear the storage to persist changes, invalidating the cache + unsafe { + RawCall::new_with_value(amount) + .skip_return_data() + .call(to, &[])?; + } + Ok(()) +} + +/// Transfers an amount of ETH in wei to the given account. +/// Note that this method will call the other contract, which may in turn call others. +/// +/// All gas is supplied, which the recipient may burn. +/// If this is not desired, the [`call`] method can be used directly. +#[cfg(not(all(feature = "storage-cache", feature = "reentrant")))] +pub fn transfer_eth(to: Address, amount: U256) -> Result<(), Vec> { + RawCall::new_with_value(amount) + .skip_return_data() + .call(to, &[])?; + Ok(()) +} diff --git a/stylus-sdk/src/contract.rs b/stylus-sdk/src/contract.rs index 9a858e9..8f6909c 100644 --- a/stylus-sdk/src/contract.rs +++ b/stylus-sdk/src/contract.rs @@ -16,6 +16,7 @@ use crate::{ hostio::{self, wrap_hostio}, types::AddressVM, }; +use alloc::vec::Vec; use alloy_primitives::{Address, U256}; /// Reads the invocation's calldata. diff --git a/stylus-sdk/src/deploy/raw.rs b/stylus-sdk/src/deploy/raw.rs index b43ea62..219e9b6 100644 --- a/stylus-sdk/src/deploy/raw.rs +++ b/stylus-sdk/src/deploy/raw.rs @@ -6,9 +6,10 @@ use crate::{ contract::{read_return_data, RETURN_DATA_LEN}, hostio, }; +use alloc::vec::Vec; use alloy_primitives::{Address, B256, U256}; -#[cfg(feature = "storage-cache")] +#[cfg(all(feature = "storage-cache", feature = "reentrant"))] use crate::storage::StorageCache; /// Mechanism for performing raw deploys of other contracts. @@ -86,7 +87,7 @@ impl RawDeploy { /// For extra flexibility, this method does not clear the global storage cache. /// See [`StorageCache::flush`] and [`StorageCache::clear`] for more information. pub unsafe fn deploy(self, code: &[u8], endowment: U256) -> Result> { - #[cfg(feature = "storage-cache")] + #[cfg(all(feature = "storage-cache", feature = "reentrant"))] match self.cache_policy { CachePolicy::Clear => StorageCache::clear(), CachePolicy::Flush => StorageCache::flush(), diff --git a/stylus-sdk/src/evm.rs b/stylus-sdk/src/evm.rs index a88cc0b..5914e49 100644 --- a/stylus-sdk/src/evm.rs +++ b/stylus-sdk/src/evm.rs @@ -13,6 +13,7 @@ //! ``` use crate::hostio::{self, wrap_hostio}; +use alloc::{vec, vec::Vec}; use alloy_primitives::B256; use alloy_sol_types::{token::WordToken, SolEvent, TopicList}; diff --git a/stylus-sdk/src/hostio.rs b/stylus-sdk/src/hostio.rs index b5026d3..0e78148 100644 --- a/stylus-sdk/src/hostio.rs +++ b/stylus-sdk/src/hostio.rs @@ -23,7 +23,7 @@ #[link(wasm_import_module = "vm_hooks")] extern "C" { /// Gets the ETH balance in wei of the account at the given address. - /// The semantics are equivalent to that of the EVM’s [`BALANCE`] opcode. + /// The semantics are equivalent to that of the EVM's [`BALANCE`] opcode. /// /// [`BALANCE`]: https://www.evm.codes/#31 pub fn account_balance(address: *const u8, dest: *mut u8); diff --git a/stylus-sdk/src/lib.rs b/stylus-sdk/src/lib.rs index 1e29bdb..a4a6c02 100644 --- a/stylus-sdk/src/lib.rs +++ b/stylus-sdk/src/lib.rs @@ -1,6 +1,33 @@ // Copyright 2022-2023, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md +//! The Stylus SDK. +//! +//! The Stylus SDK makes it easy to develop Solidity ABI-equivalent Stylus contracts in Rust. +//! Included is a full suite of types and shortcuts that abstract away the details of Solidity's storage layout, +//! method selectors, affordances, and more, making it easy to *just write Rust*. +//! For a guided exploration of the features, please see the comprehensive [Feature Overview][overview]. +//! +//! Some of the features available in the SDK include: +//! - **Generic**, storage-backed Rust types for programming **Solidity-equivalent** smart contracts with optimal +//! storage caching. +//! - Simple macros for writing **language-agnostic** methods and entrypoints. +//! - Automatic export of Solidity interfaces for interoperability across programming languages. +//! - Powerful **primitive types** backed by the feature-rich [Alloy][alloy]. +//! +//! Rust programs written with the Stylus SDK can **call and be called** by Solidity smart contracts +//! due to ABI equivalence with Ethereum programming languages. In fact, existing Solidity DEXs can list Rust +//! tokens without modification, and vice versa. +//! +//! [overview]: https://docs.arbitrum.io/stylus/reference/rust-sdk-guide +//! [alloy]: https://docs.rs/alloy-primitives/latest/alloy_primitives/ + +#![doc(html_favicon_url = "https://arbitrum.io/assets/stylus/Arbitrum_Stylus-Logomark.png")] +#![doc(html_logo_url = "https://arbitrum.io/assets/stylus/Arbitrum_Stylus-Logomark.png")] +#![warn(missing_docs)] +// Only allow the standard library in tests and for exports +#![cfg_attr(not(any(test, feature = "export-abi")), no_std)] + extern crate alloc; pub use alloy_primitives; @@ -35,5 +62,7 @@ pub mod hostio; #[cfg(not(feature = "hostio"))] mod hostio; +use alloc::vec::Vec; + /// Represents a contract invocation outcome. pub type ArbResult = Result, Vec>; diff --git a/stylus-sdk/src/storage/array.rs b/stylus-sdk/src/storage/array.rs index 074da03..697ec49 100644 --- a/stylus-sdk/src/storage/array.rs +++ b/stylus-sdk/src/storage/array.rs @@ -3,7 +3,7 @@ use super::{Erase, StorageGuard, StorageGuardMut, StorageType}; use alloy_primitives::U256; -use std::marker::PhantomData; +use core::marker::PhantomData; /// Accessor for a storage-backed array. pub struct StorageArray { diff --git a/stylus-sdk/src/storage/bytes.rs b/stylus-sdk/src/storage/bytes.rs index caafaca..721a04f 100644 --- a/stylus-sdk/src/storage/bytes.rs +++ b/stylus-sdk/src/storage/bytes.rs @@ -3,6 +3,10 @@ use super::{Erase, GlobalStorage, Storage, StorageB8, StorageGuard, StorageGuardMut, StorageType}; use crate::crypto; +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; use alloy_primitives::{U256, U8}; use core::cell::OnceCell; @@ -82,7 +86,7 @@ impl StorageBytes { self.write_len(len) } - /// Updates the length while being concious of representation. + /// Updates the length while being conscious of representation. unsafe fn write_len(&mut self, len: usize) { if len < 32 { // place the len in the last byte of the root with the long bit low @@ -218,7 +222,7 @@ impl StorageBytes { (slot, (index % 32) as u8) } - /// Determines where in storage indices start. Could be made const in the future. + /// Determines where in storage indices start. Could be made `const` in the future. fn base(&self) -> &U256 { self.base .get_or_init(|| crypto::keccak(self.root.to_be_bytes::<32>()).into()) diff --git a/stylus-sdk/src/storage/map.rs b/stylus-sdk/src/storage/map.rs index 76424ba..8b25f0c 100644 --- a/stylus-sdk/src/storage/map.rs +++ b/stylus-sdk/src/storage/map.rs @@ -4,6 +4,7 @@ use crate::crypto; use super::{Erase, SimpleStorageType, StorageGuard, StorageGuardMut, StorageType}; +use alloc::{string::String, vec::Vec}; use alloy_primitives::{Address, FixedBytes, Signed, Uint, B256, U160, U256}; use core::marker::PhantomData; @@ -74,7 +75,7 @@ where K: StorageKey, V: SimpleStorageType<'a>, { - /// Sets the element at a given key, overwritting what may have been there. + /// Sets the element at a given key, overwriting what may have been there. pub fn insert(&mut self, key: K, value: V::Wraps<'a>) { let mut store = self.setter(key); store.set_by_wrapped(value); @@ -122,8 +123,10 @@ where } /// Trait that allows types to be the key of a [`StorageMap`]. +/// /// Note: the assignment of slots must be injective. pub trait StorageKey { + /// Assigns a slot based on the key and where the map is rooted. fn to_slot(&self, root: B256) -> U256; } diff --git a/stylus-sdk/src/storage/mod.rs b/stylus-sdk/src/storage/mod.rs index ab0344b..80853ba 100644 --- a/stylus-sdk/src/storage/mod.rs +++ b/stylus-sdk/src/storage/mod.rs @@ -2,10 +2,30 @@ // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md //! Solidity compatible storage types and persistent storage access. +//! +//! The Stylus node software is composed of two, fully-composable virtual machines. +//! - The Stylus VM, which compiles WASM contracts built with SDKs like this one. +//! - The Ethereum Virtual Machine, which interprets EVM bytecode from languages like Solidity and Vyper. +//! +//! Though these two VMs differ in execution, they are backed by the same EVM State Trie. +//! This means that Stylus contracts have access to the same, key-value based persistent storage +//! familiar to Solidity devs. +//! +//! Because this resource is foreign to Rust, this module provides standard types and traits for +//! accessing state when writing programs. To protect the user, the Stylus SDK safeguards storage access +//! by leveraging Rust's borrow checker. It should never be possible to alias Storage without `unsafe` Rust, +//! eliminating entire classes of errors at compile time. +//! +//! Storage Operations are also cached by default, ensuring that efficient usage is clean and auditable. +//! +//! For a walkthrough of this module's features, please see [The Feature Overview][overview]. +//! +//! [overview]: https://docs.arbitrum.io/stylus/reference/rust-sdk-guide#storage use crate::hostio; use alloy_primitives::{Address, BlockHash, BlockNumber, FixedBytes, Signed, Uint, B256, U256}; use alloy_sol_types::sol_data::{ByteCount, SupportedFixedBytes}; +use cfg_if::cfg_if; use core::{cell::OnceCell, marker::PhantomData, ops::Deref}; pub use array::StorageArray; @@ -17,29 +37,30 @@ pub use traits::{ }; pub use vec::StorageVec; -#[cfg(feature = "storage-cache")] -pub use cache::StorageCache; - -#[cfg(any(not(feature = "storage-cache"), feature = "docs"))] -pub use eager::EagerStorage; - mod array; mod bytes; mod map; mod traits; mod vec; -#[cfg(feature = "storage-cache")] -mod cache; +cfg_if! { + if #[cfg(any(not(feature = "storage-cache"), feature = "docs"))] { + mod eager; + pub use eager::EagerStorage; + } +} -#[cfg(any(not(feature = "storage-cache"), feature = "docs"))] -mod eager; +cfg_if! { + if #[cfg(feature = "storage-cache")] { + mod cache; -#[cfg(feature = "storage-cache")] -pub(crate) type Storage = StorageCache; + pub use cache::StorageCache; -#[cfg(not(feature = "storage-cache"))] -pub(crate) type Storage = EagerStorage; + pub(crate) type Storage = StorageCache; + } else { + pub(crate) type Storage = EagerStorage; + } +} /// Retrieves a 32-byte EVM word from persistent storage directly, bypassing all caches. /// diff --git a/stylus-sdk/src/storage/traits.rs b/stylus-sdk/src/storage/traits.rs index 27c8c4e..19f3a4e 100644 --- a/stylus-sdk/src/storage/traits.rs +++ b/stylus-sdk/src/storage/traits.rs @@ -15,7 +15,7 @@ use derivative::Derivative; /// /// [`the same way`]: https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html pub trait StorageType: Sized { - /// For primative types, this is the type being stored. + /// For primitive types, this is the type being stored. /// For collections, this is the [`StorageType`] being collected. type Wraps<'a>: 'a where diff --git a/stylus-sdk/src/storage/vec.rs b/stylus-sdk/src/storage/vec.rs index 4ae0c7f..3237727 100644 --- a/stylus-sdk/src/storage/vec.rs +++ b/stylus-sdk/src/storage/vec.rs @@ -178,7 +178,7 @@ impl StorageVec { 32 / S::SLOT_BYTES } - /// Determines where in storage indices start. Could be made const in the future. + /// Determines where in storage indices start. Could be made `const` in the future. fn base(&self) -> &U256 { self.base .get_or_init(|| crypto::keccak(self.slot.to_be_bytes::<32>()).into())