Created
January 22, 2025 03:12
-
-
Save anegg0/693c25189175b950a64c6f8d5dbae314 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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