Skip to content

Instantly share code, notes, and snippets.

@anegg0
Created January 22, 2025 03:12
Show Gist options
  • Save anegg0/693c25189175b950a64c6f8d5dbae314 to your computer and use it in GitHub Desktop.
Save anegg0/693c25189175b950a64c6f8d5dbae314 to your computer and use it in GitHub Desktop.
Directory structure:
└── offchainlabs-stylus-sdk-rs/
├── Cargo.toml
├── rust-toolchain.toml
├── ci/
├── examples/
│ ├── erc20/
│ │ ├── Cargo.toml
│ │ ├── rust-toolchain.toml
│ │ ├── node_modules/
│ │ │ └── .yarn-integrity
│ │ ├── scripts/
│ │ │ ├── .env.example
│ │ │ ├── .gitignore
│ │ │ └── src/
│ │ │ └── mint.js
│ │ └── src/
│ │ ├── erc20.rs
│ │ ├── lib.rs
│ │ └── main.rs
│ ├── erc721/
│ │ ├── Cargo.toml
│ │ ├── rust-toolchain.toml
│ │ ├── scripts/
│ │ │ ├── .env.example
│ │ │ ├── .gitignore
│ │ │ └── src/
│ │ │ └── mint.js
│ │ └── src/
│ │ ├── erc721.rs
│ │ ├── lib.rs
│ │ └── main.rs
│ └── single_call/
│ ├── Cargo.toml
│ ├── rust-toolchain.toml
│ ├── .gitignore
│ ├── scripts/
│ │ ├── hardhat.config.js
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── external_contracts/
│ │ │ └── Counter.sol
│ │ ├── ignition/
│ │ │ └── modules/
│ │ │ └── deploy_counter.js
│ │ └── src/
│ │ └── main.js
│ └── src/
│ ├── lib.rs
│ └── main.rs
├── licenses/
├── mini-alloc/
│ ├── Cargo.toml
│ ├── src/
│ │ ├── imp.rs
│ │ └── lib.rs
│ ├── tests/
│ │ └── misc.rs
│ └── .cargo/
│ └── config.toml
├── stylus-proc/
│ ├── Cargo.toml
│ ├── src/
│ │ ├── consts.rs
│ │ ├── imports.rs
│ │ ├── lib.rs
│ │ ├── types.rs
│ │ ├── impls/
│ │ │ ├── abi_proxy.rs
│ │ │ └── mod.rs
│ │ ├── macros/
│ │ │ ├── entrypoint.rs
│ │ │ ├── mod.rs
│ │ │ ├── sol_interface.rs
│ │ │ ├── storage.rs
│ │ │ ├── derive/
│ │ │ │ ├── abi_type.rs
│ │ │ │ ├── erase.rs
│ │ │ │ ├── mod.rs
│ │ │ │ └── solidity_error/
│ │ │ │ ├── export_abi.rs
│ │ │ │ └── mod.rs
│ │ │ ├── public/
│ │ │ │ ├── attrs.rs
│ │ │ │ ├── export_abi.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── overrides.rs
│ │ │ │ └── types.rs
│ │ │ └── sol_storage/
│ │ │ ├── mod.rs
│ │ │ └── proc.rs
│ │ └── utils/
│ │ ├── attrs.rs
│ │ ├── mod.rs
│ │ └── testing.rs
│ └── tests/
│ ├── derive_abi_type.rs
│ ├── derive_erase.rs
│ ├── derive_solidity_error.rs
│ ├── public.rs
│ ├── sol_interface.rs
│ └── fail/
│ ├── derive_abi_type/
│ │ ├── missing_sol_macro.rs
│ │ └── missing_sol_macro.stderr
│ ├── derive_solidity_error/
│ │ ├── invalid_variants.rs
│ │ └── invalid_variants.stderr
│ ├── public/
│ │ ├── generated.rs
│ │ ├── generated.stderr
│ │ ├── macro_errors.rs
│ │ └── macro_errors.stderr
│ └── sol_interface/
│ ├── generated.rs
│ ├── generated.stderr
│ ├── macro_errors.rs
│ └── macro_errors.stderr
└── stylus-sdk/
├── Cargo.toml
└── src/
├── block.rs
├── contract.rs
├── crypto.rs
├── debug.rs
├── evm.rs
├── hostio.rs
├── lib.rs
├── methods.rs
├── msg.rs
├── prelude.rs
├── tx.rs
├── types.rs
├── util.rs
├── abi/
│ ├── bytes.rs
│ ├── const_string.rs
│ ├── impls.rs
│ ├── internal.rs
│ ├── ints.rs
│ ├── mod.rs
│ └── export/
│ ├── internal.rs
│ └── mod.rs
├── call/
│ ├── context.rs
│ ├── error.rs
│ ├── mod.rs
│ ├── raw.rs
│ ├── traits.rs
│ └── transfer.rs
├── deploy/
│ ├── mod.rs
│ └── raw.rs
├── host/
│ ├── mod.rs
│ └── wasm.rs
└── storage/
├── array.rs
├── bytes.rs
├── map.rs
├── mod.rs
├── traits.rs
└── vec.rs
================================================
File: Cargo.toml
================================================
[workspace]
members = ["stylus-sdk", "stylus-proc", "mini-alloc"]
resolver = "2"
[workspace.package]
version = "0.7.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.8.14", default-features = false , features = ["native-keccak"] }
alloy-sol-types = { version = "=0.8.14", 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"
lazy_static = "1.5.0"
sha3 = "0.10.8"
# proc macros
convert_case = "0.6.0"
paste = "1.0.15"
proc-macro-error = "1.0"
proc-macro2 = "1.0"
quote = "1.0"
regex = "1.10.6"
syn = { version = "2.0.77", features = ["full", "visit-mut"] }
syn-solidity = "0.8.3"
# proc macro dev
pretty_assertions = "1.4.1"
prettyplease = "0.2.22"
trybuild = "1.0"
# members
mini-alloc = { path = "mini-alloc", version = "0.7.0-rc.1" }
stylus-sdk = { path = "stylus-sdk" }
stylus-proc = { path = "stylus-proc", version = "0.7.0-rc.1" }
================================================
File: rust-toolchain.toml
================================================
[toolchain]
channel = "1.83.0"
================================================
File: examples/erc20/Cargo.toml
================================================
[package]
name = "erc20"
version = "0.1.0"
edition = "2021"
[dependencies]
alloy-primitives = "=0.8.14"
alloy-sol-types = "=0.8.14"
stylus-sdk = { path = "../../stylus-sdk" }
mini-alloc = { path = "../../mini-alloc" }
[features]
export-abi = ["stylus-sdk/export-abi"]
[lib]
crate-type = ["lib", "cdylib"]
[profile.release]
codegen-units = 1
strip = true
lto = true
panic = "abort"
opt-level = "s"
[workspace]
================================================
File: examples/erc20/rust-toolchain.toml
================================================
[toolchain]
channel = "1.81.0"
================================================
File: examples/erc20/node_modules/.yarn-integrity
================================================
{
"systemParams": "darwin-arm64-115",
"modulesFolders": [],
"flags": [],
"linkedModules": [],
"topLevelPatterns": [],
"lockfileEntries": {},
"files": [],
"artifacts": {}
}
================================================
File: examples/erc20/scripts/.env.example
================================================
RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
TX_EXPLORER_URL=https://sepolia.arbiscan.io/tx/
CONTRACT_ADDRESS=
PRIVATE_KEY=
AMOUNT_TO_MINT=
================================================
File: examples/erc20/scripts/.gitignore
================================================
node_modules
.env
================================================
File: examples/erc20/scripts/src/mint.js
================================================
const {
Contract,
Wallet,
JsonRpcProvider,
parseEther,
formatEther,
} = require("ethers");
require("dotenv").config();
// Initial checks
if (
!process.env.RPC_URL ||
process.env.RPC_URL == "" ||
!process.env.CONTRACT_ADDRESS ||
process.env.CONTRACT_ADDRESS == "" ||
!process.env.PRIVATE_KEY ||
process.env.PRIVATE_KEY == ""
) {
console.error(
"The following environment variables are needed (set them in .env): RPC_URL, CONTRACT_ADDRESS, PRIVATE_KEY"
);
return;
}
// ABI of the token (used functions)
const abi = [
"function mint(uint256 value) external",
"function mintTo(address to, uint256 value) external",
// Read-Only Functions
"function balanceOf(address owner) external view returns (uint256)",
];
// Address of the token
const address = process.env.CONTRACT_ADDRESS;
// Transaction Explorer URL
const tx_explorer_url = process.env.TX_EXPLORER_URL;
// Private key and ethers provider
const walletPrivateKey = process.env.PRIVATE_KEY;
const stylusRpcProvider = new JsonRpcProvider(process.env.RPC_URL);
const signer = new Wallet(walletPrivateKey, stylusRpcProvider);
// Amount of tokens to mint
const amountToMint = process.env.AMOUNT_TO_MINT || "1000";
// Main function
const main = async () => {
// Presentation message
console.log(
`Minting ${amountToMint} tokens of the contract ${address} to account ${signer.address}`
);
// Connecting to the ERC-20 contract
const erc20 = new Contract(address, abi, signer);
// Current balance of user
const currentBalance = await erc20.balanceOf(signer.address);
console.log(`Current balance: ${formatEther(currentBalance)}`);
// Minting tokens
const mintTransaction = await erc20.mint(parseEther(amountToMint));
await mintTransaction.wait();
console.log(`Transaction hash: ${mintTransaction.hash}`);
console.log(`View tx on explorer: ${tx_explorer_url}${mintTransaction.hash}`);
// Final balance of user
const finalBalance = await erc20.balanceOf(signer.address);
console.log(`Final balance: ${formatEther(finalBalance)}`);
};
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
================================================
File: examples/erc20/src/erc20.rs
================================================
//! Implementation of the ERC-20 standard
//!
//! The eponymous [`Erc20`] type provides all the standard methods,
//! and is intended to be inherited by other contract types.
//!
//! You can configure the behavior of [`Erc20`] via the [`Erc20Params`] trait,
//! which allows specifying the name, symbol, and decimals of the token.
//!
//! Note that this code is unaudited and not fit for production use.
// Imported packages
use alloy_primitives::{Address, U256};
use alloy_sol_types::sol;
use core::marker::PhantomData;
use stylus_sdk::{evm, msg, prelude::*};
pub trait Erc20Params {
/// Immutable token name
const NAME: &'static str;
/// Immutable token symbol
const SYMBOL: &'static str;
/// Immutable token decimals
const DECIMALS: u8;
}
sol_storage! {
/// Erc20 implements all ERC-20 methods.
pub struct Erc20<T> {
/// Maps users to balances
mapping(address => uint256) balances;
/// Maps users to a mapping of each spender's allowance
mapping(address => mapping(address => uint256)) allowances;
/// The total supply of the token
uint256 total_supply;
/// Used to allow [`Erc20Params`]
PhantomData<T> phantom;
}
}
// Declare events and Solidity error types
sol! {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
error InsufficientBalance(address from, uint256 have, uint256 want);
error InsufficientAllowance(address owner, address spender, uint256 have, uint256 want);
}
/// Represents the ways methods may fail.
#[derive(SolidityError)]
pub enum Erc20Error {
InsufficientBalance(InsufficientBalance),
InsufficientAllowance(InsufficientAllowance),
}
// These methods aren't exposed to other contracts
// Methods marked as "pub" here are usable outside of the erc20 module (i.e. they're callable from lib.rs)
// Note: modifying storage will become much prettier soon
impl<T: Erc20Params> Erc20<T> {
/// Movement of funds between 2 accounts
/// (invoked by the public transfer() and transfer_from() functions )
pub fn _transfer(&mut self, from: Address, to: Address, value: U256) -> Result<(), Erc20Error> {
// Decreasing sender balance
let mut sender_balance = self.balances.setter(from);
let old_sender_balance = sender_balance.get();
if old_sender_balance < value {
return Err(Erc20Error::InsufficientBalance(InsufficientBalance {
from,
have: old_sender_balance,
want: value,
}));
}
sender_balance.set(old_sender_balance - value);
// Increasing receiver balance
let mut to_balance = self.balances.setter(to);
let new_to_balance = to_balance.get() + value;
to_balance.set(new_to_balance);
// Emitting the transfer event
evm::log(Transfer { from, to, value });
Ok(())
}
/// Mints `value` tokens to `address`
pub fn mint(&mut self, address: Address, value: U256) -> Result<(), Erc20Error> {
// Increasing balance
let mut balance = self.balances.setter(address);
let new_balance = balance.get() + value;
balance.set(new_balance);
// Increasing total supply
self.total_supply.set(self.total_supply.get() + value);
// Emitting the transfer event
evm::log(Transfer {
from: Address::ZERO,
to: address,
value,
});
Ok(())
}
/// Burns `value` tokens from `address`
pub fn burn(&mut self, address: Address, value: U256) -> Result<(), Erc20Error> {
// Decreasing balance
let mut balance = self.balances.setter(address);
let old_balance = balance.get();
if old_balance < value {
return Err(Erc20Error::InsufficientBalance(InsufficientBalance {
from: address,
have: old_balance,
want: value,
}));
}
balance.set(old_balance - value);
// Decreasing the total supply
self.total_supply.set(self.total_supply.get() - value);
// Emitting the transfer event
evm::log(Transfer {
from: address,
to: Address::ZERO,
value,
});
Ok(())
}
}
// These methods are public to other contracts
// Note: modifying storage will become much prettier soon
#[public]
impl<T: Erc20Params> Erc20<T> {
/// Immutable token name
pub fn name() -> String {
T::NAME.into()
}
/// Immutable token symbol
pub fn symbol() -> String {
T::SYMBOL.into()
}
/// Immutable token decimals
pub fn decimals() -> u8 {
T::DECIMALS
}
/// Total supply of tokens
pub fn total_supply(&self) -> U256 {
self.total_supply.get()
}
/// Balance of `address`
pub fn balance_of(&self, owner: Address) -> U256 {
self.balances.get(owner)
}
/// Transfers `value` tokens from msg::sender() to `to`
pub fn transfer(&mut self, to: Address, value: U256) -> Result<bool, Erc20Error> {
self._transfer(msg::sender(), to, value)?;
Ok(true)
}
/// Transfers `value` tokens from `from` to `to`
/// (msg::sender() must be able to spend at least `value` tokens from `from`)
pub fn transfer_from(
&mut self,
from: Address,
to: Address,
value: U256,
) -> Result<bool, Erc20Error> {
// Check msg::sender() allowance
let mut sender_allowances = self.allowances.setter(from);
let mut allowance = sender_allowances.setter(msg::sender());
let old_allowance = allowance.get();
if old_allowance < value {
return Err(Erc20Error::InsufficientAllowance(InsufficientAllowance {
owner: from,
spender: msg::sender(),
have: old_allowance,
want: value,
}));
}
// Decreases allowance
allowance.set(old_allowance - value);
// Calls the internal transfer function
self._transfer(from, to, value)?;
Ok(true)
}
/// Approves the spenditure of `value` tokens of msg::sender() to `spender`
pub fn approve(&mut self, spender: Address, value: U256) -> bool {
self.allowances.setter(msg::sender()).insert(spender, value);
evm::log(Approval {
owner: msg::sender(),
spender,
value,
});
true
}
/// Returns the allowance of `spender` on `owner`'s tokens
pub fn allowance(&self, owner: Address, spender: Address) -> U256 {
self.allowances.getter(owner).get(spender)
}
}
================================================
File: examples/erc20/src/lib.rs
================================================
// Only run this as a WASM if the export-abi feature is not set.
#![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
extern crate alloc;
// Modules and imports
mod erc20;
use crate::erc20::{Erc20, Erc20Error, Erc20Params};
use alloy_primitives::{Address, U256};
use stylus_sdk::{msg, prelude::*};
/// Immutable definitions
struct StylusTestTokenParams;
impl Erc20Params for StylusTestTokenParams {
const NAME: &'static str = "StylusTestToken";
const SYMBOL: &'static str = "STTK";
const DECIMALS: u8 = 18;
}
// Define the entrypoint as a Solidity storage object. The sol_storage! macro
// will generate Rust-equivalent structs with all fields mapped to Solidity-equivalent
// storage slots and types.
sol_storage! {
#[entrypoint]
struct StylusTestToken {
// Allows erc20 to access StylusTestToken's storage and make calls
#[borrow]
Erc20<StylusTestTokenParams> erc20;
}
}
#[public]
#[inherit(Erc20<StylusTestTokenParams>)]
impl StylusTestToken {
/// Mints tokens
pub fn mint(&mut self, value: U256) -> Result<(), Erc20Error> {
self.erc20.mint(msg::sender(), value)?;
Ok(())
}
/// Mints tokens to another address
pub fn mint_to(&mut self, to: Address, value: U256) -> Result<(), Erc20Error> {
self.erc20.mint(to, value)?;
Ok(())
}
/// Burns tokens
pub fn burn(&mut self, value: U256) -> Result<(), Erc20Error> {
self.erc20.burn(msg::sender(), value)?;
Ok(())
}
}
================================================
File: examples/erc20/src/main.rs
================================================
#[cfg(feature = "export-abi")]
fn main() {
erc20::print_abi("MIT-OR-APACHE-2.0", "pragma solidity ^0.8.23;");
}
================================================
File: examples/erc721/Cargo.toml
================================================
[package]
name = "erc721"
version = "0.1.0"
edition = "2021"
[dependencies]
alloy-primitives = "=0.8.14"
alloy-sol-types = "=0.8.14"
stylus-sdk = { path = "../../stylus-sdk" }
mini-alloc = { path = "../../mini-alloc" }
[features]
export-abi = ["stylus-sdk/export-abi"]
[lib]
crate-type = ["lib", "cdylib"]
[profile.release]
codegen-units = 1
strip = true
lto = true
panic = "abort"
opt-level = "s"
[workspace]
================================================
File: examples/erc721/rust-toolchain.toml
================================================
[toolchain]
channel = "1.81.0"
================================================
File: examples/erc721/scripts/.env.example
================================================
RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
TX_EXPLORER_URL=https://sepolia.arbiscan.io/tx/
CONTRACT_ADDRESS=
PRIVATE_KEY=
================================================
File: examples/erc721/scripts/.gitignore
================================================
node_modules
.env
================================================
File: examples/erc721/scripts/src/mint.js
================================================
const { Contract, Wallet, JsonRpcProvider } = require("ethers");
require("dotenv").config();
// Initial checks
if (
!process.env.RPC_URL ||
process.env.RPC_URL == "" ||
!process.env.CONTRACT_ADDRESS ||
process.env.CONTRACT_ADDRESS == "" ||
!process.env.PRIVATE_KEY ||
process.env.PRIVATE_KEY == ""
) {
console.error(
"The following environment variables are needed (set them in .env): RPC_URL, CONTRACT_ADDRESS, PRIVATE_KEY"
);
return;
}
// ABI of the token (used functions)
const abi = [
"function mint() external",
"function mintTo(address to) external",
// Read-Only Functions
"function balanceOf(address owner) external view returns (uint256)",
];
// Address of the token
const address = process.env.CONTRACT_ADDRESS;
// Transaction Explorer URL
const tx_explorer_url = process.env.TX_EXPLORER_URL;
// Private key and ethers provider
const walletPrivateKey = process.env.PRIVATE_KEY;
const stylusRpcProvider = new JsonRpcProvider(process.env.RPC_URL);
const signer = new Wallet(walletPrivateKey, stylusRpcProvider);
// Main function
const main = async () => {
// Presentation message
console.log(
`Minting an NFT of the contract ${address} to account ${signer.address}`
);
// Connecting to the ERC-721 contract
const erc721 = new Contract(address, abi, signer);
// Current balance of user
const currentBalance = await erc721.balanceOf(signer.address);
console.log(`Current balance: ${currentBalance}`);
// Minting tokens
const mintTransaction = await erc721.mint();
await mintTransaction.wait();
console.log(`Transaction hash: ${mintTransaction.hash}`);
console.log(`View tx on explorer: ${tx_explorer_url}${mintTransaction.hash}`);
// Final balance of user
const finalBalance = await erc721.balanceOf(signer.address);
console.log(`Final balance: ${finalBalance}`);
};
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
================================================
File: examples/erc721/src/erc721.rs
================================================
//! Implementation of the ERC-721 standard
//!
//! The eponymous [`Erc721`] type provides all the standard methods,
//! and is intended to be inherited by other contract types.
//!
//! You can configure the behavior of [`Erc721`] via the [`Erc721Params`] trait,
//! which allows specifying the name, symbol, and token uri.
//!
//! Note that this code is unaudited and not fit for production use.
use alloc::vec;
use alloy_primitives::{Address, FixedBytes, U256};
use alloy_sol_types::sol;
use core::{borrow::BorrowMut, marker::PhantomData};
use stylus_sdk::{abi::Bytes, evm, msg, prelude::*};
pub trait Erc721Params {
/// Immutable NFT name.
const NAME: &'static str;
/// Immutable NFT symbol.
const SYMBOL: &'static str;
/// The NFT's Uniform Resource Identifier.
fn token_uri(token_id: U256) -> String;
}
sol_storage! {
/// Erc721 implements all ERC-721 methods
pub struct Erc721<T: Erc721Params> {
/// Token id to owner map
mapping(uint256 => address) owners;
/// User to balance map
mapping(address => uint256) balances;
/// Token id to approved user map
mapping(uint256 => address) token_approvals;
/// User to operator map (the operator can manage all NFTs of the owner)
mapping(address => mapping(address => bool)) operator_approvals;
/// Total supply
uint256 total_supply;
/// Used to allow [`Erc721Params`]
PhantomData<T> phantom;
}
}
// Declare events and Solidity error types
sol! {
event Transfer(address indexed from, address indexed to, uint256 indexed token_id);
event Approval(address indexed owner, address indexed approved, uint256 indexed token_id);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
// Token id has not been minted, or it has been burned
error InvalidTokenId(uint256 token_id);
// The specified address is not the owner of the specified token id
error NotOwner(address from, uint256 token_id, address real_owner);
// The specified address does not have allowance to spend the specified token id
error NotApproved(address owner, address spender, uint256 token_id);
// Attempt to transfer token id to the Zero address
error TransferToZero(uint256 token_id);
// The receiver address refused to receive the specified token id
error ReceiverRefused(address receiver, uint256 token_id, bytes4 returned);
}
/// Represents the ways methods may fail.
#[derive(SolidityError)]
pub enum Erc721Error {
InvalidTokenId(InvalidTokenId),
NotOwner(NotOwner),
NotApproved(NotApproved),
TransferToZero(TransferToZero),
ReceiverRefused(ReceiverRefused),
}
// External interfaces
sol_interface! {
/// Allows calls to the `onERC721Received` method of other contracts implementing `IERC721TokenReceiver`.
interface IERC721TokenReceiver {
function onERC721Received(address operator, address from, uint256 token_id, bytes data) external returns(bytes4);
}
}
/// Selector for `onERC721Received`, which is returned by contracts implementing `IERC721TokenReceiver`.
const ERC721_TOKEN_RECEIVER_ID: u32 = 0x150b7a02;
// These methods aren't public, but are helpers used by public methods.
// Methods marked as "pub" here are usable outside of the erc721 module (i.e. they're callable from lib.rs).
impl<T: Erc721Params> Erc721<T> {
/// Requires that msg::sender() is authorized to spend a given token
fn require_authorized_to_spend(
&self,
from: Address,
token_id: U256,
) -> Result<(), Erc721Error> {
// `from` must be the owner of the token_id
let owner = self.owner_of(token_id)?;
if from != owner {
return Err(Erc721Error::NotOwner(NotOwner {
from,
token_id,
real_owner: owner,
}));
}
// caller is the owner
if msg::sender() == owner {
return Ok(());
}
// caller is an operator for the owner (can manage their tokens)
if self.operator_approvals.getter(owner).get(msg::sender()) {
return Ok(());
}
// caller is approved to manage this token_id
if msg::sender() == self.token_approvals.get(token_id) {
return Ok(());
}
// otherwise, caller is not allowed to manage this token_id
Err(Erc721Error::NotApproved(NotApproved {
owner,
spender: msg::sender(),
token_id,
}))
}
/// Transfers `token_id` from `from` to `to`.
/// This function does check that `from` is the owner of the token, but it does not check
/// that `to` is not the zero address, as this function is usable for burning.
pub fn transfer(
&mut self,
token_id: U256,
from: Address,
to: Address,
) -> Result<(), Erc721Error> {
let mut owner = self.owners.setter(token_id);
let previous_owner = owner.get();
if previous_owner != from {
return Err(Erc721Error::NotOwner(NotOwner {
from,
token_id,
real_owner: previous_owner,
}));
}
owner.set(to);
// right now working with storage can be verbose, but this will change upcoming version of the Stylus SDK
let mut from_balance = self.balances.setter(from);
let balance = from_balance.get() - U256::from(1);
from_balance.set(balance);
let mut to_balance = self.balances.setter(to);
let balance = to_balance.get() + U256::from(1);
to_balance.set(balance);
// cleaning app the approved mapping for this token
self.token_approvals.delete(token_id);
evm::log(Transfer { from, to, token_id });
Ok(())
}
/// Calls `onERC721Received` on the `to` address if it is a contract.
/// Otherwise it does nothing
fn call_receiver<S: TopLevelStorage>(
storage: &mut S,
token_id: U256,
from: Address,
to: Address,
data: Vec<u8>,
) -> Result<(), Erc721Error> {
if to.has_code() {
let receiver = IERC721TokenReceiver::new(to);
let received = receiver
.on_erc_721_received(&mut *storage, msg::sender(), from, token_id, data.into())
.map_err(|_e| {
Erc721Error::ReceiverRefused(ReceiverRefused {
receiver: receiver.address,
token_id,
returned: FixedBytes(0_u32.to_be_bytes()),
})
})?
.0;
if u32::from_be_bytes(received) != ERC721_TOKEN_RECEIVER_ID {
return Err(Erc721Error::ReceiverRefused(ReceiverRefused {
receiver: receiver.address,
token_id,
returned: FixedBytes(received),
}));
}
}
Ok(())
}
/// Transfers and calls `onERC721Received`
pub fn safe_transfer<S: TopLevelStorage + BorrowMut<Self>>(
storage: &mut S,
token_id: U256,
from: Address,
to: Address,
data: Vec<u8>,
) -> Result<(), Erc721Error> {
storage.borrow_mut().transfer(token_id, from, to)?;
Self::call_receiver(storage, token_id, from, to, data)
}
/// Mints a new token and transfers it to `to`
pub fn mint(&mut self, to: Address) -> Result<(), Erc721Error> {
let new_token_id = self.total_supply.get();
self.total_supply.set(new_token_id + U256::from(1u8));
self.transfer(new_token_id, Address::default(), to)?;
Ok(())
}
/// Burns the token `token_id` from `from`
/// Note that total_supply is not reduced since it's used to calculate the next token_id to mint
pub fn burn(&mut self, from: Address, token_id: U256) -> Result<(), Erc721Error> {
self.transfer(token_id, from, Address::default())?;
Ok(())
}
}
// these methods are public to other contracts
#[public]
impl<T: Erc721Params> Erc721<T> {
/// Immutable NFT name.
pub fn name() -> Result<String, Erc721Error> {
Ok(T::NAME.into())
}
/// Immutable NFT symbol.
pub fn symbol() -> Result<String, Erc721Error> {
Ok(T::SYMBOL.into())
}
/// The NFT's Uniform Resource Identifier.
#[selector(name = "tokenURI")]
pub fn token_uri(&self, token_id: U256) -> Result<String, Erc721Error> {
self.owner_of(token_id)?; // require NFT exist
Ok(T::token_uri(token_id))
}
/// Gets the number of NFTs owned by an account.
pub fn balance_of(&self, owner: Address) -> Result<U256, Erc721Error> {
Ok(self.balances.get(owner))
}
/// Gets the owner of the NFT, if it exists.
pub fn owner_of(&self, token_id: U256) -> Result<Address, Erc721Error> {
let owner = self.owners.get(token_id);
if owner.is_zero() {
return Err(Erc721Error::InvalidTokenId(InvalidTokenId { token_id }));
}
Ok(owner)
}
/// Transfers an NFT, but only after checking the `to` address can receive the NFT.
/// It includes additional data for the receiver.
#[selector(name = "safeTransferFrom")]
pub fn safe_transfer_from_with_data<S: TopLevelStorage + BorrowMut<Self>>(
storage: &mut S,
from: Address,
to: Address,
token_id: U256,
data: Bytes,
) -> Result<(), Erc721Error> {
if to.is_zero() {
return Err(Erc721Error::TransferToZero(TransferToZero { token_id }));
}
storage
.borrow_mut()
.require_authorized_to_spend(from, token_id)?;
Self::safe_transfer(storage, token_id, from, to, data.0)
}
/// Equivalent to [`safe_transfer_from_with_data`], but without the additional data.
///
/// Note: because Rust doesn't allow multiple methods with the same name,
/// we use the `#[selector]` macro attribute to simulate solidity overloading.
#[selector(name = "safeTransferFrom")]
pub fn safe_transfer_from<S: TopLevelStorage + BorrowMut<Self>>(
storage: &mut S,
from: Address,
to: Address,
token_id: U256,
) -> Result<(), Erc721Error> {
Self::safe_transfer_from_with_data(storage, from, to, token_id, Bytes(vec![]))
}
/// Transfers the NFT.
pub fn transfer_from(
&mut self,
from: Address,
to: Address,
token_id: U256,
) -> Result<(), Erc721Error> {
if to.is_zero() {
return Err(Erc721Error::TransferToZero(TransferToZero { token_id }));
}
self.require_authorized_to_spend(from, token_id)?;
self.transfer(token_id, from, to)?;
Ok(())
}
/// Grants an account the ability to manage the sender's NFT.
pub fn approve(&mut self, approved: Address, token_id: U256) -> Result<(), Erc721Error> {
let owner = self.owner_of(token_id)?;
// require authorization
if msg::sender() != owner && !self.operator_approvals.getter(owner).get(msg::sender()) {
return Err(Erc721Error::NotApproved(NotApproved {
owner,
spender: msg::sender(),
token_id,
}));
}
self.token_approvals.insert(token_id, approved);
evm::log(Approval {
approved,
owner,
token_id,
});
Ok(())
}
/// Grants an account the ability to manage all of the sender's NFTs.
pub fn set_approval_for_all(
&mut self,
operator: Address,
approved: bool,
) -> Result<(), Erc721Error> {
let owner = msg::sender();
self.operator_approvals
.setter(owner)
.insert(operator, approved);
evm::log(ApprovalForAll {
owner,
operator,
approved,
});
Ok(())
}
/// Gets the account managing an NFT, or zero if unmanaged.
pub fn get_approved(&mut self, token_id: U256) -> Result<Address, Erc721Error> {
Ok(self.token_approvals.get(token_id))
}
/// Determines if an account has been authorized to managing all of a user's NFTs.
pub fn is_approved_for_all(
&mut self,
owner: Address,
operator: Address,
) -> Result<bool, Erc721Error> {
Ok(self.operator_approvals.getter(owner).get(operator))
}
/// Whether the NFT supports a given standard.
pub fn supports_interface(interface: FixedBytes<4>) -> Result<bool, Erc721Error> {
let interface_slice_array: [u8; 4] = interface.as_slice().try_into().unwrap();
if u32::from_be_bytes(interface_slice_array) == 0xffffffff {
// special cased in the ERC165 standard
return Ok(false);
}
const IERC165: u32 = 0x01ffc9a7;
const IERC721: u32 = 0x80ac58cd;
const IERC721_METADATA: u32 = 0x5b5e139f;
Ok(matches!(
u32::from_be_bytes(interface_slice_array),
IERC165 | IERC721 | IERC721_METADATA
))
}
}
================================================
File: examples/erc721/src/lib.rs
================================================
// Only run this as a WASM if the export-abi feature is not set.
#![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
extern crate alloc;
// Modules and imports
mod erc721;
use crate::erc721::{Erc721, Erc721Error, Erc721Params};
use alloy_primitives::{Address, U256};
/// Import the Stylus SDK along with alloy primitive types for use in our program.
use stylus_sdk::{msg, prelude::*};
/// Immutable definitions
struct StylusTestNFTParams;
impl Erc721Params for StylusTestNFTParams {
const NAME: &'static str = "StylusTestNFT";
const SYMBOL: &'static str = "STNFT";
fn token_uri(token_id: U256) -> String {
format!("{}{}{}", "https://my-nft-metadata.com/", token_id, ".json")
}
}
// Define the entrypoint as a Solidity storage object. The sol_storage! macro
// will generate Rust-equivalent structs with all fields mapped to Solidity-equivalent
// storage slots and types.
sol_storage! {
#[entrypoint]
struct StylusTestNFT {
#[borrow] // Allows erc721 to access StylusTestNFT's storage and make calls
Erc721<StylusTestNFTParams> erc721;
}
}
#[public]
#[inherit(Erc721<StylusTestNFTParams>)]
impl StylusTestNFT {
/// Mints an NFT
pub fn mint(&mut self) -> Result<(), Erc721Error> {
let minter = msg::sender();
self.erc721.mint(minter)?;
Ok(())
}
/// Mints an NFT to another address
pub fn mint_to(&mut self, to: Address) -> Result<(), Erc721Error> {
self.erc721.mint(to)?;
Ok(())
}
/// Burns an NFT
pub fn burn(&mut self, token_id: U256) -> Result<(), Erc721Error> {
// This function checks that msg::sender() owns the specified token_id
self.erc721.burn(msg::sender(), token_id)?;
Ok(())
}
/// Total supply
pub fn total_supply(&mut self) -> Result<U256, Erc721Error> {
Ok(self.erc721.total_supply.get())
}
}
================================================
File: examples/erc721/src/main.rs
================================================
#[cfg(feature = "export-abi")]
fn main() {
erc721::print_abi("MIT-OR-APACHE-2.0", "pragma solidity ^0.8.23;");
}
================================================
File: examples/single_call/Cargo.toml
================================================
[package]
name = "stylus-single-call"
version = "0.1.5"
edition = "2021"
license = "MIT OR Apache-2.0"
keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
description = "Stylus single call router contract"
[dependencies]
alloy-primitives = "=0.8.14"
alloy-sol-types = "=0.8.14"
hex = "0.4.3"
stylus-sdk = { path = "../../stylus-sdk" }
mini-alloc = { path = "../../mini-alloc" }
[features]
export-abi = ["stylus-sdk/export-abi"]
[lib]
crate-type = ["lib", "cdylib"]
[profile.release]
codegen-units = 1
strip = true
lto = true
panic = "abort"
opt-level = "s"
[workspace]
================================================
File: examples/single_call/rust-toolchain.toml
================================================
[toolchain]
channel = "1.81.0"
================================================
File: examples/single_call/.gitignore
================================================
/target
.deployment_key
================================================
File: examples/single_call/scripts/hardhat.config.js
================================================
require("dotenv").config();
require("@nomicfoundation/hardhat-ignition-ethers");
// Initial checks
if (
!process.env.RPC_URL ||
process.env.RPC_URL == "" ||
!process.env.PRIVATE_KEY ||
process.env.PRIVATE_KEY == ""
) {
console.error(
"The following environment variables are needed (set them in .env): RPC_URL, PRIVATE_KEY"
);
return;
}
const RPC_URL = process.env.RPC_URL;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
module.exports = {
solidity: "0.8.18",
paths: {
sources: "./external_contracts",
},
networks: {
arb_sepolia: {
url: RPC_URL,
accounts: [PRIVATE_KEY],
chainId: 421614,
},
},
};
================================================
File: examples/single_call/scripts/.env.example
================================================
RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
TX_EXPLORER_URL=https://sepolia.arbiscan.io/tx/
SINGLE_CALL_CONTRACT_ADDRESS=0xb27fc74caf34c5c26d27a7654358017331330cee
COUNTER_CONTRACT_ADDRESS=
PRIVATE_KEY=
================================================
File: examples/single_call/scripts/.gitignore
================================================
node_modules
.env
cache
artifacts
ignition/deployments
================================================
File: examples/single_call/scripts/external_contracts/Counter.sol
================================================
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Counter {
uint256 public number;
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
}
================================================
File: examples/single_call/scripts/ignition/modules/deploy_counter.js
================================================
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");
module.exports = buildModule("deploy_counter", (m) => {
const deployCounter = m.contract("Counter", []);
m.call(deployCounter, "setNumber", [42]);
return { deployCounter };
});
================================================
File: examples/single_call/scripts/src/main.js
================================================
const {
Contract,
Wallet,
JsonRpcProvider,
parseEther,
formatEther,
} = require("ethers");
require("dotenv").config();
// Initial checks
if (
!process.env.RPC_URL ||
process.env.RPC_URL == "" ||
!process.env.SINGLE_CALL_CONTRACT_ADDRESS ||
process.env.SINGLE_CALL_CONTRACT_ADDRESS == "" ||
!process.env.COUNTER_CONTRACT_ADDRESS ||
process.env.COUNTER_CONTRACT_ADDRESS == "" ||
!process.env.PRIVATE_KEY ||
process.env.PRIVATE_KEY == ""
) {
console.error(
"The following environment variables are needed (set them in .env): RPC_URL, SINGLE_CALL_CONTRACT_ADDRESS, COUNTER_CONTRACT_ADDRESS, PRIVATE_KEY"
);
return;
}
// ABI of the SingleCall contract
const ABI_SINGLE_CALL = [
"function execute(address target, bytes data) external returns (bytes)",
];
// ABI of the Counter contract
const ABI_COUNTER = [
"function number() external view returns (uint256)",
"function setNumber(uint256 value) external",
"function increment() external",
];
// Addresses for the contracts
const SINGLE_CALL_ADDRESS = process.env.SINGLE_CALL_CONTRACT_ADDRESS;
const COUNTER_ADDRESS = process.env.COUNTER_CONTRACT_ADDRESS;
// Transaction Explorer URL
const TX_EXPLORER_URL = process.env.TX_EXPLORER_URL;
// Private key and ethers provider
const WALLET_PRIVATE_KEY = process.env.PRIVATE_KEY;
const stylusRpcProvider = new JsonRpcProvider(process.env.RPC_URL);
const signer = new Wallet(WALLET_PRIVATE_KEY, stylusRpcProvider);
// Main function
const main = async () => {
// // Presentation message
console.log(
`Incrementing the Counter contract at ${COUNTER_ADDRESS} via the SingleCall router at ${SINGLE_CALL_ADDRESS}`
);
// Connecting to the contracts
const singleCall = new Contract(SINGLE_CALL_ADDRESS, ABI_SINGLE_CALL, signer);
const counter = new Contract(COUNTER_ADDRESS, ABI_COUNTER, signer);
// Current value for the Counter
const currentCount = await counter.number();
console.log(`Current count: ${currentCount}`);
// Encode the function call data
const encodedData = counter.interface.encodeFunctionData("increment");
console.log(encodedData);
// Route the calldata through the SingleCall contract to the Counter contract
const incTransaction = await singleCall.execute(COUNTER_ADDRESS, encodedData);
await incTransaction.wait();
console.log(`Transaction hash: ${incTransaction.hash}`);
console.log(`View tx on explorer: ${TX_EXPLORER_URL}${incTransaction.hash}`);
// Check the Counter value again
const updatedCount = await counter.number();
console.log(`Updated count: ${updatedCount}`);
};
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
================================================
File: examples/single_call/src/lib.rs
================================================
#![cfg_attr(not(feature = "export-abi"), no_main)]
extern crate alloc;
use stylus_sdk::{abi::Bytes, alloy_primitives::Address, call::RawCall, prelude::*};
#[storage]
#[entrypoint]
pub struct SingleCall;
#[public]
impl SingleCall {
pub fn execute(&self, target: Address, data: Bytes) -> Bytes {
let result = RawCall::new().call(target, data.to_vec().as_slice());
result.unwrap().into()
}
}
================================================
File: examples/single_call/src/main.rs
================================================
#![cfg_attr(not(feature = "export-abi"), no_main)]
#[cfg(feature = "export-abi")]
fn main() {
stylus_single_call::print_abi("MIT-OR-APACHE-2.0", "pragma solidity ^0.8.23;");
}
================================================
File: mini-alloc/Cargo.toml
================================================
[package]
name = "mini-alloc"
keywords = ["wasm", "stylus", "allocator"]
description = "Very simple global allocator"
readme = "README.md"
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
[dependencies]
cfg-if = "1.0.0"
================================================
File: mini-alloc/src/imp.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use core::{
alloc::{GlobalAlloc, Layout},
arch::wasm32,
num::NonZeroUsize as NonZero,
};
pub struct MiniAlloc;
/// This is not a valid implementation of [`Sync`] but is ok in single-threaded WASM.
unsafe impl Sync for MiniAlloc {}
impl MiniAlloc {
pub const INIT: Self = MiniAlloc;
/// The WASM page size, or 2^16 bytes.
pub const PAGE_SIZE: usize = 1 << 16;
}
unsafe impl GlobalAlloc for MiniAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
alloc_impl(layout).unwrap_or(core::ptr::null_mut())
}
#[inline]
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
self.alloc(layout)
}
#[inline]
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {}
}
extern "C" {
/// This symbol is created by the LLVM linker.
static __heap_base: u8;
}
/// Represents the negation of the allocator's bump offset and boundary
///
/// We store the negation because we can align the negative offset in fewer
/// instructions than the positive offset.
static mut STATE: Option<(NonZero, usize)> = None;
unsafe fn alloc_impl(layout: Layout) -> Option<*mut u8> {
let (neg_offset, neg_bound) = STATE.get_or_insert_with(|| {
let heap_base = &__heap_base as *const u8 as usize;
let bound = MiniAlloc::PAGE_SIZE * wasm32::memory_size(0) - 1;
(
NonZero::new_unchecked(heap_base.wrapping_neg()),
bound.wrapping_neg(),
)
});
let neg_aligned = make_aligned(neg_offset.get(), layout.align());
let next_neg_offset = neg_aligned.checked_sub(layout.size())?;
let bytes_needed = neg_bound.saturating_sub(next_neg_offset + 1);
if bytes_needed != 0 {
let pages_needed = 1 + (bytes_needed - 1) / MiniAlloc::PAGE_SIZE;
if wasm32::memory_grow(0, pages_needed) == usize::MAX {
return None;
}
*neg_bound -= MiniAlloc::PAGE_SIZE * pages_needed;
}
*neg_offset = NonZero::new_unchecked(next_neg_offset);
Some(neg_aligned.wrapping_neg() as *mut u8)
}
/// Returns `value` rounded down to the next multiple of `align`.
/// Note: `align` must be a power of two, which is guaranteed by [`Layout::align`].
#[inline(always)]
fn make_aligned(value: usize, align: usize) -> usize {
value & align.wrapping_neg()
}
================================================
File: mini-alloc/src/lib.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
#![no_std]
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(target_arch = "wasm32")] {
mod imp;
pub use imp::MiniAlloc;
}
}
================================================
File: mini-alloc/tests/misc.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
#![no_std]
extern crate alloc;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
#[cfg(target_arch = "wasm32")]
#[global_allocator]
static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT;
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen_test]
fn vec_test() {
use alloc::vec::Vec;
let p1 = Vec::<u8>::with_capacity(700);
let p2 = Vec::<u8>::with_capacity(65536);
let p3 = Vec::<u8>::with_capacity(700000);
let p4 = Vec::<u32>::with_capacity(1);
let p5 = Vec::<u8>::with_capacity(1);
let p6 = Vec::<u16>::with_capacity(1);
assert_eq!(p1.as_ptr() as usize + 700, p2.as_ptr() as usize);
assert_eq!(p2.as_ptr() as usize + 65536, p3.as_ptr() as usize);
assert!((p4.as_ptr() as usize) < p3.as_ptr() as usize + 700004);
assert!((p4.as_ptr() as usize) >= p3.as_ptr() as usize + 700000);
assert_eq!(p4.as_ptr() as usize & 3, 0);
assert_eq!(p4.as_ptr() as usize + 4, p5.as_ptr() as usize);
assert_eq!(p5.as_ptr() as usize + 2, p6.as_ptr() as usize);
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen_test]
fn vec_test_loop() {
use alloc::vec::Vec;
let mut size = 1usize;
let mut p = Vec::<u8>::with_capacity(size);
for _ in 0..22 {
let new_size = size * 2;
let new_p = Vec::<u8>::with_capacity(new_size);
assert_eq!(p.as_ptr() as usize + size, new_p.as_ptr() as usize);
size = new_size;
p = new_p;
}
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen_test]
#[should_panic]
fn vec_test_overallocate() {
use alloc::vec::Vec;
let _ = Vec::<u8>::with_capacity(0xFFFFFFFF);
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen_test]
fn edge_cases() {
use alloc::alloc::{alloc, Layout};
use core::arch::wasm32;
use mini_alloc::MiniAlloc;
const PAGE_SIZE: usize = MiniAlloc::PAGE_SIZE;
const PAGE_LIMIT: usize = 65536;
fn size() -> usize {
wasm32::memory_size(0) as usize
}
fn size_bytes() -> usize {
size() * PAGE_SIZE
}
fn next(size: usize) -> usize {
let align = 1;
let layout = Layout::from_size_align(size, align).unwrap();
unsafe { alloc(layout) as usize }
}
assert_eq!(size(), 1);
// check that zero-allocs don't bump
let start = next(0);
assert_eq!(start, next(0));
assert_eq!(start / PAGE_SIZE, 0);
assert_eq!(size(), 1);
// fill the rest of the page
let rest = size_bytes() - start;
let end = next(rest);
assert_eq!(end / PAGE_SIZE, 0);
assert_eq!(end, start);
assert_eq!(size(), 1);
// allocate a second page
let first = next(1);
assert_eq!(first / PAGE_SIZE, 1);
assert_eq!(first, PAGE_SIZE);
assert_eq!(size(), 2);
// fill the rest of the second page
let rest = size_bytes() - (first + 1);
let end = next(rest);
assert_eq!(end, first + 1);
assert_eq!(size(), 2);
// jump 4 pages
let jump = next(4 * PAGE_SIZE);
assert_eq!(jump, 2 * PAGE_SIZE);
assert_eq!(size(), 6);
// allocate many pages
let mut rng: usize = 0;
while size() < PAGE_LIMIT / 2 {
rng = rng.wrapping_mul(1664525).wrapping_add(1013904223);
let rest = usize::MAX - next(0) + 1;
let bytes = match rng % 4 {
0 => rng % 1024,
1 => rng % PAGE_SIZE,
2 => next(size_bytes() - next(0)), // rest of page
_ => rng % (PAGE_SIZE * 200),
};
let offset = next(bytes.min(rest));
if offset == size_bytes() {
assert_eq!(bytes, 0);
} else {
assert!(offset < size_bytes());
}
}
}
================================================
File: mini-alloc/.cargo/config.toml
================================================
[build]
target = "wasm32-unknown-unknown"
[target.wasm32-unknown-unknown]
rustflags = [
"-C", "link-arg=-zstack-size=8192",
]
================================================
File: stylus-proc/Cargo.toml
================================================
[package]
name = "stylus-proc"
keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
description = "Procedural macros for stylus-sdk"
readme = "../README.md"
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
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-macro-error.workspace = true
proc-macro2.workspace = true
quote.workspace = true
regex.workspace = true
sha3.workspace = true
syn.workspace = true
syn-solidity.workspace = true
trybuild.workspace = true
[dev-dependencies]
paste.workspace = true
pretty_assertions.workspace = true
prettyplease.workspace = true
stylus-sdk.workspace = true
[features]
default = []
export-abi = ["stylus-sdk/export-abi"]
reentrant = []
[package.metadata.docs.rs]
features = ["export-abi"]
[lib]
proc-macro = true
================================================
File: stylus-proc/src/consts.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Constants for name definitions in generated code.
//!
//! Any generated globals or associated items should use a `__stylus` prefix to avoid name
//! collisions.
use proc_macro2::{Span, TokenStream};
use quote::ToTokens;
/// Name of the entrypoint function that is generated for struct-based contracts.
pub const STRUCT_ENTRYPOINT_FN: ConstIdent = ConstIdent("__stylus_struct_entrypoint");
/// Name of the associated function that can be called to assert safe overrides at compile-time.
pub const ASSERT_OVERRIDES_FN: ConstIdent = ConstIdent("__stylus_assert_overrides");
/// Name of the associated function that can be called to check safe overriding of a single
/// function.
pub const ALLOW_OVERRIDE_FN: ConstIdent = ConstIdent("__stylus_allow_override");
pub const STYLUS_HOST_FIELD: ConstIdent = ConstIdent("__stylus_host");
/// Definition of a constant identifier
pub struct ConstIdent(&'static str);
impl ConstIdent {
pub fn as_ident(&self) -> syn::Ident {
syn::Ident::new(self.0, Span::call_site())
}
}
impl ToTokens for ConstIdent {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.as_ident().to_tokens(tokens);
}
}
================================================
File: stylus-proc/src/imports.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Constants for referencing imports within generated code.
//!
//! These constants use a fully qualified path with dependencies nested within [`stylus_sdk`] to
//! ensure compatibility.
//!
//! Usage:
//! ```compile_fail
//! use crate::imports::alloy_primitives::Address;
//!
//! let _ = quote! {
//! let addr = #Address::random();
//! };
//! ```
#![allow(non_upper_case_globals)]
use proc_macro2::TokenStream;
use quote::ToTokens;
pub mod alloy_primitives {
use crate::imports::ConstPath;
pub const Address: ConstPath = ConstPath("stylus_sdk::alloy_primitives::Address");
}
pub mod alloy_sol_types {
use crate::imports::ConstPath;
pub const SolType: ConstPath = ConstPath("stylus_sdk::alloy_sol_types::SolType");
pub const SolValue: ConstPath = ConstPath("stylus_sdk::alloy_sol_types::SolValue");
pub mod private {
use crate::imports::ConstPath;
pub const SolTypeValue: ConstPath =
ConstPath("stylus_sdk::alloy_sol_types::private::SolTypeValue");
}
pub mod sol_data {
use syn::parse::Parse;
use crate::imports::ConstPath;
pub const Address: ConstPath = ConstPath("stylus_sdk::alloy_sol_types::sol_data::Address");
/// Build path or type to member of the `alloy_sol_types::sol_data` module.
///
/// This should not be used on user input, as parsing should be expected to succeed.
pub fn join<T: Parse>(name: &str) -> T {
syn::parse_str(&format!("stylus_sdk::alloy_sol_types::sol_data::{name}")).unwrap()
}
}
}
pub mod stylus_sdk {
pub mod abi {
use crate::imports::ConstPath;
pub const AbiType: ConstPath = ConstPath("stylus_sdk::abi::AbiType");
pub const Router: ConstPath = ConstPath("stylus_sdk::abi::Router");
}
}
/// Definition of a fully-qualified path for generated code.
pub struct ConstPath(&'static str);
impl ConstPath {
/// Interpret the path as a [`syn::Type`].
pub fn as_type(&self) -> syn::Type {
syn::parse_str(self.0).unwrap()
}
}
impl ToTokens for ConstPath {
fn to_tokens(&self, tokens: &mut TokenStream) {
let path: syn::Path = syn::parse_str(self.0).unwrap();
path.to_tokens(tokens);
}
}
#[cfg(test)]
mod tests {
use syn::parse_quote;
#[test]
fn test_const_path() {
assert_eq!(
super::alloy_primitives::Address.as_type(),
parse_quote!(stylus_sdk::alloy_primitives::Address),
);
}
}
================================================
File: stylus-proc/src/lib.rs
================================================
// Copyright 2022-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Procedural macros for [The Stylus SDK][sdk].
//!
//! You can import these via
//!
//! ```
//! 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;
use proc_macro_error::proc_macro_error;
/// Generates a pretty error message.
/// Note that this macro is declared before all modules so that they can use it.
macro_rules! error {
($tokens:expr, $($msg:expr),+ $(,)?) => {{
let error = syn::Error::new(syn::spanned::Spanned::span(&$tokens), format!($($msg),+));
return error.to_compile_error().into();
}};
(@ $tokens:expr, $($msg:expr),+ $(,)?) => {{
return Err(syn::Error::new(syn::spanned::Spanned::span(&$tokens), format!($($msg),+)))
}};
}
mod consts;
mod impls;
mod imports;
mod macros;
mod types;
mod utils;
/// Allows a Rust `struct` to be used in persistent storage.
///
/// ```
/// extern crate alloc;
/// # use stylus_sdk::storage::{StorageAddress, StorageBool};
/// # use stylus_proc::storage;
/// # use stylus_sdk::prelude::*;
/// #[storage]
/// pub struct Contract {
/// owner: StorageAddress,
/// active: StorageBool,
/// sub_struct: SubStruct,
///}
///
///#[storage]
///pub struct SubStruct {
/// number: StorageBool,
///}
/// ```
///
/// Each field must implement [`StorageType`]. This includes other structs, which will
/// implement the `trait` automatically when [`#[storage]`][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.
///
/// [storage]: macro@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]
#[proc_macro_error]
pub fn storage(attr: TokenStream, input: TokenStream) -> TokenStream {
macros::storage(attr, input)
}
#[doc(hidden)]
#[deprecated = "please use `#[storage]` instead"]
#[proc_macro_attribute]
#[proc_macro_error]
pub fn solidity_storage(attr: TokenStream, input: TokenStream) -> TokenStream {
macros::storage(attr, input)
}
/// The types in [`#[storage]`][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.
///
/// ```
/// extern crate alloc;
/// # use stylus_sdk::prelude::*;
/// # use stylus_proc::sol_storage;
/// 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
/// }
/// pub struct Delegate {
/// }
/// }
/// ```
///
/// 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.
///
/// Note that one exception to this storage layout guarantee is contracts which utilize
/// inheritance. The current solution in Stylus using `#[borrow]` and `#[inherits(...)]` packs
/// nested (inherited) structs into their own slots. This is consistent with regular struct nesting
/// in solidity, but not inherited structs. We plan to revisit this behavior in an upcoming
/// release.
///
/// 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.
///
/// [storage]: macro@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/main/examples/erc20/src/main.rs
#[proc_macro]
#[proc_macro_error]
pub fn sol_storage(input: TokenStream) -> TokenStream {
macros::sol_storage(input)
}
/// Facilitates calls to other contracts.
///
/// This macro defines a `struct` for each of the Solidity interfaces provided.
///
/// ```
/// # use stylus_proc::sol_interface;
/// sol_interface! {
/// interface IService {
/// function makePayment(address user) external payable returns (string);
/// function getConstant() external 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`].
///
/// Currently only functions are supported, and any other items in the interface will cause an
/// error. Additionally, each function must be marked `external`. Inheritance is not supported.
///
/// ```
/// use stylus_sdk::call::{Call, Error};
/// use alloy_primitives::Address;
/// # use stylus_proc::sol_interface;
///
/// # sol_interface! {
/// # interface IService {
/// # function makePayment(address user) external payable returns (string);
/// # }
/// # }
/// # mod evm { pub fn gas_left() -> u64 { 100 } }
/// # mod msg { pub fn value() -> alloy_primitives::U256 { 100.try_into().unwrap() } }
/// pub fn do_call(account: IService, user: Address) -> Result<String, Error> {
/// 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`.
///
/// Note that structs may be used, as return types for example. Trying to reference structs using
/// the Solidity path separator (`module.MyStruct`) is supported and paths will be converted to
/// Rust syntax (`module::MyStruct`).
///
/// # Reentrant calls
///
/// Contracts that opt into reentrancy via the `reentrant` feature flag require extra care.
/// When enabled, cross-contract calls must [`flush`] or [`clear`] the [`StorageCache`] to safeguard state.
/// This happens automatically via the type system.
///
/// ```
/// # extern crate alloc;
/// # use stylus_sdk::call::Call;
/// # use stylus_sdk::prelude::*;
/// # use stylus_proc::{entrypoint, public, sol_interface, storage};
/// sol_interface! {
/// interface IMethods {
/// function pureFoo() external pure;
/// function viewFoo() external view;
/// function writeFoo() external;
/// function payableFoo() external payable;
/// }
/// }
///
/// #[entrypoint] #[storage] struct Contract {}
/// #[public]
/// impl Contract {
/// pub fn call_pure(&self, methods: IMethods) -> Result<(), Vec<u8>> {
/// Ok(methods.pure_foo(self)?) // `pure` methods might lie about not being `view`
/// }
///
/// pub fn call_view(&self, methods: IMethods) -> Result<(), Vec<u8>> {
/// Ok(methods.view_foo(self)?)
/// }
///
/// pub fn call_write(&mut self, methods: IMethods) -> Result<(), Vec<u8>> {
/// methods.view_foo(&mut *self)?; // allows `pure` and `view` methods too
/// Ok(methods.write_foo(self)?)
/// }
///
/// #[payable]
/// pub fn call_payable(&mut self, methods: IMethods) -> Result<(), Vec<u8>> {
/// 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.
///
/// ```
/// use stylus_sdk::{call::{Call, Error}, storage::TopLevelStorage};
/// use alloy_primitives::Address;
/// # use stylus_proc::sol_interface;
///
/// # sol_interface! {
/// # interface IService {
/// # function makePayment(address user) external payable returns (string);
/// # }
/// # }
/// # mod evm { pub fn gas_left() -> u64 { 100 } }
/// # mod msg { pub fn value() -> alloy_primitives::U256 { 100.try_into().unwrap() } }
/// 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<String, Error> {
///
/// 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 a [`#[public]`][public] 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
/// [public]: macro@public
/// [`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]
#[proc_macro_error]
pub fn sol_interface(input: TokenStream) -> TokenStream {
macros::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.
///
/// ```
/// extern crate alloc;
/// # use stylus_proc::{Erase, sol_storage};
/// 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)]
#[proc_macro_error]
pub fn derive_erase(input: TokenStream) -> TokenStream {
macros::derive_erase(input)
}
/// Allows an error `enum` to be used in method signatures.
///
/// ```
/// # use alloy_sol_types::sol;
/// # use stylus_proc::{public, SolidityError};
/// # extern crate alloc;
/// sol! {
/// error InsufficientBalance(address from, uint256 have, uint256 want);
/// error InsufficientAllowance(address owner, address spender, uint256 have, uint256 want);
/// }
///
/// #[derive(SolidityError)]
/// pub enum Erc20Error {
/// InsufficientBalance(InsufficientBalance),
/// InsufficientAllowance(InsufficientAllowance),
/// }
///
/// # struct Contract {}
/// #[public]
/// impl Contract {
/// pub fn fallible_method() -> Result<(), Erc20Error> {
/// // code that might revert
/// # Ok(())
/// }
/// }
/// ```
///
/// Under the hood, the above macro works by implementing `From<Erc20Error>` for `Vec<u8>`
/// along with printing code for abi-export.
#[proc_macro_derive(SolidityError)]
#[proc_macro_error]
pub fn derive_solidity_error(input: TokenStream) -> TokenStream {
macros::derive_solidity_error(input)
}
/// 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`.
///
/// ```
/// # extern crate alloc;
/// # use stylus_proc::{entrypoint, public, sol_storage};
/// # use stylus_sdk::prelude::*;
/// sol_storage! {
/// #[entrypoint]
/// pub struct Contract {
/// }
///
/// // only one entrypoint is allowed
/// pub struct SubStruct {
/// }
/// }
/// # #[public] impl Contract {}
/// ```
///
/// The above will make the public methods of Contract the first to consider during invocation.
/// See [`#[public]`][public] for more information on method selection.
///
/// # 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.
///
/// ```
/// extern crate alloc;
/// # use stylus_sdk::ArbResult;
/// # use stylus_proc::entrypoint;
/// # use stylus_sdk::prelude::*;
/// #[entrypoint]
/// fn entrypoint(calldata: Vec<u8>, _: alloc::boxed::Box<dyn stylus_sdk::host::Host>) -> ArbResult {
/// // bytes-in, bytes-out programming
/// # Ok(Vec::new())
/// }
/// ```
///
/// # 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
/// [public]: macro@public
/// [check]: https://github.com/OffchainLabs/cargo-stylus#developing-with-stylus
#[proc_macro_attribute]
#[proc_macro_error]
pub fn entrypoint(attr: TokenStream, input: TokenStream) -> TokenStream {
macros::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 "public" so that other contracts can call them by implementing the [`Router`] trait.
///
/// ```
/// # extern crate alloc;
/// # use stylus_sdk::storage::StorageAddress;
/// # use stylus_proc::public;
/// # use alloy_primitives::Address;
/// # struct Contract {
/// # owner: StorageAddress,
/// # }
/// #[public]
/// impl Contract {
/// // our owner method is now callable by other contracts
/// pub fn owner(&self) -> Result<Address, Vec<u8>> {
/// Ok(self.owner.get())
/// }
/// }
///
/// impl Contract {
/// // our set_owner method is not
/// pub fn set_owner(&mut self, new_owner: Address) -> Result<(), Vec<u8>> {
/// // ...
/// # Ok(())
/// }
/// }
/// ```
///
/// In is example, [`Vec<u8>`] becomes the program's revert data.
///
/// # [`#[payable]`][payable]
///
/// As in Solidity, methods may accept ETH as call value.
///
/// ```
/// # extern crate alloc;
/// # use alloy_primitives::Address;
/// # use stylus_proc::{entrypoint, public, storage};
/// # use stylus_sdk::prelude::*;
/// # #[entrypoint] #[storage] struct Contract { #[borrow] erc20: Erc20, }
/// # mod msg {
/// # use alloy_primitives::Address;
/// # pub fn sender() -> Address { Address::ZERO }
/// # pub fn value() -> u32 { 0 }
/// # }
/// #[public]
/// impl Contract {
/// #[payable]
/// pub fn credit(&mut self) -> Result<(), Vec<u8>> {
/// self.erc20.add_balance(msg::sender(), msg::value())
/// }
/// }
/// # #[storage] struct Erc20;
/// # #[public]
/// # impl Erc20 {
/// # pub fn add_balance(&self, sender: Address, value: u32) -> Result<(), Vec<u8>> {
/// # Ok(())
/// # }
/// # }
/// ```
///
/// 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 non-payable methods the [`#[public]`][public] macro can figure state mutability out for you based
/// on the types of the arguments. Functions with `&self` will be considered `view`, those with
/// `&mut self` will be considered `write`, and those with neither will be considered `pure`. Please note that
/// `pure` and `view` functions may change the state of other contracts by calling into them, or
/// even this one if the `reentrant` feature is enabled.
///
/// 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
/// [`#[public]`][public] provides, can be connected via inheritance.
///
/// ```
/// # extern crate alloc;
/// # use alloy_primitives::U256;
/// # use stylus_proc::{entrypoint, public, storage};
/// # use stylus_sdk::prelude::*;
/// # #[entrypoint] #[storage] struct Token { #[borrow] erc20: Erc20, }
/// #[public]
/// #[inherit(Erc20)]
/// impl Token {
/// pub fn mint(&mut self, amount: U256) -> Result<(), Vec<u8>> {
/// // ...
/// # Ok(())
/// }
/// }
///
/// #[storage] struct Erc20;
/// #[public]
/// impl Erc20 {
/// pub fn balance_of() -> Result<U256, Vec<u8>> {
/// // ...
/// # Ok(U256::ZERO)
/// }
/// }
/// ```
///
/// 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.
///
/// Stylus does not currently contain explicit `override` or `virtual` keywords for explicitly
/// marking override functions. It is important, therefore, to carefully ensure that contracts are
/// only overriding the functions.
///
/// 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<Erc20>`][Borrow] and
/// [`BorrowMut<Erc20>`][BorrowMut]. You can implement this yourself, but for simplicity,
/// [`#[storage]`][storage] and [`sol_storage!`][sol_storage] provide a
/// `#[borrow]` annotation.
///
/// ```
/// # extern crate alloc;
/// # use stylus_sdk::prelude::*;
/// # use stylus_proc::{entrypoint, public, sol_storage};
/// sol_storage! {
/// #[entrypoint]
/// pub struct Token {
/// #[borrow]
/// Erc20 erc20;
/// }
///
/// pub struct Erc20 {
/// uint256 total;
/// }
/// }
/// # #[public] impl Token {}
/// # #[public] impl 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
/// [`#[public]`][public] and [`#[entrypoint]`][entrypoint] macros to generate a `main` function
/// that prints the Solidity ABI to the console.
///
/// ```sh
/// cargo run --features export-abi --target <triple>
/// ```
///
/// 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.
///
/// ```no_run
/// #![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;
/// }
/// ```
///
/// [storage]: macro@storage
/// [sol_storage]: macro@sol_storage
/// [entrypoint]: macro@entrypoint
/// [public]: macro@public
/// [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]
#[proc_macro_error]
pub fn public(attr: TokenStream, input: TokenStream) -> TokenStream {
macros::public(attr, input)
}
#[doc(hidden)]
#[deprecated = "please use `#[public]` instead"]
#[proc_macro_attribute]
#[proc_macro_error]
pub fn external(attr: TokenStream, input: TokenStream) -> TokenStream {
public(attr, input)
}
/// Implements the AbiType for arbitrary structs, allowing them to be used in external method
/// return types and parameters. This derive is intended to be used within the
/// [alloy_sol_types::sol] macro.
///
/// ```
/// # use alloy_sol_types::sol;
/// # use stylus_proc::AbiType;
/// sol! {
/// #[derive(AbiType)]
/// struct Foo {
/// uint256 bar;
/// }
/// }
/// ```
#[proc_macro_derive(AbiType)]
#[proc_macro_error]
pub fn derive_abi_type(input: TokenStream) -> TokenStream {
macros::derive_abi_type(input)
}
================================================
File: stylus-proc/src/types.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use std::{fmt::Display, num::NonZeroU16, str::FromStr};
use syn::{parse_quote, Token};
use crate::imports::alloy_sol_types::sol_data;
/// The purity of a Solidity method
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Purity {
Pure,
View,
Write,
Payable,
}
impl Purity {
/// How to reference this purity from inside a contract.
pub fn as_path(&self) -> syn::Path {
match self {
Purity::Pure => parse_quote!(stylus_sdk::methods::Purity::Pure),
Purity::View => parse_quote!(stylus_sdk::methods::Purity::View),
Purity::Write => parse_quote!(stylus_sdk::methods::Purity::Write),
Purity::Payable => parse_quote!(stylus_sdk::methods::Purity::Payable),
}
}
/// Infer the purity of the function by inspecting the first argument. Also returns whether the
/// function has a self parameter.
pub fn infer(func: &syn::ImplItemFn) -> (Self, bool) {
match func.sig.inputs.first() {
Some(syn::FnArg::Receiver(recv)) => (recv.mutability.into(), true),
Some(syn::FnArg::Typed(syn::PatType { ty, .. })) => match &**ty {
syn::Type::Reference(ty) => (ty.mutability.into(), false),
_ => (Self::Pure, false),
},
_ => (Self::Pure, false),
}
}
}
impl Default for Purity {
fn default() -> Self {
Self::Pure
}
}
impl FromStr for Purity {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"pure" => Self::Pure,
"view" => Self::View,
"write" => Self::Write,
"payable" => Self::Payable,
_ => return Err(()),
})
}
}
impl Display for Purity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pure => write!(f, "pure"),
Self::View => write!(f, "view"),
Self::Write => write!(f, "write"),
Self::Payable => write!(f, "payable"),
}
}
}
impl From<Option<Token![mut]>> for Purity {
fn from(value: Option<Token![mut]>) -> Self {
match value.is_some() {
true => Self::Write,
false => Self::View,
}
}
}
/// Alloy type and ABI for a Solidity type
#[derive(Debug)]
pub struct SolidityTypeInfo {
pub alloy_type: syn::Type,
pub sol_type: syn_solidity::Type,
}
impl SolidityTypeInfo {
fn new(alloy_type: syn::Type, sol_type: syn_solidity::Type) -> Self {
Self {
alloy_type,
sol_type,
}
}
}
/// Get type info from given Solidity type
impl From<&syn_solidity::Type> for SolidityTypeInfo {
fn from(sol_type: &syn_solidity::Type) -> Self {
use syn_solidity::Type;
let alloy_type = match sol_type {
Type::Bool(_) => sol_data::join("Bool"),
Type::Address(_, _) => sol_data::join("Address"),
Type::String(_) => sol_data::join("String"),
Type::Bytes(_) => sol_data::join("Bytes"),
Type::FixedBytes(_, size) => sol_data::join(&format!("FixedBytes<{size}>")),
Type::Uint(_, size) => {
let size = size.unwrap_or(NonZeroU16::new(256).unwrap());
sol_data::join(&format!("Uint<{size}>"))
}
Type::Int(_, size) => {
let size = size.unwrap_or(NonZeroU16::new(256).unwrap());
sol_data::join(&format!("Int<{size}>"))
}
Type::Array(array) => {
let Self { alloy_type, .. } = Self::from(&*array.ty);
match array.size() {
Some(size) => {
parse_quote!(stylus_sdk::alloy_sol_types::sol_data::FixedArray<#alloy_type, #size>)
}
None => parse_quote!(stylus_sdk::alloy_sol_types::sol_data::Array<#alloy_type>),
}
}
Type::Tuple(tup) => {
if tup.types.is_empty() {
parse_quote! { () }
} else if tup.types.len() == 1 {
return Self::from(&tup.types[0]);
} else {
let type_info = tup.types.iter().map(Self::from);
let alloy_types = type_info.clone().map(|info| info.alloy_type);
parse_quote! {
(#(#alloy_types,)*)
}
}
}
Type::Custom(path) => {
let path = syn::Path {
leading_colon: None,
segments: path.iter().cloned().map(syn::PathSegment::from).collect(),
};
syn::TypePath { qself: None, path }.into()
}
_ => todo!("Solidity type {sol_type} is not yet implemented in sol_interface!"),
};
Self::new(alloy_type, sol_type.clone())
}
}
#[cfg(test)]
mod tests {
use syn::parse_quote;
use super::SolidityTypeInfo;
macro_rules! sol_type_test {
($sol:ident, $alloy:ty) => {
sol_type_test!($sol, stringify!($sol), @parse_quote!($alloy));
};
($name:ident, $sol:expr, $alloy:ty) => {
sol_type_test!($name, $sol, @parse_quote!($alloy));
};
($name:ident, $sol:expr, @$alloy:expr) => {
paste::paste! {
#[test]
fn [<test_sol_ $name>]() {
let sol_type = syn::parse_str($sol).unwrap();
let info = SolidityTypeInfo::from(&sol_type);
assert_eq!(info.sol_type, sol_type);
assert_eq!(info.sol_type.to_string(), $sol);
assert_eq!(
info.alloy_type,
$alloy,
);
}
}
};
}
sol_type_test!(bool, stylus_sdk::alloy_sol_types::sol_data::Bool);
sol_type_test!(address, stylus_sdk::alloy_sol_types::sol_data::Address);
sol_type_test!(string, stylus_sdk::alloy_sol_types::sol_data::String);
sol_type_test!(bytes, stylus_sdk::alloy_sol_types::sol_data::Bytes);
sol_type_test!(
fixed_bytes,
"bytes10",
stylus_sdk::alloy_sol_types::sol_data::FixedBytes<10>
);
sol_type_test!(uint160, stylus_sdk::alloy_sol_types::sol_data::Uint<160>);
sol_type_test!(int32, stylus_sdk::alloy_sol_types::sol_data::Int<32>);
#[rustfmt::skip]
sol_type_test!(
array,
"int256[]",
stylus_sdk::alloy_sol_types::sol_data::Array<
stylus_sdk::alloy_sol_types::sol_data::Int<256>
>
);
#[rustfmt::skip]
sol_type_test!(
fixed_array,
"int256[100]",
stylus_sdk::alloy_sol_types::sol_data::FixedArray<
stylus_sdk::alloy_sol_types::sol_data::Int<256>,
100usize
>
);
sol_type_test!(
tuple,
"(uint256,bytes,string)",
@parse_quote! {(
stylus_sdk::alloy_sol_types::sol_data::Uint<256>,
stylus_sdk::alloy_sol_types::sol_data::Bytes,
stylus_sdk::alloy_sol_types::sol_data::String,
)}
);
sol_type_test!(custom_path, "foo.bar.baz", foo::bar::baz);
}
================================================
File: stylus-proc/src/impls/abi_proxy.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Generate implementations of [`stylus_sdk::abi::AbiType`] and all required associated traits by
//! proxying to an existing type which implements these traits.
//!
//! The type being implemented must implement `From<ProxyType>` and `Deref<Target = ProxyType>`.
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::parse_quote;
use crate::imports::{
alloy_sol_types::{private::SolTypeValue, SolType, SolValue},
stylus_sdk::abi::AbiType,
};
/// Implementations of all traits required for a [`stylus_sdk::abi::AbiType`].
#[derive(Debug)]
pub struct ImplAbiProxy {
abi_type: syn::ItemImpl,
sol_type: syn::ItemImpl,
sol_value: syn::ItemImpl,
sol_type_value: syn::ItemImpl,
}
impl ImplAbiProxy {
/// Generate all the required implementations.
pub fn new(self_ty: &syn::Ident, proxy_ty: &syn::Type, sol_ty: &syn::Type) -> Self {
Self {
abi_type: impl_abi_type(self_ty, proxy_ty),
sol_type: impl_sol_type(self_ty, sol_ty),
sol_value: impl_sol_value(self_ty),
sol_type_value: impl_sol_type_value(self_ty, sol_ty),
}
}
}
impl ToTokens for ImplAbiProxy {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.abi_type.to_tokens(tokens);
self.sol_type.to_tokens(tokens);
self.sol_value.to_tokens(tokens);
self.sol_type_value.to_tokens(tokens);
}
}
/// Implement [`stylus_sdk::abi::AbiType`].
fn impl_abi_type(self_ty: &syn::Ident, proxy_ty: &syn::Type) -> syn::ItemImpl {
parse_quote! {
impl #AbiType for #self_ty {
type SolType = #self_ty;
const ABI: stylus_sdk::abi::ConstString = <#proxy_ty as #AbiType>::ABI;
}
}
}
/// Implement [`alloy_sol_types::SolType`].
fn impl_sol_type(self_ty: &syn::Ident, sol_ty: &syn::Type) -> syn::ItemImpl {
parse_quote! {
impl #SolType for #self_ty {
type RustType = #self_ty;
type Token<'a> = <#sol_ty as #SolType>::Token<'a>;
const SOL_NAME: &'static str = <#sol_ty as #SolType>::SOL_NAME;
const ENCODED_SIZE: Option<usize> = <#sol_ty as #SolType>::ENCODED_SIZE;
const PACKED_ENCODED_SIZE: Option<usize> = <#sol_ty as #SolType>::PACKED_ENCODED_SIZE;
fn valid_token(token: &Self::Token<'_>) -> bool {
<#sol_ty as #SolType>::valid_token(token)
}
fn detokenize(token: Self::Token<'_>) -> Self::RustType {
#sol_ty::detokenize(token).into()
}
}
}
}
/// Implement [`alloy_sol_types::SolValue`].
fn impl_sol_value(self_ty: &syn::Ident) -> syn::ItemImpl {
parse_quote! {
impl #SolValue for #self_ty {
type SolType = #self_ty;
}
}
}
/// Implement [`alloy_sol_types::private::SolTypeValue`].
fn impl_sol_type_value(self_ty: &syn::Ident, sol_ty: &syn::Type) -> syn::ItemImpl {
parse_quote! {
impl #SolTypeValue<Self> for #self_ty {
#[inline]
fn stv_to_tokens(&self) -> <Self as #SolType>::Token<'_> {
use core::ops::Deref;
<#sol_ty as #SolType>::tokenize(self.deref())
}
#[inline]
fn stv_abi_encoded_size(&self) -> usize {
use core::ops::Deref;
<#sol_ty as #SolType>::abi_encoded_size(self.deref())
}
#[inline]
fn stv_abi_packed_encoded_size(&self) -> usize {
use core::ops::Deref;
<#sol_ty as #SolType>::abi_packed_encoded_size(self.deref())
}
#[inline]
fn stv_eip712_data_word(&self) -> stylus_sdk::alloy_sol_types::Word {
use core::ops::Deref;
<#sol_ty as #SolType>::eip712_data_word(self.deref())
}
#[inline]
fn stv_abi_encode_packed_to(&self, out: &mut stylus_sdk::alloy_sol_types::private::Vec<u8>) {
use core::ops::Deref;
<#sol_ty as #SolType>::abi_encode_packed_to(self.deref(), out)
}
}
}
}
#[cfg(test)]
mod tests {
use syn::parse_quote;
use super::ImplAbiProxy;
use crate::utils::testing::assert_ast_eq;
#[test]
fn test_impl_abi_proxy() {
let proxy = ImplAbiProxy::new(&parse_quote!(Foo), &parse_quote!(Bar), &parse_quote!(Baz));
assert_ast_eq(
proxy.abi_type,
parse_quote! {
impl stylus_sdk::abi::AbiType for Foo {
type SolType = Foo;
const ABI: stylus_sdk::abi::ConstString = <Bar as stylus_sdk::abi::AbiType>::ABI;
}
},
);
assert_ast_eq(
proxy.sol_type,
parse_quote! {
impl stylus_sdk::alloy_sol_types::SolType for Foo {
type RustType = Foo;
type Token<'a> = <Baz as stylus_sdk::alloy_sol_types::SolType>::Token<'a>;
const SOL_NAME: &'static str = <Baz as stylus_sdk::alloy_sol_types::SolType>::SOL_NAME;
const ENCODED_SIZE: Option<usize> = <Baz as stylus_sdk::alloy_sol_types::SolType>::ENCODED_SIZE;
const PACKED_ENCODED_SIZE: Option<usize> = <Baz as stylus_sdk::alloy_sol_types::SolType>::PACKED_ENCODED_SIZE;
fn valid_token(token: &Self::Token<'_>) -> bool {
<Baz as stylus_sdk::alloy_sol_types::SolType>::valid_token(token)
}
fn detokenize(token: Self::Token<'_>) -> Self::RustType {
Baz::detokenize(token).into()
}
}
},
);
assert_ast_eq(
proxy.sol_value,
parse_quote! {
impl stylus_sdk::alloy_sol_types::SolValue for Foo {
type SolType = Foo;
}
},
);
assert_ast_eq(
proxy.sol_type_value,
parse_quote! {
impl stylus_sdk::alloy_sol_types::private::SolTypeValue<Self> for Foo {
#[inline]
fn stv_to_tokens(&self) -> <Self as stylus_sdk::alloy_sol_types::SolType>::Token<'_> {
use core::ops::Deref;
<Baz as stylus_sdk::alloy_sol_types::SolType>::tokenize(self.deref())
}
#[inline]
fn stv_abi_encoded_size(&self) -> usize {
use core::ops::Deref;
<Baz as stylus_sdk::alloy_sol_types::SolType>::abi_encoded_size(self.deref())
}
#[inline]
fn stv_abi_packed_encoded_size(&self) -> usize {
use core::ops::Deref;
<Baz as stylus_sdk::alloy_sol_types::SolType>::abi_packed_encoded_size(self.deref())
}
#[inline]
fn stv_eip712_data_word(&self) -> stylus_sdk::alloy_sol_types::Word {
use core::ops::Deref;
<Baz as stylus_sdk::alloy_sol_types::SolType>::eip712_data_word(self.deref())
}
#[inline]
fn stv_abi_encode_packed_to(&self, out: &mut stylus_sdk::alloy_sol_types::private::Vec<u8>) {
use core::ops::Deref;
<Baz as stylus_sdk::alloy_sol_types::SolType>::abi_encode_packed_to(self.deref(), out)
}
}
},
);
}
}
================================================
File: stylus-proc/src/impls/mod.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Generate trait implementations.
pub mod abi_proxy;
================================================
File: stylus-proc/src/macros/entrypoint.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use cfg_if::cfg_if;
use proc_macro2::{Ident, Span, TokenStream};
use proc_macro_error::{abort, emit_error};
use quote::ToTokens;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, parse_quote,
};
use crate::consts::{ASSERT_OVERRIDES_FN, STRUCT_ENTRYPOINT_FN};
/// Implementation for the [`#[entrypoint]`][crate::entrypoint] macro.
pub fn entrypoint(
attr: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
if !attr.is_empty() {
emit_error!(Span::mixed_site(), "this macro is not configurable");
}
let entrypoint: Entrypoint = parse_macro_input!(input);
entrypoint.into_token_stream().into()
}
struct Entrypoint {
kind: EntrypointKind,
mark_used_fn: syn::ItemFn,
user_entrypoint_fn: syn::ItemFn,
}
impl Parse for Entrypoint {
fn parse(input: ParseStream) -> syn::Result<Self> {
let item: syn::Item = input.parse()?;
let kind = match item {
syn::Item::Fn(item) => EntrypointKind::Fn(EntrypointFn { item }),
syn::Item::Struct(item) => EntrypointKind::Struct(EntrypointStruct {
top_level_storage_impl: top_level_storage_impl(&item),
struct_entrypoint_fn: struct_entrypoint_fn(&item.ident),
assert_overrides_const: assert_overrides_const(&item.ident),
print_abi_fn: print_abi_fn(&item.ident),
item,
}),
_ => abort!(item, "not a struct or fn"),
};
Ok(Self {
user_entrypoint_fn: user_entrypoint_fn(kind.entrypoint_fn_name()),
mark_used_fn: mark_used_fn(),
kind,
})
}
}
impl ToTokens for Entrypoint {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.kind.to_tokens(tokens);
self.mark_used_fn.to_tokens(tokens);
self.user_entrypoint_fn.to_tokens(tokens);
}
}
enum EntrypointKind {
Fn(EntrypointFn),
Struct(EntrypointStruct),
}
impl EntrypointKind {
fn entrypoint_fn_name(&self) -> Ident {
match self {
EntrypointKind::Fn(EntrypointFn { item }) => item.sig.ident.clone(),
EntrypointKind::Struct(EntrypointStruct { item, .. }) => {
let mut ident = STRUCT_ENTRYPOINT_FN.as_ident();
ident.set_span(item.ident.span());
ident
}
}
}
}
impl ToTokens for EntrypointKind {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
EntrypointKind::Fn(inner) => inner.to_tokens(tokens),
EntrypointKind::Struct(inner) => inner.to_tokens(tokens),
}
}
}
struct EntrypointFn {
item: syn::ItemFn,
}
impl ToTokens for EntrypointFn {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.item.to_tokens(tokens);
}
}
struct EntrypointStruct {
item: syn::ItemStruct,
top_level_storage_impl: syn::ItemImpl,
struct_entrypoint_fn: syn::ItemFn,
assert_overrides_const: syn::ItemConst,
print_abi_fn: Option<syn::ItemFn>,
}
impl ToTokens for EntrypointStruct {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.item.to_tokens(tokens);
self.top_level_storage_impl.to_tokens(tokens);
self.struct_entrypoint_fn.to_tokens(tokens);
self.assert_overrides_const.to_tokens(tokens);
self.print_abi_fn.to_tokens(tokens);
}
}
fn top_level_storage_impl(item: &syn::ItemStruct) -> syn::ItemImpl {
let name = &item.ident;
let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
parse_quote! {
unsafe impl #impl_generics stylus_sdk::storage::TopLevelStorage for #name #ty_generics #where_clause {}
}
}
fn struct_entrypoint_fn(name: &Ident) -> syn::ItemFn {
parse_quote! {
fn #STRUCT_ENTRYPOINT_FN(input: alloc::vec::Vec<u8>, host: alloc::boxed::Box<dyn stylus_sdk::host::Host>) -> stylus_sdk::ArbResult {
stylus_sdk::abi::router_entrypoint::<#name, #name>(input, host)
}
}
}
fn assert_overrides_const(name: &Ident) -> syn::ItemConst {
parse_quote! {
const _: () = {
<#name>::#ASSERT_OVERRIDES_FN();
};
}
}
fn mark_used_fn() -> syn::ItemFn {
parse_quote! {
#[no_mangle]
pub unsafe fn mark_used() {
let host = stylus_sdk::host::wasm::WasmHost{};
host.pay_for_memory_grow(0);
panic!();
}
}
}
fn user_entrypoint_fn(user_fn: Ident) -> syn::ItemFn {
let deny_reentrant = deny_reentrant();
parse_quote! {
#[no_mangle]
pub extern "C" fn user_entrypoint(len: usize) -> usize {
let host = alloc::boxed::Box::new(stylus_sdk::host::wasm::WasmHost{});
#deny_reentrant
host.pay_for_memory_grow(0);
let input = host.read_args(len);
let (data, status) = match #user_fn(input, alloc::boxed::Box::new(stylus_sdk::host::wasm::WasmHost{})) {
Ok(data) => (data, 0),
Err(data) => (data, 1),
};
host.flush_cache(false /* do not clear */);
host.write_result(&data);
status
}
}
}
/// Revert on reentrancy unless explicitly enabled
fn deny_reentrant() -> Option<syn::ExprIf> {
cfg_if! {
if #[cfg(feature = "reentrant")] {
None
} else {
Some(parse_quote! {
if host.msg_reentrant() {
return 1; // revert
}
})
}
}
}
fn print_abi_fn(ident: &syn::Ident) -> Option<syn::ItemFn> {
let _ = ident;
cfg_if! {
if #[cfg(feature = "export-abi")] {
Some(parse_quote! {
pub fn print_abi(license: &str, pragma: &str) {
stylus_sdk::abi::export::print_abi::<#ident>(license, pragma);
}
})
} else {
None
}
}
}
================================================
File: stylus-proc/src/macros/mod.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
mod derive;
mod entrypoint;
mod public;
mod sol_interface;
mod sol_storage;
mod storage;
pub use derive::abi_type::derive_abi_type;
pub use derive::erase::derive_erase;
pub use derive::solidity_error::derive_solidity_error;
pub use entrypoint::entrypoint;
pub use public::public;
pub use sol_interface::sol_interface;
pub use sol_storage::sol_storage;
pub use storage::storage;
================================================
File: stylus-proc/src/macros/sol_interface.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use convert_case::{Case, Casing};
use proc_macro2::TokenStream;
use proc_macro_error::emit_error;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, parse_quote};
use syn_solidity::{visit, Spanned, Visit};
use crate::{
impls::abi_proxy::ImplAbiProxy,
imports::{
alloy_primitives::Address as AlloyAddress,
alloy_sol_types::{sol_data::Address as SolAddress, SolType},
},
types::{Purity, SolidityTypeInfo},
utils::build_selector,
};
/// Implementation of the [`sol_interface!`][crate::sol_interface] macro.
///
/// This implementation uses [`SolInterfaceVisitor`] which implements [`syn_solidity::Visit`] to
/// collect interface declarations and convert them to Rust structs with a method for each of the
/// interface's functions.
pub fn sol_interface(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let file = parse_macro_input!(input as syn_solidity::File);
SolInterfaceVisitor::from(&file).into_token_stream().into()
}
/// Visitor for the [`sol_interface!`][crate::sol_interface] macro.
///
/// Collects all defined interfaces, doing error checking along the way.
#[derive(Debug, Default)]
struct SolInterfaceVisitor {
interfaces: Vec<Interface>,
}
impl From<&syn_solidity::File> for SolInterfaceVisitor {
fn from(file: &syn_solidity::File) -> Self {
let mut visitor = Self::default();
visitor.visit_file(file);
visitor
}
}
impl ToTokens for SolInterfaceVisitor {
fn to_tokens(&self, tokens: &mut TokenStream) {
for iface in &self.interfaces {
iface.to_tokens(tokens);
}
}
}
impl<'ast> Visit<'ast> for SolInterfaceVisitor {
fn visit_file(&mut self, file: &'ast syn_solidity::File) {
for attr in &file.attrs {
emit_error!(attr, "attribute not supported");
}
visit::visit_file(self, file);
}
fn visit_item(&mut self, item: &'ast syn_solidity::Item) {
if !matches!(item, syn_solidity::Item::Contract(..)) {
emit_error!(item.span(), "not an interface");
}
visit::visit_item(self, item);
}
fn visit_item_contract(&mut self, contract: &'ast syn_solidity::ItemContract) {
if !contract.is_interface() {
emit_error!(contract.span(), "not an interface");
return;
}
if let Some(inheritance) = &contract.inheritance {
emit_error!(inheritance.span(), "inheritance not supported");
}
self.interfaces.push(Interface::from(contract));
}
}
/// Interface defined in the [`sol_interface!`][crate::sol_interface] macro.
#[derive(Debug)]
struct Interface {
item_struct: syn::ItemStruct,
item_impl: syn::ItemImpl,
impl_deref: syn::ItemImpl,
impl_from_address: syn::ItemImpl,
impl_abi_proxy: ImplAbiProxy,
}
impl Interface {
/// Add a function to the interface definition.
fn add_function(
&mut self,
attrs: &[syn::Attribute],
name: &syn_solidity::SolIdent,
purity: Purity,
params: FunctionParameters,
return_type: syn::Type,
) {
let rust_name = syn::Ident::new(&name.to_string().to_case(Case::Snake), name.span());
// build selector
let selector = build_selector(name, params.params.iter().map(|p| &p.type_info.sol_type));
let [selector0, selector1, selector2, selector3] = selector;
// determine which context and kind of call to use
let (context, call) = match purity {
Purity::Pure | Purity::View => (
quote!(stylus_sdk::call::StaticCallContext),
quote!(stylus_sdk::call::static_call),
),
Purity::Write => (
quote!(stylus_sdk::call::NonPayableCallContext),
quote!(stylus_sdk::call::call),
),
Purity::Payable => (
quote!(stylus_sdk::call::MutatingCallContext),
quote!(stylus_sdk::call::call),
),
};
let sol_args = params
.params
.iter()
.map(|param| param.type_info.alloy_type.clone());
let rust_args = params.params.iter().map(|param| -> syn::FnArg {
let FunctionParameter {
name,
type_info: SolidityTypeInfo { alloy_type, .. },
..
} = param;
parse_quote!(#name: <#alloy_type as #SolType>::RustType)
});
let rust_arg_names = params.params.iter().map(|param| param.name.clone());
self.item_impl.items.push(parse_quote! {
#(#attrs)*
pub fn #rust_name(&self, context: impl #context #(, #rust_args)*) ->
Result<<#return_type as #SolType>::RustType, stylus_sdk::call::Error>
{
let args = <(#(#sol_args,)*) as #SolType>::abi_encode_params(&(#(#rust_arg_names,)*));
let mut calldata = vec![#selector0, #selector1, #selector2, #selector3];
calldata.extend(args);
let returned = #call(context, self.address, &calldata)?;
Ok(<(#return_type,) as #SolType>::abi_decode_params(&returned, true)?.0)
}
});
}
}
impl From<&syn_solidity::ItemContract> for Interface {
fn from(contract: &syn_solidity::ItemContract) -> Self {
let name = contract.name.clone().into();
let attrs = &contract.attrs;
let mut iface = Self {
item_struct: parse_quote! {
#(#attrs)*
pub struct #name {
pub address: #AlloyAddress,
}
},
item_impl: parse_quote! {
impl #name {
pub fn new(address: #AlloyAddress) -> Self {
Self { address }
}
}
},
impl_deref: parse_quote! {
impl core::ops::Deref for #name {
type Target = #AlloyAddress;
fn deref(&self) -> &Self::Target {
&self.address
}
}
},
impl_from_address: parse_quote! {
impl From<#AlloyAddress> for #name {
fn from(address: #AlloyAddress) -> Self {
Self::new(address)
}
}
},
impl_abi_proxy: ImplAbiProxy::new(
&name,
&AlloyAddress.as_type(),
&SolAddress.as_type(),
),
};
iface.visit_item_contract(contract);
iface
}
}
impl ToTokens for Interface {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.item_struct.to_tokens(tokens);
self.item_impl.to_tokens(tokens);
self.impl_deref.to_tokens(tokens);
self.impl_from_address.to_tokens(tokens);
self.impl_abi_proxy.to_tokens(tokens);
}
}
impl<'ast> Visit<'ast> for Interface {
fn visit_item(&mut self, item: &'ast syn_solidity::Item) {
if !matches!(item, syn_solidity::Item::Function(_)) {
emit_error!(item.span(), "unsupported interface item");
}
visit::visit_item(self, item);
}
fn visit_item_function(&mut self, function: &'ast syn_solidity::ItemFunction) {
if !matches!(function.kind, syn_solidity::FunctionKind::Function(_)) {
emit_error!(function.span(), "unsupported function type");
}
let Some(name) = &function.name else {
emit_error!(function.span(), "function has no name");
return;
};
// determine the purity and check for external attribute
let mut purity = None;
let mut external = false;
for attr in &function.attributes.0 {
match attr {
syn_solidity::FunctionAttribute::Mutability(mutability) => {
if purity.is_some() {
emit_error!(attr.span(), "more than one purity attribute specified");
continue;
}
purity = Some(match mutability {
syn_solidity::Mutability::Pure(_) => Purity::Pure,
syn_solidity::Mutability::View(_) => Purity::View,
syn_solidity::Mutability::Payable(_) => Purity::Payable,
syn_solidity::Mutability::Constant(_) => {
emit_error!(
mutability.span(),
"constant mutibility no longer supported"
);
continue;
}
});
}
syn_solidity::FunctionAttribute::Visibility(vis) => match vis {
syn_solidity::Visibility::External(_) => external = true,
_ => {
emit_error!(vis.span(), "visibility must be external");
}
},
_ => emit_error!(attr.span(), "unsupported function attribute"),
}
}
let purity = purity.unwrap_or(Purity::Write);
if !external {
emit_error!(function.span(), "visibility must be external");
}
// build the parameter list
let mut params = FunctionParameters::new();
params.visit_parameter_list(&function.parameters);
// get the return type
let return_type = match function.return_type() {
Some(ty) => SolidityTypeInfo::from(&ty).alloy_type,
None => parse_quote!(()),
};
self.add_function(&function.attrs, name, purity, params, return_type);
}
}
struct FunctionParameters {
params: Vec<FunctionParameter>,
}
impl FunctionParameters {
fn new() -> Self {
Self { params: Vec::new() }
}
}
impl Visit<'_> for FunctionParameters {
fn visit_variable_declaration(&mut self, var: &syn_solidity::VariableDeclaration) {
let type_info = SolidityTypeInfo::from(&var.ty);
let name = match &var.name {
Some(name) => name.clone().into(),
None => syn::Ident::new(&format!("__argument_{}", self.params.len()), var.span()),
};
self.params.push(FunctionParameter { name, type_info });
}
}
struct FunctionParameter {
name: syn::Ident,
type_info: SolidityTypeInfo,
}
#[cfg(test)]
mod tests {
use quote::quote;
use syn::parse_quote;
use super::SolInterfaceVisitor;
use crate::utils::testing::assert_ast_eq;
#[test]
fn test_sol_interface() {
let file = syn_solidity::parse2(quote! {
#[interface_attr]
interface IService {
#[function_attr]
function makePayment(address user) payable external returns (string);
function getConstant() pure external returns (bytes32);
function getFoo() pure external returns (inner.Foo);
}
interface ITree {
// Define more interface methods here
}
})
.unwrap();
let visitor = SolInterfaceVisitor::from(&file);
assert_ast_eq(
&visitor.interfaces[0].item_struct,
&parse_quote! {
#[interface_attr]
pub struct IService {
pub address: stylus_sdk::alloy_primitives::Address,
}
},
);
assert_ast_eq(
&visitor.interfaces[0].item_impl,
&parse_quote! {
impl IService {
pub fn new(address: stylus_sdk::alloy_primitives::Address) -> Self {
Self { address }
}
#[function_attr]
pub fn make_payment(
&self,
context: impl stylus_sdk::call::MutatingCallContext,
user: <stylus_sdk::alloy_sol_types::sol_data::Address as stylus_sdk::alloy_sol_types::SolType>::RustType,
) ->
Result<<stylus_sdk::alloy_sol_types::sol_data::String as stylus_sdk::alloy_sol_types::SolType>::RustType, stylus_sdk::call::Error>
{
let args = <(
stylus_sdk::alloy_sol_types::sol_data::Address,
) as stylus_sdk::alloy_sol_types::SolType>::abi_encode_params(&(user,));
let mut calldata = vec![48u8, 11u8, 228u8, 252u8];
calldata.extend(args);
let returned = stylus_sdk::call::call(context, self.address, &calldata)?;
Ok(<(
stylus_sdk::alloy_sol_types::sol_data::String,
) as stylus_sdk::alloy_sol_types::SolType>::abi_decode_params(&returned, true)?.0)
}
pub fn get_constant(
&self,
context: impl stylus_sdk::call::StaticCallContext,
) ->
Result<<stylus_sdk::alloy_sol_types::sol_data::FixedBytes<32> as stylus_sdk::alloy_sol_types::SolType>::RustType, stylus_sdk::call::Error>
{
let args = <() as stylus_sdk::alloy_sol_types::SolType>::abi_encode_params(&());
let mut calldata = vec![241u8, 58u8, 56u8, 166u8];
calldata.extend(args);
let returned = stylus_sdk::call::static_call(context, self.address, &calldata)?;
Ok(<(stylus_sdk::alloy_sol_types::sol_data::FixedBytes<32>,) as stylus_sdk::alloy_sol_types::SolType>::abi_decode_params(&returned, true)?.0)
}
pub fn get_foo(
&self,
context: impl stylus_sdk::call::StaticCallContext,
) ->
Result<<inner::Foo as stylus_sdk::alloy_sol_types::SolType>::RustType, stylus_sdk::call::Error>
{
let args = <() as stylus_sdk::alloy_sol_types::SolType>::abi_encode_params(&());
let mut calldata = vec![36u8, 61u8, 200u8, 218u8];
calldata.extend(args);
let returned = stylus_sdk::call::static_call(context, self.address, &calldata)?;
Ok(<(inner::Foo,) as stylus_sdk::alloy_sol_types::SolType>::abi_decode_params(&returned, true)?.0)
}
}
},
);
assert_ast_eq(
&visitor.interfaces[0].impl_deref,
&parse_quote! {
impl core::ops::Deref for IService {
type Target = stylus_sdk::alloy_primitives::Address;
fn deref(&self) -> &Self::Target {
&self.address
}
}
},
);
assert_ast_eq(
&visitor.interfaces[0].impl_from_address,
&parse_quote! {
impl From<stylus_sdk::alloy_primitives::Address> for IService {
fn from(address: stylus_sdk::alloy_primitives::Address) -> Self {
Self::new(address)
}
}
},
);
}
}
================================================
File: stylus-proc/src/macros/storage.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use proc_macro2::TokenStream;
use proc_macro_error::emit_error;
use quote::{quote, ToTokens};
use syn::{
parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, ItemStruct, Token,
Type,
};
use crate::{consts::STYLUS_HOST_FIELD, utils::attrs::consume_flag};
/// Implementation of the [`#[storage]`][crate::storage] macro.
pub fn storage(
attr: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
if !attr.is_empty() {
emit_error!(
TokenStream::from(attr).span(),
"this macro is not configurable"
);
}
let item = parse_macro_input!(input as ItemStruct);
let ItemStruct {
attrs,
vis,
ident,
generics,
fields,
..
} = item;
// Handle fields based on their type (named or unnamed)
let expanded_fields = match fields {
syn::Fields::Named(named_fields) => {
// Extract the original fields.
let original_fields = named_fields.named;
quote! {
#STYLUS_HOST_FIELD: *const dyn stylus_sdk::host::Host,
#original_fields
}
}
syn::Fields::Unnamed(_) => {
// Handle tuple structs if needed.
emit_error!(
fields.span(),
"Tuple structs are not supported by #[storage]"
);
return fields.to_token_stream().into();
}
syn::Fields::Unit => {
// Handle unit structs if needed.
quote! {
#STYLUS_HOST_FIELD: *const dyn stylus_sdk::host::Host,
}
}
};
// Inject the host trait generic into the item struct if not defined.
let mut host_injected_item: syn::ItemStruct = parse_quote! {
#(#attrs)*
#vis struct #ident #generics {
#expanded_fields
}
};
let storage = Storage::from(&mut host_injected_item);
let mut output = host_injected_item.into_token_stream();
storage.to_tokens(&mut output);
output.into()
}
#[derive(Debug)]
struct Storage {
name: syn::Ident,
generics: syn::Generics,
fields: Vec<StorageField>,
}
impl Storage {
fn item_impl(&self) -> syn::ItemImpl {
let name = &self.name;
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
let size = TokenStream::from_iter(self.fields.iter().map(StorageField::size));
parse_quote! {
impl #impl_generics #name #ty_generics #where_clause {
const fn required_slots() -> usize {
use stylus_sdk::storage;
let mut total: usize = 0;
let mut space: usize = 32;
#size
if space != 32 || total == 0 {
total += 1;
}
total
}
}
}
}
fn impl_storage_type(&self) -> syn::ItemImpl {
let name = &self.name;
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
let init = Punctuated::<syn::FieldValue, Token![,]>::from_iter(
self.fields.iter().filter_map(StorageField::init),
);
parse_quote! {
impl #impl_generics stylus_sdk::storage::StorageType for #name #ty_generics #where_clause {
type Wraps<'a> = stylus_sdk::storage::StorageGuard<'a, Self> where Self: 'a;
type WrapsMut<'a> = stylus_sdk::storage::StorageGuardMut<'a, Self> where Self: 'a;
// start a new word
const SLOT_BYTES: usize = 32;
const REQUIRED_SLOTS: usize = Self::required_slots();
unsafe fn new(mut root: stylus_sdk::alloy_primitives::U256, offset: u8, host: *const dyn stylus_sdk::host::Host) -> Self {
use stylus_sdk::{storage, alloy_primitives};
debug_assert!(offset == 0);
let mut space: usize = 32;
let mut slot: usize = 0;
let accessor = Self {
#STYLUS_HOST_FIELD: host,
#init
};
accessor
}
fn load<'s>(self) -> Self::Wraps<'s> {
stylus_sdk::storage::StorageGuard::new(self)
}
fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
stylus_sdk::storage::StorageGuardMut::new(self)
}
}
}
}
fn impl_host_access(&self) -> syn::ItemImpl {
let name = &self.name;
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
parse_quote! {
impl #impl_generics stylus_sdk::host::HostAccess for #name #ty_generics #where_clause {
fn vm(&self) -> alloc::boxed::Box<dyn stylus_sdk::host::Host> {
// SAFETY: Host is guaranteed to be valid and non-null for the lifetime of the storage
// as injected by the Stylus entrypoint function.
unsafe { alloc::boxed::Box::from_raw(self.#STYLUS_HOST_FIELD as *mut dyn stylus_sdk::host::Host) }
}
}
}
}
}
impl From<&mut syn::ItemStruct> for Storage {
fn from(node: &mut syn::ItemStruct) -> Self {
let name = node.ident.clone();
let generics = node.generics.clone();
let fields = node
.fields
.iter_mut()
.enumerate()
.filter_map(|(idx, field)| {
if let syn::Type::Path(..) = &field.ty {
Some(StorageField::new(idx, field))
} else {
None
}
})
.collect();
Self {
name,
generics,
fields,
}
}
}
impl ToTokens for Storage {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.item_impl().to_tokens(tokens);
self.impl_storage_type().to_tokens(tokens);
self.impl_host_access().to_tokens(tokens);
for field in &self.fields {
field.impl_borrow(&self.name).to_tokens(tokens);
field.impl_borrow_mut(&self.name).to_tokens(tokens);
}
}
}
#[derive(Debug)]
struct StorageField {
name: Option<syn::Ident>,
ty: syn::Type,
accessor: syn::Member,
borrow: bool,
}
impl StorageField {
fn new(idx: usize, field: &mut syn::Field) -> Self {
check_type(field);
let name = field.ident.clone();
let ty = field.ty.clone();
let accessor = field
.ident
.clone()
.map(syn::Member::from)
.unwrap_or_else(|| idx.into());
let borrow = consume_flag(&mut field.attrs, "borrow");
Self {
name,
ty,
accessor,
borrow,
}
}
fn init(&self) -> Option<syn::FieldValue> {
let Some(ident) = &self.name else {
return None;
};
if *ident == STYLUS_HOST_FIELD.as_ident() {
return None;
}
let ty = &self.ty;
Some(parse_quote! {
#ident: {
let bytes = <#ty as storage::StorageType>::SLOT_BYTES;
let words = <#ty as storage::StorageType>::REQUIRED_SLOTS;
if space < bytes {
space = 32;
slot += 1;
}
space -= bytes;
let root = root + alloy_primitives::U256::from(slot);
let field = <#ty as storage::StorageType>::new(root, space as u8, host);
if words > 0 {
slot += words;
space = 32;
}
field
}
})
}
fn size(&self) -> TokenStream {
let ty = &self.ty;
let Some(ident) = &self.name else {
return quote! {};
};
if *ident == STYLUS_HOST_FIELD.as_ident() {
return quote! {};
}
quote! {
let bytes = <#ty as storage::StorageType>::SLOT_BYTES;
let words = <#ty as storage::StorageType>::REQUIRED_SLOTS;
if words > 0 {
total += words;
space = 32;
} else {
if space < bytes {
space = 32;
total += 1;
}
space -= bytes;
}
}
}
fn impl_borrow(&self, name: &syn::Ident) -> Option<syn::ItemImpl> {
let Self { ty, accessor, .. } = self;
self.borrow.then(|| {
parse_quote! {
impl core::borrow::Borrow<#ty> for #name {
fn borrow(&self) -> &#ty {
&self.#accessor
}
}
}
})
}
fn impl_borrow_mut(&self, name: &syn::Ident) -> Option<syn::ItemImpl> {
let Self { ty, accessor, .. } = self;
self.borrow.then(|| {
parse_quote! {
impl core::borrow::BorrowMut<#ty> for #name {
fn borrow_mut(&mut self) -> &mut #ty {
&mut self.#accessor
}
}
}
})
}
}
fn check_type(field: &syn::Field) {
let Type::Path(ty) = &field.ty else {
unreachable!();
};
let path = &ty.path.segments.last().unwrap().ident;
let not_supported = format!("Type `{path}` not supported for EVM state storage");
match path.to_string().as_str() {
x @ ("u8" | "u16" | "u32" | "u64" | "u128" | "i8" | "i16" | "i32" | "i64" | "i128"
| "U8" | "U16" | "U32" | "U64" | "U128" | "I8" | "I16" | "I32" | "I64" | "I128") => {
emit_error!(
&field,
"{not_supported}. Instead try `Storage{}`.",
x.to_uppercase()
);
}
"usize" => emit_error!(&field, "{not_supported}."),
"isize" => emit_error!(&field, "{not_supported}."),
"bool" => emit_error!(&field, "{not_supported}. Instead try `StorageBool`."),
"f32" | "f64" => emit_error!(&field, "{not_supported}. Consider fixed-point arithmetic."),
_ => {}
}
}
================================================
File: stylus-proc/src/macros/derive/abi_type.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{parse_macro_input, parse_quote};
use crate::imports::stylus_sdk::abi::AbiType;
/// Implementation of the [`#[derive(AbiType)]`][crate::AbiType] macro.
pub fn derive_abi_type(input: TokenStream) -> TokenStream {
let item = parse_macro_input!(input as syn::ItemStruct);
impl_abi_type(&item).into_token_stream().into()
}
/// Implement [`stylus_sdk::abi::AbiType`] for the given struct.
///
/// The name is used for the ABI name to match the
/// [`SolType::SOL_NAME`][alloy_sol_types::SolType::SOL_NAME] generated by the
/// [`sol!`][alloy_sol_types::sol] macro.
fn impl_abi_type(item: &syn::ItemStruct) -> syn::ItemImpl {
let name = &item.ident;
let name_str = name.to_string();
let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
parse_quote! {
impl #impl_generics #AbiType for #name #ty_generics #where_clause {
type SolType = Self;
const ABI: stylus_sdk::abi::ConstString = stylus_sdk::abi::ConstString::new(#name_str);
}
}
}
#[cfg(test)]
mod tests {
use syn::parse_quote;
use super::impl_abi_type;
use crate::utils::testing::assert_ast_eq;
#[test]
fn test_impl_abi_type() {
assert_ast_eq(
impl_abi_type(&parse_quote! {
struct Foo<T>
where T: Bar {
a: bool,
b: String,
t: T,
}
}),
parse_quote! {
impl<T> stylus_sdk::abi::AbiType for Foo<T>
where T: Bar {
type SolType = Self;
const ABI: stylus_sdk::abi::ConstString = stylus_sdk::abi::ConstString::new("Foo");
}
},
)
}
}
================================================
File: stylus-proc/src/macros/derive/erase.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{parse_macro_input, parse_quote};
use crate::consts::STYLUS_HOST_FIELD;
/// Implementation of the [`#[derive(Erase)]`][crate::derive_erase] macro.
pub fn derive_erase(input: TokenStream) -> TokenStream {
let node = parse_macro_input!(input as syn::ItemStruct);
impl_erase(&node).into_token_stream().into()
}
/// Implement [`stylus_sdk::storage::Erase`] for the given struct.
///
/// Calls `Erase::erase()` on each of the members of the struct.
fn impl_erase(node: &syn::ItemStruct) -> syn::ItemImpl {
let name = &node.ident;
let (impl_generics, ty_generics, where_clause) = node.generics.split_for_impl();
let filtered_fields = node
.fields
.clone()
.into_iter()
.filter_map(|field| match field.ident {
Some(ident) if ident == STYLUS_HOST_FIELD.as_ident() => None,
_ => field.ident,
});
parse_quote! {
impl #impl_generics stylus_sdk::storage::Erase for #name #ty_generics #where_clause {
fn erase(&mut self) {
#(
self.#filtered_fields.erase();
)*
}
}
}
}
#[cfg(test)]
mod tests {
use syn::parse_quote;
use super::*;
use crate::utils::testing::assert_ast_eq;
#[test]
fn test_impl_erase() {
assert_ast_eq(
impl_erase(&parse_quote! {
struct Foo<T: Erase> {
field1: StorageString,
field2: T,
}
}),
parse_quote! {
impl<T: Erase> stylus_sdk::storage::Erase for Foo<T> {
fn erase(&mut self) {
self.field1.erase();
self.field2.erase();
}
}
},
);
}
}
================================================
File: stylus-proc/src/macros/derive/mod.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Implementations of derive macros.
pub mod abi_type;
pub mod erase;
pub mod solidity_error;
================================================
File: stylus-proc/src/macros/derive/solidity_error/export_abi.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use syn::parse_quote;
use super::{DeriveSolidityError, SolidityErrorExtension};
#[derive(Debug, Default)]
pub struct InnerTypesExtension {
errors: Vec<syn::Field>,
}
impl SolidityErrorExtension for InnerTypesExtension {
type Ast = syn::ItemImpl;
fn add_variant(&mut self, field: syn::Field) {
self.errors.push(field);
}
fn codegen(err: &DeriveSolidityError<Self>) -> syn::ItemImpl {
let name = &err.name;
let errors = err._ext.errors.iter();
parse_quote! {
impl stylus_sdk::abi::export::internal::InnerTypes for #name {
fn inner_types() -> alloc::vec::Vec<stylus_sdk::abi::export::internal::InnerType> {
use alloc::{format, vec};
use core::any::TypeId;
use stylus_sdk::abi::export::internal::InnerType;
use stylus_sdk::alloy_sol_types::SolError;
vec![
#(
InnerType {
name: format!("error {};", <#errors as SolError>::SIGNATURE.replace(',', ", ")),
id: TypeId::of::<#errors>(),
}
),*
]
}
}
}
}
}
================================================
File: stylus-proc/src/macros/derive/solidity_error/mod.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use cfg_if::cfg_if;
use proc_macro2::TokenStream;
use proc_macro_error::emit_error;
use quote::ToTokens;
use syn::{parse::Nothing, parse_macro_input, parse_quote, Fields};
cfg_if! {
if #[cfg(feature = "export-abi")] {
mod export_abi;
type Extension = export_abi::InnerTypesExtension;
} else {
type Extension = ();
}
}
/// Implementation of the [`#[derive(SolidityError]`][crate::SolidityError] macro.
pub fn derive_solidity_error(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let item = parse_macro_input!(input as syn::ItemEnum);
DeriveSolidityError::from(&item).into_token_stream().into()
}
#[derive(Debug)]
struct DeriveSolidityError<E = Extension> {
name: syn::Ident,
from_impls: Vec<syn::ItemImpl>,
match_arms: Vec<syn::Arm>,
_ext: E,
}
impl DeriveSolidityError {
fn new(name: syn::Ident) -> Self {
Self {
name,
from_impls: Vec::new(),
match_arms: Vec::new(),
_ext: Extension::default(),
}
}
fn add_variant(&mut self, name: &syn::Ident, field: syn::Field) {
let self_name = &self.name;
let ty = &field.ty;
self.from_impls.push(parse_quote! {
impl From<#ty> for #self_name {
fn from(value: #ty) -> Self {
#self_name::#name(value)
}
}
});
self.match_arms.push(parse_quote! {
#self_name::#name(e) => stylus_sdk::call::MethodError::encode(e),
});
#[allow(clippy::unit_arg)]
self._ext.add_variant(field);
}
fn vec_u8_from_impl(&self) -> syn::ItemImpl {
let name = &self.name;
let match_arms = self.match_arms.iter();
parse_quote! {
impl From<#name> for alloc::vec::Vec<u8> {
fn from(err: #name) -> Self {
match err {
#(#match_arms)*
}
}
}
}
}
}
impl From<&syn::ItemEnum> for DeriveSolidityError {
fn from(item: &syn::ItemEnum) -> Self {
let mut output = DeriveSolidityError::new(item.ident.clone());
for variant in &item.variants {
match &variant.fields {
Fields::Unnamed(e) if variant.fields.len() == 1 => {
let field = e.unnamed.first().unwrap().clone();
output.add_variant(&variant.ident, field);
}
Fields::Unit => {
emit_error!(variant, "variant not a 1-tuple");
}
_ => {
emit_error!(variant.fields, "variant not a 1-tuple");
}
};
}
output
}
}
impl ToTokens for DeriveSolidityError {
fn to_tokens(&self, tokens: &mut TokenStream) {
for from_impl in &self.from_impls {
from_impl.to_tokens(tokens);
}
self.vec_u8_from_impl().to_tokens(tokens);
Extension::codegen(self).to_tokens(tokens);
}
}
trait SolidityErrorExtension: Default {
type Ast: ToTokens;
fn add_variant(&mut self, field: syn::Field);
fn codegen(err: &DeriveSolidityError<Self>) -> Self::Ast;
}
impl SolidityErrorExtension for () {
type Ast = Nothing;
fn add_variant(&mut self, _field: syn::Field) {}
fn codegen(_err: &DeriveSolidityError<Self>) -> Self::Ast {
Nothing
}
}
#[cfg(test)]
mod tests {
use syn::parse_quote;
use super::DeriveSolidityError;
use crate::utils::testing::assert_ast_eq;
#[test]
fn test_derive_solidity_error() {
let derived = DeriveSolidityError::from(&parse_quote! {
enum MyError {
Foo(FooError),
Bar(BarError),
}
});
assert_ast_eq(
&derived.from_impls[0],
&parse_quote! {
impl From<FooError> for MyError {
fn from(value: FooError) -> Self {
MyError::Foo(value)
}
}
},
);
assert_ast_eq(
derived.vec_u8_from_impl(),
parse_quote! {
impl From<MyError> for alloc::vec::Vec<u8> {
fn from(err: MyError) -> Self {
match err {
MyError::Foo(e) => stylus_sdk::call::MethodError::encode(e),
MyError::Bar(e) => stylus_sdk::call::MethodError::encode(e),
}
}
}
},
);
}
}
================================================
File: stylus-proc/src/macros/public/attrs.rs
================================================
// Copyright 2022-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
Token,
};
/// Inherit from parent contracts.
///
/// Used for the `#[inherit(Parent1, Parent2]` attribute.
pub struct Inherit {
pub types: Punctuated<syn::Type, Token![,]>,
}
impl Parse for Inherit {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
types: Punctuated::parse_terminated(input)?,
})
}
}
/// Selector name overloading for public functions.
///
/// Used for the `#[selector(name = "...")]` attribute.
#[derive(Debug)]
pub struct Selector {
_name: kw::name,
_eq_token: Token![=],
pub value: syn::LitStr,
}
impl Parse for Selector {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
_name: input.parse()?,
_eq_token: input.parse()?,
value: input.parse()?,
})
}
}
mod kw {
syn::custom_keyword!(name);
}
================================================
File: stylus-proc/src/macros/public/export_abi.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse_quote;
use crate::types::Purity;
use super::types::{FnArgExtension, FnExtension, FnKind, InterfaceExtension, PublicImpl};
#[derive(Debug)]
pub struct InterfaceAbi;
impl InterfaceExtension for InterfaceAbi {
type FnExt = FnAbi;
type Ast = syn::ItemImpl;
fn build(_node: &syn::ItemImpl) -> Self {
InterfaceAbi
}
fn codegen(iface: &PublicImpl<Self>) -> Self::Ast {
let PublicImpl {
generic_params,
self_ty,
where_clause,
inheritance,
funcs,
..
} = iface;
let name = match self_ty {
syn::Type::Path(path) => path.path.segments.last().unwrap().ident.clone().to_string(),
_ => todo!(),
};
let mut types = Vec::new();
for item in funcs {
if let Some(ty) = &item.extension.output {
types.push(ty);
}
}
let type_decls = quote! {
let mut seen = HashSet::new();
for item in ([] as [InnerType; 0]).iter() #(.chain(&<#types as InnerTypes>::inner_types()))* {
if seen.insert(item.id) {
writeln!(f, "\n {}", item.name)?;
}
}
};
// write the "is" clause in Solidity
let mut is_clause = match inheritance.is_empty() {
true => quote! {},
false => quote! { write!(f, " is ")?; },
};
is_clause.extend(inheritance.iter().enumerate().map(|(i, ty)| {
let comma = (i > 0).then_some(", ").unwrap_or_default();
quote! {
write!(f, "{}I{}", #comma, <#ty as GenerateAbi>::NAME)?;
}
}));
let mut abi = TokenStream::new();
for func in funcs {
if !matches!(func.kind, FnKind::Function) {
continue;
}
let sol_name = func.sol_name.to_string();
let sol_args = func.inputs.iter().enumerate().map(|(i, arg)| {
let comma = (i > 0).then_some(", ").unwrap_or_default();
let name = arg.extension.pattern_ident.as_ref().map(ToString::to_string).unwrap_or_default();
let ty = &arg.ty;
quote! {
write!(f, "{}{}{}", #comma, <#ty as AbiType>::EXPORT_ABI_ARG, underscore_if_sol(#name))?;
}
});
let sol_outs = if let Some(ty) = &func.extension.output {
quote!(write_solidity_returns::<#ty>(f)?;)
} else {
quote!()
};
let sol_purity = match func.purity {
Purity::Write => String::new(),
x => format!(" {x}"),
};
abi.extend(quote! {
write!(f, "\n function {}(", #sol_name)?;
#(#sol_args)*
write!(f, ") external")?;
write!(f, #sol_purity)?;
#sol_outs
writeln!(f, ";")?;
});
}
parse_quote! {
impl<#generic_params> stylus_sdk::abi::GenerateAbi for #self_ty where #where_clause {
const NAME: &'static str = #name;
fn fmt_abi(f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
use stylus_sdk::abi::{AbiType, GenerateAbi};
use stylus_sdk::abi::internal::write_solidity_returns;
use stylus_sdk::abi::export::{underscore_if_sol, internal::{InnerType, InnerTypes}};
use std::collections::HashSet;
#(
<#inheritance as GenerateAbi>::fmt_abi(f)?;
writeln!(f)?;
)*
write!(f, "interface I{}", #name)?;
#is_clause
write!(f, " {{")?;
#abi
#type_decls
writeln!(f, "}}")?;
Ok(())
}
}
}
}
}
#[derive(Debug)]
pub struct FnAbi {
pub output: Option<syn::Type>,
}
impl FnExtension for FnAbi {
type FnArgExt = FnArgAbi;
fn build(node: &syn::ImplItemFn) -> Self {
let output = match &node.sig.output {
syn::ReturnType::Default => None,
syn::ReturnType::Type(_, ty) => Some(*ty.clone()),
};
FnAbi { output }
}
}
#[derive(Debug)]
pub struct FnArgAbi {
pub pattern_ident: Option<syn::Ident>,
}
impl FnArgExtension for FnArgAbi {
fn build(node: &syn::FnArg) -> Self {
let pattern_ident = if let syn::FnArg::Typed(pat_type) = node {
pattern_ident(&pat_type.pat)
} else {
None
};
FnArgAbi { pattern_ident }
}
}
/// finds the root type for a given arg
fn pattern_ident(pat: &syn::Pat) -> Option<syn::Ident> {
match pat {
syn::Pat::Ident(pat_ident) => Some(pat_ident.ident.clone()),
syn::Pat::Reference(pat_ref) => pattern_ident(&pat_ref.pat),
_ => None,
}
}
================================================
File: stylus-proc/src/macros/public/mod.rs
================================================
// Copyright 2022-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use cfg_if::cfg_if;
use convert_case::{Case, Casing};
use proc_macro::TokenStream;
use proc_macro_error::emit_error;
use quote::ToTokens;
use syn::{parse_macro_input, spanned::Spanned};
use crate::{
types::Purity,
utils::{
attrs::{check_attr_is_empty, consume_attr, consume_flag},
split_item_impl_for_impl,
},
};
use types::{
FnArgExtension, FnExtension, FnKind, InterfaceExtension, PublicFn, PublicFnArg, PublicImpl,
};
mod attrs;
mod overrides;
mod types;
cfg_if! {
if #[cfg(feature = "export-abi")] {
mod export_abi;
type Extension = export_abi::InterfaceAbi;
} else {
type Extension = ();
}
}
/// Implementation of the [`#[public]`][crate::public] macro.
///
/// This implementation performs the following steps:
/// - Parse the input as [`syn::ItemImpl`]
/// - Generate AST items within a [`PublicImpl`]
/// - Expand those AST items into tokens for output
pub fn public(attr: TokenStream, input: TokenStream) -> TokenStream {
check_attr_is_empty(attr);
let mut item_impl = parse_macro_input!(input as syn::ItemImpl);
let public_impl = PublicImpl::<Extension>::from(&mut item_impl);
let mut output = item_impl.into_token_stream();
public_impl.to_tokens(&mut output);
output.into()
}
impl ToTokens for PublicImpl {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
self.impl_router().to_tokens(tokens);
self.impl_override_checks().to_tokens(tokens);
Extension::codegen(self).to_tokens(tokens);
}
}
impl From<&mut syn::ItemImpl> for PublicImpl {
fn from(node: &mut syn::ItemImpl) -> Self {
// parse inheritance from #[inherits(...)] attribute
let mut inheritance = Vec::new();
if let Some(inherits) = consume_attr::<attrs::Inherit>(&mut node.attrs, "inherit") {
inheritance.extend(inherits.types);
}
// collect public functions
let funcs = node
.items
.iter_mut()
.filter_map(|item| match item {
syn::ImplItem::Fn(func) => Some(PublicFn::from(func)),
syn::ImplItem::Const(_) => {
emit_error!(item, "unsupported impl item");
None
}
_ => {
// allow other item types
None
}
})
.collect();
let (generic_params, self_ty, where_clause) = split_item_impl_for_impl(node);
#[allow(clippy::let_unit_value)]
let extension = <Extension as InterfaceExtension>::build(node);
Self {
self_ty,
generic_params,
where_clause,
inheritance,
funcs,
extension,
}
}
}
impl<E: FnExtension> From<&mut syn::ImplItemFn> for PublicFn<E> {
fn from(node: &mut syn::ImplItemFn) -> Self {
// parse attributes
let payable = consume_flag(&mut node.attrs, "payable");
let selector_override =
consume_attr::<attrs::Selector>(&mut node.attrs, "selector").map(|s| s.value.value());
let fallback = consume_flag(&mut node.attrs, "fallback");
let receive = consume_flag(&mut node.attrs, "receive");
let kind = match (fallback, receive) {
(true, false) => {
// Fallback functions may have two signatures, either
// with input calldata and output bytes, or no input and output.
// node.sig.
let has_inputs = node.sig.inputs.len() > 1;
if has_inputs {
FnKind::FallbackWithArgs
} else {
FnKind::FallbackNoArgs
}
}
(false, true) => FnKind::Receive,
(false, false) => FnKind::Function,
(true, true) => {
emit_error!(node.span(), "function cannot be both fallback and receive");
FnKind::Function
}
};
// name for generated rust, and solidity abi
let name = node.sig.ident.clone();
if matches!(kind, FnKind::Function) && (name == "receive" || name == "fallback") {
emit_error!(
node.span(),
"receive and/or fallback functions can only be defined using the #[receive] or "
.to_string()
+ "#[fallback] attribute instead of names",
);
}
let sol_name = syn_solidity::SolIdent::new(
&selector_override.unwrap_or(name.to_string().to_case(Case::Camel)),
);
// determine state mutability
let (inferred_purity, has_self) = Purity::infer(node);
let purity = if payable || matches!(kind, FnKind::Receive) {
Purity::Payable
} else {
inferred_purity
};
let mut args = node.sig.inputs.iter();
if inferred_purity > Purity::Pure {
// skip self or storage argument
args.next();
}
let inputs = match kind {
FnKind::Function => args.map(PublicFnArg::from).collect(),
_ => Vec::new(),
};
let input_span = node.sig.inputs.span();
let output = match &node.sig.output {
syn::ReturnType::Default => None,
syn::ReturnType::Type(_, ty) => Some(*ty.clone()),
};
let output_span = output
.as_ref()
.map(Spanned::span)
.unwrap_or(node.sig.output.span());
let extension = E::build(node);
Self {
name,
sol_name,
purity,
inferred_purity,
kind,
has_self,
inputs,
input_span,
output_span,
extension,
}
}
}
impl<E: FnArgExtension> From<&syn::FnArg> for PublicFnArg<E> {
fn from(node: &syn::FnArg) -> Self {
match node {
syn::FnArg::Typed(pat_type) => {
let extension = E::build(node);
Self {
ty: *pat_type.ty.clone(),
extension,
}
}
_ => unreachable!(),
}
}
}
#[cfg(test)]
mod tests {
use syn::parse_quote;
use super::types::PublicImpl;
#[test]
fn test_public_consumes_inherit() {
let mut impl_item = parse_quote! {
#[derive(Debug)]
#[inherit(Parent)]
impl Contract {
}
};
let _public = PublicImpl::from(&mut impl_item);
assert_eq!(impl_item.attrs, vec![parse_quote! { #[derive(Debug)] }]);
}
#[test]
fn test_public_consumes_payable() {
let mut impl_item = parse_quote! {
#[derive(Debug)]
impl Contract {
#[payable]
#[other]
fn func() {}
}
};
let _public = PublicImpl::from(&mut impl_item);
let syn::ImplItem::Fn(syn::ImplItemFn { attrs, .. }) = &impl_item.items[0] else {
unreachable!();
};
assert_eq!(attrs, &vec![parse_quote! { #[other] }]);
}
}
================================================
File: stylus-proc/src/macros/public/overrides.rs
================================================
// Copyright 2022-2024, Offchain Labs, Inc.
// use crate::consts::{ALLOW_OVERRIDE_FN, ASSERT_OVERRIDES_FN};
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Ensure that public functions follow safe override rules.
use proc_macro2::Span;
use syn::parse_quote;
use super::types::{FnExtension, PublicFn, PublicImpl};
use crate::consts::{ALLOW_OVERRIDE_FN, ASSERT_OVERRIDES_FN};
impl PublicImpl {
pub fn impl_override_checks(&self) -> syn::ItemImpl {
let Self {
self_ty,
generic_params,
where_clause,
..
} = self;
let selector_consts = self
.funcs
.iter()
.map(PublicFn::selector_const)
.collect::<Vec<_>>();
let override_arms = self.funcs.iter().map(PublicFn::override_arm);
let inheritance_overrides = self.inheritance_overrides();
let override_checks = self.override_checks();
parse_quote! {
impl<#generic_params> #self_ty where #where_clause {
/// Whether or not to allow overriding a selector by a child contract and method with
/// the given purity. This is currently implemented as a hidden function to allow it to
/// be `const`. A trait would be better, but `const` is not currently supported for
/// trait fns.
#[doc(hidden)]
pub const fn #ALLOW_OVERRIDE_FN(selector: u32, purity: stylus_sdk::methods::Purity) -> bool {
use stylus_sdk::function_selector;
#(#selector_consts)*
if !match selector {
#(#override_arms)*
_ => true
} { return false; }
#(#inheritance_overrides)*
true
}
/// Check the functions defined in an entrypoint for valid overrides.
#[doc(hidden)]
pub const fn #ASSERT_OVERRIDES_FN() {
use stylus_sdk::function_selector;
#(#selector_consts)*
#(#override_checks)*
}
}
}
}
fn inheritance_overrides(&self) -> impl Iterator<Item = syn::ExprIf> + '_ {
self.inheritance.iter().map(|ty| {
parse_quote! {
if !<#ty>::#ALLOW_OVERRIDE_FN(selector, purity) {
return false;
}
}
})
}
fn override_checks(&self) -> impl Iterator<Item = syn::Stmt> + '_ {
self.funcs
.iter()
.map(|func| func.assert_override(&self.self_ty))
.chain(self.inheritance.iter().map(|ty| {
parse_quote! {
<#ty>::#ASSERT_OVERRIDES_FN();
}
}))
}
}
impl<E: FnExtension> PublicFn<E> {
fn override_arm(&self) -> syn::Arm {
let constant = self.selector_name();
let purity = self.purity.as_path();
parse_quote! {
#[allow(non_upper_case_globals)]
#constant => #purity.allow_override(purity),
}
}
fn override_error(&self) -> syn::LitStr {
syn::LitStr::new(
&format!(
"function {} cannot be overriden with function marked {:?}",
self.name, self.purity,
),
Span::mixed_site(),
)
}
fn assert_override(&self, self_ty: &syn::Type) -> syn::Stmt {
let purity = self.purity.as_path();
let selector_name = self.selector_name();
let error = self.override_error();
parse_quote! {
assert!(<#self_ty>::#ALLOW_OVERRIDE_FN(#selector_name, #purity), #error);
}
}
}
================================================
File: stylus-proc/src/macros/public/types.rs
================================================
// Copyright 2022-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use proc_macro2::{Span, TokenStream};
use proc_macro_error::emit_error;
use quote::{quote, ToTokens};
use syn::{
parse::Nothing, parse_quote, parse_quote_spanned, punctuated::Punctuated, spanned::Spanned,
Token,
};
use crate::{
imports::{
alloy_sol_types::SolType,
stylus_sdk::abi::{AbiType, Router},
},
types::Purity,
};
use super::Extension;
pub struct PublicImpl<E: InterfaceExtension = Extension> {
pub self_ty: syn::Type,
pub generic_params: Punctuated<syn::GenericParam, Token![,]>,
pub where_clause: Punctuated<syn::WherePredicate, Token![,]>,
pub inheritance: Vec<syn::Type>,
pub funcs: Vec<PublicFn<E::FnExt>>,
#[allow(dead_code)]
pub extension: E,
}
impl PublicImpl {
pub fn impl_router(&self) -> syn::ItemImpl {
let Self {
self_ty,
generic_params,
where_clause,
inheritance,
..
} = self;
let function_iter = self
.funcs
.iter()
.filter(|&func| matches!(func.kind, FnKind::Function));
let selector_consts = function_iter.clone().map(PublicFn::selector_const);
let selector_arms = function_iter
.map(PublicFn::selector_arm)
.collect::<Vec<_>>();
let inheritance_routes = self.inheritance_routes();
let call_fallback = self.call_fallback();
let inheritance_fallback = self.inheritance_fallback();
let (fallback, fallback_purity) = call_fallback.unwrap_or_else(|| {
// If there is no fallback function specified, we rely on any inherited fallback.
(
parse_quote!({
#(#inheritance_fallback)*
None
}),
Purity::Payable, // Let the inherited fallback deal with purity.
)
});
let fallback_deny: Option<syn::ExprIf> = match fallback_purity {
Purity::Payable => None,
_ => Some(parse_quote! {
if let Err(err) = stylus_sdk::abi::internal::deny_value("fallback") {
return Some(Err(err));
}
}),
};
let call_receive = self.call_receive();
let inheritance_receive = self.inheritance_receive();
let receive = call_receive.unwrap_or_else(|| {
parse_quote!({
#(#inheritance_receive)*
None
})
});
parse_quote! {
impl<S, #generic_params> #Router<S> for #self_ty
where
S: stylus_sdk::storage::TopLevelStorage + core::borrow::BorrowMut<Self>,
#(
S: core::borrow::BorrowMut<#inheritance>,
)*
#where_clause
{
type Storage = Self;
#[inline(always)]
#[deny(unreachable_patterns)]
fn route(storage: &mut S, selector: u32, input: &[u8]) -> Option<stylus_sdk::ArbResult> {
use stylus_sdk::function_selector;
use stylus_sdk::abi::{internal, internal::EncodableReturnType};
use alloc::vec;
#(#selector_consts)*
match selector {
#(#selector_arms)*
_ => {
#(#inheritance_routes)*
None
}
}
}
#[inline(always)]
fn fallback(storage: &mut S, input: &[u8]) -> Option<stylus_sdk::ArbResult> {
#fallback_deny
#fallback
}
#[inline(always)]
fn receive(storage: &mut S) -> Option<Result<(), Vec<u8>>> {
#receive
}
}
}
}
fn inheritance_routes(&self) -> impl Iterator<Item = syn::ExprIf> + '_ {
self.inheritance.iter().map(|ty| {
parse_quote! {
if let Some(result) = <#ty as #Router<S>>::route(storage, selector, input) {
return Some(result);
}
}
})
}
fn call_fallback(&self) -> Option<(syn::Stmt, Purity)> {
let mut fallback_purity = Purity::View;
let fallbacks: Vec<syn::Stmt> = self
.funcs
.iter()
.filter(|&func| {
if matches!(func.kind, FnKind::FallbackWithArgs)
|| matches!(func.kind, FnKind::FallbackNoArgs)
{
fallback_purity = func.purity;
return true;
}
false
})
.map(PublicFn::call_fallback)
.collect();
if fallbacks.is_empty() {
return None;
}
if fallbacks.len() > 1 {
emit_error!(
"multiple fallbacks",
"contract can only have one #[fallback] method defined"
);
}
fallbacks
.first()
.cloned()
.map(|func| (func, fallback_purity))
}
fn inheritance_fallback(&self) -> impl Iterator<Item = syn::ExprIf> + '_ {
self.inheritance.iter().map(|ty| {
parse_quote! {
if let Some(res) = <#ty as #Router<S>>::fallback(storage, input) {
return Some(res);
}
}
})
}
fn call_receive(&self) -> Option<syn::Stmt> {
let receives: Vec<syn::Stmt> = self
.funcs
.iter()
.filter(|&func| matches!(func.kind, FnKind::Receive))
.map(PublicFn::call_receive)
.collect();
if receives.is_empty() {
return None;
}
if receives.len() > 1 {
emit_error!(
"multiple receives",
"contract can only have one #[receive] method defined"
);
}
receives.first().cloned()
}
fn inheritance_receive(&self) -> impl Iterator<Item = syn::ExprIf> + '_ {
self.inheritance.iter().map(|ty| {
parse_quote! {
if let Some(res) = <#ty as #Router<S>>::receive(storage) {
return Some(res);
}
}
})
}
}
#[derive(Debug)]
pub enum FnKind {
Function,
FallbackWithArgs,
FallbackNoArgs,
Receive,
}
pub struct PublicFn<E: FnExtension> {
pub name: syn::Ident,
pub sol_name: syn_solidity::SolIdent,
pub purity: Purity,
pub inferred_purity: Purity,
pub kind: FnKind,
pub has_self: bool,
pub inputs: Vec<PublicFnArg<E::FnArgExt>>,
pub input_span: Span,
pub output_span: Span,
#[allow(dead_code)]
pub extension: E,
}
impl<E: FnExtension> PublicFn<E> {
pub fn selector_name(&self) -> syn::Ident {
syn::Ident::new(&format!("__SELECTOR_{}", self.name), self.name.span())
}
fn selector_value(&self) -> syn::Expr {
let sol_name = syn::LitStr::new(&self.sol_name.as_string(), self.sol_name.span());
let arg_types = self.arg_types();
parse_quote! {
u32::from_be_bytes(function_selector!(#sol_name #(, #arg_types )*))
}
}
pub fn selector_const(&self) -> Option<syn::ItemConst> {
let name = self.selector_name();
let value = self.selector_value();
Some(parse_quote! {
#[allow(non_upper_case_globals)]
const #name: u32 = #value;
})
}
fn selector_arm(&self) -> Option<syn::Arm> {
if !matches!(self.kind, FnKind::Function) {
return None;
}
let name = &self.name;
let constant = self.selector_name();
let deny_value = self.deny_value();
let decode_inputs = self.decode_inputs();
let storage_arg = self.storage_arg();
let expand_args = self.expand_args();
let encode_output = self.encode_output();
Some(parse_quote! {
#[allow(non_upper_case_globals)]
#constant => {
#deny_value
let args = match <#decode_inputs as #SolType>::abi_decode_params(input, true) {
Ok(args) => args,
Err(err) => {
internal::failed_to_decode_arguments(err);
return Some(Err(Vec::new()));
}
};
let result = Self::#name(#storage_arg #(#expand_args, )* );
Some(#encode_output)
}
})
}
fn decode_inputs(&self) -> syn::Type {
let arg_types = self.arg_types();
parse_quote_spanned! {
self.input_span => <(#( #arg_types, )*) as #AbiType>::SolType
}
}
fn arg_types(&self) -> impl Iterator<Item = &syn::Type> {
self.inputs.iter().map(|arg| &arg.ty)
}
fn storage_arg(&self) -> TokenStream {
if self.inferred_purity == Purity::Pure {
quote!()
} else if self.has_self {
quote! { core::borrow::BorrowMut::borrow_mut(storage), }
} else {
quote! { storage, }
}
}
fn expand_args(&self) -> impl Iterator<Item = syn::Expr> + '_ {
self.arg_types().enumerate().map(|(index, ty)| {
let index = syn::Index {
index: index as u32,
span: ty.span(),
};
parse_quote! { args.#index }
})
}
fn encode_output(&self) -> syn::Expr {
parse_quote_spanned! {
self.output_span => EncodableReturnType::encode(result)
}
}
fn deny_value(&self) -> Option<syn::ExprIf> {
if self.purity == Purity::Payable {
None
} else {
let name = self.name.to_string();
Some(parse_quote! {
if let Err(err) = internal::deny_value(#name) {
return Some(Err(err));
}
})
}
}
fn call_fallback(&self) -> syn::Stmt {
let name = &self.name;
let storage_arg = self.storage_arg();
if matches!(self.kind, FnKind::FallbackNoArgs) {
return parse_quote! {
return Some({
if let Err(err) = Self::#name(#storage_arg) {
Err(err)
} else {
Ok(Vec::new())
}
});
};
}
parse_quote! {
return Some(Self::#name(#storage_arg input));
}
}
fn call_receive(&self) -> syn::Stmt {
let name = &self.name;
let storage_arg = self.storage_arg();
parse_quote! {
return Some(Self::#name(#storage_arg));
}
}
}
pub struct PublicFnArg<E: FnArgExtension> {
pub ty: syn::Type,
#[allow(dead_code)]
pub extension: E,
}
pub trait InterfaceExtension: Sized {
type FnExt: FnExtension;
type Ast: ToTokens;
fn build(node: &syn::ItemImpl) -> Self;
fn codegen(iface: &PublicImpl<Self>) -> Self::Ast;
}
pub trait FnExtension {
type FnArgExt: FnArgExtension;
fn build(node: &syn::ImplItemFn) -> Self;
}
pub trait FnArgExtension {
fn build(node: &syn::FnArg) -> Self;
}
impl InterfaceExtension for () {
type FnExt = ();
type Ast = Nothing;
fn build(_node: &syn::ItemImpl) -> Self {}
fn codegen(_iface: &PublicImpl<Self>) -> Self::Ast {
Nothing
}
}
impl FnExtension for () {
type FnArgExt = ();
fn build(_node: &syn::ImplItemFn) -> Self {}
}
impl FnArgExtension for () {
fn build(_node: &syn::FnArg) -> Self {}
}
================================================
File: stylus-proc/src/macros/sol_storage/mod.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, parse_quote, punctuated::Punctuated, Token};
use proc::{SolidityField, SolidityFields, SolidityStruct, SolidityStructs};
mod proc;
pub fn sol_storage(input: TokenStream) -> TokenStream {
let SolidityStructs(decls) = parse_macro_input!(input as SolidityStructs);
let mut out = quote!();
for decl in decls {
let SolidityStruct {
attrs,
vis,
name,
generics,
fields: SolidityFields(fields),
} = decl;
let fields: Punctuated<_, Token![,]> = fields
.into_iter()
.map(|SolidityField { attrs, name, ty }| -> syn::Field {
parse_quote! {
#(#attrs)*
pub #name: #ty
}
})
.collect();
out.extend(quote! {
#(#attrs)*
#[stylus_sdk::stylus_proc::storage]
#vis struct #name #generics {
#fields
}
});
}
out.into()
}
================================================
File: stylus-proc/src/macros/sol_storage/proc.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use lazy_static::lazy_static;
use proc_macro2::{Ident, Literal};
use quote::quote;
use regex::Regex;
use syn::{
braced, bracketed, parenthesized,
parse::{Parse, ParseStream},
punctuated::Punctuated,
token::Bracket,
Attribute, Error, Generics, Path, Result, Token, Visibility,
};
macro_rules! sdk {
($($msg:expr),+) => {
format!("stylus_sdk::storage::{}", format!($($msg),+))
};
}
pub struct SolidityStructs(pub Vec<SolidityStruct>);
impl Parse for SolidityStructs {
fn parse(input: ParseStream) -> Result<Self> {
let mut structs = Vec::new();
while !input.is_empty() {
structs.push(input.parse()?);
}
Ok(Self(structs))
}
}
pub struct SolidityStruct {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub name: Ident,
pub generics: Generics,
pub fields: SolidityFields,
}
impl Parse for SolidityStruct {
fn parse(input: ParseStream) -> Result<Self> {
// #[attrs?]
// pub? struct name
let attrs: Vec<Attribute> = Attribute::parse_outer(input)?;
let vis: Visibility = input.parse()?;
let _: Token![struct] = input.parse()?;
let name: Ident = input.parse()?;
let generics: Generics = input.parse()?;
let content;
let _ = braced!(content in input);
let fields = content.parse()?;
Ok(Self {
attrs,
vis,
name,
generics,
fields,
})
}
}
pub struct SolidityFields(pub Punctuated<SolidityField, Token![;]>);
impl Parse for SolidityFields {
fn parse(input: ParseStream) -> Result<Self> {
let fields = Punctuated::parse_terminated(input)?;
Ok(Self(fields))
}
}
pub struct SolidityField {
pub attrs: Vec<Attribute>,
pub name: Ident,
pub ty: Path,
}
impl Parse for SolidityField {
fn parse(input: ParseStream) -> Result<Self> {
let attrs: Vec<Attribute> = Attribute::parse_outer(input)?;
let ty = SolidityTy::parse(input)?.0;
let name: Ident = input.parse()?;
Ok(SolidityField { attrs, name, ty })
}
}
pub struct SolidityTy(Path);
impl Parse for SolidityTy {
fn parse(input: ParseStream) -> Result<Self> {
let start: Path = input.parse()?;
let mut path: Path;
if start.is_ident("mapping") {
let content;
let _ = parenthesized!(content in input);
// key => value
let key = content.parse::<PrimitiveKey>()?.0;
let _: Token![=>] = content.parse()?;
let value = content.parse::<SolidityTy>()?.0;
let ty = format!(
"{}<{}, {}>",
sdk!("StorageMap"),
quote!(#key),
quote!(#value)
);
path = syn::parse_str(&ty)?;
} else {
let base: Primitive = start.try_into()?;
path = base.0
};
while input.peek(Bracket) {
let content;
let _ = bracketed!(content in input);
if content.is_empty() {
let outer = sdk!("StorageVec");
let inner = quote! { #path };
path = syn::parse_str(&format!("{outer}<{inner}>"))?;
} else {
let content: Literal = content.parse()?;
let Ok(size) = content.to_string().parse::<usize>() else {
error!(@content, "Array size must be a positive integer");
};
let outer = sdk!("StorageArray");
let inner = quote! { #path };
path = syn::parse_str(&format!("{outer}<{inner}, {size}>"))?;
}
}
Ok(SolidityTy(path))
}
}
pub struct Primitive(Path);
lazy_static! {
static ref UINT_REGEX: Regex = Regex::new(r"^uint(\d+)$").unwrap();
static ref INT_REGEX: Regex = Regex::new(r"^int(\d+)$").unwrap();
static ref BYTES_REGEX: Regex = Regex::new(r"^bytes(\d+)$").unwrap();
static ref LOWER_REGEX: Regex = Regex::new(r"^[0-9a-z]+$").unwrap();
}
impl Parse for Primitive {
fn parse(input: ParseStream) -> Result<Self> {
let path: Path = input.parse()?;
path.try_into()
}
}
impl TryFrom<Path> for Primitive {
type Error = Error;
fn try_from(path: Path) -> std::result::Result<Self, Self::Error> {
let Some(ident) = path.get_ident() else {
return Ok(Self(path));
};
let name = &ident.to_string();
macro_rules! ty {
($($msg:expr),+) => {{
let path = sdk!($($msg),+);
Ok(Self(syn::parse_str(&path)?))
}};
}
macro_rules! error {
($msg:expr) => {
Err(Error::new_spanned(&ident, $msg))
};
}
if let Some(caps) = UINT_REGEX.captures(name) {
let bits: usize = caps[1].parse().unwrap();
let limbs = (63 + bits) / 64;
if bits > 256 {
return error!("Type not supported: too many bits");
}
return ty!("StorageUint<{}, {}>", bits, limbs);
}
if let Some(caps) = INT_REGEX.captures(name) {
let bits: usize = caps[1].parse().unwrap();
let limbs = (63 + bits) / 64;
if bits > 256 {
return error!("Type not supported: too many bits");
}
return ty!("StorageSigned<{}, {}>", bits, limbs);
}
if let Some(caps) = BYTES_REGEX.captures(name) {
let bytes: usize = caps[1].parse().unwrap();
if bytes > 32 {
return error!("Type not supported: too many bytes");
}
return ty!("StorageFixedBytes<{}>", bytes);
}
let ty = match name.as_str() {
"address" => "StorageAddress",
"bool" => "StorageBool",
"bytes" => "StorageBytes",
"int" => "StorageI256",
"string" => "StorageString",
"uint" => "StorageU256",
x => match LOWER_REGEX.is_match(x) {
true => return Err(Error::new_spanned(ident, "Type not supported")),
false => return Ok(Self(syn::parse_str(x)?)),
},
};
ty!("{ty}")
}
}
pub struct PrimitiveKey(Path);
impl Parse for PrimitiveKey {
fn parse(input: ParseStream) -> Result<Self> {
let path: Path = input.parse()?;
path.try_into()
}
}
impl TryFrom<Path> for PrimitiveKey {
type Error = Error;
fn try_from(path: Path) -> std::result::Result<Self, Self::Error> {
let Some(ident) = path.get_ident() else {
return Ok(Self(path));
};
let name = &ident.to_string();
macro_rules! ty {
($($msg:expr),+) => {{
let path = format!($($msg),+);
let path = format!("stylus_sdk::alloy_primitives::{path}");
Ok(Self(syn::parse_str(&path)?))
}};
}
macro_rules! error {
($msg:expr) => {
Err(Error::new_spanned(&ident, $msg))
};
}
if let Some(caps) = UINT_REGEX.captures(name) {
let bits: usize = caps[1].parse().unwrap();
let limbs = (63 + bits) / 64;
if bits > 256 {
return error!("Type not supported: too many bits");
}
return ty!("Uint<{}, {}>", bits, limbs);
}
if let Some(caps) = INT_REGEX.captures(name) {
let bits: usize = caps[1].parse().unwrap();
let limbs = (63 + bits) / 64;
if bits > 256 {
return error!("Type not supported: too many bits");
}
return ty!("Signed<{}, {}>", bits, limbs);
}
if let Some(caps) = BYTES_REGEX.captures(name) {
let bytes: usize = caps[1].parse().unwrap();
if bytes > 32 {
return error!("Type not supported: too many bytes");
}
return ty!("FixedBytes<{}>", bytes);
}
let ty = match name.as_str() {
"address" => "Address",
"bool" => "U8",
"int" => "I256",
"uint" => "U256",
"bytes" => return Ok(Self(syn::parse_str("Vec<u8>")?)),
"string" => return Ok(Self(syn::parse_str("String")?)),
x => match LOWER_REGEX.is_match(x) {
true => return Err(Error::new_spanned(ident, "Type not supported")),
false => return Ok(Self(syn::parse_str(x)?)),
},
};
ty!("{ty}")
}
}
================================================
File: stylus-proc/src/utils/attrs.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Utilities for handling macro attributes.
use proc_macro2::TokenStream;
use proc_macro_error::emit_error;
use syn::parse::{Nothing, Parse};
/// Consume any used attributes, leaving unused attributes in the list.
pub fn consume_attr<T: Parse>(
attrs: &mut Vec<syn::Attribute>,
ident_str: &'static str,
) -> Option<T> {
let mut result = None;
for attr in core::mem::take(attrs) {
// skip all other attrs, adding them back to the Vec
if !attr_ident_matches(&attr, ident_str) {
attrs.push(attr);
continue;
}
if result.is_some() {
emit_error!(attr, "duplicate attribute");
}
let tokens = get_attr_tokens(&attr).unwrap_or_default();
match syn::parse2(tokens) {
Ok(value) => result = Some(value),
Err(err) => {
emit_error!(err.span(), "{}", err);
}
}
}
result
}
/// Consume a flag attribute (no input tokens)
pub fn consume_flag(attrs: &mut Vec<syn::Attribute>, ident_str: &'static str) -> bool {
consume_attr::<Nothing>(attrs, ident_str).is_some()
}
/// Check that an attribute stream is empty.
pub fn check_attr_is_empty(attr: impl Into<TokenStream>) {
let attr = attr.into();
if let Err(err) = syn::parse2::<Nothing>(attr) {
emit_error!(err.span(), "{}", err);
}
}
/// Check if attribute is a simple [`syn::Ident`] and matches a given string
fn attr_ident_matches(attr: &syn::Attribute, value: &'static str) -> bool {
matches!(attr.path().get_ident(), Some(ident) if *ident == value)
}
/// Get tokens for parsing from a [`syn::Attribute`].
fn get_attr_tokens(attr: &syn::Attribute) -> Option<TokenStream> {
if let syn::Meta::List(syn::MetaList { tokens, .. }) = &attr.meta {
Some(tokens.clone())
} else {
None
}
}
================================================
File: stylus-proc/src/utils/mod.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Macro generation utilities.
use sha3::{Digest, Keccak256};
use syn::{punctuated::Punctuated, Token};
use syn_solidity::SolIdent;
pub mod attrs;
#[cfg(test)]
pub mod testing;
/// Like [`syn::Generics::split_for_impl`] but for [`syn::ItemImpl`].
///
/// [`syn::Generics::split_for_impl`] does not work in this case because the `name` of the
/// implemented type is not easy to get, but the type including generics is.
pub fn split_item_impl_for_impl(
node: &syn::ItemImpl,
) -> (
Punctuated<syn::GenericParam, Token![,]>,
syn::Type,
Punctuated<syn::WherePredicate, Token![,]>,
) {
let generic_params = node.generics.params.clone();
let self_ty = (*node.self_ty).clone();
let where_clause = node
.generics
.where_clause
.clone()
.map(|c| c.predicates)
.unwrap_or_default();
(generic_params, self_ty, where_clause)
}
/// Build [function selector](https://solidity-by-example.org/function-selector/) byte array.
pub fn build_selector<'a>(
name: &SolIdent,
params: impl Iterator<Item = &'a syn_solidity::Type>,
) -> [u8; 4] {
let mut selector = Keccak256::new();
selector.update(name.to_string());
selector.update("(");
for (i, param) in params.enumerate() {
if i > 0 {
selector.update(",");
}
selector.update(param.to_string());
}
selector.update(")");
let selector = selector.finalize();
[selector[0], selector[1], selector[2], selector[3]]
}
================================================
File: stylus-proc/src/utils/testing.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Utilities for testing.
use quote::ToTokens;
/// Assert equality of two AST nodes, with pretty diff output for failures.
pub fn assert_ast_eq<T: ToTokens>(left: T, right: T) {
let left = pprint(left);
let right = pprint(right);
pretty_assertions::assert_str_eq!(left, right);
}
fn pprint<T: ToTokens>(node: T) -> String {
let tokens = node.into_token_stream();
let file = syn::parse2(tokens).unwrap();
prettyplease::unparse(&file)
}
================================================
File: stylus-proc/tests/derive_abi_type.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use stylus_proc::AbiType;
use stylus_sdk::abi::AbiType;
use stylus_sdk::alloy_sol_types::sol;
use stylus_sdk::alloy_sol_types::SolType;
sol! {
#[derive(Debug, PartialEq, AbiType)]
struct MyStruct {
uint8 bar;
}
}
#[test]
fn test_abi_type() {
assert_eq!(<MyStruct as AbiType>::ABI.as_str(), "MyStruct");
assert_eq!(
<MyStruct as AbiType>::ABI.as_str(),
<MyStruct as SolType>::SOL_NAME,
);
}
#[test]
fn test_abi_encode() {
let mut expected = [0u8; 32];
expected[31] = 100;
assert_eq!(
<MyStruct as SolType>::abi_encode(&MyStruct { bar: 100 }),
expected,
);
}
#[test]
fn test_abi_decode() {
let mut input = [0u8; 32];
input[31] = 100;
assert_eq!(
<MyStruct as SolType>::abi_decode(&input, true),
Ok(MyStruct { bar: 100 }),
);
}
#[test]
fn test_derive_abi_type_failures() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/fail/derive_abi_type/missing_sol_macro.rs");
}
================================================
File: stylus-proc/tests/derive_erase.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
extern crate alloc;
use stylus_proc::{storage, Erase};
use stylus_sdk::storage::{StorageU256, StorageVec};
#[storage]
#[derive(Erase)]
pub struct Erasable {
arr: StorageVec<StorageU256>,
}
================================================
File: stylus-proc/tests/derive_solidity_error.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
extern crate alloc;
use alloy_primitives::{Address, U256};
use alloy_sol_types::sol;
use stylus_proc::{public, SolidityError};
sol! {
error InsufficientBalance(address from, uint256 have, uint256 want);
error InsufficientAllowance(address owner, address spender, uint256 have, uint256 want);
}
#[derive(SolidityError)]
pub enum Erc20Error {
InsufficientBalance(InsufficientBalance),
InsufficientAllowance(InsufficientAllowance),
}
struct Contract {}
#[public]
impl Contract {
/// Test using the defined error in a result value
pub fn fallible_method() -> Result<(), Erc20Error> {
Err(InsufficientBalance {
from: Address::ZERO,
have: U256::ZERO,
want: U256::ZERO,
}
.into())
}
}
#[test]
fn test_derive_solidity_error_failures() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/fail/derive_solidity_error/invalid_variants.rs");
}
================================================
File: stylus-proc/tests/public.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Integration test for the `#[public]` macro
//!
//! Currently this simply checks that a contract using this macro can compile successfully.
extern crate alloc;
use stylus_proc::public;
struct Contract {}
#[public]
impl Contract {
#[payable]
fn method() {}
}
#[test]
fn test_public_failures() {
let t = trybuild::TestCases::new();
#[cfg(not(feature = "export-abi"))]
t.compile_fail("tests/fail/public/generated.rs");
t.compile_fail("tests/fail/public/macro_errors.rs");
}
================================================
File: stylus-proc/tests/sol_interface.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Integration test for the `sol_interface!` macro
use stylus_proc::sol_interface;
mod inner {
use alloy_sol_types::sol;
sol! {
struct Foo {
uint256 bar;
}
}
}
sol_interface! {
#[derive(Debug)]
interface IService {
function makePayment(address user) payable external returns (string);
function getConstant() pure external returns (bytes32);
function getFoo() pure external returns (inner.Foo);
}
interface ITree {
// Define more interface methods here
}
}
#[test]
fn sol_interface_failures() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/fail/sol_interface/macro_errors.rs");
t.compile_fail("tests/fail/sol_interface/generated.rs");
}
================================================
File: stylus-proc/tests/fail/derive_abi_type/missing_sol_macro.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Compilation will fail if the type is not wrapped in the [`sol!`][alloy_sol_types::sol] macro.
use stylus_proc::AbiType;
use stylus_sdk::storage::StorageBool;
#[derive(AbiType)]
struct MyStruct {
bar: StorageBool,
}
fn main() {}
================================================
File: stylus-proc/tests/fail/derive_abi_type/missing_sol_macro.stderr
================================================
error[E0277]: the trait bound `MyStruct: SolType` is not satisfied
--> tests/fail/derive_abi_type/missing_sol_macro.rs:9:10
|
9 | #[derive(AbiType)]
| ^^^^^^^ the trait `SolType` is not implemented for `MyStruct`
|
= help: the following other types implement trait `SolType`:
()
(T1, T2)
(T1, T2, T3)
(T1, T2, T3, T4)
(T1, T2, T3, T4, T5)
(T1, T2, T3, T4, T5, T6)
(T1, T2, T3, T4, T5, T6, T7)
(T1, T2, T3, T4, T5, T6, T7, T8)
and $N others
note: required by a bound in `stylus_sdk::abi::AbiType::SolType`
--> $WORKSPACE/stylus-sdk/src/abi/mod.rs
|
| type SolType: SolType<RustType = Self>;
| ^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `AbiType::SolType`
= note: this error originates in the derive macro `AbiType` (in Nightly builds, run with -Z macro-backtrace for more info)
================================================
File: stylus-proc/tests/fail/derive_solidity_error/invalid_variants.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use alloy_sol_types::sol;
use stylus_proc::SolidityError;
sol! {
error InsufficientBalance(address from, uint256 have, uint256 want);
error InsufficientAllowance(address owner, address spender, uint256 have, uint256 want);
}
#[derive(SolidityError)]
enum MyError {
Unit,
Two(InsufficientBalance, InsufficientAllowance),
Named { balance: InsufficientBalance },
}
fn main() {}
================================================
File: stylus-proc/tests/fail/derive_solidity_error/invalid_variants.stderr
================================================
error: variant not a 1-tuple
--> tests/fail/derive_solidity_error/invalid_variants.rs:15:5
|
15 | Unit,
| ^^^^
error: variant not a 1-tuple
--> tests/fail/derive_solidity_error/invalid_variants.rs:16:8
|
16 | Two(InsufficientBalance, InsufficientAllowance),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: variant not a 1-tuple
--> tests/fail/derive_solidity_error/invalid_variants.rs:17:11
|
17 | Named { balance: InsufficientBalance },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
================================================
File: stylus-proc/tests/fail/public/generated.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Compilation failures after macro generation completes.
extern crate alloc;
use stylus_proc::public;
struct UnsupportedType;
struct Contract {}
#[public]
impl Contract {
fn unsupported_input(_arg: UnsupportedType) {}
fn unsupported_output() -> UnsupportedType {
UnsupportedType
}
}
fn main() {}
================================================
File: stylus-proc/tests/fail/public/generated.stderr
================================================
error[E0277]: the trait bound `UnsupportedType: AbiType` is not satisfied
--> tests/fail/public/generated.rs:16:32
|
16 | fn unsupported_input(_arg: UnsupportedType) {}
| ^^^^^^^^^^^^^^^ the trait `AbiType` is not implemented for `UnsupportedType`
|
= help: the following other types implement trait `AbiType`:
()
(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
and $N others
error[E0277]: the trait bound `UnsupportedType: AbiType` is not satisfied
--> tests/fail/public/generated.rs:16:26
|
16 | fn unsupported_input(_arg: UnsupportedType) {}
| ^^^^ the trait `AbiType` is not implemented for `UnsupportedType`, which is required by `(UnsupportedType,): AbiType`
|
= help: the following other types implement trait `AbiType`:
()
(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
and $N others
= note: required for `(UnsupportedType,)` to implement `AbiType`
error[E0277]: the trait bound `UnsupportedType: EncodableReturnType` is not satisfied
--> tests/fail/public/generated.rs:18:32
|
18 | fn unsupported_output() -> UnsupportedType {
| ^^^^^^^^^^^^^^^ the trait `AbiType` is not implemented for `UnsupportedType`, which is required by `UnsupportedType: EncodableReturnType`
|
= help: the following other types implement trait `AbiType`:
()
(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
(G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
and $N others
= note: required for `UnsupportedType` to implement `EncodableReturnType`
error: could not evaluate constant pattern
--> tests/fail/public/generated.rs:16:8
|
16 | fn unsupported_input(_arg: UnsupportedType) {}
| ^^^^^^^^^^^^^^^^^
================================================
File: stylus-proc/tests/fail/public/macro_errors.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Compilation should fail for any unsupported attributes or other features.
extern crate alloc;
use stylus_proc::public;
struct Contract {}
#[public(unsupported)]
impl Contract {
#[payable(unsupported)]
fn test_method() {}
}
fn main() {}
================================================
File: stylus-proc/tests/fail/public/macro_errors.stderr
================================================
error: unexpected token
--> tests/fail/public/macro_errors.rs:12:10
|
12 | #[public(unsupported)]
| ^^^^^^^^^^^
error: unexpected token
--> tests/fail/public/macro_errors.rs:14:15
|
14 | #[payable(unsupported)]
| ^^^^^^^^^^^
================================================
File: stylus-proc/tests/fail/sol_interface/generated.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Compilation failures after macro generation completes.
extern crate alloc;
use stylus_proc::sol_interface;
sol_interface! {
interface IService {
#[function_attr]
function makePayment(address user) payable external returns (string);
function getConstant() pure external returns (bytes32);
}
#[interface_attr]
interface ITree {
// Define more interface methods here
}
}
fn main() {}
================================================
File: stylus-proc/tests/fail/sol_interface/generated.stderr
================================================
error: cannot find attribute `function_attr` in this scope
--> tests/fail/sol_interface/generated.rs:12:11
|
12 | #[function_attr]
| ^^^^^^^^^^^^^
error: cannot find attribute `interface_attr` in this scope
--> tests/fail/sol_interface/generated.rs:17:7
|
17 | #[interface_attr]
| ^^^^^^^^^^^^^^
================================================
File: stylus-proc/tests/fail/sol_interface/macro_errors.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Compilation should fail for any unsupported Solidity features.
use stylus_proc::sol_interface;
sol_interface! {
#![file_attribute]
interface IParent {
function makePayment(address user) payable external returns (string);
function getConstant() pure external returns (bytes32);
}
interface IChild is IParent {
}
contract TestContract {
}
function sum(uint[] memory arr) pure returns (uint s) {
for (uint i = 0; i < arr.length; i++)
s += arr[i];
}
}
fn main() {}
================================================
File: stylus-proc/tests/fail/sol_interface/macro_errors.stderr
================================================
error: attribute not supported
--> tests/fail/sol_interface/macro_errors.rs:9:5
|
9 | #![file_attribute]
| ^^^^^^^^^^^^^^^^^^
error: inheritance not supported
--> tests/fail/sol_interface/macro_errors.rs:16:22
|
16 | interface IChild is IParent {
| ^^
error: not an interface
--> tests/fail/sol_interface/macro_errors.rs:19:14
|
19 | contract TestContract {
| ^^^^^^^^^^^^
error: not an interface
--> tests/fail/sol_interface/macro_errors.rs:22:14
|
22 | function sum(uint[] memory arr) pure returns (uint s) {
| ^^^
================================================
File: stylus-sdk/Cargo.toml
================================================
[package]
name = "stylus-sdk"
keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
description = "Rust smart contracts with Arbitrum Stylus"
readme = "../README.md"
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
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
lazy_static.workspace = true
# export-abi
regex = { workspace = true, optional = true }
# local deps
mini-alloc = { workspace = true, optional = true }
stylus-proc.workspace = true
[dev-dependencies]
paste.workspace = true
sha3.workspace = true
[package.metadata.docs.rs]
features = ["default", "docs", "debug", "export-abi"]
[features]
default = ["mini-alloc", "hostio-caching"]
export-abi = ["debug", "regex", "stylus-proc/export-abi", "alloy-primitives/tiny-keccak"]
debug = []
docs = []
hostio = []
mini-alloc = ["dep:mini-alloc"]
reentrant = ["stylus-proc/reentrant"]
hostio-caching = []
================================================
File: stylus-sdk/src/block.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! VM affordances for inspecting the current block.
//!
//! See also [`contract`](crate::contract), [`crypto`](crate::crypto), [`evm`](crate::evm),
//! [`msg`](crate::msg), and [`tx`](crate::tx).
//!
//! ```no_run
//! use stylus_sdk::block;
//!
//! let number = block::number();
//! ```
use crate::hostio::{self, wrap_hostio};
use alloy_primitives::{Address, B256, U256};
wrap_hostio!(
/// Gets the basefee of the current block.
basefee BASEFEE block_basefee U256
);
wrap_hostio!(
/// Gets the unique chain identifier of the Arbitrum chain.
chainid CHAINID chainid u64
);
wrap_hostio!(
/// Gets the coinbase of the current block, which on Arbitrum chains is the L1 batch poster's
/// address.
coinbase COINBASE block_coinbase Address
);
wrap_hostio!(
/// Gets the gas limit of the current block.
gas_limit GAS_LIMIT block_gas_limit u64
);
wrap_hostio!(
/// Gets a bounded estimate of the L1 block number at which the Sequencer sequenced the
/// transaction. See [`Block Numbers and Time`] for more information on how this value is
/// determined.
///
/// [`Block Numbers and Time`]: https://developer.arbitrum.io/time
number NUMBER block_number u64
);
wrap_hostio!(
/// Gets a bounded estimate of the Unix timestamp at which the Sequencer sequenced the
/// transaction. See [`Block Numbers and Time`] for more information on how this value is
/// determined.
///
/// [`Block Numbers and Time`]: https://developer.arbitrum.io/time
timestamp TIMESTAMP block_timestamp u64
);
================================================
File: stylus-sdk/src/contract.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! VM affordances for inspecting the contract itself.
//!
//! See also [`block`](crate::block), [`crypto`](crate::crypto), [`evm`](crate::evm),
//! [`msg`](crate::msg), and [`tx`](crate::tx).
//!
//! ```no_run
//! use stylus_sdk::contract;
//!
//! let balance = contract::balance();
//! ```
use crate::{
hostio::{self, wrap_hostio},
types::AddressVM,
};
use alloc::vec::Vec;
use alloy_primitives::{Address, U256};
/// Reads the invocation's calldata.
/// The [`entrypoint`](macro@stylus_proc::entrypoint) macro uses this under the hood.
pub fn args(len: usize) -> Vec<u8> {
let mut input = Vec::with_capacity(len);
unsafe {
hostio::read_args(input.as_mut_ptr());
input.set_len(len);
}
input
}
/// Writes the contract's return data.
/// The [`entrypoint`](macro@stylus_proc::entrypoint) macro uses this under the hood.
pub fn output(data: &[u8]) {
unsafe {
hostio::write_result(data.as_ptr(), data.len());
}
}
/// Copies the bytes of the last EVM call or deployment return result.
/// Note: this function does not revert if out of bounds, but rather will copy the overlapping portion.
pub fn read_return_data(offset: usize, size: Option<usize>) -> Vec<u8> {
let size = size.unwrap_or_else(|| return_data_len().saturating_sub(offset));
let mut data = Vec::with_capacity(size);
if size > 0 {
unsafe {
let bytes_written = hostio::read_return_data(data.as_mut_ptr(), offset, size);
debug_assert!(bytes_written <= size);
data.set_len(bytes_written);
}
};
data
}
wrap_hostio!(
/// Returns the length of the last EVM call or deployment return result, or `0` if neither have
/// happened during the program's execution.
return_data_len RETURN_DATA_LEN return_data_size usize
);
wrap_hostio!(
/// Gets the address of the current program.
address ADDRESS contract_address Address
);
/// Gets the balance of the current program.
pub fn balance() -> U256 {
address().balance()
}
================================================
File: stylus-sdk/src/crypto.rs
================================================
// Copyright 2022-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! VM-accelerated cryptography.
//!
//! See also [`block`](crate::block), [`contract`](crate::contract), [`evm`](crate::evm),
//! [`msg`](crate::msg), and [`tx`](crate::tx).
//!
//! ```no_run
//! use stylus_sdk::crypto;
//! use stylus_sdk::alloy_primitives::address;
//!
//! let preimage = address!("361594F5429D23ECE0A88E4fBE529E1c49D524d8");
//! let hash = crypto::keccak(&preimage);
//! ```
use alloy_primitives::B256;
/// Efficiently computes the [`keccak256`] hash of the given preimage.
///
/// [`keccak256`]: https://en.wikipedia.org/wiki/SHA-3
pub fn keccak<T: AsRef<[u8]>>(bytes: T) -> B256 {
alloy_primitives::keccak256(bytes)
}
================================================
File: stylus-sdk/src/debug.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Debug-only items for printing to the console.
//!
//! ```no_run
//! use stylus_sdk::console;
//! use stylus_sdk::alloy_primitives::address;
//! extern crate alloc;
//!
//! let arbinaut = address!("361594F5429D23ECE0A88E4fBE529E1c49D524d8");
//! console!("Gm {}", arbinaut); // prints nothing in production
//! ```
/// Prints a UTF-8 encoded string to the console. Only available in debug mode.
#[cfg(feature = "debug")]
pub fn console_log<T: AsRef<str>>(text: T) {
let text = text.as_ref();
unsafe { crate::hostio::log_txt(text.as_ptr(), text.len()) };
}
/// Prints to the console when executing in a debug environment. Otherwise does nothing.
#[cfg(feature = "debug")]
#[macro_export]
macro_rules! console {
($($msg:tt)*) => {
$crate::debug::console_log(alloc::format!($($msg)*));
};
}
/// Prints to the console when executing in a debug environment. Otherwise does nothing.
#[cfg(not(feature = "debug"))]
#[macro_export]
macro_rules! console {
($($msg:tt)*) => {{}};
}
================================================
File: stylus-sdk/src/evm.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Affordances for the Ethereum Virtual Machine.
//!
//! See also [`block`](crate::block), [`contract`](crate::contract), [`crypto`](crate::crypto),
//! [`msg`](crate::msg), and [`tx`](crate::msg).
//!
//! ```no_run
//! use stylus_sdk::evm;
//!
//! let gas = evm::gas_left();
//! ```
use crate::hostio::{self, wrap_hostio};
use alloc::{vec, vec::Vec};
use alloy_primitives::B256;
use alloy_sol_types::{abi::token::WordToken, SolEvent, TopicList};
/// Emits an evm log from combined topics and data.
fn emit_log(bytes: &[u8], num_topics: usize) {
unsafe { hostio::emit_log(bytes.as_ptr(), bytes.len(), num_topics) }
}
/// Emits an EVM log from its raw topics and data.
/// Most users should prefer the alloy-typed [`log`].
pub fn raw_log(topics: &[B256], data: &[u8]) -> Result<(), &'static str> {
if topics.len() > 4 {
return Err("too many topics");
}
let mut bytes: Vec<u8> = vec![];
bytes.extend(topics.iter().flat_map(|x| x.0.iter()));
bytes.extend(data);
emit_log(&bytes, topics.len());
Ok(())
}
/// Emits a typed alloy log.
pub fn log<T: SolEvent>(event: T) {
// According to the alloy docs, encode_topics_raw fails only if the array is too small
let mut topics = [WordToken::default(); 4];
event.encode_topics_raw(&mut topics).unwrap();
let count = T::TopicList::COUNT;
let mut bytes = Vec::with_capacity(32 * count);
for topic in &topics[..count] {
bytes.extend_from_slice(topic.as_slice());
}
event.encode_data_to(&mut bytes);
emit_log(&bytes, count);
}
/// This function exists to force the compiler to import this symbol.
/// Calling it will unproductively consume gas.
pub fn pay_for_memory_grow(pages: u16) {
unsafe { hostio::pay_for_memory_grow(pages) }
}
wrap_hostio!(
/// Gets the amount of gas remaining. See [`Ink and Gas`] for more information on Stylus's compute pricing.
///
/// [`Ink and Gas`]: https://docs.arbitrum.io/stylus/concepts/gas-metering
gas_left evm_gas_left u64
);
wrap_hostio!(
/// Gets the amount of ink remaining. See [`Ink and Gas`] for more information on Stylus's compute pricing.
///
/// [`Ink and Gas`]: https://docs.arbitrum.io/stylus/concepts/gas-metering
ink_left evm_ink_left u64
);
================================================
File: stylus-sdk/src/hostio.rs
================================================
// Copyright 2022-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Raw host I/Os for low-level access to the Stylus runtime.
//!
//! This module is only available when the `hostio` feature flag is enabled, which exposes
//! low-level functions for accessing the VM directly. Most users should instead use the
//! high-level equivalents of [`block`](crate::block), [`contract`](crate::contract),
//! [`crypto`](crate::crypto), [`evm`](crate::evm), [`msg`](crate::msg), and [`tx`](crate::tx).
//!
#![cfg_attr(
feature = "hostio",
doc = r##"
```no_run
use stylus_sdk::hostio;
use stylus_sdk::{alloy_primitives::Address, msg};
let mut sender = Address::ZERO;
unsafe {
hostio::msg_sender(sender.as_mut_ptr());
}
assert_eq!(sender, msg::sender());
```
"##
)]
use cfg_if::cfg_if;
macro_rules! vm_hooks {
(
$(#[$block_meta:meta])* // macros & docstrings to apply to all funcs
module($link:literal, $stub:ident); // configures the wasm_import_module to link
// all the function declarations
$($(#[$meta:meta])* $vis:vis fn $func:ident ($($arg:ident : $arg_type:ty),* ) $(-> $return_type:ty)?);*
) => {
cfg_if! {
if #[cfg(feature = "export-abi")] {
// Generate a stub for each function.
// We use a module for the block macros & docstrings.
$(#[$block_meta])*
mod $stub {
$(
$(#[$meta])*
#[allow(unused_variables, clippy::missing_safety_doc)]
$vis unsafe fn $func($($arg : $arg_type),*) $(-> $return_type)? {
unimplemented!()
}
)*
}
pub use $stub::*;
} else {
// Generate a wasm import for each function.
$(#[$block_meta])*
#[link(wasm_import_module = $link)]
extern "C" {
$(
$(#[$meta])*
$vis fn $func($($arg : $arg_type),*) $(-> $return_type)?;
)*
}
}
}
};
}
vm_hooks! {
module("vm_hooks", vm_hooks);
/// 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.
///
/// [`BALANCE`]: https://www.evm.codes/#31
pub fn account_balance(address: *const u8, dest: *mut u8);
/// Gets a subset of the code from the account at the given address. The semantics are identical to that
/// of the EVM's [`EXT_CODE_COPY`] opcode, aside from one small detail: the write to the buffer `dest` will
/// stop after the last byte is written. This is unlike the EVM, which right pads with zeros in this scenario.
/// The return value is the number of bytes written, which allows the caller to detect if this has occurred.
///
/// [`EXT_CODE_COPY`]: https://www.evm.codes/#3C
pub fn account_code(address: *const u8, offset: usize, size: usize, dest: *mut u8) -> usize;
/// Gets the size of the code in bytes at the given address. The semantics are equivalent
/// to that of the EVM's [`EXT_CODESIZE`].
///
/// [`EXT_CODESIZE`]: https://www.evm.codes/#3B
pub fn account_code_size(address: *const u8) -> usize;
/// Gets the code hash of the account at the given address. The semantics are equivalent
/// to that of the EVM's [`EXT_CODEHASH`] opcode. Note that the code hash of an account without
/// code will be the empty hash
/// `keccak("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`.
///
/// [`EXT_CODEHASH`]: https://www.evm.codes/#3F
pub fn account_codehash(address: *const u8, dest: *mut u8);
/// Reads a 32-byte value from permanent storage. Stylus's storage format is identical to
/// that of the EVM. This means that, under the hood, this hostio is accessing the 32-byte
/// value stored in the EVM state trie at offset `key`, which will be `0` when not previously
/// set. The semantics, then, are equivalent to that of the EVM's [`SLOAD`] opcode.
///
/// Note: the Stylus VM implements storage caching. This means that repeated calls to the same key
/// will cost less than in the EVM.
///
/// [`SLOAD`]: https://www.evm.codes/#54
pub fn storage_load_bytes32(key: *const u8, dest: *mut u8);
/// Writes a 32-byte value to the permanent storage cache. Stylus's storage format is identical to that
/// of the EVM. This means that, under the hood, this hostio represents storing a 32-byte value into
/// the EVM state trie at offset `key`. Refunds are tabulated exactly as in the EVM. The semantics, then,
/// are equivalent to that of the EVM's [`SSTORE`] opcode.
///
/// Note: because the value is cached, one must call `storage_flush_cache` to persist it.
///
/// [`SSTORE`]: https://www.evm.codes/#55
pub fn storage_cache_bytes32(key: *const u8, value: *const u8);
/// Persists any dirty values in the storage cache to the EVM state trie, dropping the cache entirely if requested.
/// Analogous to repeated invocations of [`SSTORE`].
///
/// [`SSTORE`]: https://www.evm.codes/#55
pub fn storage_flush_cache(clear: bool);
/// Gets the basefee of the current block. The semantics are equivalent to that of the EVM's
/// [`BASEFEE`] opcode.
///
/// [`BASEFEE`]: https://www.evm.codes/#48
pub fn block_basefee(basefee: *mut u8);
/// Gets the unique chain identifier of the Arbitrum chain. The semantics are equivalent to
/// that of the EVM's [`CHAIN_ID`] opcode.
///
/// [`CHAIN_ID`]: https://www.evm.codes/#46
pub fn chainid() -> u64;
/// Gets the coinbase of the current block, which on Arbitrum chains is the L1 batch poster's
/// address. This differs from Ethereum where the validator including the transaction
/// determines the coinbase.
pub fn block_coinbase(coinbase: *mut u8);
/// Gets the gas limit of the current block. The semantics are equivalent to that of the EVM's
/// [`GAS_LIMIT`] opcode. Note that as of the time of this writing, `evm.codes` incorrectly
/// implies that the opcode returns the gas limit of the current transaction. When in doubt,
/// consult [`The Ethereum Yellow Paper`].
///
/// [`GAS_LIMIT`]: https://www.evm.codes/#45
/// [`The Ethereum Yellow Paper`]: https://ethereum.github.io/yellowpaper/paper.pdf
pub fn block_gas_limit() -> u64;
/// Gets a bounded estimate of the L1 block number at which the Sequencer sequenced the
/// transaction. See [`Block Numbers and Time`] for more information on how this value is
/// determined.
///
/// [`Block Numbers and Time`]: https://developer.arbitrum.io/time
pub fn block_number() -> u64;
/// Gets a bounded estimate of the Unix timestamp at which the Sequencer sequenced the
/// transaction. See [`Block Numbers and Time`] for more information on how this value is
/// determined.
///
/// [`Block Numbers and Time`]: https://developer.arbitrum.io/time
pub fn block_timestamp() -> u64;
/// Calls the contract at the given address with options for passing value and to limit the
/// amount of gas supplied. The return status indicates whether the call succeeded, and is
/// nonzero on failure.
///
/// In both cases `return_data_len` will store the length of the result, the bytes of which can
/// be read via the `read_return_data` hostio. The bytes are not returned directly so that the
/// programmer can potentially save gas by choosing which subset of the return result they'd
/// like to copy.
///
/// The semantics are equivalent to that of the EVM's [`CALL`] opcode, including callvalue
/// stipends and the 63/64 gas rule. This means that supplying the `u64::MAX` gas can be used
/// to send as much as possible.
///
/// [`CALL`]: https://www.evm.codes/#f1
pub fn call_contract(
contract: *const u8,
calldata: *const u8,
calldata_len: usize,
value: *const u8,
gas: u64,
return_data_len: *mut usize
) -> u8;
/// Gets the address of the current program. The semantics are equivalent to that of the EVM's
/// [`ADDRESS`] opcode.
///
/// [`ADDRESS`]: https://www.evm.codes/#30
pub fn contract_address(address: *mut u8);
/// Deploys a new contract using the init code provided, which the EVM executes to construct
/// the code of the newly deployed contract. The init code must be written in EVM bytecode, but
/// the code it deploys can be that of a Stylus program. The code returned will be treated as
/// WASM if it begins with the EOF-inspired header `0xEFF000`. Otherwise the code will be
/// interpreted as that of a traditional EVM-style contract. See [`Deploying Stylus Programs`]
/// for more information on writing init code.
///
/// On success, this hostio returns the address of the newly created account whose address is
/// a function of the sender and nonce. On failure the address will be `0`, `return_data_len`
/// will store the length of the revert data, the bytes of which can be read via the
/// `read_return_data` hostio. The semantics are equivalent to that of the EVM's [`CREATE`]
/// opcode, which notably includes the exact address returned.
///
/// [`Deploying Stylus Programs`]: https://docs.arbitrum.io/stylus/quickstart
/// [`CREATE`]: https://www.evm.codes/#f0
pub fn create1(
code: *const u8,
code_len: usize,
endowment: *const u8,
contract: *mut u8,
revert_data_len: *mut usize
);
/// Deploys a new contract using the init code provided, which the EVM executes to construct
/// the code of the newly deployed contract. The init code must be written in EVM bytecode, but
/// the code it deploys can be that of a Stylus program. The code returned will be treated as
/// WASM if it begins with the EOF-inspired header `0xEFF000`. Otherwise the code will be
/// interpreted as that of a traditional EVM-style contract. See [`Deploying Stylus Programs`]
/// for more information on writing init code.
///
/// On success, this hostio returns the address of the newly created account whose address is a
/// function of the sender, salt, and init code. On failure the address will be `0`,
/// `return_data_len` will store the length of the revert data, the bytes of which can be read
/// via the `read_return_data` hostio. The semantics are equivalent to that of the EVM's
/// `[CREATE2`] opcode, which notably includes the exact address returned.
///
/// [`Deploying Stylus Programs`]: https://docs.arbitrum.io/stylus/quickstart
/// [`CREATE2`]: https://www.evm.codes/#f5
pub fn create2(
code: *const u8,
code_len: usize,
endowment: *const u8,
salt: *const u8,
contract: *mut u8,
revert_data_len: *mut usize
);
/// Delegate calls the contract at the given address, with the option to limit the amount of
/// gas supplied. The return status indicates whether the call succeeded, and is nonzero on
/// failure.
///
/// In both cases `return_data_len` will store the length of the result, the bytes of which
/// can be read via the `read_return_data` hostio. The bytes are not returned directly so that
/// the programmer can potentially save gas by choosing which subset of the return result
/// they'd like to copy.
///
/// The semantics are equivalent to that of the EVM's [`DELEGATE_CALL`] opcode, including the
/// 63/64 gas rule. This means that supplying `u64::MAX` gas can be used to send as much as
/// possible.
///
/// [`DELEGATE_CALL`]: https://www.evm.codes/#F4
pub fn delegate_call_contract(
contract: *const u8,
calldata: *const u8,
calldata_len: usize,
gas: u64,
return_data_len: *mut usize
) -> u8;
/// Emits an EVM log with the given number of topics and data, the first bytes of which should
/// be the 32-byte-aligned topic data. The semantics are equivalent to that of the EVM's
/// [`LOG0`], [`LOG1`], [`LOG2`], [`LOG3`], and [`LOG4`] opcodes based on the number of topics
/// specified. Requesting more than `4` topics will induce a revert.
///
/// [`LOG0`]: https://www.evm.codes/#a0
/// [`LOG1`]: https://www.evm.codes/#a1
/// [`LOG2`]: https://www.evm.codes/#a2
/// [`LOG3`]: https://www.evm.codes/#a3
/// [`LOG4`]: https://www.evm.codes/#a4
pub fn emit_log(data: *const u8, len: usize, topics: usize);
/// Gets the amount of gas left after paying for the cost of this hostio. The semantics are
/// equivalent to that of the EVM's [`GAS`] opcode.
///
/// [`GAS`]: https://www.evm.codes/#5a
pub fn evm_gas_left() -> u64;
/// Gets the amount of ink remaining after paying for the cost of this hostio. The semantics
/// are equivalent to that of the EVM's [`GAS`] opcode, except the units are in ink. See
/// [`Ink and Gas`] for more information on Stylus's compute pricing.
///
/// [`GAS`]: https://www.evm.codes/#5a
/// [`Ink and Gas`]: https://docs.arbitrum.io/stylus/concepts/gas-metering
pub fn evm_ink_left() -> u64;
/// The `entrypoint!` macro handles importing this hostio, which is required if the
/// program's memory grows. Otherwise compilation through the `ArbWasm` precompile will revert.
/// Internally the Stylus VM forces calls to this hostio whenever new WASM pages are allocated.
/// Calls made voluntarily will unproductively consume gas.
pub fn pay_for_memory_grow(pages: u16);
/// Whether the current call is reentrant.
pub fn msg_reentrant() -> bool;
/// Gets the address of the account that called the program. For normal L2-to-L2 transactions
/// the semantics are equivalent to that of the EVM's [`CALLER`] opcode, including in cases
/// arising from [`DELEGATE_CALL`].
///
/// For L1-to-L2 retryable ticket transactions, the top-level sender's address will be aliased.
/// See [`Retryable Ticket Address Aliasing`] for more information on how this works.
///
/// [`CALLER`]: https://www.evm.codes/#33
/// [`DELEGATE_CALL`]: https://www.evm.codes/#f4
/// [`Retryable Ticket Address Aliasing`]: https://developer.arbitrum.io/arbos/l1-to-l2-messaging#address-aliasing
pub fn msg_sender(sender: *mut u8);
/// Get the ETH value in wei sent to the program. The semantics are equivalent to that of the
/// EVM's [`CALLVALUE`] opcode.
///
/// [`CALLVALUE`]: https://www.evm.codes/#34
pub fn msg_value(value: *mut u8);
/// Efficiently computes the [`keccak256`] hash of the given preimage.
/// The semantics are equivalent to that of the EVM's [`SHA3`] opcode.
///
/// [`keccak256`]: https://en.wikipedia.org/wiki/SHA-3
/// [`SHA3`]: https://www.evm.codes/#20
#[allow(unused)]
pub fn native_keccak256(bytes: *const u8, len: usize, output: *mut u8);
/// Reads the program calldata. The semantics are equivalent to that of the EVM's
/// [`CALLDATA_COPY`] opcode when requesting the entirety of the current call's calldata.
///
/// [`CALLDATA_COPY`]: https://www.evm.codes/#37
pub fn read_args(dest: *mut u8);
/// Copies the bytes of the last EVM call or deployment return result. Does not revert if out of
/// bounds, but rather copies the overlapping portion. The semantics are otherwise equivalent
/// to that of the EVM's [`RETURN_DATA_COPY`] opcode.
///
/// Returns the number of bytes written.
///
/// [`RETURN_DATA_COPY`]: https://www.evm.codes/#3e
pub fn read_return_data(dest: *mut u8, offset: usize, size: usize) -> usize;
/// Writes the final return data. If not called before the program exists, the return data will
/// be 0 bytes long. Note that this hostio does not cause the program to exit, which happens
/// naturally when `user_entrypoint` returns.
pub fn write_result(data: *const u8, len: usize);
/// Returns the length of the last EVM call or deployment return result, or `0` if neither have
/// happened during the program's execution. The semantics are equivalent to that of the EVM's
/// [`RETURN_DATA_SIZE`] opcode.
///
/// [`RETURN_DATA_SIZE`]: https://www.evm.codes/#3d
pub fn return_data_size() -> usize;
/// Static calls the contract at the given address, with the option to limit the amount of gas
/// supplied. The return status indicates whether the call succeeded, and is nonzero on
/// failure.
///
/// In both cases `return_data_len` will store the length of the result, the bytes of which can
/// be read via the `read_return_data` hostio. The bytes are not returned directly so that the
/// programmer can potentially save gas by choosing which subset of the return result they'd
/// like to copy.
///
/// The semantics are equivalent to that of the EVM's [`STATIC_CALL`] opcode, including the
/// 63/64 gas rule. This means that supplying `u64::MAX` gas can be used to send as much as
/// possible.
///
/// [`STATIC_CALL`]: https://www.evm.codes/#FA
pub fn static_call_contract(
contract: *const u8,
calldata: *const u8,
calldata_len: usize,
gas: u64,
return_data_len: *mut usize
) -> u8;
/// Gets the gas price in wei per gas, which on Arbitrum chains equals the basefee. The
/// semantics are equivalent to that of the EVM's [`GAS_PRICE`] opcode.
///
/// [`GAS_PRICE`]: https://www.evm.codes/#3A
pub fn tx_gas_price(gas_price: *mut u8);
/// Gets the price of ink in evm gas basis points. See [`Ink and Gas`] for more information on
/// Stylus's compute-pricing model.
///
/// [`Ink and Gas`]: https://docs.arbitrum.io/stylus/concepts/gas-metering
pub fn tx_ink_price() -> u32;
/// Gets the top-level sender of the transaction. The semantics are equivalent to that of the
/// EVM's [`ORIGIN`] opcode.
///
/// [`ORIGIN`]: https://www.evm.codes/#32
pub fn tx_origin(origin: *mut u8)
}
vm_hooks! {
#[allow(dead_code)]
module("console", console);
/// Prints a 32-bit floating point number to the console. Only available in debug mode with
/// floating point enabled.
pub fn log_f32(value: f32);
/// Prints a 64-bit floating point number to the console. Only available in debug mode with
/// floating point enabled.
pub fn log_f64(value: f64);
/// Prints a 32-bit integer to the console, which can be either signed or unsigned.
/// Only available in debug mode.
pub fn log_i32(value: i32);
/// Prints a 64-bit integer to the console, which can be either signed or unsigned.
/// Only available in debug mode.
pub fn log_i64(value: i64);
/// Prints a UTF-8 encoded string to the console. Only available in debug mode.
pub fn log_txt(text: *const u8, len: usize)
}
macro_rules! wrap_hostio {
($(#[$meta:meta])* $name:ident $hostio:ident u64) => {
wrap_hostio!(@simple $(#[$meta])* $name, $hostio, u64); // uncached
};
($(#[$meta:meta])* $name:ident $cache:ident $hostio:ident bool) => {
wrap_hostio!(@simple $(#[$meta])* $name, $cache, $hostio, bool);
};
($(#[$meta:meta])* $name:ident $cache:ident $hostio:ident u32) => {
wrap_hostio!(@simple $(#[$meta])* $name, $cache, $hostio, u32);
};
($(#[$meta:meta])* $name:ident $cache:ident $hostio:ident u64) => {
wrap_hostio!(@simple $(#[$meta])* $name, $cache, $hostio, u64);
};
($(#[$meta:meta])* $name:ident $cache:ident $hostio:ident usize) => {
wrap_hostio!(@simple $(#[$meta])* $name, $cache, $hostio, usize);
};
($(#[$meta:meta])* $name:ident $cache:ident $hostio:ident Address) => {
wrap_hostio!(@convert $(#[$meta])* $name, $cache, $hostio, Address, Address);
};
($(#[$meta:meta])* $name:ident $cache:ident $hostio:ident U256) => {
wrap_hostio!(@convert $(#[$meta])* $name, $cache, $hostio, B256, U256);
};
(@simple $(#[$meta:meta])* $name:ident, $hostio:ident, $ty:ident) => {
$(#[$meta])*
pub fn $name() -> $ty {
unsafe { $ty::from(hostio::$hostio()) }
}
};
(@simple $(#[$meta:meta])* $name:ident, $cache:ident, $hostio:ident, $ty:ident) => {
cfg_if::cfg_if! {
if #[cfg(feature = "hostio-caching")] {
$(#[$meta])*
pub fn $name() -> $ty {
$cache.get()
}
pub(crate) static $cache: hostio::CachedOption<$ty> = hostio::CachedOption::new(|| unsafe { hostio::$hostio() });
} else {
wrap_hostio!(@simple $(#[$meta])* $name, $hostio, $ty); // uncached
}
}
};
(@convert $(#[$meta:meta])* $name:ident, $hostio:ident, $from:ident, $ty:ident) => {
$(#[$meta])*
pub fn $name() -> $ty {
let mut data = $from::ZERO;
unsafe { hostio::$hostio(data.as_mut_ptr()) };
data.into()
}
};
(@convert $(#[$meta:meta])* $name:ident, $cache:ident, $hostio:ident, $from:ident, $ty:ident) => {
cfg_if::cfg_if! {
if #[cfg(feature = "hostio-caching")] {
$(#[$meta])*
pub fn $name() -> $ty {
$cache.get()
}
pub(crate) static $cache: hostio::CachedOption<$ty> = hostio::CachedOption::new(|| {
let mut data = $from::ZERO;
unsafe { hostio::$hostio(data.as_mut_ptr()) };
data.into()
});
} else {
wrap_hostio!(@convert $(#[$meta])* $name, $hostio, $from, $ty); // uncached
}
}
};
}
pub(crate) use wrap_hostio;
use core::cell::UnsafeCell;
use core::mem::MaybeUninit;
/// Caches a value to avoid paying for hostio invocations.
pub(crate) struct CachedOption<T: Copy> {
value: UnsafeCell<MaybeUninit<T>>,
initialized: UnsafeCell<bool>,
loader: fn() -> T,
}
impl<T: Copy> CachedOption<T> {
/// Creates a new [`CachedOption`], which will use the `loader` during `get`.
pub const fn new(loader: fn() -> T) -> Self {
Self {
value: UnsafeCell::new(MaybeUninit::uninit()),
initialized: UnsafeCell::new(false),
loader,
}
}
/// # Safety
/// Must only be called from a single-threaded context.
pub fn get(&self) -> T {
unsafe {
if !*self.initialized.get() {
let value = (self.loader)();
(*self.value.get()).write(value);
*self.initialized.get() = true;
value
} else {
(*self.value.get()).assume_init()
}
}
}
/// # Safety
/// Must only be called from a single-threaded context.
pub fn set(&self, value: T) {
unsafe {
(*self.value.get()).write(value);
*self.initialized.get() = true;
}
}
}
// Required to use in statics even in single-threaded context.
unsafe impl<T: Copy> Sync for CachedOption<T> {}
================================================
File: stylus-sdk/src/lib.rs
================================================
// Copyright 2022-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/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)]
/// Use an efficient WASM allocator.
///
/// If a different custom allocator is desired, disable the `mini-alloc` feature.
#[cfg(all(target_arch = "wasm32", feature = "mini-alloc"))]
#[global_allocator]
static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT;
extern crate alloc;
pub use alloy_primitives;
pub use alloy_sol_types;
pub use hex;
pub use keccak_const;
pub use stylus_proc;
#[macro_use]
pub mod abi;
#[macro_use]
pub mod debug;
pub mod block;
pub mod call;
pub mod contract;
pub mod crypto;
pub mod deploy;
pub mod evm;
pub mod host;
pub mod methods;
pub mod msg;
pub mod prelude;
pub mod storage;
pub mod tx;
pub mod types;
mod util;
#[cfg(feature = "hostio")]
pub mod hostio;
#[cfg(not(feature = "hostio"))]
mod hostio;
use alloc::vec::Vec;
/// Represents a contract invocation outcome.
pub type ArbResult = Result<Vec<u8>, Vec<u8>>;
================================================
File: stylus-sdk/src/methods.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md
//! Types relating to method definitions.
/// State mutability of a contract fuction. This is currently used for checking whether contracts
/// are allowed to override a function from another contract they inherit from.
/// Users should not need this type outside of proc macros.
#[derive(Debug, Clone, Copy)]
pub enum Purity {
/// No state read/write.
Pure,
/// No state write.
View,
/// Cannot receive Ether.
Write,
/// Everything is allowed.
Payable,
}
impl Purity {
/// Returns whether a function defined with this purity may be overridden
/// by one with the given purity.
pub const fn allow_override(&self, other: Purity) -> bool {
use Purity::*;
matches!(
(*self, other),
(Payable, Payable)
| (Write, Write)
| (Write, View)
| (Write, Pure)
| (View, View)
| (View, Pure)
| (Pure, Pure)
)
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_allow_override() {
use super::Purity::*;
assert!(Payable.allow_override(Payable));
assert!(!Payable.allow_override(Write));
assert!(!Payable.allow_override(View));
assert!(!Payable.allow_override(Pure));
assert!(!Write.allow_override(Payable));
assert!(Write.allow_override(Write));
assert!(Write.allow_override(View));
assert!(Write.allow_override(Pure));
assert!(!View.allow_override(Payable));
assert!(!View.allow_override(Write));
assert!(View.allow_override(View));
assert!(View.allow_override(Pure));
assert!(!Pure.allow_override(Payable));
assert!(!Pure.allow_override(Write));
assert!(!Pure.allow_override(View));
assert!(Pure.allow_override(Pure));
}
}
================================================
File: stylus-sdk/src/msg.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! VM affordances for inspecting the current call.
//!
//! See also [`block`](crate::block), [`contract`](crate::contract), [`evm`](crate::evm),
//! [`msg`](crate::msg), and [`tx`](crate::tx).
//!
//! ```no_run
//! use stylus_sdk::msg;
//!
//! let call_value = msg::value();
//! ```
use crate::hostio::{self, wrap_hostio};
use alloy_primitives::{Address, B256, U256};
wrap_hostio!(
/// Whether the current call is reentrant.
reentrant REENTRANT msg_reentrant bool
);
wrap_hostio!(
/// Gets the address of the account that called the program. For normal L2-to-L2 transactions
/// the semantics are equivalent to that of the EVM's [`CALLER`] opcode, including in cases
/// arising from [`DELEGATE_CALL`].
///
/// For L1-to-L2 retryable ticket transactions, the top-level sender's address will be aliased.
/// See [`Retryable Ticket Address Aliasing`] for more information on how this works.
///
/// [`CALLER`]: https://www.evm.codes/#33
/// [`DELEGATE_CALL`]: https://www.evm.codes/#f4
/// [`Retryable Ticket Address Aliasing`]: https://developer.arbitrum.io/arbos/l1-to-l2-messaging#address-aliasing
sender SENDER msg_sender Address
);
wrap_hostio!(
/// Get the ETH value in wei sent to the program.
value VALUE msg_value U256
);
================================================
File: stylus-sdk/src/prelude.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Common imports for Stylus contracts.
//!
//! Included are all the proc macros and common traits.
//!
//! ```
//! use stylus_sdk::prelude::*;
//! ```
pub use crate::host::*;
pub use crate::storage::{Erase, SimpleStorageType, StorageType, TopLevelStorage};
pub use crate::stylus_proc::*;
pub use crate::types::AddressVM;
================================================
File: stylus-sdk/src/tx.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! VM affordances for inspecting the current tx.
//!
//! See also [`block`](crate::block), [`contract`](crate::contract), [`crypto`](crate::crypto),
//! [`evm`](crate::evm), and [`msg`](crate::msg).
//!
//! ```no_run
//! use stylus_sdk::tx;
//!
//! let gas_price = tx::gas_price();
//! ```
use crate::hostio::{self, wrap_hostio};
use alloy_primitives::{Address, B256, U256};
wrap_hostio! {
/// Gets the price of ink in evm gas basis points. See [`Ink and Gas`] for more information on
/// Stylus's compute-pricing model.
///
/// [`Ink and Gas`]: https://docs.arbitrum.io/stylus/concepts/gas-metering
ink_price INK_PRICE tx_ink_price u32
}
/// Converts evm gas to ink. See [`Ink and Gas`] for more information on
/// Stylus's compute-pricing model.
///
/// [`Ink and Gas`]: https://docs.arbitrum.io/stylus/concepts/gas-metering
pub fn gas_to_ink(gas: u64) -> u64 {
gas.saturating_mul(ink_price().into())
}
/// Converts ink to evm gas. See [`Ink and Gas`] for more information on
/// Stylus's compute-pricing model.
///
/// [`Ink and Gas`]: https://docs.arbitrum.io/stylus/concepts/gas-metering
pub fn ink_to_gas(ink: u64) -> u64 {
ink / ink_price() as u64
}
wrap_hostio!(
/// Gets the gas price in wei per gas, which on Arbitrum chains equals the basefee.
gas_price GAS_PRICE tx_gas_price U256
);
wrap_hostio!(
/// Gets the top-level sender of the transaction. The semantics are equivalent to that of the
/// EVM's [`ORIGIN`] opcode.
///
/// [`ORIGIN`]: https://www.evm.codes/#32
origin ORIGIN tx_origin Address
);
================================================
File: stylus-sdk/src/types.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Traits for common types.
//!
//! The contents of this module are typically imported via the [`prelude`](crate::prelude).
//!
//! ```no_run
//! use stylus_sdk::prelude::*;
//! use alloy_primitives::{address, Address};
//!
//! let account = address!("361594F5429D23ECE0A88E4fBE529E1c49D524d8");
//! let balance = account.balance();
//! ```
use crate::hostio;
use alloc::vec::Vec;
use alloy_primitives::{b256, Address, B256, U256};
/// Trait that allows the [`Address`] type to inspect the corresponding account's balance and codehash.
pub trait AddressVM {
/// The balance in wei of the account.
fn balance(&self) -> U256;
/// Gets the code at the given address. The semantics are equivalent to that of the EVM's [`EXT_CODESIZE`].
///
/// [`EXT_CODE_COPY`]: https://www.evm.codes/#3C
fn code(&self) -> Vec<u8>;
/// Gets the size of the code in bytes at the given address. The semantics are equivalent
/// to that of the EVM's [`EXT_CODESIZE`].
///
/// [`EXT_CODESIZE`]: https://www.evm.codes/#3B
fn code_size(&self) -> usize;
/// The codehash of the contract or [`EOA`] at the given address.
///
/// [`EOA`]: https://ethereum.org/en/developers/docs/accounts/#types-of-account
fn code_hash(&self) -> B256;
/// Determines if an account has code. Note that this is insufficient to determine if an address is an
/// [`EOA`]. During contract deployment, an account only gets its code at the very end, meaning that
/// this method will return `false` while the constructor is executing.
///
/// [`EOA`]: https://ethereum.org/en/developers/docs/accounts/#types-of-account
fn has_code(&self) -> bool;
}
impl AddressVM for Address {
fn balance(&self) -> U256 {
let mut data = [0; 32];
unsafe { hostio::account_balance(self.as_ptr(), data.as_mut_ptr()) };
U256::from_be_bytes(data)
}
fn code(&self) -> Vec<u8> {
let size = self.code_size();
let mut dest = Vec::with_capacity(size);
unsafe {
hostio::account_code(self.as_ptr(), 0, size, dest.as_mut_ptr());
dest.set_len(size);
dest
}
}
fn code_size(&self) -> usize {
unsafe { hostio::account_code_size(self.as_ptr()) }
}
fn code_hash(&self) -> B256 {
let mut data = [0; 32];
unsafe { hostio::account_codehash(self.as_ptr(), data.as_mut_ptr()) };
data.into()
}
fn has_code(&self) -> bool {
let hash = self.code_hash();
!hash.is_zero()
&& hash != b256!("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470")
}
}
================================================
File: stylus-sdk/src/util.rs
================================================
// Copyright 2022-2024, Offchain Labs, Inc.
// For licensing, seeghttps://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
/// Returns the minimum number of EVM words needed to store `bytes` bytes.
pub(crate) const fn evm_words(bytes: usize) -> usize {
(bytes + 31) / 32
}
/// Pads a length to the next multiple of 32 bytes
pub(crate) const fn evm_padded_length(bytes: usize) -> usize {
evm_words(bytes) * 32
}
================================================
File: stylus-sdk/src/abi/bytes.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use crate::{
abi::{AbiType, ConstString},
util::evm_padded_length,
};
use alloc::vec::Vec;
use alloy_sol_types::{abi::token::PackedSeqToken, private::SolTypeValue, SolType, SolValue};
use core::ops::{Deref, DerefMut};
/// Represents a [`bytes`] in Solidity.
///
/// [`bytes`]: https://docs.soliditylang.org/en/latest/types.html#bytes-and-string-as-arrays
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Bytes(pub Vec<u8>);
impl From<Bytes> for Vec<u8> {
fn from(value: Bytes) -> Self {
value.0
}
}
impl From<Vec<u8>> for Bytes {
fn from(b: Vec<u8>) -> Self {
Self(b)
}
}
impl From<alloy_primitives::Bytes> for Bytes {
fn from(value: alloy_primitives::Bytes) -> Self {
Self(value.to_vec())
}
}
impl Deref for Bytes {
type Target = Vec<u8>;
fn deref(&self) -> &Vec<u8> {
&self.0
}
}
impl DerefMut for Bytes {
fn deref_mut(&mut self) -> &mut Vec<u8> {
&mut self.0
}
}
impl AsRef<[u8]> for Bytes {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl AsMut<[u8]> for Bytes {
fn as_mut(&mut self) -> &mut [u8] {
&mut self.0
}
}
/// Provides a corresponding [`SolType`] for an [`abi`] [`Bytes`].
///
/// [`abi`]: crate::abi
pub struct BytesSolType;
impl SolTypeValue<Self> for Bytes {
#[inline]
fn stv_to_tokens(&self) -> <Self as alloy_sol_types::SolType>::Token<'_> {
self.0.tokenize()
}
#[inline]
fn stv_abi_encoded_size(&self) -> usize {
32 + evm_padded_length(self.len())
}
#[inline]
fn stv_abi_packed_encoded_size(&self) -> usize {
32 + evm_padded_length(self.len())
}
#[inline]
fn stv_eip712_data_word(&self) -> alloy_sol_types::Word {
self.0.eip712_data_word()
}
#[inline]
fn stv_abi_encode_packed_to(&self, out: &mut alloy_sol_types::private::Vec<u8>) {
self.0.abi_encode_packed_to(out)
}
}
impl SolType for Bytes {
type RustType = Bytes;
type Token<'a> = PackedSeqToken<'a>;
const ENCODED_SIZE: Option<usize> = None;
const PACKED_ENCODED_SIZE: Option<usize> = None;
const SOL_NAME: &'static str = "bytes";
#[inline]
fn valid_token(_: &Self::Token<'_>) -> bool {
true // Any PackedSeqToken is valid bytes
}
#[inline]
fn detokenize(token: Self::Token<'_>) -> Self::RustType {
Bytes(token.0.into())
}
}
impl SolValue for Bytes {
type SolType = Self;
}
impl AbiType for Bytes {
type SolType = Self;
const ABI: ConstString = ConstString::new("bytes");
const EXPORT_ABI_ARG: ConstString = Self::ABI.concat(ConstString::new(" calldata"));
const EXPORT_ABI_RET: ConstString = Self::ABI.concat(ConstString::new(" memory"));
}
#[cfg(test)]
mod tests {
use alloy_primitives::hex;
use super::*;
use crate::abi;
#[test]
fn bytes_sol_type() {
assert_eq!(Bytes::ABI.as_str(), "bytes");
assert_eq!(Bytes::EXPORT_ABI_ARG.as_str(), "bytes calldata");
assert_eq!(Bytes::EXPORT_ABI_RET.as_str(), "bytes memory");
}
#[test]
fn bytes_abi() {
assert_eq!(Bytes::SOL_NAME, "bytes");
assert_eq!(Bytes::ENCODED_SIZE, None);
assert!(Bytes::DYNAMIC);
assert_eq!(
<Bytes as SolType>::abi_encoded_size(&Bytes(vec![1, 2, 3, 4])),
64
);
}
#[test]
fn encode_decode_empty_bytes() {
abi::test_encode_decode_params(
(Bytes(vec![]),),
hex!(
"0000000000000000000000000000000000000000000000000000000000000020"
"0000000000000000000000000000000000000000000000000000000000000000"
),
);
}
#[test]
fn encode_decode_one_byte() {
abi::test_encode_decode_params(
(Bytes(vec![100]),),
hex!(
"0000000000000000000000000000000000000000000000000000000000000020"
"0000000000000000000000000000000000000000000000000000000000000001"
"6400000000000000000000000000000000000000000000000000000000000000"
),
);
}
#[test]
fn encode_decode_several_bytes() {
let mut input = Vec::with_capacity(40);
input.extend([1, 2, 3, 4]);
input.extend([0u8; 32]);
input.extend([5, 6, 7, 8]);
let value = (Bytes(input),);
let encoded = hex!(
"0000000000000000000000000000000000000000000000000000000000000020"
"0000000000000000000000000000000000000000000000000000000000000028"
"0102030400000000000000000000000000000000000000000000000000000000"
"0000000005060708000000000000000000000000000000000000000000000000"
);
abi::test_encode_decode_params(value, encoded);
}
#[test]
fn encode_decode_bytes_tuple() {
let mut input = Vec::with_capacity(40);
input.extend([1, 2, 3, 4]);
input.extend([0u8; 32]);
input.extend([5, 6, 7, 8]);
let value = (Bytes(input), Bytes(vec![]), Bytes(vec![1, 2, 3, 4]));
let encoded = hex!(
"0000000000000000000000000000000000000000000000000000000000000060"
"00000000000000000000000000000000000000000000000000000000000000C0"
"00000000000000000000000000000000000000000000000000000000000000E0"
"0000000000000000000000000000000000000000000000000000000000000028"
"0102030400000000000000000000000000000000000000000000000000000000"
"0000000005060708000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000004"
"0102030400000000000000000000000000000000000000000000000000000000"
);
abi::test_encode_decode_params(value, encoded)
}
}
================================================
File: stylus-sdk/src/abi/const_string.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Provides [`ConstString`], a mechanism for string operations in `const` contexts.
use core::{
fmt::{Debug, Display},
ops::Deref,
};
/// Maximum length of a [`ConstString`] in bytes.
pub const MAX_CONST_STRING_LENGTH: usize = 1024;
/// Represents a string with a bounded length at compile time.
/// This allows something approximating string operations in `const` contexts.
#[derive(Clone)]
pub struct ConstString {
/// The signature's text encoding. Must be valid UTF-8.
data: [u8; MAX_CONST_STRING_LENGTH],
/// The length of the string in bytes.
len: usize,
}
/// Copies data from `source` to `dest` in a `const` context.
/// This function is very inefficient for other purposes.
const fn memcpy<const N: usize>(
mut source: &[u8],
mut dest: [u8; N],
mut offset: usize,
) -> [u8; N] {
if offset > dest.len() {
panic!("out-of-bounds memcpy");
}
while !source.is_empty() {
dest[offset] = source[0];
offset += 1;
(_, source) = source.split_at(1);
}
dest
}
impl ConstString {
/// Creates a new [`ConstString`] equivalent to the empty string.
pub const fn new(s: &str) -> ConstString {
let mut data = [0u8; MAX_CONST_STRING_LENGTH];
data = memcpy(s.as_bytes(), data, 0);
ConstString { data, len: s.len() }
}
/// Creates a new [`ConstString`] from a decimal number.
/// For example, the number 42 maps to "42".
pub const fn from_decimal_number(mut number: usize) -> ConstString {
let mut data = [0u8; MAX_CONST_STRING_LENGTH];
let digits = number.checked_ilog10();
let digits = match digits {
// TODO: simplify when `const_precise_live_drops` is stabilized
// https://github.com/rust-lang/rust/issues/73255
Some(digits) => digits as usize + 1,
None => 1,
};
if digits > MAX_CONST_STRING_LENGTH {
panic!("from_decimal_number: too many digits");
}
let mut position = digits;
while position > 0 {
position -= 1;
data[position] = b'0' + (number % 10) as u8;
number /= 10;
}
Self { data, len: digits }
}
/// Selects a [`ConstString`] depending on the condition.
pub const fn select(cond: bool, true_value: &str, false_value: &str) -> Self {
match cond {
true => Self::new(true_value),
false => Self::new(false_value),
}
}
/// Clones a [`ConstString`] in a `const` context.
pub const fn const_clone(&self) -> Self {
Self {
data: self.data,
len: self.len,
}
}
/// Concatenates two [`ConstString`]'s.
pub const fn concat(&self, other: ConstString) -> ConstString {
let mut new = self.const_clone();
new.data = memcpy(other.as_bytes(), new.data, self.len);
new.len += other.len;
new
}
/// Converts a [`ConstString`] to a slice.
pub const fn as_bytes(&self) -> &[u8] {
self.data.split_at(self.len).0
}
/// Converts a [`ConstString`] to an equivalent [`str`].
pub const fn as_str(&self) -> &str {
// # Safety
// A `ConstString` represents a valid, utf8-encoded string
unsafe { core::str::from_utf8_unchecked(self.as_bytes()) }
}
}
impl Deref for ConstString {
type Target = [u8];
fn deref(&self) -> &Self::Target {
self.as_bytes()
}
}
impl Display for ConstString {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl Debug for ConstString {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:?}", self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_decimal() {
for i in (0..=100).chain(1000..=1001) {
assert_eq!(ConstString::from_decimal_number(i).as_str(), i.to_string());
}
}
#[test]
fn test_concat() {
assert_eq!(
ConstString::new("foo")
.concat(ConstString::new("bar"))
.as_str(),
"foobar"
);
}
}
================================================
File: stylus-sdk/src/abi/impls.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use super::{AbiType, ConstString};
use alloc::{string::String, vec::Vec};
use alloy_primitives::{Address, FixedBytes};
use alloy_sol_types::sol_data::{self, ByteCount, IntBitCount, SupportedFixedBytes, SupportedInt};
/// Generates a test to ensure the two-way relationship between Rust Types and Sol Types is bijective.
macro_rules! test_type {
($name:tt, $as_arg:expr, $($ty:tt)*) => {
#[cfg(test)]
paste::paste! {
#[allow(non_snake_case)]
#[test]
fn [<test_ $name>]() {
assert_eq!(
<$($ty)* as AbiType>::ABI.as_str(),
<<$($ty)* as AbiType>::SolType as alloy_sol_types::SolType>::SOL_NAME,
"{}'s ABI didn't match its SolType sol_type_name",
stringify!($($ty)*),
);
assert_eq!(
<$($ty)* as AbiType>::EXPORT_ABI_ARG.as_str(),
$as_arg,
);
}
}
};
}
macro_rules! append {
($stem:expr, $leaf:expr) => {
$stem.concat(ConstString::new($leaf))
};
}
macro_rules! append_dec {
($stem:expr, $num:expr) => {
ConstString::new($stem).concat(ConstString::from_decimal_number($num))
};
}
impl<const N: usize> AbiType for FixedBytes<N>
where
ByteCount<N>: SupportedFixedBytes,
{
type SolType = sol_data::FixedBytes<N>;
const ABI: ConstString = append_dec!("bytes", N);
}
test_type!(bytes, "bytes calldata", super::Bytes);
test_type!(uint160, "uint160", alloy_primitives::Uint<160, 3>);
test_type!(uint256, "uint256", alloy_primitives::Uint<256, 4>);
macro_rules! impl_int {
($bits:literal, $as_arg:expr, $unsigned:ty, $signed:ty) => {
impl AbiType for $unsigned
where
IntBitCount<$bits>: SupportedInt<Uint = Self>,
{
type SolType = sol_data::Uint<$bits>;
const ABI: ConstString = append_dec!("uint", $bits);
}
impl AbiType for $signed
where
IntBitCount<$bits>: SupportedInt<Int = Self>,
{
type SolType = sol_data::Int<$bits>;
const ABI: ConstString = append_dec!("int", $bits);
}
test_type!($unsigned, format!("u{}", $as_arg), $unsigned);
test_type!($signed, $as_arg, $signed);
};
}
impl_int!(8, "int8", u8, i8);
impl_int!(16, "int16", u16, i16);
impl_int!(32, "int32", u32, i32);
impl_int!(64, "int64", u64, i64);
impl_int!(128, "int128", u128, i128);
macro_rules! impl_alloy {
($rust_type:ty, $sol_type:ident $(<$generic:tt>)?, $signature:literal) => {
impl AbiType for $rust_type {
type SolType = sol_data::$sol_type $(<$generic>)*;
const ABI: ConstString = ConstString::new($signature);
}
test_type!($signature, $signature, $rust_type);
};
}
impl_alloy!(bool, Bool, "bool");
impl_alloy!(Address, Address, "address");
impl AbiType for String {
type SolType = sol_data::String;
const ABI: ConstString = ConstString::new("string");
const EXPORT_ABI_ARG: ConstString = append!(Self::ABI, " calldata");
const EXPORT_ABI_RET: ConstString = append!(Self::ABI, " memory");
}
impl<T: AbiType> AbiType for Vec<T> {
type SolType = sol_data::Array<T::SolType>;
const ABI: ConstString = append!(T::ABI, "[]");
const EXPORT_ABI_ARG: ConstString = Self::EXPORT_ABI_RET; // vectors are never calldata
const EXPORT_ABI_RET: ConstString = append!(T::ABI, "[] memory");
const CAN_BE_CALLDATA: bool = false;
}
test_type!(vec_of_u8s, "uint8[] memory", Vec<u8>);
test_type!(
vec_of_u256s,
"uint256[] memory",
Vec<alloy_primitives::U256>
);
test_type!(vec_of_bytes, "bytes[] memory", Vec<super::Bytes>);
test_type!(vec_of_fixed_bytes, "bytes18[] memory", Vec<FixedBytes<18>>);
impl<T: AbiType, const N: usize> AbiType for [T; N] {
type SolType = sol_data::FixedArray<T::SolType, N>;
const ABI: ConstString = T::ABI
.concat(ConstString::new("["))
.concat(ConstString::from_decimal_number(N))
.concat(ConstString::new("]"));
const EXPORT_ABI_ARG: ConstString = Self::ABI.concat(ConstString::select(
T::CAN_BE_CALLDATA,
" calldata",
" memory",
));
const EXPORT_ABI_RET: ConstString = append!(Self::ABI, " memory");
const CAN_BE_CALLDATA: bool = T::CAN_BE_CALLDATA;
}
test_type!(array_of_bools, "bool[5] calldata", [bool; 5]);
test_type!(array_of_nested_u32s, "uint32[2][4] calldata", [[u32; 2]; 4]);
test_type!(
array_of_fixed_bytes,
"bytes32[] memory",
Vec<FixedBytes<32>>
);
impl AbiType for () {
type SolType = ();
const ABI: ConstString = ConstString::new("()");
}
test_type!(empty_tuple, "()", ());
macro_rules! impl_tuple {
() => {};
($first:ident $(, $rest:ident)*) => {
impl<$first: AbiType $(, $rest: AbiType)*> AbiType for ( $first $(, $rest)* , ) {
type SolType = ( $first::SolType $(, $rest::SolType)* , );
const ABI: ConstString = ConstString::new("(")
.concat($first::ABI)
$(
.concat(ConstString::new(","))
.concat($rest::ABI)
)*
.concat(ConstString::new(")"));
const EXPORT_ABI_ARG: ConstString = ConstString::new("(")
.concat($first::EXPORT_ABI_ARG)
$(
.concat(ConstString::new(", "))
.concat($rest::EXPORT_ABI_ARG)
)*
.concat(ConstString::new(")"));
const EXPORT_ABI_RET: ConstString = ConstString::new("(")
.concat($first::EXPORT_ABI_RET)
$(
.concat(ConstString::new(", "))
.concat($rest::EXPORT_ABI_RET)
)*
.concat(ConstString::new(")"));
const CAN_BE_CALLDATA: bool = false;
}
impl_tuple! { $($rest),* }
};
}
impl_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X);
test_type!(tuple_of_single_u8, "(uint8)", (u8,));
test_type!(tuple_of_single_u256, "(uint256)", (alloy_primitives::U256,));
test_type!(tuple_of_two_u8s, "(uint8, uint8)", (u8, u8));
test_type!(
tuple_of_u8_and_u256,
"(uint8, uint256)",
(u8, alloy_primitives::U256)
);
test_type!(
tuple_of_five_types,
"(uint8, uint256[] memory, bytes calldata, bytes2, bool[][8] memory)",
(
u8,
Vec<alloy_primitives::U256>,
super::Bytes,
FixedBytes<2>,
[Vec<bool>; 8],
)
);
#[cfg(test)]
mod tests {
use alloy_primitives::{hex, FixedBytes, U256};
use crate::abi::{test_encode_decode_params, Bytes};
#[test]
fn encode_tuple_of_single_u8() {
let value = (100u8,);
let encoded = hex!("0000000000000000000000000000000000000000000000000000000000000064");
test_encode_decode_params(value, encoded)
}
#[test]
fn encode_tuple_of_single_u256() {
let value = (U256::from(100),);
let encoded = hex!("0000000000000000000000000000000000000000000000000000000000000064");
test_encode_decode_params(value, encoded)
}
#[test]
fn encode_tuple_of_two_u8s() {
let value = (100u8, 200u8);
let encoded = hex!(
"0000000000000000000000000000000000000000000000000000000000000064"
"00000000000000000000000000000000000000000000000000000000000000C8"
);
test_encode_decode_params(value, encoded)
}
#[test]
fn encode_tuple_of_u8_and_u256() {
let value = (100u8, U256::from(200));
let encoded = hex!(
"0000000000000000000000000000000000000000000000000000000000000064"
"00000000000000000000000000000000000000000000000000000000000000C8"
);
test_encode_decode_params(value, encoded)
}
#[test]
fn encode_tuple_of_five_types() {
let value = (
100u8,
vec![U256::from(1), U256::from(2)],
Bytes(vec![1, 2, 3, 4]),
FixedBytes::new([5, 6]),
[vec![true, false, true], vec![false, true, false]],
);
let encoded = hex!(
"0000000000000000000000000000000000000000000000000000000000000064"
"00000000000000000000000000000000000000000000000000000000000000A0"
"0000000000000000000000000000000000000000000000000000000000000100"
"0506000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000140"
"0000000000000000000000000000000000000000000000000000000000000002"
"0000000000000000000000000000000000000000000000000000000000000001"
"0000000000000000000000000000000000000000000000000000000000000002"
"0000000000000000000000000000000000000000000000000000000000000004"
"0102030400000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000040"
"00000000000000000000000000000000000000000000000000000000000000C0"
"0000000000000000000000000000000000000000000000000000000000000003"
"0000000000000000000000000000000000000000000000000000000000000001"
"0000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000001"
"0000000000000000000000000000000000000000000000000000000000000003"
"0000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000001"
"0000000000000000000000000000000000000000000000000000000000000000"
);
test_encode_decode_params(value, encoded)
}
}
================================================
File: stylus-sdk/src/abi/internal.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! This module provides functions for code generated by `stylus-sdk-proc`.
//! Most users shouldn't call these.
use crate::{abi::AbiType, console, msg, ArbResult};
use alloc::{vec, vec::Vec};
use alloy_primitives::U256;
use alloy_sol_types::SolType;
use core::fmt;
pub trait EncodableReturnType {
fn encode(self) -> ArbResult;
}
impl<T> EncodableReturnType for T
where
T: AbiType + alloy_sol_types::private::SolTypeValue<<T as AbiType>::SolType>,
{
#[inline(always)]
fn encode(self) -> ArbResult {
// coerce types into a tuple of at least 1 element
Ok(<<T as AbiType>::SolType>::abi_encode(&self))
}
}
impl<T, E: Into<Vec<u8>>> EncodableReturnType for Result<T, E>
where
T: AbiType + alloy_sol_types::private::SolTypeValue<<T as AbiType>::SolType>,
{
#[inline(always)]
fn encode(self) -> ArbResult {
match self {
Ok(result) => result.encode(),
Err(err) => Err(err.into()),
}
}
}
#[inline(always)]
pub const fn digest_to_selector(digest: [u8; 32]) -> [u8; 4] {
let mut selector = [0u8; 4];
selector[0] = digest[0];
selector[1] = digest[1];
selector[2] = digest[2];
selector[3] = digest[3];
selector
}
#[allow(unused)]
pub fn deny_value(method_name: &str) -> Result<(), Vec<u8>> {
if msg::value() == U256::ZERO {
return Ok(());
}
console!("method {method_name} not payable");
Err(vec![])
}
#[allow(unused)]
pub fn failed_to_decode_arguments(err: alloy_sol_types::Error) {
console!("failed to decode arguments: {err}");
}
pub trait AbiResult {
type OkType;
}
impl<O, E> AbiResult for Result<O, E> {
type OkType = O;
}
impl<T: AbiType> AbiResult for T {
type OkType = T;
}
pub fn write_solidity_returns<T: AbiResult>(f: &mut fmt::Formatter) -> fmt::Result
where
T::OkType: AbiType,
{
let abi = T::OkType::EXPORT_ABI_RET.as_str();
if abi == "()" {
Ok(())
} else if abi.starts_with('(') {
write!(f, " returns {abi}")
} else {
write!(f, " returns ({abi})")
}
}
================================================
File: stylus-sdk/src/abi/ints.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md
//! Support for generic integer types found in [alloy_primitives].
use alloy_primitives::{Signed, Uint};
use alloy_sol_types::{
private::SolTypeValue,
sol_data::{self, IntBitCount, SupportedInt},
SolType,
};
use super::{AbiType, ConstString};
macro_rules! impl_alloy_int {
($BITS:expr, $LIMBS:expr) => {
impl_alloy_int!($BITS, $LIMBS, sol_data::Uint<$BITS>, sol_data::Int<$BITS>);
};
(@$BITS:expr, $LIMBS:expr) => {
impl_alloy_int!($BITS, $LIMBS, OverloadUint<$BITS, $LIMBS>, OverloadInt<$BITS, $LIMBS>);
};
($BITS:expr, $LIMBS:expr, $uint_ty:ty, $int_ty:ty) => {
impl AbiType for Uint<$BITS, $LIMBS> {
type SolType = $uint_ty;
const ABI: ConstString = ConstString::new(Self::SolType::SOL_NAME);
}
impl AbiType for Signed<$BITS, $LIMBS> {
type SolType = $int_ty;
const ABI: ConstString = ConstString::new(Self::SolType::SOL_NAME);
}
};
}
impl_alloy_int!(@8, 1);
impl_alloy_int!(@16, 1);
impl_alloy_int!(24, 1);
impl_alloy_int!(@32, 1);
impl_alloy_int!(40, 1);
impl_alloy_int!(48, 1);
impl_alloy_int!(56, 1);
impl_alloy_int!(@64, 1);
impl_alloy_int!(72, 2);
impl_alloy_int!(80, 2);
impl_alloy_int!(88, 2);
impl_alloy_int!(96, 2);
impl_alloy_int!(104, 2);
impl_alloy_int!(112, 2);
impl_alloy_int!(120, 2);
impl_alloy_int!(@128, 2);
impl_alloy_int!(136, 3);
impl_alloy_int!(144, 3);
impl_alloy_int!(152, 3);
impl_alloy_int!(160, 3);
impl_alloy_int!(168, 3);
impl_alloy_int!(176, 3);
impl_alloy_int!(184, 3);
impl_alloy_int!(192, 3);
impl_alloy_int!(200, 4);
impl_alloy_int!(208, 4);
impl_alloy_int!(216, 4);
impl_alloy_int!(224, 4);
impl_alloy_int!(232, 4);
impl_alloy_int!(240, 4);
impl_alloy_int!(248, 4);
impl_alloy_int!(256, 4);
pub struct OverloadInt<const BITS: usize, const LIMBS: usize>;
impl<const BITS: usize, const LIMBS: usize> SolType for OverloadInt<BITS, LIMBS>
where
IntBitCount<BITS>: SupportedInt,
Self: ConvertInt<
Int = <sol_data::Int<BITS> as SolType>::RustType,
AlloyInt = Signed<BITS, LIMBS>,
>,
{
type RustType = Signed<BITS, LIMBS>;
type Token<'a> = <sol_data::Int<BITS> as SolType>::Token<'a>;
const SOL_NAME: &'static str = <sol_data::Int<BITS> as SolType>::SOL_NAME;
const ENCODED_SIZE: Option<usize> = <sol_data::Int<BITS> as SolType>::ENCODED_SIZE;
const PACKED_ENCODED_SIZE: Option<usize> =
<sol_data::Int<BITS> as SolType>::PACKED_ENCODED_SIZE;
const DYNAMIC: bool = <sol_data::Int<BITS> as SolType>::DYNAMIC;
#[inline]
fn valid_token(token: &Self::Token<'_>) -> bool {
<sol_data::Int<BITS> as SolType>::valid_token(token)
}
#[inline]
fn detokenize(token: Self::Token<'_>) -> Self::RustType {
<Self as ConvertInt>::to_alloy(<sol_data::Int<BITS> as SolType>::detokenize(token))
}
}
impl<const BITS: usize, const LIMBS: usize> SolTypeValue<OverloadInt<BITS, LIMBS>>
for Signed<BITS, LIMBS>
where
IntBitCount<BITS>: SupportedInt,
OverloadInt<BITS, LIMBS>: ConvertInt<
Int = <sol_data::Int<BITS> as SolType>::RustType,
AlloyInt = Signed<BITS, LIMBS>,
>,
{
#[inline]
fn stv_abi_encoded_size(&self) -> usize {
<OverloadInt<BITS, LIMBS> as ConvertInt>::to_int(*self).stv_abi_encoded_size()
}
#[inline]
fn stv_to_tokens(&self) -> <OverloadInt<BITS, LIMBS> as SolType>::Token<'_> {
<OverloadInt<BITS, LIMBS> as ConvertInt>::to_int(*self).stv_to_tokens()
}
#[inline]
fn stv_abi_packed_encoded_size(&self) -> usize {
<OverloadInt<BITS, LIMBS> as ConvertInt>::to_int(*self).stv_abi_packed_encoded_size()
}
#[inline]
fn stv_abi_encode_packed_to(&self, out: &mut alloc::vec::Vec<u8>) {
<OverloadInt<BITS, LIMBS> as ConvertInt>::to_int(*self).stv_abi_encode_packed_to(out)
}
#[inline]
fn stv_eip712_data_word(&self) -> alloy_sol_types::Word {
<OverloadInt<BITS, LIMBS> as ConvertInt>::to_int(*self).stv_eip712_data_word()
}
}
pub struct OverloadUint<const BITS: usize, const LIMBS: usize>;
impl<const BITS: usize, const LIMBS: usize> SolType for OverloadUint<BITS, LIMBS>
where
IntBitCount<BITS>: SupportedInt,
Self:
ConvertInt<Int = <sol_data::Uint<BITS> as SolType>::RustType, AlloyInt = Uint<BITS, LIMBS>>,
{
type RustType = Uint<BITS, LIMBS>;
type Token<'a> = <sol_data::Uint<BITS> as SolType>::Token<'a>;
const SOL_NAME: &'static str = <sol_data::Uint<BITS> as SolType>::SOL_NAME;
const ENCODED_SIZE: Option<usize> = <sol_data::Uint<BITS> as SolType>::ENCODED_SIZE;
const PACKED_ENCODED_SIZE: Option<usize> =
<sol_data::Uint<BITS> as SolType>::PACKED_ENCODED_SIZE;
const DYNAMIC: bool = <sol_data::Uint<BITS> as SolType>::DYNAMIC;
#[inline]
fn valid_token(token: &Self::Token<'_>) -> bool {
<sol_data::Int<BITS> as SolType>::valid_token(token)
}
#[inline]
fn detokenize(token: Self::Token<'_>) -> Self::RustType {
<Self as ConvertInt>::to_alloy(<sol_data::Uint<BITS> as SolType>::detokenize(token))
}
}
impl<const BITS: usize, const LIMBS: usize> SolTypeValue<OverloadUint<BITS, LIMBS>>
for Uint<BITS, LIMBS>
where
IntBitCount<BITS>: SupportedInt,
OverloadUint<BITS, LIMBS>:
ConvertInt<Int = <sol_data::Uint<BITS> as SolType>::RustType, AlloyInt = Uint<BITS, LIMBS>>,
{
#[inline]
fn stv_abi_encoded_size(&self) -> usize {
<OverloadUint<BITS, LIMBS> as ConvertInt>::to_int(*self).stv_abi_encoded_size()
}
#[inline]
fn stv_to_tokens(&self) -> <OverloadUint<BITS, LIMBS> as SolType>::Token<'_> {
<OverloadUint<BITS, LIMBS> as ConvertInt>::to_int(*self).stv_to_tokens()
}
#[inline]
fn stv_abi_packed_encoded_size(&self) -> usize {
<OverloadUint<BITS, LIMBS> as ConvertInt>::to_int(*self).stv_abi_packed_encoded_size()
}
#[inline]
fn stv_abi_encode_packed_to(&self, out: &mut alloc::vec::Vec<u8>) {
<OverloadUint<BITS, LIMBS> as ConvertInt>::to_int(*self).stv_abi_encode_packed_to(out)
}
#[inline]
fn stv_eip712_data_word(&self) -> alloy_sol_types::Word {
<OverloadUint<BITS, LIMBS> as ConvertInt>::to_int(*self).stv_eip712_data_word()
}
}
trait ConvertInt {
type Int: TryInto<Self::AlloyInt>;
type AlloyInt: TryInto<Self::Int>;
fn to_int(value: Self::AlloyInt) -> Self::Int {
value
.try_into()
.map_err(|_| "int conversion error")
.unwrap()
}
fn to_alloy(value: Self::Int) -> Self::AlloyInt {
value
.try_into()
.map_err(|_| "int conversion error")
.unwrap()
}
}
impl ConvertInt for OverloadUint<8, 1> {
type Int = u8;
type AlloyInt = Uint<8, 1>;
}
impl ConvertInt for OverloadUint<16, 1> {
type Int = u16;
type AlloyInt = Uint<16, 1>;
}
impl ConvertInt for OverloadUint<32, 1> {
type Int = u32;
type AlloyInt = Uint<32, 1>;
}
impl ConvertInt for OverloadUint<64, 1> {
type Int = u64;
type AlloyInt = Uint<64, 1>;
}
impl ConvertInt for OverloadUint<128, 2> {
type Int = u128;
type AlloyInt = Uint<128, 2>;
}
impl ConvertInt for OverloadInt<8, 1> {
type Int = i8;
type AlloyInt = Signed<8, 1>;
}
impl ConvertInt for OverloadInt<16, 1> {
type Int = i16;
type AlloyInt = Signed<16, 1>;
}
impl ConvertInt for OverloadInt<32, 1> {
type Int = i32;
type AlloyInt = Signed<32, 1>;
}
impl ConvertInt for OverloadInt<64, 1> {
type Int = i64;
type AlloyInt = Signed<64, 1>;
}
impl ConvertInt for OverloadInt<128, 2> {
type Int = i128;
type AlloyInt = Signed<128, 2>;
}
#[cfg(test)]
mod tests {
use alloy_primitives::{hex, Uint};
use crate::abi::test_encode_decode_params;
#[test]
fn encode_decode_u24() {
let value = (Uint::<24, 1>::from(10),);
let encoded = hex!("000000000000000000000000000000000000000000000000000000000000000A");
test_encode_decode_params(value, encoded);
}
#[test]
fn encode_decode_u160() {
let value = (Uint::<160, 3>::from(999),);
let encoded = hex!("00000000000000000000000000000000000000000000000000000000000003E7");
test_encode_decode_params(value, encoded);
}
}
================================================
File: stylus-sdk/src/abi/mod.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Solidity ABIs for Rust types.
//!
//! Alloy provides a 1-way mapping of Solidity types to Rust ones via [`SolType`].
//! 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 inter-op between Rust and Solidity contracts.
//!
//! Notably, the SDK treats `Vec<u8>` as a Solidity `uint8[]`.
//! For a Solidity `bytes`, see [`Bytes`].
//!
//! [prelude]: crate::prelude
//!
use alloc::boxed::Box;
use alloc::vec::Vec;
use alloy_primitives::U256;
use core::borrow::BorrowMut;
use alloy_sol_types::{abi::TokenSeq, private::SolTypeValue, SolType};
use crate::{
console,
storage::{StorageType, TopLevelStorage},
ArbResult,
};
pub use bytes::{Bytes, BytesSolType};
pub use const_string::ConstString;
#[cfg(feature = "export-abi")]
pub use export::GenerateAbi;
#[cfg(feature = "export-abi")]
pub mod export;
mod bytes;
mod const_string;
mod impls;
mod ints;
#[doc(hidden)]
pub mod internal;
/// Executes a method given a selector and calldata.
/// This trait can be automatically implemented via `#[public]`.
/// Composition with other routers is possible via `#[inherit]`.
pub trait Router<S>
where
S: TopLevelStorage + BorrowMut<Self::Storage>,
{
/// 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.
/// Routes add via `#[inherit]` will only execute if no match is found among `Self`.
/// This means that it is possible to override a method by redefining it in `Self`.
fn route(storage: &mut S, selector: u32, input: &[u8]) -> Option<ArbResult>;
/// Receive function for this contract. Called when no calldata is provided.
/// A receive function may not be defined, in which case this method will return None.
/// Receive functions are always payable, take in no inputs, and return no outputs.
/// If defined, they will always be called when a transaction does not send any
/// calldata, regardless of the transaction having a value attached.
fn receive(storage: &mut S) -> Option<Result<(), Vec<u8>>>;
/// Called when no receive function is defined.
/// If no #[fallback] function is defined in the contract, then any transactions that do not
/// match a selector will revert.
/// A fallback function may have two different implementations. It can be either declared
/// without any input or output, or with bytes input calldata and bytes output. If a user
/// defines a fallback function with no input or output, then this method will be called
/// and the underlying user-defined function will simply be invoked with no input.
/// A fallback function can be declared as payable. If not payable, then any transactions
/// that trigger a fallback with value attached will revert.
fn fallback(storage: &mut S, calldata: &[u8]) -> Option<ArbResult>;
}
/// Entrypoint used when `#[entrypoint]` is used on a contract struct.
/// Solidity requires specific routing logic for situations in which no function selector
/// matches the input calldata in the form of two different functions named "receive" and "fallback".
/// The purity and type definitions are as follows:
///
/// - receive takes no input data, returns no data, and is always payable.
/// - fallback offers two possible implementations. It can be either declared without input or return
// parameters, or with input bytes calldata and return bytes memory.
//
// The fallback function MAY be payable. If not payable, then any transactions not matching any
// other function which send value will revert.
//
// The order of routing semantics for receive and fallback work as follows:
//
// - If a receive function exists, it is always called whenever calldata is empty, even
// if no value is received in the transaction. It is implicitly payable.
// - Fallback is called when no other function matches a selector. If a receive function is not
// defined, then calls with no input calldata will be routed to the fallback function.
pub fn router_entrypoint<R, S>(
input: alloc::vec::Vec<u8>,
host: alloc::boxed::Box<dyn crate::host::Host>,
) -> ArbResult
where
R: Router<S>,
S: StorageType + TopLevelStorage + BorrowMut<R::Storage>,
{
let mut storage = unsafe { S::new(U256::ZERO, 0, Box::into_raw(host)) };
if input.is_empty() {
console!("no calldata provided");
if let Some(res) = R::receive(&mut storage) {
return res.map(|_| Vec::new());
}
// Try fallback function with no inputs if defined.
if let Some(res) = R::fallback(&mut storage, &[]) {
return res;
}
// Revert as no receive or fallback were defined.
return Err(Vec::new());
}
if input.len() >= 4 {
let selector = u32::from_be_bytes(TryInto::try_into(&input[..4]).unwrap());
if let Some(res) = R::route(&mut storage, selector, &input[4..]) {
return res;
} else {
console!("unknown method selector: {selector:08x}");
}
}
// Try fallback function.
if let Some(res) = R::fallback(&mut storage, &input) {
return res;
}
Err(Vec::new())
}
/// 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`].
pub trait AbiType {
/// The associated Solidity type.
type SolType: SolType<RustType = Self>;
/// Equivalent to [`SolType::sol_type_name`], but `const`.
const ABI: ConstString;
/// String to use when the type is an interface method argument.
const EXPORT_ABI_ARG: ConstString = Self::ABI;
/// String to use when the type is an interface method return value.
const EXPORT_ABI_RET: ConstString = Self::ABI;
/// Whether the type is allowed in calldata
const CAN_BE_CALLDATA: bool = true;
}
/// Generates a function selector for the given method and its args.
#[macro_export]
macro_rules! function_selector {
($name:literal $(,)?) => {{
const DIGEST: [u8; 32] = $crate::keccak_const::Keccak256::new()
.update($name.as_bytes())
.update(b"()")
.finalize();
$crate::abi::internal::digest_to_selector(DIGEST)
}};
($name:literal, $first:ty $(, $ty:ty)* $(,)?) => {{
const DIGEST: [u8; 32] = $crate::keccak_const::Keccak256::new()
.update($name.as_bytes())
.update(b"(")
.update(<$first as $crate::abi::AbiType>::ABI.as_bytes())
$(
.update(b",")
.update(<$ty as $crate::abi::AbiType>::ABI.as_bytes())
)*
.update(b")")
.finalize();
$crate::abi::internal::digest_to_selector(DIGEST)
}};
}
/// ABI decode a tuple of parameters
pub fn decode_params<T>(data: &[u8]) -> alloy_sol_types::Result<T>
where
T: AbiType + SolTypeValue<<T as AbiType>::SolType>,
for<'a> <<T as AbiType>::SolType as SolType>::Token<'a>: TokenSeq<'a>,
{
T::SolType::abi_decode_params(data, true)
}
/// ABI encode a value
pub fn encode<T>(value: &T) -> Vec<u8>
where
T: AbiType + SolTypeValue<<T as AbiType>::SolType>,
{
T::SolType::abi_encode(value)
}
/// ABI encode a tuple of parameters
pub fn encode_params<T>(value: &T) -> Vec<u8>
where
T: AbiType + SolTypeValue<<T as AbiType>::SolType>,
for<'a> <<T as AbiType>::SolType as SolType>::Token<'a>: TokenSeq<'a>,
{
T::SolType::abi_encode_params(value)
}
/// Encoded size of some sol type
pub fn encoded_size<T>(value: &T) -> usize
where
T: AbiType + SolTypeValue<<T as AbiType>::SolType>,
{
T::SolType::abi_encoded_size(value)
}
/// Parform a test of both the encode and decode functions for a given type
///
/// This is intended for use within unit tests.
#[cfg(test)]
fn test_encode_decode_params<T, B>(value: T, buffer: B)
where
T: core::fmt::Debug + PartialEq + AbiType + SolTypeValue<<T as AbiType>::SolType>,
for<'a> <<T as AbiType>::SolType as SolType>::Token<'a>: TokenSeq<'a>,
B: core::fmt::Debug + AsRef<[u8]>,
{
let encoded = encode_params(&value);
assert_eq!(encoded, buffer.as_ref());
let decoded = decode_params::<T>(buffer.as_ref()).unwrap();
assert_eq!(decoded, value);
}
#[cfg(test)]
mod tests {
#[test]
fn test_function_selector() {
use alloy_primitives::{Address, U256};
assert_eq!(u32::from_be_bytes(function_selector!("foo")), 0xc2985578);
assert_eq!(function_selector!("foo", Address), [0xfd, 0xf8, 0x0b, 0xda]);
const TEST_SELECTOR: [u8; 4] = function_selector!("foo", Address, U256);
assert_eq!(TEST_SELECTOR, 0xbd0d639f_u32.to_be_bytes());
}
}
================================================
File: stylus-sdk/src/abi/export/internal.rs
================================================
// Copyright 2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! This module provides functions for code generated by `stylus-sdk-proc` for the `export-abi` command.
//! Most users shouldn't call these.
use core::any::TypeId;
use alloy_primitives::{Address, FixedBytes, Signed, Uint};
use crate::abi::Bytes;
/// Represents a unique Solidity Type.
pub struct InnerType {
/// Full interface string.
pub name: String,
/// Unique identifier for de-duplication when printing interfaces.
pub id: TypeId,
}
/// Trait for collecting structs and error types.
pub trait InnerTypes {
/// Collect any structs and errors under the type.
/// Empty for primitives.
fn inner_types() -> Vec<InnerType> {
vec![]
}
}
impl<O, E> InnerTypes for Result<O, E>
where
O: InnerTypes,
E: InnerTypes,
{
fn inner_types() -> Vec<InnerType> {
let mut out = O::inner_types();
out.extend(E::inner_types());
out
}
}
impl<T: InnerTypes> InnerTypes for Vec<T> {
fn inner_types() -> Vec<InnerType> {
T::inner_types()
}
}
impl<const N: usize, T: InnerTypes> InnerTypes for [T; N] {
fn inner_types() -> Vec<InnerType> {
T::inner_types()
}
}
macro_rules! impl_inner {
($ty:ident $($rest:ident)+) => {
impl_inner!($ty);
impl_inner!($($rest)+);
};
($ty:ident) => {
impl InnerTypes for $ty {}
};
}
impl_inner!(bool u8 u16 u32 u64 u128 i8 i16 i32 i64 i128 String Address Bytes);
impl<const B: usize, const L: usize> InnerTypes for Uint<B, L> {}
impl<const B: usize, const L: usize> InnerTypes for Signed<B, L> {}
impl<const N: usize> InnerTypes for FixedBytes<N> {}
macro_rules! impl_tuple {
() => {
impl InnerTypes for () {}
};
($first:ident $(, $rest:ident)*) => {
impl<$first: InnerTypes $(, $rest: InnerTypes)*> InnerTypes for ( $first $(, $rest)* , ) {
fn inner_types() -> Vec<InnerType> {
vec![]
}
}
impl_tuple! { $($rest),* }
};
}
impl_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X);
================================================
File: stylus-sdk/src/abi/export/mod.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Traits for exporting Solidity interfaces.
//!
//! The contents of this module are imported when the `export-abi` feature flag is enabled,
//! which happens automatically during [`cargo stylus export-abi`][cargo].
//!
//! [cargo]: https://github.com/OffchainLabs/cargo-stylus#exporting-solidity-abis
use core::{fmt, marker::PhantomData};
use lazy_static::lazy_static;
use regex::Regex;
#[doc(hidden)]
pub mod internal;
/// Trait for storage types so that users can print a Solidity interface to the console.
/// This is auto-derived via the [`external`] macro when the `export-abi` feature is enabled.
///
/// [`external`]: stylus-proc::external
pub trait GenerateAbi {
/// The interface's name.
const NAME: &'static str;
/// How to format the ABI. Analogous to [`Display`](std::fmt::Display).
fn fmt_abi(f: &mut fmt::Formatter<'_>) -> fmt::Result;
}
/// Type that makes an ABI printable.
struct AbiPrinter<T: GenerateAbi>(PhantomData<T>);
impl<T: GenerateAbi> fmt::Display for AbiPrinter<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
T::fmt_abi(f)
}
}
/// Prints the full contract ABI to standard out
pub fn print_abi<T: GenerateAbi>(license: &str, pragma: &str) {
println!("/**");
println!(" * This file was automatically generated by Stylus and represents a Rust program.");
println!(" * For more information, please see [The Stylus SDK](https://github.com/OffchainLabs/stylus-sdk-rs).");
println!(" */");
println!();
println!("// SPDX-License-Identifier: {license}");
println!("{pragma}");
println!();
print!("{}", AbiPrinter::<T>(PhantomData));
}
lazy_static! {
static ref UINT_REGEX: Regex = Regex::new(r"^uint(\d+)$").unwrap();
static ref INT_REGEX: Regex = Regex::new(r"^int(\d+)$").unwrap();
static ref BYTES_REGEX: Regex = Regex::new(r"^bytes(\d+)$").unwrap();
}
/// Prepends the string with an underscore if it is a Solidity keyword.
/// Otherwise, the string is unchanged.
/// Note: also prepends a space when the input is nonempty.
pub fn underscore_if_sol(name: &str) -> String {
let underscore = || format!(" _{name}");
if let Some(caps) = UINT_REGEX.captures(name) {
let bits: usize = caps[1].parse().unwrap();
if bits % 8 == 0 {
return underscore();
}
}
if let Some(caps) = INT_REGEX.captures(name) {
let bits: usize = caps[1].parse().unwrap();
if bits % 8 == 0 {
return underscore();
}
}
if let Some(caps) = BYTES_REGEX.captures(name) {
let bits: usize = caps[1].parse().unwrap();
if bits <= 32 {
return underscore();
}
}
match name {
"" => "".to_string(),
// other types
"address" | "bool" | "int" | "uint" => underscore(),
// other words
"is" | "contract" | "interface" => underscore(),
// reserved keywords
"after" | "alias" | "apply" | "auto" | "byte" | "case" | "copyof" | "default"
| "define" | "final" | "implements" | "in" | "inline" | "let" | "macro" | "match"
| "mutable" | "null" | "of" | "partial" | "promise" | "reference" | "relocatable"
| "sealed" | "sizeof" | "static" | "supports" | "switch" | "typedef" | "typeof" | "var" => {
underscore()
}
_ => format!(" {name}"),
}
}
================================================
File: stylus-sdk/src/call/context.rs
================================================
// Copyright 2022-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
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 Call<S, const HAS_VALUE: bool = false> {
gas: u64,
value: Option<U256>,
storage: S,
}
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.
/// Although this reference to [`TopLevelStorage`] is not used, the lifetime is still required
/// to ensure safety of the storage cache.
///
/// ```
/// 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) external 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<String, Error> {
///
/// 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
/// }
/// ```
///
/// [`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: 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 {
Self { gas, ..self }
}
/// 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) -> Call<S, true> {
Call {
value: Some(value),
gas: self.gas,
storage: self.storage,
}
}
}
impl<S, const HAS_VALUE: bool> CallContext for Call<S, HAS_VALUE> {
fn gas(&self) -> u64 {
self.gas
}
}
// allow &self as a context
impl<T> CallContext for &T
where
T: TopLevelStorage,
{
fn gas(&self) -> u64 {
u64::MAX
}
}
// allow &mut self as a context
impl<T> CallContext for &mut T
where
T: TopLevelStorage,
{
fn gas(&self) -> u64 {
u64::MAX
}
}
// allow &self to be a `pure` and `static` call context
impl<T> StaticCallContext for &T where T: TopLevelStorage {}
// allow &mut self to be a `pure` and `static` call context
impl<T> StaticCallContext for &mut T where T: TopLevelStorage {}
// allow &mut self to be a `write` and `payable` call context
unsafe impl<T> MutatingCallContext for &mut T
where
T: TopLevelStorage,
{
fn value(&self) -> U256 {
U256::ZERO
}
}
// allow &mut self to be a `write`-only call context
impl<T> NonPayableCallContext for &mut T where T: TopLevelStorage {}
cfg_if! {
if #[cfg(feature = "reentrant")] {
// The following impls safeguard state during reentrancy scenarios
impl<S: TopLevelStorage> StaticCallContext for Call<&S, false> {}
impl<S: TopLevelStorage> StaticCallContext for Call<&mut 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()
}
}
}
}
cfg_if! {
if #[cfg(any(not(feature = "reentrant"), feature = "docs"))] {
impl Default for Call<(), false> {
fn default() -> Self {
Self::new()
}
}
impl Call<(), false> {
/// Begin configuring a call, similar to how [`RawCall`](super::RawCall) and
/// [`std::fs::OpenOptions`][OpenOptions] work.
///
/// This is not available if `reentrant` feature is enabled, as it may lead to
/// vulnerability to reentrancy attacks. See [`Call::new_in`].
///
/// ```no_compile
/// 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) external payable returns (string);
/// }
/// }
///
/// pub fn do_call(account: IService, user: Address) -> Result<String, Error> {
/// 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
/// }
/// ```
///
/// [OpenOptions]: https://doc.rust-lang.org/stable/std/fs/struct.OpenOptions.html
pub fn new() -> Self {
Self {
gas: u64::MAX,
value: None,
storage: (),
}
}
}
}
}
================================================
File: stylus-sdk/src/call/error.rs
================================================
// Copyright 2022-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/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<u8>),
/// Failure to decode the other contract's return data.
AbiDecodingFailed(alloy_sol_types::Error),
}
impl From<alloy_sol_types::Error> for Error {
fn from(err: alloy_sol_types::Error) -> Self {
Error::AbiDecodingFailed(err)
}
}
/// Encode an error.
///
/// This is useful so that users can use `Error` as a variant in their error
/// types. It should not be necessary to implement this.
pub trait MethodError {
/// Users should not have to call this.
fn encode(self) -> Vec<u8>;
}
impl MethodError for Error {
#[inline]
fn encode(self) -> Vec<u8> {
From::from(self)
}
}
impl<T: SolError> MethodError for T {
#[inline]
fn encode(self) -> Vec<u8> {
SolError::abi_encode(&self)
}
}
impl From<Error> for Vec<u8> {
#[allow(unused)]
fn from(err: Error) -> Vec<u8> {
match err {
Error::Revert(data) => data,
Error::AbiDecodingFailed(err) => {
console!("failed to decode return data from external call: {err}");
Panic::from(PanicKind::Generic).abi_encode()
}
}
}
}
================================================
File: stylus-sdk/src/call/mod.rs
================================================
// Copyright 2022-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Call other contracts.
//!
//! There are two primary ways to make calls to other contracts via the Stylus SDK.
//! - [`Call`] with [`sol_interface!`][sol_interface] for richly-typed calls.
//! - [`RawCall`] for `unsafe`, bytes-in bytes-out calls.
//!
//! Additional helpers exist for specific use-cases like [`transfer_eth`].
//!
//! [sol_interface]: crate::prelude::sol_interface
use alloc::vec::Vec;
use alloy_primitives::Address;
pub use self::{
context::Call, error::Error, error::MethodError, raw::RawCall, traits::*,
transfer::transfer_eth,
};
pub(crate) use raw::CachePolicy;
#[cfg(feature = "reentrant")]
use crate::storage::Storage;
mod context;
mod error;
mod raw;
mod traits;
mod transfer;
macro_rules! unsafe_reentrant {
($block:block) => {
#[cfg(feature = "reentrant")]
unsafe {
$block
}
#[cfg(not(feature = "reentrant"))]
$block
};
}
/// Static calls the contract at the given address.
pub fn static_call(
context: impl StaticCallContext,
to: Address,
data: &[u8],
) -> Result<Vec<u8>, Error> {
#[cfg(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<Vec<u8>, Error> {
#[cfg(feature = "reentrant")]
Storage::clear(); // clear the storage to persist changes, invalidating the cache
RawCall::new_delegate()
.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<Vec<u8>, Error> {
#[cfg(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)
}}
}
================================================
File: stylus-sdk/src/call/raw.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use crate::{contract::read_return_data, hostio, tx, ArbResult};
use alloy_primitives::{Address, B256, U256};
use cfg_if::cfg_if;
#[cfg(feature = "reentrant")]
use crate::storage::StorageCache;
macro_rules! unsafe_reentrant {
($(#[$meta:meta])* pub fn $name:ident $($rest:tt)*) => {
cfg_if! {
if #[cfg(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 {
kind: CallKind,
callvalue: U256,
gas: Option<u64>,
offset: usize,
size: Option<usize>,
#[allow(unused)]
cache_policy: CachePolicy,
}
/// What kind of call to perform.
#[derive(Clone, Default, PartialEq)]
enum CallKind {
#[default]
Basic,
Delegate,
Static,
}
/// How to manage the storage cache, if enabled.
#[allow(unused)]
#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum CachePolicy {
#[default]
DoNothing,
Flush,
Clear,
}
#[derive(Copy, Clone)]
#[repr(C)]
struct RustVec {
ptr: *mut u8,
len: usize,
cap: usize,
}
impl Default for RustVec {
fn default() -> Self {
Self {
ptr: core::ptr::null_mut(),
len: 0,
cap: 0,
}
}
}
impl RawCall {
/// Begin configuring the raw call, similar to how [`std::fs::OpenOptions`][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
/// }
/// ```
///
/// [OpenOptions]: https://doc.rust-lang.org/stable/std/fs/struct.OpenOptions.html
pub fn new() -> Self {
Default::default()
}
/// Configures a call that supplies callvalue, denominated in wei.
pub fn new_with_value(callvalue: U256) -> Self {
Self {
callvalue,
..Default::default()
}
}
/// Begin configuring a [`delegate call`].
///
/// [`DELEGATE_CALL`]: https://www.evm.codes/#F4
pub fn new_delegate() -> Self {
Self {
kind: CallKind::Delegate,
..Default::default()
}
}
/// Begin configuring a [`static call`].
///
/// [`STATIC_CALL`]: https://www.evm.codes/#FA
pub fn new_static() -> Self {
Self {
kind: CallKind::Static,
..Default::default()
}
}
/// Configures the amount of gas to supply.
/// Note: large values are clipped to the amount of gas remaining.
pub fn gas(mut self, gas: u64) -> Self {
self.gas = Some(gas);
self
}
/// Configures the amount of ink to supply.
/// Note: values are clipped to the amount of ink remaining.
/// See [`Ink and Gas`] for more information on Stylus's compute-pricing model.
///
/// [`Ink and Gas`]: https://docs.arbitrum.io/stylus/concepts/gas-metering
pub fn ink(mut self, ink: u64) -> Self {
self.gas = Some(tx::ink_to_gas(ink));
self
}
/// Configures what portion of the return data to copy.
/// Does not revert if out of bounds, but rather copies the overlapping portion.
pub fn limit_return_data(mut self, offset: usize, size: usize) -> Self {
self.offset = offset;
self.size = Some(size);
self
}
/// Configures the call to avoid copying any return data.
/// Equivalent to `limit_return_data(0, 0)`.
pub fn skip_return_data(self) -> Self {
self.limit_return_data(0, 0)
}
/// Write all cached values to persistent storage before the call.
#[cfg(any(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(any(feature = "reentrant", feature = "docs"))]
pub fn clear_storage_cache(mut self) -> Self {
self.cache_policy = CachePolicy::Clear;
self
}
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` feature is 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: usize = 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 = "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,
),
}
};
cfg_if::cfg_if! {
if #[cfg(feature = "hostio-caching")] {
crate::contract::RETURN_DATA_LEN.set(outs_len);
}
}
let outs = read_return_data(self.offset, self.size);
match status {
0 => Ok(outs),
_ => Err(outs),
}
}
}
}
================================================
File: stylus-sdk/src/call/traits.rs
================================================
// Copyright 2022-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use alloy_primitives::U256;
/// Trait for calling other contracts.
/// Users should rarely implement this trait outside of proc macros.
pub trait CallContext {
/// Amount of gas to supply the call.
/// Note: values are clipped to the amount of gas remaining.
fn gas(&self) -> u64;
}
/// Trait for calling the `view` methods of other contracts.
/// Users should rarely implement this trait outside of proc macros.
pub trait StaticCallContext: CallContext {}
/// Trait for calling the mutable methods of other contracts.
/// Users should rarely implement this trait outside of proc macros.
///
/// # Safety
///
/// The type must contain a [`TopLevelStorage`][TLS] to prevent aliasing in cases of reentrancy.
///
/// [TLS]: crate::storage::TopLevelStorage
pub unsafe trait MutatingCallContext: CallContext {
/// Amount of ETH in wei to give the other contract.
fn value(&self) -> U256;
}
/// 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 {}
================================================
File: stylus-sdk/src/call/transfer.rs
================================================
// Copyright 2022-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use crate::call::RawCall;
use alloc::vec::Vec;
use alloy_primitives::{Address, U256};
#[cfg(feature = "reentrant")]
use crate::storage::TopLevelStorage;
#[cfg(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`](super::call) function may be used directly.
///
/// [`call`]: super::call
#[cfg(feature = "reentrant")]
pub fn transfer_eth(
_storage: &mut impl TopLevelStorage,
to: Address,
amount: U256,
) -> Result<(), Vec<u8>> {
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`](super::call) function may be used directly.
///
/// ```
/// # use stylus_sdk::call::{call, Call, transfer_eth};
/// # fn wrap() -> Result<(), Vec<u8>> {
/// # let value = alloy_primitives::U256::ZERO;
/// # let recipient = alloy_primitives::Address::ZERO;
/// transfer_eth(recipient, value)?; // these two are equivalent
/// call(Call::new().value(value), recipient, &[])?; // these two are equivalent
/// # Ok(())
/// # }
/// ```
#[cfg(not(feature = "reentrant"))]
pub fn transfer_eth(to: Address, amount: U256) -> Result<(), Vec<u8>> {
RawCall::new_with_value(amount)
.skip_return_data()
.call(to, &[])?;
Ok(())
}
================================================
File: stylus-sdk/src/deploy/mod.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Deploy other contracts.
//!
//! Currently this module only supports low-level contract creation via [`RawDeploy`],
//! but work is being done to introduce high-level deployment patterns.
pub use raw::RawDeploy;
mod raw;
================================================
File: stylus-sdk/src/deploy/raw.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use crate::{call::CachePolicy, contract::read_return_data, hostio};
use alloc::vec::Vec;
use alloy_primitives::{Address, B256, U256};
#[cfg(feature = "reentrant")]
use crate::storage::StorageCache;
/// Mechanism for performing raw deploys of other contracts.
#[derive(Clone, Default)]
#[must_use]
pub struct RawDeploy {
salt: Option<B256>,
#[allow(unused)]
cache_policy: CachePolicy,
}
impl RawDeploy {
/// Begin configuring the raw deploy.
pub fn new() -> Self {
Default::default()
}
/// Configure the deploy to use the salt provided.
/// This will use [`CREATE2`] under the hood to provide a deterministic address.
///
/// [`CREATE2`]: https://www.evm.codes/#f5
pub fn salt(mut self, salt: B256) -> Self {
self.salt = Some(salt);
self
}
/// Configure the deploy to use the salt provided.
/// This will use [`CREATE2`] under the hood to provide a deterministic address if [`Some`].
///
/// [`CREATE2`]: https://www.evm.codes/#f5
pub fn salt_option(mut self, salt: Option<B256>) -> Self {
self.salt = salt;
self
}
/// Write all cached values to persistent storage before the init code.
#[cfg(feature = "reentrant")]
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 init code.
#[cfg(feature = "reentrant")]
pub fn clear_storage_cache(mut self) -> Self {
self.cache_policy = CachePolicy::Clear;
self
}
/// Performs a raw deploy of another contract with the given `endowment` and init `code`.
/// Returns the address of the newly deployed contract, or the error data in case of failure.
///
/// # Safety
///
/// Note that the EVM allows init code to make calls to other contracts, which provides a vector for
/// reentrancy. This means that this method may enable storage aliasing if used in the middle of a storage
/// reference's lifetime and if reentrancy is allowed.
///
/// For extra flexibility, this method does not clear the global storage cache.
/// See [`StorageCache::flush`][flush] and [`StorageCache::clear`][clear] for more information.
///
/// [flush]: crate::storage::StorageCache::flush
/// [clear]: crate::storage::StorageCache::clear
pub unsafe fn deploy(self, code: &[u8], endowment: U256) -> Result<Address, Vec<u8>> {
#[cfg(feature = "reentrant")]
match self.cache_policy {
CachePolicy::Clear => StorageCache::clear(),
CachePolicy::Flush => StorageCache::flush(),
CachePolicy::DoNothing => {}
}
let mut contract = Address::default();
let mut revert_data_len: usize = 0;
let endowment: B256 = endowment.into();
if let Some(salt) = self.salt {
hostio::create2(
code.as_ptr(),
code.len(),
endowment.as_ptr(),
salt.as_ptr(),
contract.as_mut_ptr(),
&mut revert_data_len as *mut _,
);
} else {
hostio::create1(
code.as_ptr(),
code.len(),
endowment.as_ptr(),
contract.as_mut_ptr(),
&mut revert_data_len as *mut _,
);
}
cfg_if::cfg_if! {
if #[cfg(feature = "hostio-caching")] {
crate::contract::RETURN_DATA_LEN.set(revert_data_len);
}
}
if contract.is_zero() {
return Err(read_return_data(0, None));
}
Ok(contract)
}
}
================================================
File: stylus-sdk/src/host/mod.rs
================================================
// Copyright 2024-2025, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
//! Defines host environment methods Stylus SDK contracts have access to.
use alloc::vec::Vec;
use alloy_primitives::{Address, B256, U256};
/// The `wasm` module contains the default implementation of the host trait for all programs
/// that are built for a WASM target.
pub mod wasm;
/// The host trait defines methods a Stylus contract can use to interact
/// with a host environment, such as the EVM. It is a composition
/// of traits with different access to host values and modifications.
/// Stylus contracts in the SDK parametrized by a host trait allow for safe access
/// to hostios without the need for global invocations. The host trait may be implemented
/// by test frameworks as an easier way of mocking hostio invocations for testing
/// Stylus contracts.
pub trait Host:
CryptographyAccess
+ CalldataAccess
+ DeploymentAccess
+ StorageAccess
+ CallAccess
+ BlockAccess
+ ChainAccess
+ AccountAccess
+ MemoryAccess
+ MessageAccess
+ MeteringAccess
{
}
/// Defines a trait that allows a Stylus contract to access its host safely.
pub trait HostAccess {
/// Provides access to the parametrized host of a contract, giving access
/// to all the desired hostios from the user.
fn vm(&self) -> alloc::boxed::Box<dyn crate::host::Host>;
}
/// Provides access to native cryptography extensions provided by
/// a Stylus contract host, such as keccak256.
pub trait CryptographyAccess {
/// Efficiently computes the [`keccak256`] hash of the given preimage.
/// The semantics are equivalent to that of the EVM's [`SHA3`] opcode.
///
/// [`keccak256`]: https://en.wikipedia.org/wiki/SHA-3
/// [`SHA3`]: https://www.evm.codes/#20
fn native_keccak256(&self, input: &[u8]) -> B256;
}
/// Provides access to host methods relating to the accessing the calldata
/// of a Stylus contract transaction.
pub trait CalldataAccess {
/// Reads the program calldata. The semantics are equivalent to that of the EVM's
/// [`CALLDATA_COPY`] opcode when requesting the entirety of the current call's calldata.
///
/// [`CALLDATA_COPY`]: https://www.evm.codes/#37
fn read_args(&self, len: usize) -> Vec<u8>;
/// Copies the bytes of the last EVM call or deployment return result. Does not revert if out of
/// bounds, but rather copies the overlapping portion. The semantics are otherwise equivalent
/// to that of the EVM's [`RETURN_DATA_COPY`] opcode.
///
/// Returns the number of bytes written.
///
/// [`RETURN_DATA_COPY`]: https://www.evm.codes/#3e
fn read_return_data(&self, offset: usize, size: Option<usize>) -> Vec<u8>;
/// Returns the length of the last EVM call or deployment return result, or `0` if neither have
/// happened during the program's execution. The semantics are equivalent to that of the EVM's
/// [`RETURN_DATA_SIZE`] opcode.
///
/// [`RETURN_DATA_SIZE`]: https://www.evm.codes/#3d
fn return_data_size(&self) -> usize;
/// Writes the final return data. If not called before the program exists, the return data will
/// be 0 bytes long. Note that this hostio does not cause the program to exit, which happens
/// naturally when `user_entrypoint` returns.
fn write_result(&self, data: &[u8]);
}
/// Provides access to programmatic creation of contracts via the host environment's CREATE
/// and CREATE2 opcodes in the EVM.
///
/// # Safety
/// These methods should only be used in advanced cases when lowest-level access
/// to create1 and create2 opcodes is needed. Using the methods by themselves will not protect
/// against reentrancy safety, storage aliasing, or cache flushing. For safe contract deployment,
/// utilize a [`RawDeploy`] struct instead.
pub unsafe trait DeploymentAccess {
/// Deploys a new contract using the init code provided, which the EVM executes to construct
/// the code of the newly deployed contract. The init code must be written in EVM bytecode, but
/// the code it deploys can be that of a Stylus program. The code returned will be treated as
/// WASM if it begins with the EOF-inspired header `0xEFF000`. Otherwise the code will be
/// interpreted as that of a traditional EVM-style contract. See [`Deploying Stylus Programs`]
/// for more information on writing init code.
///
/// On success, this hostio returns the address of the newly created account whose address is
/// a function of the sender and nonce. On failure the address will be `0`, `return_data_len`
/// will store the length of the revert data, the bytes of which can be read via the
/// `read_return_data` hostio. The semantics are equivalent to that of the EVM's [`CREATE`]
/// opcode, which notably includes the exact address returned.
///
/// [`Deploying Stylus Programs`]: https://docs.arbitrum.io/stylus/quickstart
/// [`CREATE`]: https://www.evm.codes/#f0
///
/// # Safety
/// This method should only be used in advanced cases when lowest-level access to create1 is required.
/// Safe usage needs to consider reentrancy, storage aliasing, and cache flushing.
/// utilize a [`RawDeploy`] struct instead for safety.
unsafe fn create1(
&self,
code: Address,
endowment: U256,
contract: &mut Address,
revert_data_len: &mut usize,
);
/// Deploys a new contract using the init code provided, which the EVM executes to construct
/// the code of the newly deployed contract. The init code must be written in EVM bytecode, but
/// the code it deploys can be that of a Stylus program. The code returned will be treated as
/// WASM if it begins with the EOF-inspired header `0xEFF000`. Otherwise the code will be
/// interpreted as that of a traditional EVM-style contract. See [`Deploying Stylus Programs`]
/// for more information on writing init code.
///
/// On success, this hostio returns the address of the newly created account whose address is a
/// function of the sender, salt, and init code. On failure the address will be `0`,
/// `return_data_len` will store the length of the revert data, the bytes of which can be read
/// via the `read_return_data` hostio. The semantics are equivalent to that of the EVM's
/// `[CREATE2`] opcode, which notably includes the exact address returned.
///
/// [`Deploying Stylus Programs`]: https://docs.arbitrum.io/stylus/quickstart
/// [`CREATE2`]: https://www.evm.codes/#f5
///
/// # Safety
/// This method should only be used in advanced cases when lowest-level access to create2 is required.
/// Safe usage needs to consider reentrancy, storage aliasing, and cache flushing.
/// utilize a [`RawDeploy`] struct instead for safety.
unsafe fn create2(
&self,
code: Address,
endowment: U256,
salt: B256,
contract: &mut Address,
revert_data_len: &mut usize,
);
}
/// Provides access to storage access and mutation via host methods.
pub trait StorageAccess {
/// Emits an EVM log with the given number of topics and data, the first bytes of which should
/// be the 32-byte-aligned topic data. The semantics are equivalent to that of the EVM's
/// [`LOG0`], [`LOG1`], [`LOG2`], [`LOG3`], and [`LOG4`] opcodes based on the number of topics
/// specified. Requesting more than `4` topics will induce a revert.
///
/// [`LOG0`]: https://www.evm.codes/#a0
/// [`LOG1`]: https://www.evm.codes/#a1
/// [`LOG2`]: https://www.evm.codes/#a2
/// [`LOG3`]: https://www.evm.codes/#a3
/// [`LOG4`]: https://www.evm.codes/#a4
fn emit_log(&self, input: &[u8], num_topics: usize);
/// Reads a 32-byte value from permanent storage. Stylus's storage format is identical to
/// that of the EVM. This means that, under the hood, this hostio is accessing the 32-byte
/// value stored in the EVM state trie at offset `key`, which will be `0` when not previously
/// set. The semantics, then, are equivalent to that of the EVM's [`SLOAD`] opcode.
///
/// Note: the Stylus VM implements storage caching. This means that repeated calls to the same key
/// will cost less than in the EVM.
///
/// [`SLOAD`]: https://www.evm.codes/#54
fn storage_load_bytes32(&self, key: U256) -> B256;
/// Writes a 32-byte value to the permanent storage cache. Stylus's storage format is identical to that
/// of the EVM. This means that, under the hood, this hostio represents storing a 32-byte value into
/// the EVM state trie at offset `key`. Refunds are tabulated exactly as in the EVM. The semantics, then,
/// are equivalent to that of the EVM's [`SSTORE`] opcode.
///
/// Note: because the value is cached, one must call `storage_flush_cache` to persist it.
///
/// [`SSTORE`]: https://www.evm.codes/#55
///
/// # Safety
/// May alias storage.
unsafe fn storage_cache_bytes32(&self, key: U256, value: B256);
/// Persists any dirty values in the storage cache to the EVM state trie, dropping the cache entirely if requested.
/// Analogous to repeated invocations of [`SSTORE`].
///
/// [`SSTORE`]: https://www.evm.codes/#55
fn flush_cache(&self, clear: bool);
}
/// Provides access to calling other contracts using host semantics.
///
/// # Safety
/// These methods should only be used in advanced cases when lowest-level access
/// to call, static_call, and delegate_call methods is required. Using the methods by themselves will not protect
/// against reentrancy safety, storage aliasing, or cache flushing. For safe contract calls,
/// utilize a [`RawCall`] struct instead.
pub unsafe trait CallAccess {
/// Calls the contract at the given address with options for passing value and to limit the
/// amount of gas supplied. The return status indicates whether the call succeeded, and is
/// nonzero on failure.
///
/// In both cases `return_data_len` will store the length of the result, the bytes of which can
/// be read via the `read_return_data` hostio. The bytes are not returned directly so that the
/// programmer can potentially save gas by choosing which subset of the return result they'd
/// like to copy.
///
/// The semantics are equivalent to that of the EVM's [`CALL`] opcode, including callvalue
/// stipends and the 63/64 gas rule. This means that supplying the `u64::MAX` gas can be used
/// to send as much as possible.
///
/// [`CALL`]: https://www.evm.codes/#f1
///
/// # Safety
/// This method should only be used in advanced cases when lowest-level access to calls is required.
/// Safe usage needs to consider reentrancy, storage aliasing, and cache flushing.
/// utilize a [`RawCall`] struct instead for safety.
unsafe fn call_contract(
&self,
to: Address,
data: &[u8],
value: U256,
gas: u64,
outs_len: &mut usize,
) -> u8;
/// Static calls the contract at the given address, with the option to limit the amount of gas
/// supplied. The return status indicates whether the call succeeded, and is nonzero on
/// failure.
///
/// In both cases `return_data_len` will store the length of the result, the bytes of which can
/// be read via the `read_return_data` hostio. The bytes are not returned directly so that the
/// programmer can potentially save gas by choosing which subset of the return result they'd
/// like to copy.
///
/// The semantics are equivalent to that of the EVM's [`STATIC_CALL`] opcode, including the
/// 63/64 gas rule. This means that supplying `u64::MAX` gas can be used to send as much as
/// possible.
///
/// [`STATIC_CALL`]: https://www.evm.codes/#FA
///
/// # Safety
/// This method should only be used in advanced cases when lowest-level access to calls is required.
/// Safe usage needs to consider reentrancy, storage aliasing, and cache flushing.
/// utilize a [`RawCall`] struct instead for safety.
unsafe fn static_call_contract(
&self,
to: Address,
data: &[u8],
gas: u64,
outs_len: &mut usize,
) -> u8;
/// Delegate calls the contract at the given address, with the option to limit the amount of
/// gas supplied. The return status indicates whether the call succeeded, and is nonzero on
/// failure.
///
/// In both cases `return_data_len` will store the length of the result, the bytes of which
/// can be read via the `read_return_data` hostio. The bytes are not returned directly so that
/// the programmer can potentially save gas by choosing which subset of the return result
/// they'd like to copy.
///
/// The semantics are equivalent to that of the EVM's [`DELEGATE_CALL`] opcode, including the
/// 63/64 gas rule. This means that supplying `u64::MAX` gas can be used to send as much as
/// possible.
///
/// [`DELEGATE_CALL`]: https://www.evm.codes/#F4
///
/// # Safety
/// This method should only be used in advanced cases when lowest-level access to calls is required.
/// Safe usage needs to consider reentrancy, storage aliasing, and cache flushing.
/// utilize a [`RawCall`] struct instead for safety.
unsafe fn delegate_call_contract(
&self,
to: Address,
data: &[u8],
gas: u64,
outs_len: &mut usize,
) -> u8;
}
/// Provides access to host methods relating to the block a transactions
/// to a Stylus contract is included in.
pub trait BlockAccess {
/// Gets the basefee of the current block. The semantics are equivalent to that of the EVM's
/// [`BASEFEE`] opcode.
///
/// [`BASEFEE`]: https://www.evm.codes/#48
fn block_basefee(&self) -> U256;
/// Gets the coinbase of the current block, which on Arbitrum chains is the L1 batch poster's
/// address. This differs from Ethereum where the validator including the transaction
/// determines the coinbase.
fn block_coinbase(&self) -> Address;
/// Gets a bounded estimate of the L1 block number at which the Sequencer sequenced the
/// transaction. See [`Block Numbers and Time`] for more information on how this value is
/// determined.
///
/// [`Block Numbers and Time`]: https://developer.arbitrum.io/time
fn block_number(&self) -> u64;
/// Gets a bounded estimate of the Unix timestamp at which the Sequencer sequenced the
/// transaction. See [`Block Numbers and Time`] for more information on how this value is
/// determined.
///
/// [`Block Numbers and Time`]: https://developer.arbitrum.io/time
fn block_timestamp(&self) -> u64;
/// Gets the gas limit of the current block. The semantics are equivalent to that of the EVM's
/// [`GAS_LIMIT`] opcode. Note that as of the time of this writing, `evm.codes` incorrectly
/// implies that the opcode returns the gas limit of the current transaction. When in doubt,
/// consult [`The Ethereum Yellow Paper`].
///
/// [`GAS_LIMIT`]: https://www.evm.codes/#45
/// [`The Ethereum Yellow Paper`]: https://ethereum.github.io/yellowpaper/paper.pdf
fn block_gas_limit(&self) -> u64;
}
/// Provides access to the chain details of the host environment.
pub trait ChainAccess {
/// Gets the unique chain identifier of the Arbitrum chain. The semantics are equivalent to
/// that of the EVM's [`CHAIN_ID`] opcode.
///
/// [`CHAIN_ID`]: https://www.evm.codes/#46
fn chain_id(&self) -> u64;
}
/// Provides access to account details of addresses of the host environment.
pub trait AccountAccess {
/// 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.
///
/// [`BALANCE`]: https://www.evm.codes/#31
fn balance(&self, account: Address) -> U256;
/// Gets the address of the current program. The semantics are equivalent to that of the EVM's
/// [`ADDRESS`] opcode.
///
/// [`ADDRESS`]: https://www.evm.codes/#30
fn contract_address(&self) -> Address;
/// Gets a subset of the code from the account at the given address. The semantics are identical to that
/// of the EVM's [`EXT_CODE_COPY`] opcode, aside from one small detail: the write to the buffer `dest` will
/// stop after the last byte is written. This is unlike the EVM, which right pads with zeros in this scenario.
/// The return value is the number of bytes written, which allows the caller to detect if this has occurred.
///
/// [`EXT_CODE_COPY`]: https://www.evm.codes/#3C
fn code(&self, account: Address) -> Vec<u8>;
/// Gets the size of the code in bytes at the given address. The semantics are equivalent
/// to that of the EVM's [`EXT_CODESIZE`].
///
/// [`EXT_CODESIZE`]: https://www.evm.codes/#3B
fn code_size(&self, account: Address) -> usize;
/// Gets the code hash of the account at the given address. The semantics are equivalent
/// to that of the EVM's [`EXT_CODEHASH`] opcode. Note that the code hash of an account without
/// code will be the empty hash
/// `keccak("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`.
///
/// [`EXT_CODEHASH`]: https://www.evm.codes/#3F
fn code_hash(&self, account: Address) -> B256;
}
/// Provides the ability to pay for memory growth of a Stylus contract.
pub trait MemoryAccess {
/// The `entrypoint!` macro handles importing this hostio, which is required if the
/// program's memory grows. Otherwise compilation through the `ArbWasm` precompile will revert.
/// Internally the Stylus VM forces calls to this hostio whenever new WASM pages are allocated.
/// Calls made voluntarily will unproductively consume gas.
fn pay_for_memory_grow(&self, pages: u16);
}
/// Provides access to transaction details of a Stylus contract.
pub trait MessageAccess {
/// Gets the address of the account that called the program. For normal L2-to-L2 transactions
/// the semantics are equivalent to that of the EVM's [`CALLER`] opcode, including in cases
/// arising from [`DELEGATE_CALL`].
///
/// For L1-to-L2 retryable ticket transactions, the top-level sender's address will be aliased.
/// See [`Retryable Ticket Address Aliasing`] for more information on how this works.
///
/// [`CALLER`]: https://www.evm.codes/#33
/// [`DELEGATE_CALL`]: https://www.evm.codes/#f4
/// [`Retryable Ticket Address Aliasing`]: https://developer.arbitrum.io/arbos/l1-to-l2-messaging#address-aliasing
fn msg_sender(&self) -> Address;
/// Whether the current call is reentrant.
fn msg_reentrant(&self) -> bool;
/// Get the ETH value in wei sent to the program. The semantics are equivalent to that of the
/// EVM's [`CALLVALUE`] opcode.
///
/// [`CALLVALUE`]: https://www.evm.codes/#34
fn msg_value(&self) -> U256;
/// Gets the top-level sender of the transaction. The semantics are equivalent to that of the
/// EVM's [`ORIGIN`] opcode.
///
/// [`ORIGIN`]: https://www.evm.codes/#32
fn tx_origin(&self) -> Address;
}
/// Provides access to metering values such as EVM gas and Stylus ink used and remaining,
/// as well as details of their prices based on the host environment.
pub trait MeteringAccess {
/// Gets the amount of gas left after paying for the cost of this hostio. The semantics are
/// equivalent to that of the EVM's [`GAS`] opcode.
///
/// [`GAS`]: https://www.evm.codes/#5a
fn evm_gas_left(&self) -> u64;
/// Gets the amount of ink remaining after paying for the cost of this hostio. The semantics
/// are equivalent to that of the EVM's [`GAS`] opcode, except the units are in ink. See
/// [`Ink and Gas`] for more information on Stylus's compute pricing.
///
/// [`GAS`]: https://www.evm.codes/#5a
/// [`Ink and Gas`]: https://docs.arbitrum.io/stylus/concepts/gas-metering
fn evm_ink_left(&self) -> u64;
/// Gets the gas price in wei per gas, which on Arbitrum chains equals the basefee. The
/// semantics are equivalent to that of the EVM's [`GAS_PRICE`] opcode.
///
/// [`GAS_PRICE`]: https://www.evm.codes/#3A
fn tx_gas_price(&self) -> U256;
/// Gets the price of ink in evm gas basis points. See [`Ink and Gas`] for more information on
/// Stylus's compute-pricing model.
///
/// [`Ink and Gas`]: https://docs.arbitrum.io/stylus/concepts/gas-metering
fn tx_ink_price(&self) -> u32;
}
================================================
File: stylus-sdk/src/host/wasm.rs
================================================
// Copyright 2025-2026, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use crate::{block, contract, evm, host::*, hostio, msg, tx, types::AddressVM};
/// WasmHost is the default implementation of the host trait
/// for all Stylus programs using the SDK.
pub struct WasmHost;
impl Host for WasmHost {}
impl CryptographyAccess for WasmHost {
fn native_keccak256(&self, input: &[u8]) -> B256 {
let mut output = B256::ZERO;
unsafe {
hostio::native_keccak256(input.as_ptr(), input.len(), output.as_mut_ptr());
}
output
}
}
impl CalldataAccess for WasmHost {
fn read_args(&self, len: usize) -> Vec<u8> {
let mut input = Vec::with_capacity(len);
unsafe {
hostio::read_args(input.as_mut_ptr());
input.set_len(len);
}
input
}
fn read_return_data(&self, offset: usize, size: Option<usize>) -> Vec<u8> {
let size = size.unwrap_or_else(|| self.return_data_size().saturating_sub(offset));
let mut data = Vec::with_capacity(size);
if size > 0 {
unsafe {
let bytes_written = hostio::read_return_data(data.as_mut_ptr(), offset, size);
debug_assert!(bytes_written <= size);
data.set_len(bytes_written);
}
};
data
}
fn return_data_size(&self) -> usize {
contract::return_data_len()
}
fn write_result(&self, data: &[u8]) {
unsafe {
hostio::write_result(data.as_ptr(), data.len());
}
}
}
unsafe impl DeploymentAccess for WasmHost {
unsafe fn create1(
&self,
code: Address,
endowment: U256,
contract: &mut Address,
revert_data_len: &mut usize,
) {
let endowment: B256 = endowment.into();
hostio::create1(
code.as_ptr(),
code.len(),
endowment.as_ptr(),
contract.as_mut_ptr(),
revert_data_len as *mut _,
);
}
unsafe fn create2(
&self,
code: Address,
endowment: U256,
salt: B256,
contract: &mut Address,
revert_data_len: &mut usize,
) {
let endowment: B256 = endowment.into();
hostio::create2(
code.as_ptr(),
code.len(),
endowment.as_ptr(),
salt.as_ptr(),
contract.as_mut_ptr(),
revert_data_len as *mut _,
);
}
}
impl StorageAccess for WasmHost {
fn emit_log(&self, input: &[u8], num_topics: usize) {
unsafe { hostio::emit_log(input.as_ptr(), input.len(), num_topics) }
}
unsafe fn storage_cache_bytes32(&self, key: U256, value: B256) {
hostio::storage_cache_bytes32(B256::from(key).as_ptr(), value.as_ptr());
}
fn storage_load_bytes32(&self, key: U256) -> B256 {
let mut data = B256::ZERO;
unsafe { hostio::storage_load_bytes32(B256::from(key).as_ptr(), data.as_mut_ptr()) };
data
}
fn flush_cache(&self, clear: bool) {
unsafe { hostio::storage_flush_cache(clear) }
}
}
unsafe impl CallAccess for WasmHost {
unsafe fn call_contract(
&self,
to: Address,
data: &[u8],
value: U256,
gas: u64,
outs_len: &mut usize,
) -> u8 {
let value = B256::from(value);
hostio::call_contract(
to.as_ptr(),
data.as_ptr(),
data.len(),
value.as_ptr(),
gas,
outs_len,
)
}
unsafe fn delegate_call_contract(
&self,
to: Address,
data: &[u8],
gas: u64,
outs_len: &mut usize,
) -> u8 {
hostio::delegate_call_contract(to.as_ptr(), data.as_ptr(), data.len(), gas, outs_len)
}
unsafe fn static_call_contract(
&self,
to: Address,
data: &[u8],
gas: u64,
outs_len: &mut usize,
) -> u8 {
hostio::static_call_contract(to.as_ptr(), data.as_ptr(), data.len(), gas, outs_len)
}
}
impl BlockAccess for WasmHost {
fn block_basefee(&self) -> U256 {
block::basefee()
}
fn block_coinbase(&self) -> Address {
block::coinbase()
}
fn block_gas_limit(&self) -> u64 {
block::gas_limit()
}
fn block_number(&self) -> u64 {
block::number()
}
fn block_timestamp(&self) -> u64 {
block::timestamp()
}
}
impl ChainAccess for WasmHost {
fn chain_id(&self) -> u64 {
block::chainid()
}
}
impl AccountAccess for WasmHost {
fn balance(&self, account: Address) -> U256 {
account.balance()
}
fn contract_address(&self) -> Address {
contract::address()
}
fn code(&self, account: Address) -> Vec<u8> {
account.code()
}
fn code_size(&self, account: Address) -> usize {
account.code_size()
}
fn code_hash(&self, account: Address) -> B256 {
account.code_hash()
}
}
impl MemoryAccess for WasmHost {
fn pay_for_memory_grow(&self, pages: u16) {
evm::pay_for_memory_grow(pages)
}
}
impl MessageAccess for WasmHost {
fn msg_reentrant(&self) -> bool {
msg::reentrant()
}
fn msg_sender(&self) -> Address {
msg::sender()
}
fn msg_value(&self) -> U256 {
msg::value()
}
fn tx_origin(&self) -> Address {
tx::origin()
}
}
impl MeteringAccess for WasmHost {
fn evm_gas_left(&self) -> u64 {
evm::gas_left()
}
fn evm_ink_left(&self) -> u64 {
evm::ink_left()
}
fn tx_gas_price(&self) -> U256 {
tx::gas_price()
}
fn tx_ink_price(&self) -> u32 {
tx::ink_price()
}
}
================================================
File: stylus-sdk/src/storage/array.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use crate::host::HostAccess;
use super::{Erase, StorageGuard, StorageGuardMut, StorageType};
use alloc::boxed::Box;
use alloy_primitives::U256;
use core::marker::PhantomData;
/// Accessor for a storage-backed array.
pub struct StorageArray<S: StorageType, const N: usize> {
slot: U256,
marker: PhantomData<S>,
__stylus_host: *const dyn crate::host::Host,
}
impl<S: StorageType, const N: usize> StorageType for StorageArray<S, N> {
type Wraps<'a>
= StorageGuard<'a, StorageArray<S, N>>
where
Self: 'a;
type WrapsMut<'a>
= StorageGuardMut<'a, StorageArray<S, N>>
where
Self: 'a;
const REQUIRED_SLOTS: usize = Self::required_slots();
unsafe fn new(slot: U256, offset: u8, host: *const dyn crate::host::Host) -> Self {
debug_assert!(offset == 0);
Self {
slot,
marker: PhantomData,
__stylus_host: host,
}
}
fn load<'s>(self) -> Self::Wraps<'s> {
StorageGuard::new(self)
}
fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
StorageGuardMut::new(self)
}
}
impl<S: StorageType, const N: usize> HostAccess for StorageArray<S, N> {
fn vm(&self) -> alloc::boxed::Box<dyn crate::host::Host> {
unsafe { alloc::boxed::Box::from_raw(self.__stylus_host as *mut dyn crate::host::Host) }
}
}
impl<S: StorageType, const N: usize> StorageArray<S, N> {
/// Gets the number of elements stored.
///
/// Although this type will always have the same length, this method is still provided for
/// consistency with [`StorageVec`].
#[allow(clippy::len_without_is_empty)]
pub const fn len(&self) -> usize {
N
}
/// Gets an accessor to the element at a given index, if it exists.
/// Note: the accessor is protected by a [`StorageGuard`], which restricts
/// its lifetime to that of `&self`.
pub fn getter(&self, index: impl TryInto<usize>) -> Option<StorageGuard<S>> {
let store = unsafe { self.accessor(index)? };
Some(StorageGuard::new(store))
}
/// Gets a mutable accessor to the element at a given index, if it exists.
/// Note: the accessor is protected by a [`StorageGuardMut`], which restricts
/// its lifetime to that of `&mut self`.
pub fn setter(&mut self, index: impl TryInto<usize>) -> Option<StorageGuardMut<S>> {
let store = unsafe { self.accessor(index)? };
Some(StorageGuardMut::new(store))
}
/// Gets the underlying accessor to the element at a given index, if it exists.
///
/// # Safety
///
/// Enables aliasing.
unsafe fn accessor(&self, index: impl TryInto<usize>) -> Option<S> {
let index = index.try_into().ok()?;
if index >= N {
return None;
}
let (slot, offset) = self.index_slot(index);
Some(S::new(slot, offset, Box::into_raw(self.vm())))
}
/// Gets the underlying accessor to the element at a given index, even if out of bounds.
///
/// # Safety
///
/// Enables aliasing. UB if out of bounds.
unsafe fn accessor_unchecked(&self, index: usize) -> S {
let (slot, offset) = self.index_slot(index);
S::new(slot, offset, Box::into_raw(self.vm()))
}
/// Gets the element at the given index, if it exists.
pub fn get(&self, index: impl TryInto<usize>) -> Option<S::Wraps<'_>> {
let store = unsafe { self.accessor(index)? };
Some(store.load())
}
/// Gets a mutable accessor to the element at a given index, if it exists.
pub fn get_mut(&mut self, index: impl TryInto<usize>) -> Option<S::WrapsMut<'_>> {
let store = unsafe { self.accessor(index)? };
Some(store.load_mut())
}
/// Determines the slot and offset for the element at an index.
fn index_slot(&self, index: usize) -> (U256, u8) {
let width = S::SLOT_BYTES;
let words = S::REQUIRED_SLOTS.max(1);
let density = Self::density();
let slot = self.slot + U256::from(words * index / density);
let offset = 32 - (width * (1 + index % density)) as u8;
(slot, offset)
}
/// Number of elements per slot.
const fn density() -> usize {
32 / S::SLOT_BYTES
}
/// Required slots for the storage array.
const fn required_slots() -> usize {
let reserved = N * S::REQUIRED_SLOTS;
let density = Self::density();
let packed = N.div_ceil(density);
if reserved > packed {
return reserved;
}
packed
}
}
impl<S: Erase, const N: usize> Erase for StorageArray<S, N> {
fn erase(&mut self) {
for i in 0..N {
let mut store = unsafe { self.accessor_unchecked(i) };
store.erase()
}
}
}
================================================
File: stylus-sdk/src/storage/bytes.rs
================================================
// Copyright 2022-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use super::{Erase, GlobalStorage, Storage, StorageB8, StorageGuard, StorageGuardMut, StorageType};
use crate::{crypto, host::HostAccess};
use alloc::boxed::Box;
use alloc::{
string::{String, ToString},
vec::Vec,
};
use alloy_primitives::{U256, U8};
use core::cell::OnceCell;
/// Accessor for storage-backed bytes.
pub struct StorageBytes {
root: U256,
base: OnceCell<U256>,
__stylus_host: *const dyn crate::host::Host,
}
impl StorageType for StorageBytes {
type Wraps<'a>
= StorageGuard<'a, StorageBytes>
where
Self: 'a;
type WrapsMut<'a>
= StorageGuardMut<'a, StorageBytes>
where
Self: 'a;
unsafe fn new(root: U256, offset: u8, host: *const dyn crate::host::Host) -> Self {
debug_assert!(offset == 0);
Self {
root,
base: OnceCell::new(),
__stylus_host: host,
}
}
fn load<'s>(self) -> Self::Wraps<'s> {
StorageGuard::new(self)
}
fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
StorageGuardMut::new(self)
}
}
impl HostAccess for StorageBytes {
fn vm(&self) -> alloc::boxed::Box<dyn crate::host::Host> {
unsafe { alloc::boxed::Box::from_raw(self.__stylus_host as *mut dyn crate::host::Host) }
}
}
impl StorageBytes {
/// Returns `true` if the collection contains no elements.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Gets the number of bytes stored.
pub fn len(&self) -> usize {
let word = Storage::get_word(&self.vm(), self.root);
// check if the data is short
let slot: &[u8] = word.as_ref();
if slot[31] & 1 == 0 {
return (slot[31] / 2) as usize;
}
let word: U256 = word.into();
let len = word / U256::from(2);
len.try_into().unwrap()
}
/// Overwrites the collection's length, moving bytes as needed.
///
/// # Safety
///
/// May populate the vector with junk bytes from prior dirty operations.
/// Note that [`StorageBytes`] has unlimited capacity, so all lengths are valid.
pub unsafe fn set_len(&mut self, len: usize) {
let old = self.len();
// if representation hasn't changed, just update the length
if (old < 32) == (len < 32) {
return self.write_len(len);
}
// if shrinking, pull data in
if (len < 32) && (old > 32) {
let word = Storage::get_word(&self.vm(), *self.base());
Storage::set_word(&self.vm(), self.root, word);
return self.write_len(len);
}
// if growing, push data out
let mut word = Storage::get_word(&self.vm(), self.root);
word[31] = 0; // clear len byte
Storage::set_word(&self.vm(), *self.base(), word);
self.write_len(len)
}
/// 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
Storage::set_uint(&self.vm(), self.root, 31, U8::from(len * 2));
} else {
// place the len in the root with the long bit high
Storage::set_word(&self.vm(), self.root, U256::from(len * 2 + 1).into())
}
}
/// Adds a byte to the end.
pub fn push(&mut self, b: u8) {
let index = self.len();
let value = U8::from(b);
macro_rules! assign {
($slot:expr) => {
unsafe {
Storage::set_uint(&self.vm(), $slot, index % 32, value); // pack value
self.write_len(index + 1);
}
};
}
if index < 31 {
return assign!(self.root);
}
// convert to multi-word representation
if index == 31 {
// copy content over (len byte will be overwritten)
let word = Storage::get_word(&self.vm(), self.root);
unsafe { Storage::set_word(&self.vm(), *self.base(), word) };
}
let slot = self.base() + U256::from(index / 32);
assign!(slot);
}
/// Removes and returns the last byte, if it exists.
/// As an optimization, underlying storage slots are only erased when all bytes in
/// a given word are freed when in the multi-word representation.
pub fn pop(&mut self) -> Option<u8> {
let len = self.len();
if len == 0 {
return None;
}
let index = len - 1;
let clean = index % 32 == 0;
let byte = self.get(index)?;
let clear = |slot| unsafe { Storage::clear_word(&self.vm(), slot) };
// convert to single-word representation
if len == 32 {
// copy content over
let word = Storage::get_word(&self.vm(), *self.base());
unsafe { Storage::set_word(&self.vm(), self.root, word) };
clear(*self.base());
}
// clear distant word
if len > 32 && clean {
clear(self.index_slot(len - 1).0);
}
// clear the value
if len < 32 {
unsafe { Storage::set_byte(&self.vm(), self.root, index, 0) };
}
// set the new length
unsafe { self.write_len(index) };
Some(byte)
}
/// Gets the byte at the given index, if it exists.
pub fn get(&self, index: impl TryInto<usize>) -> Option<u8> {
let index = index.try_into().ok()?;
if index >= self.len() {
return None;
}
unsafe { Some(self.get_unchecked(index)) }
}
/// Gets a mutable accessor to the byte at the given index, if it exists.
pub fn get_mut(&mut self, index: impl TryInto<usize>) -> Option<StorageGuardMut<StorageB8>> {
let index = index.try_into().ok()?;
if index >= self.len() {
return None;
}
let (slot, offset) = self.index_slot(index);
let value = unsafe { StorageB8::new(slot, offset, Box::into_raw(self.vm())) };
Some(StorageGuardMut::new(value))
}
/// Gets the byte at the given index, even if beyond the collection.
///
/// # Safety
///
/// UB if index is out of bounds.
pub unsafe fn get_unchecked(&self, index: usize) -> u8 {
let (slot, offset) = self.index_slot(index);
unsafe { Storage::get_byte(&self.vm(), slot, offset.into()) }
}
/// Gets the full contents of the collection.
pub fn get_bytes(&self) -> Vec<u8> {
let len = self.len();
let mut bytes = Vec::with_capacity(len);
for i in 0..len {
let byte = unsafe { self.get_unchecked(i) };
bytes.push(byte);
}
bytes
}
/// Overwrites the contents of the collection, erasing what was previously stored.
pub fn set_bytes(&mut self, bytes: impl AsRef<[u8]>) {
self.erase();
self.extend(bytes.as_ref());
}
/// Determines the slot and offset for the element at an index.
fn index_slot(&self, index: usize) -> (U256, u8) {
let slot = match self.len() {
32.. => self.base() + U256::from(index / 32),
_ => self.root,
};
(slot, (index % 32) as u8)
}
/// 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())
}
}
impl Erase for StorageBytes {
fn erase(&mut self) {
let mut len = self.len() as isize;
if len > 31 {
while len > 0 {
let slot = self.index_slot(len as usize - 1).0;
unsafe { Storage::clear_word(&self.vm(), slot) };
len -= 32;
}
}
unsafe { Storage::clear_word(&self.vm(), self.root) };
}
}
impl Extend<u8> for StorageBytes {
fn extend<T: IntoIterator<Item = u8>>(&mut self, iter: T) {
for elem in iter {
self.push(elem);
}
}
}
impl<'a> Extend<&'a u8> for StorageBytes {
fn extend<T: IntoIterator<Item = &'a u8>>(&mut self, iter: T) {
for elem in iter {
self.push(*elem);
}
}
}
/// Accessor for storage-backed bytes
pub struct StorageString(pub StorageBytes);
impl StorageType for StorageString {
type Wraps<'a>
= StorageGuard<'a, StorageString>
where
Self: 'a;
type WrapsMut<'a>
= StorageGuardMut<'a, StorageString>
where
Self: 'a;
unsafe fn new(slot: U256, offset: u8, host: *const dyn crate::host::Host) -> Self {
Self(StorageBytes::new(slot, offset, host))
}
fn load<'s>(self) -> Self::Wraps<'s> {
StorageGuard::new(self)
}
fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
StorageGuardMut::new(self)
}
}
impl StorageString {
/// Returns `true` if the collection contains no elements.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Gets the number of bytes stored.
pub fn len(&self) -> usize {
self.0.len()
}
/// Adds a char to the end.
pub fn push(&mut self, c: char) {
for byte in c.to_string().bytes() {
self.0.push(byte)
}
}
/// Gets the underlying [`String`], ignoring any invalid data.
pub fn get_string(&self) -> String {
let bytes = self.0.get_bytes();
String::from_utf8_lossy(&bytes).into()
}
/// Overwrites the underlying [`String`], erasing what was previously stored.
pub fn set_str(&mut self, text: impl AsRef<str>) {
self.erase();
for c in text.as_ref().chars() {
self.push(c);
}
}
}
impl Erase for StorageString {
fn erase(&mut self) {
self.0.erase()
}
}
impl Extend<char> for StorageString {
fn extend<T: IntoIterator<Item = char>>(&mut self, iter: T) {
for c in iter {
self.push(c);
}
}
}
================================================
File: stylus-sdk/src/storage/map.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use crate::{crypto, host::HostAccess};
use super::{Erase, SimpleStorageType, StorageGuard, StorageGuardMut, StorageType};
use alloc::boxed::Box;
use alloc::{string::String, vec::Vec};
use alloy_primitives::{Address, FixedBytes, Signed, Uint, B256, U160, U256};
use core::marker::PhantomData;
/// Accessor for a storage-backed map.
pub struct StorageMap<K: StorageKey, V: StorageType> {
slot: U256,
marker: PhantomData<(K, V)>,
__stylus_host: *const dyn crate::host::Host,
}
impl<K, V> StorageType for StorageMap<K, V>
where
K: StorageKey,
V: StorageType,
{
type Wraps<'a>
= StorageGuard<'a, StorageMap<K, V>>
where
Self: 'a;
type WrapsMut<'a>
= StorageGuardMut<'a, StorageMap<K, V>>
where
Self: 'a;
unsafe fn new(slot: U256, offset: u8, host: *const dyn crate::host::Host) -> Self {
debug_assert!(offset == 0);
Self {
slot,
marker: PhantomData,
__stylus_host: host,
}
}
fn load<'s>(self) -> Self::Wraps<'s> {
StorageGuard::new(self)
}
fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
StorageGuardMut::new(self)
}
}
impl<K, V> HostAccess for StorageMap<K, V>
where
K: StorageKey,
V: StorageType,
{
fn vm(&self) -> alloc::boxed::Box<dyn crate::host::Host> {
unsafe { alloc::boxed::Box::from_raw(self.__stylus_host as *mut dyn crate::host::Host) }
}
}
impl<K, V> StorageMap<K, V>
where
K: StorageKey,
V: StorageType,
{
/// Where in a word to access the wrapped value.
const CHILD_OFFSET: u8 = 32 - V::SLOT_BYTES as u8;
/// Gets an accessor to the element at the given key, or the zero-value if none is there.
/// Note: the accessor is protected by a [`StorageGuard`], which restricts its lifetime
/// to that of `&self`.
pub fn getter(&self, key: K) -> StorageGuard<V> {
let slot = key.to_slot(self.slot.into());
unsafe { StorageGuard::new(V::new(slot, Self::CHILD_OFFSET, Box::into_raw(self.vm()))) }
}
/// Gets a mutable accessor to the element at the given key, or the zero-value is none is there.
/// Note: the accessor is protected by a [`StorageGuardMut`], which restricts its lifetime
/// to that of `&mut self`.
pub fn setter(&mut self, key: K) -> StorageGuardMut<V> {
let slot = key.to_slot(self.slot.into());
unsafe { StorageGuardMut::new(V::new(slot, Self::CHILD_OFFSET, Box::into_raw(self.vm()))) }
}
/// Gets the element at the given key, or the zero value if none is there.
pub fn get(&self, key: K) -> V::Wraps<'_> {
let store = self.getter(key);
unsafe { store.into_raw().load() }
}
}
impl<'a, K, V> StorageMap<K, V>
where
K: StorageKey,
V: SimpleStorageType<'a>,
{
/// 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);
}
/// Replace the element at the given key.
/// Returns the old element, or the zero-value if none was there.
pub fn replace(&mut self, key: K, value: V::Wraps<'a>) -> V::Wraps<'a> {
let slot = key.to_slot(self.slot.into());
// intentionally alias so that we can erase after load
unsafe {
let store = V::new(slot, Self::CHILD_OFFSET, Box::into_raw(self.vm()));
let mut alias = V::new(slot, Self::CHILD_OFFSET, Box::into_raw(self.vm()));
let prior = store.load();
alias.set_by_wrapped(value);
prior
}
}
/// Remove the element at the given key.
/// Returns the element, or the zero-value if none was there.
pub fn take(&mut self, key: K) -> V::Wraps<'a> {
let slot = key.to_slot(self.slot.into());
// intentionally alias so that we can erase after load
unsafe {
let store = V::new(slot, Self::CHILD_OFFSET, Box::into_raw(self.vm()));
let mut alias = V::new(slot, Self::CHILD_OFFSET, Box::into_raw(self.vm()));
let value = store.load();
alias.erase();
value
}
}
}
impl<K, V> StorageMap<K, V>
where
K: StorageKey,
V: Erase,
{
/// Delete the element at the given key, if it exists.
pub fn delete(&mut self, key: K) {
let mut store = self.setter(key);
store.erase();
}
}
/// 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;
}
impl<const B: usize, const L: usize> StorageKey for Uint<B, L> {
fn to_slot(&self, root: B256) -> U256 {
let data = B256::from(U256::from(*self));
let data = data.concat_const::<32, 64>(root);
crypto::keccak(data).into()
}
}
impl<const B: usize, const L: usize> StorageKey for Signed<B, L> {
fn to_slot(&self, root: B256) -> U256 {
let data = B256::from(U256::from(self.into_raw()));
let data = data.concat_const::<32, 64>(root);
crypto::keccak(data).into()
}
}
impl<const N: usize> StorageKey for FixedBytes<N> {
fn to_slot(&self, root: B256) -> U256 {
let mut pad = [0; 32];
pad[..N].copy_from_slice(&self.0);
let data = B256::from(pad);
let data = data.concat_const::<32, 64>(root);
crypto::keccak(data).into()
}
}
impl StorageKey for &[u8] {
fn to_slot(&self, root: B256) -> U256 {
let mut vec = self.to_vec();
vec.extend(root);
crypto::keccak(vec).into()
}
}
impl StorageKey for Vec<u8> {
fn to_slot(&self, root: B256) -> U256 {
let bytes: &[u8] = self.as_ref();
bytes.to_slot(root)
}
}
impl StorageKey for &str {
fn to_slot(&self, root: B256) -> U256 {
self.as_bytes().to_slot(root)
}
}
impl StorageKey for String {
fn to_slot(&self, root: B256) -> U256 {
self.as_bytes().to_slot(root)
}
}
impl StorageKey for Address {
fn to_slot(&self, root: B256) -> U256 {
let int: U160 = self.0.into();
int.to_slot(root)
}
}
impl StorageKey for bool {
fn to_slot(&self, root: B256) -> U256 {
let value = self.then_some(1_u8).unwrap_or_default();
value.to_slot(root)
}
}
macro_rules! impl_key {
($($uint:ident $int:ident)+) => {
$(
impl StorageKey for $uint {
fn to_slot(&self, root: B256) -> U256 {
let data = B256::from(U256::from(*self));
let data = data.concat_const::<32, 64>(root.into());
crypto::keccak(data).into()
}
}
impl StorageKey for $int {
fn to_slot(&self, root: B256) -> U256 {
let data = B256::from(U256::from(*self as $uint)); // wrap-around
let data = data.concat_const::<32, 64>(root.into());
crypto::keccak(data).into()
}
}
)+
};
}
impl_key!(u8 i8 u16 i16 u32 i32 u64 i64 u128 i128 usize isize);
================================================
File: stylus-sdk/src/storage/mod.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/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::{
host::{Host, HostAccess},
hostio,
};
use alloy_primitives::{Address, BlockHash, BlockNumber, FixedBytes, Signed, Uint, B256, U256};
use alloy_sol_types::sol_data::{ByteCount, IntBitCount, SupportedFixedBytes, SupportedInt};
use core::{cell::OnceCell, marker::PhantomData, ops::Deref};
pub use array::StorageArray;
pub use bytes::{StorageBytes, StorageString};
pub use map::{StorageKey, StorageMap};
pub use traits::{
Erase, GlobalStorage, SimpleStorageType, StorageGuard, StorageGuardMut, StorageType,
TopLevelStorage,
};
pub use vec::StorageVec;
mod array;
mod bytes;
mod map;
mod traits;
mod vec;
pub(crate) type Storage = StorageCache;
/// Global accessor to persistent storage that relies on VM-level caching.
///
/// [`LocalStorageCache`]: super::LocalStorageCache
pub struct StorageCache;
impl GlobalStorage for StorageCache {
/// Retrieves a 32-byte EVM word from persistent storage.
fn get_word(host: &alloc::boxed::Box<dyn crate::host::Host>, key: U256) -> B256 {
host.storage_load_bytes32(key)
}
/// Stores a 32-byte EVM word to persistent storage.
///
/// # Safety
///
/// May alias storage.
unsafe fn set_word(host: &alloc::boxed::Box<dyn crate::host::Host>, key: U256, value: B256) {
host.storage_cache_bytes32(key, value)
}
}
impl StorageCache {
/// Flushes the VM cache, persisting all values to the EVM state trie.
/// Note: this is used at the end of the [`entrypoint`] macro and is not typically called by user code.
///
/// [`entrypoint`]: macro@stylus_proc::entrypoint
pub fn flush() {
unsafe { hostio::storage_flush_cache(false) }
}
/// Flushes and clears the VM cache, persisting all values to the EVM state trie.
/// This is useful in cases of reentrancy to ensure cached values from one call context show up in another.
pub fn clear() {
unsafe { hostio::storage_flush_cache(true) }
}
}
/// Overwrites the value in a cell.
#[inline]
fn overwrite_cell<T>(cell: &mut OnceCell<T>, value: T) {
cell.take();
_ = cell.set(value);
}
macro_rules! alias_ints {
($($name:ident, $signed_name:ident, $bits:expr, $limbs:expr;)*) => {
$(
#[doc = concat!("Accessor for a storage-backed [`alloy_primitives::aliases::U", stringify!($bits), "`].")]
pub type $name = StorageUint<$bits, $limbs>;
#[doc = concat!("Accessor for a storage-backed [`alloy_primitives::aliases::I", stringify!($bits), "`].")]
pub type $signed_name = StorageSigned<$bits, $limbs>;
)*
};
}
macro_rules! alias_bytes {
($($name:ident, $bits:expr, $bytes:expr;)*) => {
$(
#[doc = concat!("Accessor for a storage-backed [`alloy_primitives::aliases::B", stringify!($bits), "`].")]
pub type $name = StorageFixedBytes<$bytes>;
)*
};
}
alias_ints! {
StorageU8, StorageI8, 8, 1;
StorageU16, StorageI16, 16, 1;
StorageU32, StorageI32, 32, 1;
StorageU64, StorageI64, 64, 1;
StorageU128, StorageI128, 128, 2;
StorageU160, StorageI160, 160, 3;
StorageU192, StorageI192, 192, 3;
StorageU256, StorageI256, 256, 4;
}
alias_bytes! {
StorageB8, 8, 1;
StorageB16, 16, 2;
StorageB32, 32, 4;
StorageB64, 64, 8;
StorageB96, 96, 12;
StorageB128, 128, 16;
StorageB160, 160, 20;
StorageB192, 192, 24;
StorageB224, 224, 28;
StorageB256, 256, 32;
}
/// Accessor for a storage-backed [`alloy_primitives::Uint`].
///
/// Note: in the future `L` won't be needed.
// TODO: drop L after SupportedInt provides LIMBS (waiting for clarity reasons)
// https://github.com/rust-lang/rust/issues/76560
#[derive(Debug)]
pub struct StorageUint<const B: usize, const L: usize>
where
IntBitCount<B>: SupportedInt,
{
slot: U256,
offset: u8,
cached: OnceCell<Uint<B, L>>,
__stylus_host: *const dyn Host,
}
impl<const B: usize, const L: usize> HostAccess for StorageUint<B, L>
where
IntBitCount<B>: SupportedInt,
{
fn vm(&self) -> alloc::boxed::Box<dyn crate::host::Host> {
unsafe { alloc::boxed::Box::from_raw(self.__stylus_host as *mut dyn crate::host::Host) }
}
}
impl<const B: usize, const L: usize> StorageUint<B, L>
where
IntBitCount<B>: SupportedInt,
{
/// Gets the underlying [`alloy_primitives::Uint`] in persistent storage.
pub fn get(&self) -> Uint<B, L> {
**self
}
/// Sets the underlying [`alloy_primitives::Uint`] in persistent storage.
pub fn set(&mut self, value: Uint<B, L>) {
overwrite_cell(&mut self.cached, value);
unsafe { Storage::set_uint(&self.vm(), self.slot, self.offset.into(), value) };
}
}
impl<const B: usize, const L: usize> StorageType for StorageUint<B, L>
where
IntBitCount<B>: SupportedInt,
{
type Wraps<'a> = Uint<B, L>;
type WrapsMut<'a> = StorageGuardMut<'a, Self>;
const SLOT_BYTES: usize = (B / 8);
unsafe fn new(slot: U256, offset: u8, host: *const dyn crate::host::Host) -> Self {
debug_assert!(B <= 256);
Self {
slot,
offset,
cached: OnceCell::new(),
__stylus_host: host,
}
}
fn load<'s>(self) -> Self::Wraps<'s> {
self.get()
}
fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
StorageGuardMut::new(self)
}
}
impl<'a, const B: usize, const L: usize> SimpleStorageType<'a> for StorageUint<B, L>
where
IntBitCount<B>: SupportedInt,
{
fn set_by_wrapped(&mut self, value: Self::Wraps<'a>) {
self.set(value);
}
}
impl<const B: usize, const L: usize> Erase for StorageUint<B, L>
where
IntBitCount<B>: SupportedInt,
{
fn erase(&mut self) {
self.set(Self::Wraps::ZERO);
}
}
impl<const B: usize, const L: usize> Deref for StorageUint<B, L>
where
IntBitCount<B>: SupportedInt,
{
type Target = Uint<B, L>;
fn deref(&self) -> &Self::Target {
self.cached
.get_or_init(|| unsafe { Storage::get_uint(&self.vm(), self.slot, self.offset.into()) })
}
}
impl<const B: usize, const L: usize> From<StorageUint<B, L>> for Uint<B, L>
where
IntBitCount<B>: SupportedInt,
{
fn from(value: StorageUint<B, L>) -> Self {
*value
}
}
/// Accessor for a storage-backed [`Signed`].
///
/// Note: in the future `L` won't be needed.
// TODO: drop L after SupportedInt provides LIMBS (waiting for clarity reasons)
// https://github.com/rust-lang/rust/issues/76560
#[derive(Debug)]
pub struct StorageSigned<const B: usize, const L: usize>
where
IntBitCount<B>: SupportedInt,
{
slot: U256,
offset: u8,
cached: OnceCell<Signed<B, L>>,
__stylus_host: *const dyn Host,
}
impl<const B: usize, const L: usize> HostAccess for StorageSigned<B, L>
where
IntBitCount<B>: SupportedInt,
{
fn vm(&self) -> alloc::boxed::Box<dyn crate::host::Host> {
unsafe { alloc::boxed::Box::from_raw(self.__stylus_host as *mut dyn crate::host::Host) }
}
}
impl<const B: usize, const L: usize> StorageSigned<B, L>
where
IntBitCount<B>: SupportedInt,
{
/// Gets the underlying [`Signed`] in persistent storage.
pub fn get(&self) -> Signed<B, L> {
**self
}
/// Gets the underlying [`Signed`] in persistent storage.
pub fn set(&mut self, value: Signed<B, L>) {
overwrite_cell(&mut self.cached, value);
unsafe { Storage::set_signed(&self.vm(), self.slot, self.offset.into(), value) };
}
}
impl<const B: usize, const L: usize> StorageType for StorageSigned<B, L>
where
IntBitCount<B>: SupportedInt,
{
type Wraps<'a> = Signed<B, L>;
type WrapsMut<'a> = StorageGuardMut<'a, Self>;
const SLOT_BYTES: usize = (B / 8);
unsafe fn new(slot: U256, offset: u8, host: *const dyn crate::host::Host) -> Self {
Self {
slot,
offset,
cached: OnceCell::new(),
__stylus_host: host,
}
}
fn load<'s>(self) -> Self::Wraps<'s> {
self.get()
}
fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
StorageGuardMut::new(self)
}
}
impl<'a, const B: usize, const L: usize> SimpleStorageType<'a> for StorageSigned<B, L>
where
IntBitCount<B>: SupportedInt,
{
fn set_by_wrapped(&mut self, value: Self::Wraps<'a>) {
self.set(value);
}
}
impl<const B: usize, const L: usize> Erase for StorageSigned<B, L>
where
IntBitCount<B>: SupportedInt,
{
fn erase(&mut self) {
self.set(Self::Wraps::ZERO)
}
}
impl<const B: usize, const L: usize> Deref for StorageSigned<B, L>
where
IntBitCount<B>: SupportedInt,
{
type Target = Signed<B, L>;
fn deref(&self) -> &Self::Target {
self.cached.get_or_init(|| unsafe {
Storage::get_signed(&self.vm(), self.slot, self.offset.into())
})
}
}
impl<const B: usize, const L: usize> From<StorageSigned<B, L>> for Signed<B, L>
where
IntBitCount<B>: SupportedInt,
{
fn from(value: StorageSigned<B, L>) -> Self {
*value
}
}
/// Accessor for a storage-backed [`FixedBytes`].
#[derive(Debug)]
pub struct StorageFixedBytes<const N: usize> {
slot: U256,
offset: u8,
cached: OnceCell<FixedBytes<N>>,
__stylus_host: *const dyn Host,
}
impl<const N: usize> HostAccess for StorageFixedBytes<N> {
fn vm(&self) -> alloc::boxed::Box<dyn crate::host::Host> {
unsafe { alloc::boxed::Box::from_raw(self.__stylus_host as *mut dyn crate::host::Host) }
}
}
impl<const N: usize> StorageFixedBytes<N> {
/// Gets the underlying [`FixedBytes`] in persistent storage.
pub fn get(&self) -> FixedBytes<N> {
**self
}
/// Gets the underlying [`FixedBytes`] in persistent storage.
pub fn set(&mut self, value: FixedBytes<N>) {
overwrite_cell(&mut self.cached, value);
unsafe { Storage::set(&self.vm(), self.slot, self.offset.into(), value) }
}
}
impl<const N: usize> StorageType for StorageFixedBytes<N>
where
ByteCount<N>: SupportedFixedBytes,
{
type Wraps<'a> = FixedBytes<N>;
type WrapsMut<'a> = StorageGuardMut<'a, Self>;
const SLOT_BYTES: usize = N;
unsafe fn new(slot: U256, offset: u8, host: *const dyn crate::host::Host) -> Self {
Self {
slot,
offset,
cached: OnceCell::new(),
__stylus_host: host,
}
}
fn load<'s>(self) -> Self::Wraps<'s> {
self.get()
}
fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
StorageGuardMut::new(self)
}
}
impl<'a, const N: usize> SimpleStorageType<'a> for StorageFixedBytes<N>
where
ByteCount<N>: SupportedFixedBytes,
{
fn set_by_wrapped(&mut self, value: Self::Wraps<'a>) {
self.set(value);
}
}
impl<const N: usize> Erase for StorageFixedBytes<N>
where
ByteCount<N>: SupportedFixedBytes,
{
fn erase(&mut self) {
self.set(Self::Wraps::ZERO)
}
}
impl<const N: usize> Deref for StorageFixedBytes<N> {
type Target = FixedBytes<N>;
fn deref(&self) -> &Self::Target {
self.cached
.get_or_init(|| unsafe { Storage::get(&self.vm(), self.slot, self.offset.into()) })
}
}
impl<const N: usize> From<StorageFixedBytes<N>> for FixedBytes<N> {
fn from(value: StorageFixedBytes<N>) -> Self {
*value
}
}
/// Accessor for a storage-backed [`bool`].
#[derive(Debug)]
pub struct StorageBool {
slot: U256,
offset: u8,
cached: OnceCell<bool>,
__stylus_host: *const dyn Host,
}
impl HostAccess for StorageBool {
fn vm(&self) -> alloc::boxed::Box<dyn crate::host::Host> {
unsafe { alloc::boxed::Box::from_raw(self.__stylus_host as *mut dyn crate::host::Host) }
}
}
impl StorageBool {
/// Gets the underlying [`bool`] in persistent storage.
pub fn get(&self) -> bool {
**self
}
/// Gets the underlying [`bool`] in persistent storage.
pub fn set(&mut self, value: bool) {
overwrite_cell(&mut self.cached, value);
unsafe { Storage::set_byte(&self.vm(), self.slot, self.offset.into(), value as u8) }
}
}
impl StorageType for StorageBool {
type Wraps<'a> = bool;
type WrapsMut<'a> = StorageGuardMut<'a, Self>;
const SLOT_BYTES: usize = 1;
unsafe fn new(slot: U256, offset: u8, host: *const dyn crate::host::Host) -> Self {
Self {
slot,
offset,
cached: OnceCell::new(),
__stylus_host: host,
}
}
fn load<'s>(self) -> Self::Wraps<'s> {
self.get()
}
fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
StorageGuardMut::new(self)
}
}
impl<'a> SimpleStorageType<'a> for StorageBool {
fn set_by_wrapped(&mut self, value: Self::Wraps<'a>) {
self.set(value);
}
}
impl Erase for StorageBool {
fn erase(&mut self) {
self.set(false);
}
}
impl Deref for StorageBool {
type Target = bool;
fn deref(&self) -> &Self::Target {
self.cached.get_or_init(|| unsafe {
let data = Storage::get_byte(&self.vm(), self.slot, self.offset.into());
data != 0
})
}
}
impl From<StorageBool> for bool {
fn from(value: StorageBool) -> Self {
*value
}
}
/// Accessor for a storage-backed [`Address`].
#[derive(Debug)]
pub struct StorageAddress {
slot: U256,
offset: u8,
cached: OnceCell<Address>,
__stylus_host: *const dyn Host,
}
impl HostAccess for StorageAddress {
fn vm(&self) -> alloc::boxed::Box<dyn crate::host::Host> {
unsafe { alloc::boxed::Box::from_raw(self.__stylus_host as *mut dyn crate::host::Host) }
}
}
impl StorageAddress {
/// Gets the underlying [`Address`] in persistent storage.
pub fn get(&self) -> Address {
**self
}
/// Gets the underlying [`Address`] in persistent storage.
pub fn set(&mut self, value: Address) {
overwrite_cell(&mut self.cached, value);
unsafe { Storage::set::<20>(&self.vm(), self.slot, self.offset.into(), value.into()) }
}
}
impl StorageType for StorageAddress {
type Wraps<'a> = Address;
type WrapsMut<'a> = StorageGuardMut<'a, Self>;
const SLOT_BYTES: usize = 20;
unsafe fn new(slot: U256, offset: u8, host: *const dyn crate::host::Host) -> Self {
Self {
slot,
offset,
cached: OnceCell::new(),
__stylus_host: host,
}
}
fn load<'s>(self) -> Self::Wraps<'s> {
self.get()
}
fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
StorageGuardMut::new(self)
}
}
impl<'a> SimpleStorageType<'a> for StorageAddress {
fn set_by_wrapped(&mut self, value: Self::Wraps<'a>) {
self.set(value);
}
}
impl Erase for StorageAddress {
fn erase(&mut self) {
self.set(Self::Wraps::ZERO);
}
}
impl Deref for StorageAddress {
type Target = Address;
fn deref(&self) -> &Self::Target {
self.cached.get_or_init(|| unsafe {
Storage::get::<20>(&self.vm(), self.slot, self.offset.into()).into()
})
}
}
impl From<StorageAddress> for Address {
fn from(value: StorageAddress) -> Self {
*value
}
}
/// Accessor for a storage-backed [`BlockNumber`].
///
/// This storage type allows convenient and type-safe storage of a
/// [`BlockNumber`].
#[derive(Debug)]
pub struct StorageBlockNumber {
slot: U256,
offset: u8,
cached: OnceCell<BlockNumber>,
__stylus_host: *const dyn Host,
}
impl HostAccess for StorageBlockNumber {
fn vm(&self) -> alloc::boxed::Box<dyn crate::host::Host> {
unsafe { alloc::boxed::Box::from_raw(self.__stylus_host as *mut dyn crate::host::Host) }
}
}
impl StorageBlockNumber {
/// Gets the underlying [`BlockNumber`] in persistent storage.
pub fn get(&self) -> BlockNumber {
**self
}
/// Sets the underlying [`BlockNumber`] in persistent storage.
pub fn set(&mut self, value: BlockNumber) {
overwrite_cell(&mut self.cached, value);
let value = FixedBytes::from(value.to_be_bytes());
unsafe { Storage::set::<8>(&self.vm(), self.slot, self.offset.into(), value) };
}
}
impl StorageType for StorageBlockNumber {
type Wraps<'a> = BlockNumber;
type WrapsMut<'a> = StorageGuardMut<'a, Self>;
const SLOT_BYTES: usize = 8;
unsafe fn new(slot: U256, offset: u8, host: *const dyn crate::host::Host) -> Self {
Self {
slot,
offset,
cached: OnceCell::new(),
__stylus_host: host,
}
}
fn load<'s>(self) -> Self::Wraps<'s> {
self.get()
}
fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
StorageGuardMut::new(self)
}
}
impl<'a> SimpleStorageType<'a> for StorageBlockNumber {
fn set_by_wrapped(&mut self, value: Self::Wraps<'a>) {
self.set(value);
}
}
impl Erase for StorageBlockNumber {
fn erase(&mut self) {
self.set(0);
}
}
impl Deref for StorageBlockNumber {
type Target = BlockNumber;
fn deref(&self) -> &Self::Target {
self.cached.get_or_init(|| unsafe {
let data = Storage::get::<8>(&self.vm(), self.slot, self.offset.into());
u64::from_be_bytes(data.0)
})
}
}
impl From<StorageBlockNumber> for BlockNumber {
fn from(value: StorageBlockNumber) -> Self {
*value
}
}
/// Accessor for a storage-backed [`BlockHash`].
///
/// This storage type allows convenient and type-safe storage of a
/// [`BlockHash`].
#[derive(Clone, Debug)]
pub struct StorageBlockHash {
slot: U256,
cached: OnceCell<BlockHash>,
__stylus_host: *const dyn Host,
}
impl HostAccess for StorageBlockHash {
fn vm(&self) -> alloc::boxed::Box<dyn crate::host::Host> {
unsafe { alloc::boxed::Box::from_raw(self.__stylus_host as *mut dyn crate::host::Host) }
}
}
impl StorageBlockHash {
/// Gets the underlying [`BlockHash`] in persistent storage.
pub fn get(&self) -> BlockHash {
**self
}
/// Sets the underlying [`BlockHash`] in persistent storage.
pub fn set(&mut self, value: BlockHash) {
overwrite_cell(&mut self.cached, value);
unsafe { Storage::set_word(&self.vm(), self.slot, value) }
}
}
impl StorageType for StorageBlockHash {
type Wraps<'a> = BlockHash;
type WrapsMut<'a> = StorageGuardMut<'a, Self>;
unsafe fn new(slot: U256, _offset: u8, host: *const dyn crate::host::Host) -> Self {
let cached = OnceCell::new();
Self {
slot,
cached,
__stylus_host: host,
}
}
fn load<'s>(self) -> Self::Wraps<'s> {
self.get()
}
fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
StorageGuardMut::new(self)
}
}
impl<'a> SimpleStorageType<'a> for StorageBlockHash {
fn set_by_wrapped(&mut self, value: Self::Wraps<'a>) {
self.set(value);
}
}
impl Erase for StorageBlockHash {
fn erase(&mut self) {
self.set(Self::Wraps::ZERO);
}
}
impl Deref for StorageBlockHash {
type Target = BlockHash;
fn deref(&self) -> &Self::Target {
self.cached
.get_or_init(|| Storage::get_word(&self.vm(), self.slot))
}
}
impl From<StorageBlockHash> for BlockHash {
fn from(value: StorageBlockHash) -> Self {
*value
}
}
/// We implement `StorageType` for `PhantomData` so that storage types can be generic.
impl<T> StorageType for PhantomData<T> {
type Wraps<'a>
= Self
where
Self: 'a;
type WrapsMut<'a>
= Self
where
Self: 'a;
const REQUIRED_SLOTS: usize = 0;
const SLOT_BYTES: usize = 0;
unsafe fn new(_slot: U256, _offset: u8, _host: *const dyn crate::host::Host) -> Self {
Self
}
fn load<'s>(self) -> Self::Wraps<'s>
where
Self: 's,
{
self
}
fn load_mut<'s>(self) -> Self::WrapsMut<'s>
where
Self: 's,
{
self
}
}
================================================
File: stylus-sdk/src/storage/traits.rs
================================================
// Copyright 2022-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use alloy_primitives::{FixedBytes, Signed, Uint, B256, U256};
use core::{
marker::PhantomData,
ops::{Deref, DerefMut},
ptr,
};
use derivative::Derivative;
/// Accessor trait that lets a type be used in persistent storage.
/// Users can implement this trait to add novel data structures to their contract definitions.
/// The Stylus SDK by default provides only solidity types, which are represented [`the same way`].
///
/// [`the same way`]: https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html
pub trait StorageType: Sized {
/// For primitive types, this is the type being stored.
/// For collections, this is the [`StorageType`] being collected.
type Wraps<'a>: 'a
where
Self: 'a;
/// Mutable accessor to the type being stored.
type WrapsMut<'a>: 'a
where
Self: 'a;
/// The number of bytes in a slot needed to represent the type. Must not exceed 32.
/// For types larger than 32 bytes that are stored inline with a struct's fields,
/// set this to 32 and return the full size in [`StorageType::new`].
///
/// For implementing collections, see how Solidity slots are assigned for [`Arrays and Maps`] and their
/// Stylus equivalents [`StorageVec`](super::StorageVec) and [`StorageMap`](super::StorageMap).
/// For multi-word, but still fixed-size types, see the implementations for structs
/// and [`StorageArray`](super::StorageArray).
///
/// [`Arrays and Maps`]: https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#mappings-and-dynamic-arrays
const SLOT_BYTES: usize = 32;
/// The number of words this type must fill. For primitives this is always 0.
/// For complex types requiring more than one inline word, set this to the total size.
const REQUIRED_SLOTS: usize = 0;
/// Where in persistent storage the type should live. Although useful for framework designers
/// creating new storage types, most user programs shouldn't call this.
/// Note: implementations will have to be `const` once [`generic_const_exprs`] stabilizes.
///
/// # Safety
///
/// Aliases storage if two calls to the same slot and offset occur within the same lifetime.
///
/// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560
unsafe fn new(slot: U256, offset: u8, host: *const dyn crate::host::Host) -> Self;
/// Load the wrapped type, consuming the accessor.
/// Note: most types have a `get` and/or `getter`, which don't consume `Self`.
fn load<'s>(self) -> Self::Wraps<'s>
where
Self: 's;
/// Load the wrapped mutable type, consuming the accessor.
/// Note: most types have a `set` and/or `setter`, which don't consume `Self`.
fn load_mut<'s>(self) -> Self::WrapsMut<'s>
where
Self: 's;
}
/// Trait for accessors that can be used to completely erase their underlying value.
/// Note that some collections, like [`StorageMap`](super::StorageMap), don't implement this trait.
pub trait Erase: StorageType {
/// Erase the value from persistent storage.
fn erase(&mut self);
}
/// Trait for simple accessors that store no more than their wrapped value.
/// The type's representation must be entirely inline, or storage leaks become possible.
/// Note: it is a logic error if erasure does anything more than writing the zero-value.
pub trait SimpleStorageType<'a>: StorageType + Erase + Into<Self::Wraps<'a>>
where
Self: 'a,
{
/// Write the value to persistent storage.
fn set_by_wrapped(&mut self, value: Self::Wraps<'a>);
}
/// Trait for top-level storage types, usually implemented by proc macros.
/// Top-level types are special in that their lifetimes track the entirety
/// of all the EVM state-changes throughout a contract invocation.
///
/// To prevent storage aliasing during reentrancy, you must hold a reference
/// to such a type when making an EVM call. This may change in the future
/// for programs that prevent reentrancy.
///
/// # Safety
///
/// The type must be top-level to prevent storage aliasing.
pub unsafe trait TopLevelStorage {}
/// Binds a storage accessor to a lifetime to prevent aliasing.
/// Because this type doesn't implement `DerefMut`, mutable methods on the accessor aren't available.
/// For a mutable accessor, see [`StorageGuardMut`].
#[derive(Derivative)]
#[derivative(Debug = "transparent")]
pub struct StorageGuard<'a, T: 'a> {
inner: T,
#[derivative(Debug = "ignore")]
marker: PhantomData<&'a T>,
}
impl<'a, T: 'a> StorageGuard<'a, T> {
/// Creates a new storage guard around an arbitrary type.
pub fn new(inner: T) -> Self {
Self {
inner,
marker: PhantomData,
}
}
/// Get the underlying `T` directly, bypassing the borrow checker.
///
/// # Safety
///
/// Enables storage aliasing.
pub unsafe fn into_raw(self) -> T {
self.inner
}
}
impl<'a, T: 'a> Deref for StorageGuard<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
/// Binds a storage accessor to a lifetime to prevent aliasing.
pub struct StorageGuardMut<'a, T: 'a> {
inner: T,
marker: PhantomData<&'a T>,
}
impl<'a, T: 'a> StorageGuardMut<'a, T> {
/// Creates a new storage guard around an arbitrary type.
pub fn new(inner: T) -> Self {
Self {
inner,
marker: PhantomData,
}
}
/// Get the underlying `T` directly, bypassing the borrow checker.
///
/// # Safety
///
/// Enables storage aliasing.
pub unsafe fn into_raw(self) -> T {
self.inner
}
}
impl<'a, T: 'a> Deref for StorageGuardMut<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<'a, T: 'a> DerefMut for StorageGuardMut<'a, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
/// Trait for managing access to persistent storage.
/// Implemented by the [`StorageCache`](super::StorageCache) type.
pub trait GlobalStorage {
/// Retrieves `N ≤ 32` bytes from persistent storage, performing [`SLOAD`]'s only as needed.
/// The bytes are read from slot `key`, starting `offset` bytes from the left.
/// Note that the bytes must exist within a single, 32-byte EVM word.
///
/// # Safety
///
/// UB if the read would cross a word boundary.
/// May become safe when Rust stabilizes [`generic_const_exprs`].
///
/// [`SLOAD`]: https://www.evm.codes/#54
/// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560
unsafe fn get<const N: usize>(
host: &alloc::boxed::Box<dyn crate::host::Host>,
key: U256,
offset: usize,
) -> FixedBytes<N> {
debug_assert!(N + offset <= 32);
let word = Self::get_word(host, key);
let value = &word[offset..][..N];
FixedBytes::from_slice(value)
}
/// Retrieves a [`Uint`] from persistent storage, performing [`SLOAD`]'s only as needed.
/// The integer's bytes are read from slot `key`, starting `offset` bytes from the left.
/// Note that the bytes must exist within a single, 32-byte EVM word.
///
/// # Safety
///
/// UB if the read would cross a word boundary.
/// May become safe when Rust stabilizes [`generic_const_exprs`].
///
/// [`SLOAD`]: https://www.evm.codes/#54
/// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560
unsafe fn get_uint<const B: usize, const L: usize>(
host: &alloc::boxed::Box<dyn crate::host::Host>,
key: U256,
offset: usize,
) -> Uint<B, L> {
debug_assert!(B / 8 + offset <= 32);
let word = Self::get_word(host, key);
let value = &word[offset..][..B / 8];
Uint::try_from_be_slice(value).unwrap()
}
/// Retrieves a [`Signed`] from persistent storage, performing [`SLOAD`]'s only as needed.
/// The integer's bytes are read from slot `key`, starting `offset` bytes from the left.
/// Note that the bytes must exist within a single, 32-byte EVM word.
///
/// # Safety
///
/// UB if the read would cross a word boundary.
/// May become safe when Rust stabilizes [`generic_const_exprs`].
///
/// [`SLOAD`]: https://www.evm.codes/#54
/// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560
unsafe fn get_signed<const B: usize, const L: usize>(
host: &alloc::boxed::Box<dyn crate::host::Host>,
key: U256,
offset: usize,
) -> Signed<B, L> {
Signed::from_raw(Self::get_uint(host, key, offset))
}
/// Retrieves a [`u8`] from persistent storage, performing [`SLOAD`]'s only as needed.
/// The byte is read from slot `key`, starting `offset` bytes from the left.
///
/// # Safety
///
/// UB if the read is out of bounds.
/// May become safe when Rust stabilizes [`generic_const_exprs`].
///
/// [`SLOAD`]: https://www.evm.codes/#54
/// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560
unsafe fn get_byte(
host: &alloc::boxed::Box<dyn crate::host::Host>,
key: U256,
offset: usize,
) -> u8 {
debug_assert!(offset <= 32);
let word = Self::get::<1>(host, key, offset);
word[0]
}
/// Retrieves a [`Signed`] from persistent storage, performing [`SLOAD`]'s only as needed.
/// The integer's bytes are read from slot `key`, starting `offset` bytes from the left.
/// Note that the bytes must exist within a single, 32-byte EVM word.
///
/// # Safety
///
/// UB if the read would cross a word boundary.
/// May become safe when Rust stabilizes [`generic_const_exprs`].
///
/// [`SLOAD`]: https://www.evm.codes/#54
/// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560
fn get_word(host: &alloc::boxed::Box<dyn crate::host::Host>, key: U256) -> B256;
/// Writes `N ≤ 32` bytes to persistent storage, performing [`SSTORE`]'s only as needed.
/// The bytes are written to slot `key`, starting `offset` bytes from the left.
/// Note that the bytes must be written to a single, 32-byte EVM word.
///
/// # Safety
///
/// UB if the write would cross a word boundary.
/// Aliases if called during the lifetime an overlapping accessor.
///
/// [`SSTORE`]: https://www.evm.codes/#55
unsafe fn set<const N: usize>(
host: &alloc::boxed::Box<dyn crate::host::Host>,
key: U256,
offset: usize,
value: FixedBytes<N>,
) {
debug_assert!(N + offset <= 32);
if N == 32 {
return Self::set_word(host, key, FixedBytes::from_slice(value.as_slice()));
}
let mut word = Self::get_word(host, key);
let dest = word[offset..].as_mut_ptr();
ptr::copy(value.as_ptr(), dest, N);
Self::set_word(host, key, word);
}
/// Writes a [`Uint`] to persistent storage, performing [`SSTORE`]'s only as needed.
/// The integer's bytes are written to slot `key`, starting `offset` bytes from the left.
/// Note that the bytes must be written to a single, 32-byte EVM word.
///
/// # Safety
///
/// UB if the write would cross a word boundary.
/// Aliases if called during the lifetime an overlapping accessor.
///
/// [`SSTORE`]: https://www.evm.codes/#55
unsafe fn set_uint<const B: usize, const L: usize>(
host: &alloc::boxed::Box<dyn crate::host::Host>,
key: U256,
offset: usize,
value: Uint<B, L>,
) {
debug_assert!(B % 8 == 0);
debug_assert!(B / 8 + offset <= 32);
if B == 256 {
return Self::set_word(
host,
key,
FixedBytes::from_slice(&value.to_be_bytes::<32>()),
);
}
let mut word = Self::get_word(host, key);
let value = value.to_be_bytes_vec();
let dest = word[offset..].as_mut_ptr();
ptr::copy(value.as_ptr(), dest, B / 8);
Self::set_word(host, key, word);
}
/// Writes a [`Signed`] to persistent storage, performing [`SSTORE`]'s only as needed.
/// The bytes are written to slot `key`, starting `offset` bytes from the left.
/// Note that the bytes must be written to a single, 32-byte EVM word.
///
/// # Safety
///
/// UB if the write would cross a word boundary.
/// Aliases if called during the lifetime an overlapping accessor.
///
/// [`SSTORE`]: https://www.evm.codes/#55
unsafe fn set_signed<const B: usize, const L: usize>(
host: &alloc::boxed::Box<dyn crate::host::Host>,
key: U256,
offset: usize,
value: Signed<B, L>,
) {
Self::set_uint(host, key, offset, value.into_raw())
}
/// Writes a [`u8`] to persistent storage, performing [`SSTORE`]'s only as needed.
/// The byte is written to slot `key`, starting `offset` bytes from the left.
///
/// # Safety
///
/// UB if the write is out of bounds.
/// Aliases if called during the lifetime an overlapping accessor.
///
/// [`SSTORE`]: https://www.evm.codes/#55
unsafe fn set_byte(
host: &alloc::boxed::Box<dyn crate::host::Host>,
key: U256,
offset: usize,
value: u8,
) {
let fixed = FixedBytes::from_slice(&[value]);
Self::set::<1>(host, key, offset, fixed)
}
/// Stores a 32-byte EVM word to persistent storage, performing [`SSTORE`]'s only as needed.
///
/// # Safety
///
/// Aliases if called during the lifetime an overlapping accessor.
///
/// [`SSTORE`]: https://www.evm.codes/#55
unsafe fn set_word(host: &alloc::boxed::Box<dyn crate::host::Host>, key: U256, value: B256);
/// Clears the 32-byte word at the given key, performing [`SSTORE`]'s only as needed.
///
/// # Safety
///
/// Aliases if called during the lifetime an overlapping accessor.
///
/// [`SSTORE`]: https://www.evm.codes/#55
unsafe fn clear_word(host: &alloc::boxed::Box<dyn crate::host::Host>, key: U256) {
Self::set_word(host, key, B256::ZERO)
}
}
================================================
File: stylus-sdk/src/storage/vec.rs
================================================
// Copyright 2023-2024, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
use super::{
Erase, GlobalStorage, SimpleStorageType, Storage, StorageGuard, StorageGuardMut, StorageType,
};
use crate::{crypto, host::HostAccess};
use alloc::boxed::Box;
use alloy_primitives::U256;
use core::{cell::OnceCell, marker::PhantomData};
/// Accessor for a storage-backed vector.
pub struct StorageVec<S: StorageType> {
slot: U256,
base: OnceCell<U256>,
marker: PhantomData<S>,
__stylus_host: *const dyn crate::host::Host,
}
impl<S: StorageType> StorageType for StorageVec<S> {
type Wraps<'a>
= StorageGuard<'a, StorageVec<S>>
where
Self: 'a;
type WrapsMut<'a>
= StorageGuardMut<'a, StorageVec<S>>
where
Self: 'a;
unsafe fn new(slot: U256, offset: u8, host: *const dyn crate::host::Host) -> Self {
debug_assert!(offset == 0);
Self {
slot,
base: OnceCell::new(),
marker: PhantomData,
__stylus_host: host,
}
}
fn load<'s>(self) -> Self::Wraps<'s> {
StorageGuard::new(self)
}
fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
StorageGuardMut::new(self)
}
}
impl<S: StorageType> HostAccess for StorageVec<S> {
fn vm(&self) -> alloc::boxed::Box<dyn crate::host::Host> {
unsafe { alloc::boxed::Box::from_raw(self.__stylus_host as *mut dyn crate::host::Host) }
}
}
impl<S: StorageType> StorageVec<S> {
/// Returns `true` if the collection contains no elements.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Gets the number of elements stored.
pub fn len(&self) -> usize {
let word: U256 = Storage::get_word(&self.vm(), self.slot).into();
word.try_into().unwrap()
}
/// Overwrites the vector's length.
///
/// # Safety
///
/// It must be sensible to create accessors for `S` from zero-slots,
/// or any junk data left over from prior dirty operations.
/// Note that [`StorageVec`] has unlimited capacity, so all lengths are valid.
pub unsafe fn set_len(&mut self, len: usize) {
Storage::set_word(&self.vm(), self.slot, U256::from(len).into())
}
/// Gets an accessor to the element at a given index, if it exists.
///
/// Note: the accessor is protected by a [`StorageGuard`], which restricts
/// its lifetime to that of `&self`.
pub fn getter(&self, index: impl TryInto<usize>) -> Option<StorageGuard<S>> {
let store = unsafe { self.accessor(index)? };
Some(StorageGuard::new(store))
}
/// Gets a mutable accessor to the element at a given index, if it exists.
///
/// Note: the accessor is protected by a [`StorageGuardMut`], which restricts
/// its lifetime to that of `&mut self`.
pub fn setter(&mut self, index: impl TryInto<usize>) -> Option<StorageGuardMut<S>> {
let store = unsafe { self.accessor(index)? };
Some(StorageGuardMut::new(store))
}
/// Gets the underlying accessor to the element at a given index, if it exists.
///
/// # Safety
///
/// Enables aliasing.
unsafe fn accessor(&self, index: impl TryInto<usize>) -> Option<S> {
let index = index.try_into().ok()?;
if index >= self.len() {
return None;
}
let (slot, offset) = self.index_slot(index);
Some(S::new(slot, offset, Box::into_raw(self.vm())))
}
/// Gets the underlying accessor to the element at a given index, even if out of bounds.
///
/// # Safety
///
/// Enables aliasing. UB if out of bounds.
unsafe fn accessor_unchecked(&self, index: usize) -> S {
let (slot, offset) = self.index_slot(index);
S::new(slot, offset, Box::into_raw(self.vm()))
}
/// Gets the element at the given index, if it exists.
pub fn get(&self, index: impl TryInto<usize>) -> Option<S::Wraps<'_>> {
let store = unsafe { self.accessor(index)? };
Some(store.load())
}
/// Gets a mutable accessor to the element at a given index, if it exists.
pub fn get_mut(&mut self, index: impl TryInto<usize>) -> Option<S::WrapsMut<'_>> {
let store = unsafe { self.accessor(index)? };
Some(store.load_mut())
}
/// Like [`std::vec::Vec::push`][vec_push], but returns a mutable accessor to the new slot.
/// This enables pushing elements without constructing them first.
///
/// # Example
///
/// ```no_run
/// extern crate alloc;
/// use alloc::boxed::Box;
/// use stylus_sdk::storage::{StorageVec, StorageType, StorageU256};
/// use stylus_sdk::alloy_primitives::U256;
///
/// let host = Box::new(stylus_sdk::host::wasm::WasmHost {});
/// let mut vec: StorageVec<StorageVec<StorageU256>> = unsafe { StorageVec::new(U256::ZERO, 0, Box::into_raw(host)) };
/// let mut inner_vec = vec.grow();
/// inner_vec.push(U256::from(8));
///
/// let value = inner_vec.get(0).unwrap();
/// assert_eq!(value, U256::from(8));
/// assert_eq!(inner_vec.len(), 1);
/// ```
///
/// [vec_push]: https://doc.rust-lang.org/std/vec/struct.Vec.html#method.push
pub fn grow(&mut self) -> StorageGuardMut<S> {
let index = self.len();
unsafe { self.set_len(index + 1) };
let (slot, offset) = self.index_slot(index);
let store = unsafe { S::new(slot, offset, Box::into_raw(self.vm())) };
StorageGuardMut::new(store)
}
/// Removes and returns an accessor to the last element of the vector, if any.
pub fn shrink(&mut self) -> Option<StorageGuardMut<S>> {
let index = match self.len() {
0 => return None,
x => x - 1,
};
unsafe {
self.set_len(index);
Some(StorageGuardMut::new(self.accessor_unchecked(index)))
}
}
/// Shortens the vector, keeping the first `len` elements.
///
/// Note: this method does not erase any underlying storage.
pub fn truncate(&mut self, len: usize) {
if len < self.len() {
// SAFETY: operation leaves only existing values
unsafe { self.set_len(len) }
}
}
/// Determines the slot and offset for the element at an index.
fn index_slot(&self, index: usize) -> (U256, u8) {
let width = S::SLOT_BYTES;
let words = S::REQUIRED_SLOTS.max(1);
let density = self.density();
let slot = self.base() + U256::from(words * index / density);
let offset = 32 - (width * (1 + index % density)) as u8;
(slot, offset)
}
/// Number of elements per slot.
const fn density(&self) -> usize {
32 / S::SLOT_BYTES
}
/// 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())
}
}
impl<'a, S: SimpleStorageType<'a>> StorageVec<S> {
/// Adds an element to the end of the vector.
pub fn push(&mut self, value: S::Wraps<'a>) {
let mut store = self.grow();
store.set_by_wrapped(value);
}
/// Removes and returns the last element of the vector, if it exists.
///
/// Note: the underlying storage slot is erased when all elements in a word are freed.
pub fn pop(&mut self) -> Option<S::Wraps<'a>> {
let store = unsafe { self.shrink()?.into_raw() };
let index = self.len();
let value = store.into();
let first = index % self.density() == 0;
if first {
let slot = self.index_slot(index).0;
let words = S::REQUIRED_SLOTS.max(1);
for i in 0..words {
unsafe { Storage::clear_word(&self.vm(), slot + U256::from(i)) };
}
}
Some(value)
}
}
impl<S: Erase> StorageVec<S> {
/// Removes and erases the last element of the vector.
pub fn erase_last(&mut self) {
if self.is_empty() {
return;
}
let index = self.len() - 1;
unsafe {
self.accessor_unchecked(index).erase();
self.set_len(index);
}
}
}
impl<S: Erase> Erase for StorageVec<S> {
fn erase(&mut self) {
for i in 0..self.len() {
let mut store = unsafe { self.accessor_unchecked(i) };
store.erase()
}
self.truncate(0);
}
}
impl<'a, S: SimpleStorageType<'a>> Extend<S::Wraps<'a>> for StorageVec<S> {
fn extend<T: IntoIterator<Item = S::Wraps<'a>>>(&mut self, iter: T) {
for elem in iter {
self.push(elem);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment