├── .github
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── check.yml
│ ├── integration.yml
│ ├── linux.yml
│ ├── mac.yml
│ └── smoke-test.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.lock
├── Cargo.toml
├── README.md
├── ci
├── build_and_test.sh
├── integration_test.sh
└── smoke_test.sh
├── examples
├── constructor
├── erc20
│ ├── .cargo
│ │ └── config.toml
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── node_modules
│ │ └── .yarn-integrity
│ ├── rust-toolchain.toml
│ ├── scripts
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src
│ │ │ └── mint.js
│ │ └── yarn.lock
│ ├── src
│ │ ├── erc20.rs
│ │ ├── lib.rs
│ │ └── main.rs
│ ├── tests
│ │ └── erc20_integration_test.rs
│ └── yarn.lock
├── erc721
│ ├── .cargo
│ │ └── config.toml
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── rust-toolchain.toml
│ ├── scripts
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src
│ │ │ └── mint.js
│ │ └── yarn.lock
│ ├── src
│ │ ├── erc721.rs
│ │ ├── lib.rs
│ │ └── main.rs
│ └── tests
│ │ └── erc721_integration_test.rs
└── single_call
│ ├── .cargo
│ └── config.toml
│ ├── .gitignore
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── README.md
│ ├── rust-toolchain.toml
│ ├── scripts
│ ├── .env.example
│ ├── .gitignore
│ ├── README.md
│ ├── external_contracts
│ │ └── Counter.sol
│ ├── hardhat.config.js
│ ├── ignition
│ │ └── modules
│ │ │ └── deploy_counter.js
│ ├── package.json
│ ├── src
│ │ └── main.js
│ └── yarn.lock
│ ├── src
│ ├── lib.rs
│ └── main.rs
│ └── tests
│ └── single_call_integration_test.rs
├── licenses
├── Apache-2.0
├── COPYRIGHT.md
├── DCO.txt
└── MIT
├── mini-alloc
├── .cargo
│ └── config.toml
├── Cargo.toml
├── README.md
├── src
│ ├── imp.rs
│ └── lib.rs
└── tests
│ └── misc.rs
├── rust-toolchain.toml
├── stylus-core
├── Cargo.toml
└── src
│ ├── calls
│ ├── errors.rs
│ └── mod.rs
│ ├── host.rs
│ ├── lib.rs
│ └── storage.rs
├── stylus-proc
├── Cargo.toml
├── README.md
├── src
│ ├── consts.rs
│ ├── impls
│ │ ├── abi_proxy.rs
│ │ └── mod.rs
│ ├── imports.rs
│ ├── lib.rs
│ ├── macros
│ │ ├── derive
│ │ │ ├── abi_type.rs
│ │ │ ├── erase.rs
│ │ │ ├── mod.rs
│ │ │ └── solidity_error
│ │ │ │ ├── export_abi.rs
│ │ │ │ └── mod.rs
│ │ ├── entrypoint.rs
│ │ ├── mod.rs
│ │ ├── public
│ │ │ ├── attrs.rs
│ │ │ ├── export_abi.rs
│ │ │ ├── mod.rs
│ │ │ ├── overrides.rs
│ │ │ └── types.rs
│ │ ├── sol_interface.rs
│ │ ├── sol_storage
│ │ │ ├── mod.rs
│ │ │ └── proc.rs
│ │ └── storage.rs
│ ├── types.rs
│ └── utils
│ │ ├── attrs.rs
│ │ ├── mod.rs
│ │ └── testing.rs
└── tests
│ ├── derive_abi_type.rs
│ ├── derive_erase.rs
│ ├── derive_solidity_error.rs
│ ├── fail
│ ├── derive_abi_type
│ │ ├── missing_sol_macro.rs
│ │ └── missing_sol_macro.stderr
│ ├── derive_solidity_error
│ │ ├── invalid_variants.rs
│ │ └── invalid_variants.stderr
│ ├── public
│ │ ├── constructor.rs
│ │ ├── constructor.stderr
│ │ ├── generated.rs
│ │ ├── generated.stderr
│ │ ├── macro_errors.rs
│ │ └── macro_errors.stderr
│ └── sol_interface
│ │ ├── generated.rs
│ │ ├── generated.stderr
│ │ ├── macro_errors.rs
│ │ └── macro_errors.stderr
│ ├── mutating_call_context_safety.rs
│ ├── public.rs
│ ├── public_composition.rs
│ └── sol_interface.rs
├── stylus-sdk
├── Cargo.lock
├── Cargo.toml
├── README.md
└── src
│ ├── abi
│ ├── bytes.rs
│ ├── const_string.rs
│ ├── export
│ │ ├── internal.rs
│ │ └── mod.rs
│ ├── impls.rs
│ ├── internal.rs
│ ├── ints.rs
│ └── mod.rs
│ ├── call
│ ├── mod.rs
│ ├── raw.rs
│ └── transfer.rs
│ ├── crypto.rs
│ ├── debug.rs
│ ├── deploy
│ ├── mod.rs
│ └── raw.rs
│ ├── evm.rs
│ ├── host
│ └── mod.rs
│ ├── hostio.rs
│ ├── lib.rs
│ ├── methods.rs
│ ├── prelude.rs
│ ├── storage
│ ├── array.rs
│ ├── bytes.rs
│ ├── map.rs
│ ├── mod.rs
│ ├── traits.rs
│ └── vec.rs
│ └── util.rs
├── stylus-test
├── Cargo.toml
├── README.md
└── src
│ ├── builder.rs
│ ├── constants.rs
│ ├── lib.rs
│ ├── state.rs
│ └── vm.rs
└── stylus-tools
├── Cargo.toml
└── src
├── cargo_stylus.rs
├── devnet.rs
└── lib.rs
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 | Please provide a summary of the changes and any backward incompatibilities.
4 |
5 | ## Checklist
6 |
7 | - [ ] I have documented these changes where necessary.
8 | - [ ] I have read the [DCO][DCO] and ensured that these changes comply.
9 | - [ ] I assign this work under its [open source licensing][terms].
10 |
11 | [DCO]: https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/DCO.txt
12 | [terms]: https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md
13 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - main
5 | pull_request:
6 | workflow_dispatch:
7 |
8 | name: check
9 | jobs:
10 | fmt:
11 | runs-on: ubuntu-latest
12 | name: stable / fmt
13 | steps:
14 | - uses: actions/checkout@v3
15 | with:
16 | submodules: true
17 | - name: Install stable
18 | uses: dtolnay/rust-toolchain@1.87
19 | with:
20 | components: rustfmt
21 | - name: cargo fmt --check
22 | uses: actions-rs/cargo@v1
23 | with:
24 | command: fmt
25 | args: --check
26 | clippy:
27 | runs-on: ubuntu-latest
28 | name: ${{ matrix.toolchain }} / clippy
29 | strategy:
30 | fail-fast: false
31 | matrix:
32 | toolchain: [stable, beta]
33 | steps:
34 | - uses: actions/checkout@v3
35 | with:
36 | submodules: true
37 | - name: Install ${{ matrix.toolchain }}
38 | uses: dtolnay/rust-toolchain@1.87
39 | with:
40 | toolchain: ${{ matrix.toolchain }}
41 | components: clippy
42 | - name: cargo clippy
43 | run: cargo clippy --all-targets --all-features -- -D warnings
44 |
--------------------------------------------------------------------------------
/.github/workflows/integration.yml:
--------------------------------------------------------------------------------
1 | name: integration
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | workflow_dispatch:
8 |
9 | jobs:
10 | setup-cargo-stylus:
11 | name: Setup Cargo Stylus
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Install Rust toolchain
15 | shell: bash
16 | run: |
17 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup-init.sh
18 | sh rustup-init.sh -y --default-toolchain 1.87.0
19 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH
20 | source "$HOME/.cargo/env"
21 |
22 | - name: Install cargo-stylus
23 | shell: bash
24 | run: |
25 | RUSTFLAGS="-C link-args=-rdynamic" cargo install --force cargo-stylus
26 |
27 | - name: Archive cargo-stylus binary
28 | uses: actions/upload-artifact@v4
29 | with:
30 | name: cargo-stylus-binary
31 | path: ~/.cargo/bin/cargo-stylus
32 | retention-days: 1 # Keep artifact for 1 day
33 |
34 | integration-test-example:
35 | name: Integration Test (Example ${{ matrix.test }})
36 | runs-on: ubuntu-latest
37 | needs: setup-cargo-stylus
38 | strategy:
39 | matrix:
40 | test:
41 | - "constructor"
42 | - "erc20"
43 | - "erc721"
44 | - "single_call"
45 | steps:
46 | - name: Checkout code
47 | uses: actions/checkout@v3
48 |
49 | - name: Install Rust toolchain and add wasm target
50 | shell: bash
51 | run: |
52 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup-init.sh
53 | sh rustup-init.sh -y --default-toolchain 1.87.0
54 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH
55 | source "$HOME/.cargo/env"
56 | rustup target add wasm32-unknown-unknown
57 |
58 | - name: Download cargo-stylus binary
59 | uses: actions/download-artifact@v4
60 | with:
61 | name: cargo-stylus-binary
62 | path: ~/.cargo/bin
63 |
64 | - name: Make cargo-stylus executable
65 | shell: bash
66 | run: chmod +x ~/.cargo/bin/cargo-stylus
67 |
68 | - name: Run integration tests for ${{ matrix.test }}
69 | shell: bash
70 | run: ./ci/integration_test.sh --example ${{ matrix.test }}
71 |
72 | integration-test-stylus-tools:
73 | name: Integration Test (Stylus Tools)
74 | runs-on: ubuntu-latest
75 | needs: setup-cargo-stylus
76 | steps:
77 | - name: Checkout code
78 | uses: actions/checkout@v3
79 |
80 | - name: Install Rust toolchain and add wasm target
81 | shell: bash
82 | run: |
83 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup-init.sh
84 | sh rustup-init.sh -y --default-toolchain 1.87.0
85 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH
86 | source "$HOME/.cargo/env"
87 | rustup target add wasm32-unknown-unknown
88 |
89 | - name: Download cargo-stylus binary
90 | uses: actions/download-artifact@v4
91 | with:
92 | name: cargo-stylus-binary
93 | path: ~/.cargo/bin
94 |
95 | - name: Make cargo-stylus executable
96 | shell: bash
97 | run: chmod +x ~/.cargo/bin/cargo-stylus
98 |
99 | - name: Run integration tests for Stylus Tools
100 | shell: bash
101 | run: ./ci/integration_test.sh --stylus-tools
102 |
--------------------------------------------------------------------------------
/.github/workflows/linux.yml:
--------------------------------------------------------------------------------
1 | name: linux
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | workflow_dispatch:
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | name: (${{ matrix.target }}, ${{ matrix.cfg_release_channel }})
13 | env:
14 | CFG_RELEASE_CHANNEL: ${{ matrix.cfg_release_channel }}
15 | strategy:
16 | matrix:
17 | target: [
18 | x86_64-unknown-linux-gnu,
19 | ]
20 | cfg_release_channel: [nightly, stable]
21 |
22 | steps:
23 | - name: checkout
24 | uses: actions/checkout@v3
25 |
26 | # Run build
27 | - name: install rustup
28 | run: |
29 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup-init.sh
30 | sh rustup-init.sh -y --default-toolchain none
31 | rustup target add ${{ matrix.target }}
32 |
33 | - name: Build and Test
34 | run: ./ci/build_and_test.sh
35 |
--------------------------------------------------------------------------------
/.github/workflows/mac.yml:
--------------------------------------------------------------------------------
1 | name: mac
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | workflow_dispatch:
8 |
9 | jobs:
10 | test:
11 | runs-on: macos-latest
12 | name: (${{ matrix.target }}, ${{ matrix.cfg_release_channel }})
13 | env:
14 | CFG_RELEASE_CHANNEL: ${{ matrix.cfg_release_channel }}
15 | strategy:
16 | fail-fast: false
17 | matrix:
18 | target: [
19 | aarch64-apple-darwin,
20 | ]
21 | cfg_release_channel: [nightly, stable]
22 |
23 | steps:
24 | - name: checkout
25 | uses: actions/checkout@v3
26 |
27 | # Run build
28 | - name: install rustup
29 | run: |
30 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup-init.sh
31 | sh rustup-init.sh -y --default-toolchain none
32 | rustup target add ${{ matrix.target }}
33 |
34 | - name: Build and Test
35 | run: ./ci/build_and_test.sh
36 |
--------------------------------------------------------------------------------
/.github/workflows/smoke-test.yml:
--------------------------------------------------------------------------------
1 | name: Smoke Test
2 |
3 | on:
4 | pull_request:
5 | workflow_dispatch:
6 | push:
7 | branches:
8 | - main
9 | - ci-testing
10 |
11 | jobs:
12 | smoke-test:
13 | runs-on: ubuntu-latest
14 | name: 'Test (Smoke)(${{ matrix.cfg_release_channel }})'
15 | env:
16 | CFG_RELEASE_CHANNEL: ${{ matrix.cfg_release_channel }}
17 | PRIV_KEY: ${{ secrets.SEPOLIA_PRIVATE_KEY }}
18 | strategy:
19 | matrix:
20 | target: [wasm32-unknown-unknown]
21 | cfg_release_channel: [nightly, stable]
22 | steps:
23 | - name: checkout
24 | uses: actions/checkout@v3
25 | - name: install rustup
26 | run: |
27 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup-init.sh
28 | sh rustup-init.sh -y --default-toolchain none
29 | rustup target add ${{ matrix.target }}
30 |
31 | - name: Cargo stylus
32 | run: |
33 | RUSTFLAGS="-C link-args=-rdynamic" cargo install --force cargo-stylus
34 |
35 | - uses: OffchainLabs/actions/run-nitro-test-node@main
36 | with:
37 | nitro-testnode-ref: release
38 | no-token-bridge: true
39 | # no-simple must be false for now as our nitro testnode branch doesn't have that option
40 | no-simple: false
41 |
42 | - name: Smoke Test Deployment
43 | run: ./ci/smoke_test.sh
44 |
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | .config/spellcheck.toml
3 | .config/stylus.dic
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | These crates follow [semver](https://semver.org).
4 |
5 | ## [0.8.3](https://github.com/OffchainLabs/stylus-sdk-rs/releases/tag/v0.8.3) - 2025-03-18
6 |
7 | ### Fixed
8 |
9 | - Fix stylus SDK dependencies in wasm32
10 | - Do not require contract crate to define stylus-test feature
11 |
12 | ## [0.8.2](https://github.com/OffchainLabs/stylus-sdk-rs/releases/tag/v0.8.2) - 2025-03-11
13 |
14 | ### Fixed
15 |
16 | - Fix cargo stylus replay [#226](https://github.com/OffchainLabs/stylus-sdk-rs/pull/226)
17 |
18 | ## [0.8.1](https://github.com/OffchainLabs/stylus-sdk-rs/releases/tag/v0.8.1) - 2025-02-21
19 |
20 | ### Fixed
21 |
22 | - Add Reentrant Feature to Stylus Test When Enabled in SDK [#221](https://github.com/OffchainLabs/stylus-sdk-rs/pull/221)
23 |
24 | ## [0.8.0](https://github.com/OffchainLabs/stylus-sdk-rs/releases/tag/v0.8.0) - 2025-02-12
25 |
26 | ### Added
27 |
28 | - Define a Host Trait for the Stylus SDK [#199](https://github.com/OffchainLabs/stylus-sdk-rs/pull/199)
29 | - Define Initial WasmHost Implementation for Wasm Targets [#200](https://github.com/OffchainLabs/stylus-sdk-rs/pull/200)
30 | - Use a Boxed, Dyn Host Trait for Contract Initialization [#203](https://github.com/OffchainLabs/stylus-sdk-rs/pull/203)
31 | - Define Testing VM to Implement the Mock Trait [#204](https://github.com/OffchainLabs/stylus-sdk-rs/pull/204)
32 | - Rename Stylus-Host to Stylus-Core and Make Calls Part of the VM [#206](https://github.com/OffchainLabs/stylus-sdk-rs/pull/206)
33 | - Make Deployment Logic Part of the Host Trait [#207](https://github.com/OffchainLabs/stylus-sdk-rs/pull/207)
34 | - Add unit tests to storage bytes [#213](https://github.com/OffchainLabs/stylus-sdk-rs/pull/213)
35 | - Add Missing Methods to Host Trait [#210](https://github.com/OffchainLabs/stylus-sdk-rs/pull/210)
36 | - Reduce Wasm Code Size Impact of Host Trait [#216](https://github.com/OffchainLabs/stylus-sdk-rs/pull/216)
37 | - Add a Powerful Test VM [#212](https://github.com/OffchainLabs/stylus-sdk-rs/pull/212)
38 |
39 | ### Changed
40 |
41 | - Deprecate Old Hostios and Improve TestVM Ergonomics [#209](https://github.com/OffchainLabs/stylus-sdk-rs/pull/209)
42 | - Minimize calls to storage for bytes/string [#217](https://github.com/OffchainLabs/stylus-sdk-rs/pull/217)
43 | - Make CI fail for clippy warnings [#220](https://github.com/OffchainLabs/stylus-sdk-rs/pull/220)
44 | - v0.8.0 Release Candidate [#218](https://github.com/OffchainLabs/stylus-sdk-rs/pull/218)
45 |
46 | ### Fixed
47 |
48 | - Fix storage bytes set-len when shrinking [#211](https://github.com/OffchainLabs/stylus-sdk-rs/pull/211)
49 | - Fix examples and doctest [#219](https://github.com/OffchainLabs/stylus-sdk-rs/pull/219)
50 |
51 | ## [0.7.0](https://github.com/OffchainLabs/stylus-sdk-rs/releases/tag/v0.7.0) - 2025-02-03
52 |
53 | ### Added
54 |
55 | - `impl From for stylus_sdk::Bytes`
56 | - Support for integer types from `alloy_primitives`
57 | - Fallback/receive functionality for routers created using `#[public]`
58 |
59 | ### Changed
60 |
61 | - Upgrade alloy dependency to `0.8.14`
62 | - Allow struct references within `sol_interface!` macro
63 | - `pub` structs in `sol_interface!` macro
64 | - Refactor of proc macros for better maintainability and testability
65 |
66 |
67 | ## [0.6.0](https://github.com/OffchainLabs/stylus-sdk-rs/releases/tag/v0.6.0) - 2024-08-30
68 |
69 | ### Breaking Changes
70 |
71 | - `#[selector(id = ...)]` syntax has been removed to avoid misleading contracts
72 | from being implemented.
73 | - Several methods in `RawDeploy` which were not fully implemented yet
74 | - `#[pure]`, `#[view]` and `#[write]` attributes have been removed in favor of
75 | using arguments to infer state mutability.
76 | - `stylus-sdk` now ships with `mini-alloc` enabled by default. This means that
77 | a `#[global_allocator]` should not be declared in most cases. If a custom
78 | allocator is still needed the `mini-alloc` should be disabled (enabled by
79 | default).
80 | - `StorageU1` and `StorageI1` types have been removed.
81 |
82 | ### Deprecated
83 |
84 | - The `#[external]` macro is now deprecated in favor of `#[public]` which
85 | provides the same funcitonality.
86 | - The `#[solidity_storage]` macro is now deprecated in favor of `#[storage]`
87 | which provides the same functionality.
88 |
89 | ### Changed
90 |
91 | - Ensure consistency between proc macros when parsing attributes.
92 | - Update `sol_interface!` macro to report errors when using Solidity features
93 | which have not yet been implemented.
94 |
95 | ### Fixed
96 |
97 | - Properly encode bytes when calling external contracts.
98 | - Properly encode BYtes and strings in return types.
99 | - Bytes type now works properly in `export-abi`.
100 | - `export-abi` now works for contracts with no functions with returned values.
101 | - Off-by-one error when storing strings with length 32.
102 | - Interfaces in `sol_interface!` no longer incorrectly inherit functions from
103 | previous definitions.
104 |
105 | ### Documentation
106 |
107 | - Various documentation updates for clarity.
108 | - Cleaned up typos and moved TODOs to the github issue tracker.
109 |
110 | ### Security
111 |
112 | - Function signatures which generate the same selector values will now fail
113 | at compile-time to avoid misleading contract calls.
114 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = ["stylus-sdk", "stylus-proc", "stylus-test", "mini-alloc", "stylus-core", "stylus-tools"]
3 | resolver = "2"
4 |
5 | [workspace.package]
6 | version = "0.9.0"
7 | edition = "2021"
8 | authors = ["Offchain Labs"]
9 | license = "MIT OR Apache-2.0"
10 | homepage = "https://github.com/OffchainLabs/stylus-sdk-rs"
11 | repository = "https://github.com/OffchainLabs/stylus-sdk-rs"
12 | rust-version = "1.71.0"
13 |
14 | [workspace.dependencies]
15 | alloy-primitives = { version = "1.0.1", default-features = false , features = ["native-keccak"] }
16 | alloy-sol-types = { version = "1.0.1", default-features = false }
17 | cfg-if = "1.0.0"
18 | clap = { version = "4.5.4", features = [ "derive", "color" ] }
19 | derivative = { version = "2.2.0", features = ["use_core"] }
20 | hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
21 | keccak-const = "0.2.0"
22 | lazy_static = "1.5.0"
23 | sha3 = "0.10.8"
24 | rclite = "0.2.4"
25 |
26 | # proc macros
27 | convert_case = "0.6.0"
28 | paste = "1.0.15"
29 | proc-macro-error = "1.0"
30 | proc-macro2 = "1.0"
31 | quote = "1.0"
32 | regex = "1.10.6"
33 | syn = { version = "2.0.77", features = ["full", "visit-mut"] }
34 | syn-solidity = { version = "1.0.1", features = ["visit", "visit-mut"] }
35 |
36 | # proc macro dev
37 | pretty_assertions = "1.4.1"
38 | prettyplease = "0.2.22"
39 | trybuild = "1.0"
40 |
41 | # stylus tools
42 | alloy = "1.0.1"
43 | eyre = "0.6"
44 | reqwest = "0.12"
45 | testcontainers = { version = "0.23", features = ["http_wait"] }
46 | tokio = "1.44"
47 |
48 | # members
49 | mini-alloc = { path = "mini-alloc", version = "0.9.0" }
50 | stylus-sdk = { path = "stylus-sdk" }
51 | stylus-core = { path = "stylus-core", version = "0.9.0" }
52 | stylus-test = { path = "stylus-test", version = "0.9.0" }
53 | stylus-proc = { path = "stylus-proc", version = "0.9.0" }
54 | stylus-tools = { path = "stylus-tools", version = "0.9.0" }
55 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
The Stylus SDK
8 |
9 |
10 | Rust contracts on Arbitrum »
11 |
12 |
13 |
14 |
15 | ## Overview
16 |
17 | The Stylus SDK enables smart contract developers to write programs for **Arbitrum chains** written in the [Rust](https://www.rust-lang.org/tools/install) programming language. Stylus programs are compiled to [WebAssembly](https://webassembly.org/) and can then be deployed on-chain to execute alongside Solidity smart contracts. Stylus programs are not only orders of magnitude cheaper and faster but also enable what was thought to be previously impossible for WebAssembly: **EVM-interoperability**.
18 |
19 | For information about deploying Rust smart contracts, see the [Cargo Stylus CLI Tool](https://github.com/OffchainLabs/cargo-stylus). For more information about Stylus, see [Stylus: A Gentle Introduction](https://docs.arbitrum.io/stylus/gentle-introduction). For a simpler intro to Stylus Rust development, see the [Quick Start guide](https://docs.arbitrum.io/stylus/quickstart).
20 |
21 | Comprehensive documentation on the Rust SDK can be found [here](https://docs.arbitrum.io/stylus/rust-sdk-guide).
22 |
23 | This SDK has been audited in August 2024 at commit [#62bd831](https://github.com/OffchainLabs/stylus-sdk-rs/tree/62bd8318c7f3ab5be954cbc264f85bf2ba3f4b06) by OpenZepplin which can be viewed [here](https://blog.openzeppelin.com/stylus-rust-sdk-audit).
24 |
25 | ## Feature highlights
26 |
27 | The SDK makes it easy to develop Ethereum ABI-equivalent Stylus contracts in Rust. It provides a full suite of types and shortcuts that abstract away the details of Ethereum's storage layout, making it easy to _just write Rust_. For an in depth exploration of the features, please see comprehensive [Feature Overview][overview].
28 |
29 | Some of the features available in the SDK include:
30 |
31 | - **Generic**, storage-backed Rust types for programming **Solidity-equivalent** smart contracts with optimal storage caching.
32 | - Simple macros for writing **language-agnostic** methods and entrypoints.
33 | - Automatic export of Solidity interfaces for interoperability across programming languages.
34 | - Powerful **primitive types** backed by the feature-rich Alloy.
35 |
36 | 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.
37 |
38 | ```rust
39 | use stylus_sdk::{alloy_primitives::U256, prelude::*};
40 |
41 | // Generate Solidity-equivalent, Rust structs backed by storage.
42 | sol_storage! {
43 | #[entrypoint]
44 | pub struct Counter {
45 | uint256 number;
46 | }
47 | }
48 |
49 | #[public]
50 | impl Counter {
51 | // Gets the number value from storage.
52 | pub fn number(&self) -> Result> {
53 | Ok(self.number.get())
54 | }
55 |
56 | // Sets a number in storage to a user-specified value.
57 | pub fn set_number(&mut self, new_number: U256) -> Result<(), Vec> {
58 | self.number.set(new_number);
59 | Ok(())
60 | }
61 | }
62 | ```
63 |
64 | Additionally, the Stylus SDK supports `#[no_std]` for contracts that wish to opt out of the standard library. In fact, the entire SDK is available from `#[no_std]`, so no special feature flag is required. This can be helpful for reducing binary size, and may be preferable in pure-compute use cases like cryptography.
65 |
66 | Most users will want to use the standard library, which is available since the Stylus VM supports `rustc`'s `wasm32-unknown-unknown` target triple. In the future we may add `wasm32-wasi` too, along with floating point and SIMD, which the Stylus VM does not yet support.
67 |
68 | [overview]: https://docs.arbitrum.io/stylus/reference/rust-sdk-guide
69 |
70 | ## Don't know Rust?
71 |
72 | The Stylus VM supports more than just Rust. In fact, any programming language that compiles down to WebAssembly could in principle be deployed to Stylus-enabled chains. The table below includes the official ports of the SDK, with more coming soon.
73 |
74 | | Repo | Use cases | License |
75 | |:-----------------|:----------------------------|:------------------|
76 | | [Rust SDK][Rust] | Everything! | Apache 2.0 or MIT |
77 | | [C/C++ SDK][C] | Cryptography and algorithms | Apache 2.0 or MIT |
78 | | [Bf SDK][Bf] | Educational | Apache 2.0 or MIT |
79 | | [Cargo Stylus][CargoStylus] | Deploying Stylus programs | Apache 2.0 or MIT |
80 |
81 | Want to write your own? [Join us in the `#stylus` channel on discord][discord]!
82 |
83 | [Rust]: https://github.com/OffchainLabs/stylus-sdk-rs
84 | [C]: https://github.com/OffchainLabs/stylus-sdk-c
85 | [Bf]: https://github.com/OffchainLabs/stylus-sdk-bf
86 |
87 | [discord]: https://discord.com/invite/5KE54JwyTs
88 |
89 | ## Developing Stylus Programs
90 |
91 | The Stylus SDK is just one of the building blocks in creating and deploying WebAssembly programs to Arbitrum chains. To create a new Stylus project from a hello-world example and deploy it onchain, check out some of our tools below:
92 |
93 | | Repo | Use cases | License |
94 | |:-----------------|:----------------------------|:------------------|
95 | | [Stylus Hello World][HelloWorld] | Rust Stylus starter template | Apache 2.0 or MIT |
96 | | [Cargo Stylus CLI][CargoStylus] | Deploying Stylus programs | Apache 2.0 or MIT |
97 |
98 | [HelloWorld]: https://github.com/OffchainLabs/stylus-hello-world
99 | [CargoStylus]: https://github.com/OffchainLabs/cargo-stylus
100 |
101 | ## License
102 |
103 | © 2022-2024 Offchain Labs, Inc.
104 |
105 | This project is licensed under either of
106 |
107 | - [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) ([licenses/Apache-2.0](licenses/Apache-2.0))
108 | - [MIT license](https://opensource.org/licenses/MIT) ([licenses/MIT](licenses/MIT))
109 |
110 | at your option.
111 |
112 | The [SPDX](https://spdx.dev) license identifier for this project is `MIT OR Apache-2.0`.
113 |
--------------------------------------------------------------------------------
/ci/build_and_test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -euo pipefail
4 |
5 | export RUSTFLAGS="-D warnings"
6 | export RUSTFMT_CI=1
7 |
8 | # Print version information
9 | rustc -Vv
10 | cargo -V
11 |
12 | # Build and test main crate
13 | if [ "$CFG_RELEASE_CHANNEL" == "nightly" ]; then
14 | cargo build --locked --all-features
15 | else
16 | cargo build --locked
17 | fi
18 |
19 | # Select all features but the integration test one, which will be run in another CI job.
20 | # We have to use cargo metadata because we can't exclude a feature directly in cargo test.
21 | # See: https://github.com/rust-lang/cargo/issues/3126
22 | FEATURES=$(cargo metadata --format-version=1 --no-deps | jq -r '.packages[] | select(.name == "stylus-sdk") | .features | keys | map(select(. != "integration-tests")) | join(",")')
23 | echo "testing features: $FEATURES"
24 |
25 | cargo test --features $FEATURES
26 |
--------------------------------------------------------------------------------
/ci/integration_test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -euo pipefail
4 |
5 | usage() {
6 | echo "Usage: $0 [option]"
7 | echo ""
8 | echo "Options:"
9 | echo " -e, --example Run integration tests for the specified example (e.g., erc20)"
10 | echo " -s, --stylus-tools Run integration tests for stylus-tools package only"
11 | echo " -a, --all Run all integration tests"
12 | echo " -h, --help Display this help message"
13 | exit 1
14 | }
15 |
16 | run_example_tests() {
17 | local example_name="$1"
18 | if [ -z "$example_name" ]; then
19 | echo "Error: Example name not provided for --example option."
20 | usage
21 | fi
22 | if [ ! -d "examples/$example_name" ]; then
23 | echo "Error: Directory examples/$example_name not found."
24 | exit 1
25 | fi
26 | echo "Running integration tests for example: $example_name"
27 | pushd "examples/$example_name"
28 | cargo fmt --check
29 | cargo check -F integration-tests --locked --all-targets
30 | cargo test -F integration-tests
31 | popd
32 | }
33 |
34 | run_stylus_tools_tests() {
35 | echo "Running integration tests for stylus-tools package"
36 | cargo test -p stylus-tools -F integration-tests
37 | }
38 |
39 | run_all_examples() {
40 | echo "Running integration tests for all examples in ./examples/"
41 | if [ ! -d "examples" ]; then
42 | echo "Error: 'examples' directory not found. Skipping example tests."
43 | return 1
44 | fi
45 | for example_dir in examples/*/; do
46 | if [ -d "$example_dir" ]; then # Check if it's a directory
47 | local example_name
48 | example_name=$(basename "$example_dir")
49 | run_example_tests "$example_name"
50 | fi
51 | done
52 | }
53 |
54 | if [ $# -eq 0 ]; then
55 | usage
56 | fi
57 |
58 | case "$1" in
59 | -h|--help)
60 | usage
61 | ;;
62 | -e|--example)
63 | if [ -z "${2-}" ]; then
64 | echo "Error: Missing example name for $1 option."
65 | usage
66 | fi
67 | run_example_tests "$2"
68 | ;;
69 | -s|--stylus-tools)
70 | run_stylus_tools_tests
71 | ;;
72 | -a|--all)
73 | run_stylus_tools_tests
74 | run_all_examples
75 | ;;
76 | *)
77 | echo "Error: Unknown option $1"
78 | usage
79 | ;;
80 | esac
81 |
--------------------------------------------------------------------------------
/ci/smoke_test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -euo pipefail
4 |
5 | # Print version information
6 | rustc -Vv
7 | cargo -V
8 | #cargo stylus --version
9 |
10 | cargo stylus new counter
11 | cd counter
12 | echo "[workspace]" >> Cargo.toml
13 |
14 | # Use the nitro testnode private key found from the public mnemonic
15 | # https://github.com/OffchainLabs/nitro-testnode/blob/5986e62e8fc8672858baf0550443991adc23f9c2/scripts/consts.ts#L6
16 | cargo stylus deploy --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 -e http://localhost:8547
17 |
--------------------------------------------------------------------------------
/examples/constructor/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [target.wasm32-unknown-unknown]
2 | rustflags = [
3 | "-C", "link-arg=-zstack-size=32768",
4 | "-C", "target-feature=-reference-types",
5 | ]
6 |
7 | [target.aarch64-apple-darwin]
8 | rustflags = [
9 | "-C", "link-arg=-undefined",
10 | "-C", "link-arg=dynamic_lookup",
11 | ]
12 |
13 | [target.x86_64-apple-darwin]
14 | rustflags = [
15 | "-C", "link-arg=-undefined",
16 | "-C", "link-arg=dynamic_lookup",
17 | ]
18 |
--------------------------------------------------------------------------------
/examples/constructor/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | .deployment_key
--------------------------------------------------------------------------------
/examples/constructor/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "stylus-constructor"
3 | version = "0.1.0"
4 | edition = "2021"
5 | license = "MIT OR Apache-2.0"
6 | keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
7 | description = "Stylus constructor example contract"
8 |
9 | [dependencies]
10 | alloy-primitives = "1.0.1"
11 | alloy-sol-types = "1.0.1"
12 | stylus-sdk = { path = "../../stylus-sdk" }
13 |
14 | [dev-dependencies]
15 | alloy = { version = "1.0.1", features = ["contract", "signer-local", "rpc-types", "sha3-keccak"] }
16 | eyre = "0.6"
17 | tokio = "1.44"
18 | stylus-sdk = { path = "../../stylus-sdk", features = ["stylus-test"] }
19 | stylus-tools = { path = "../../stylus-tools" }
20 |
21 | [features]
22 | export-abi = ["stylus-sdk/export-abi"]
23 | integration-tests = ["stylus-tools/integration-tests"]
24 |
25 | [lib]
26 | crate-type = ["lib", "cdylib"]
27 |
28 | [profile.release]
29 | codegen-units = 1
30 | strip = true
31 | lto = true
32 | panic = "abort"
33 | opt-level = "s"
34 |
35 | [workspace]
36 |
--------------------------------------------------------------------------------
/examples/constructor/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "1.87.0"
3 |
--------------------------------------------------------------------------------
/examples/constructor/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | #![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
5 |
6 | extern crate alloc;
7 |
8 | use alloy_primitives::{Address, U256};
9 | use alloy_sol_types::sol;
10 | use stylus_sdk::prelude::*;
11 |
12 | sol! {
13 | error Unauthorized();
14 | }
15 |
16 | sol_storage! {
17 | #[entrypoint]
18 | pub struct Contract {
19 | address owner;
20 | uint256 number;
21 | }
22 | }
23 |
24 | #[derive(SolidityError)]
25 | pub enum ContractErrors {
26 | Unauthorized(Unauthorized),
27 | }
28 |
29 | #[public]
30 | impl Contract {
31 | /// The constructor sets the owner as the EOA that deployed the contract.
32 | #[constructor]
33 | #[payable]
34 | pub fn constructor(&mut self, initial_number: U256) {
35 | // Use tx_origin instead of msg_sender because we use a factory contract in deployment.
36 | let owner = self.vm().tx_origin();
37 | self.owner.set(owner);
38 | self.number.set(initial_number);
39 | }
40 |
41 | /// Only the owner can set the number in the contract.
42 | pub fn set_number(&mut self, number: U256) -> Result<(), ContractErrors> {
43 | if self.owner.get() != self.vm().msg_sender() {
44 | return Err(ContractErrors::Unauthorized(Unauthorized {}));
45 | }
46 | self.number.set(number);
47 | Ok(())
48 | }
49 |
50 | pub fn number(&self) -> U256 {
51 | self.number.get()
52 | }
53 |
54 | pub fn owner(&self) -> Address {
55 | self.owner.get()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/examples/constructor/src/main.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | #![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
5 |
6 | #[cfg(not(any(test, feature = "export-abi")))]
7 | #[no_mangle]
8 | pub extern "C" fn main() {}
9 |
10 | #[cfg(feature = "export-abi")]
11 | fn main() {
12 | stylus_constructor::print_from_args();
13 | }
14 |
--------------------------------------------------------------------------------
/examples/constructor/tests/constructor_integration_test.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | #[cfg(feature = "integration-tests")]
5 | mod integration_test {
6 | use alloy::{
7 | primitives::{utils::parse_ether, U256},
8 | providers::Provider,
9 | sol,
10 | };
11 | use eyre::Result;
12 | use stylus_tools::devnet::{addresses::OWNER, Node, DEVNET_PRIVATE_KEY};
13 |
14 | sol! {
15 | #[sol(rpc)]
16 | interface IContract {
17 | function setNumber(uint256 number) external;
18 | function number() external view returns (uint256);
19 | function owner() external view returns (address);
20 | error Unauthorized();
21 | }
22 | }
23 |
24 | #[tokio::test]
25 | async fn constructor() -> Result<()> {
26 | let devnode = Node::new().await?;
27 | let rpc = devnode.rpc();
28 | println!("Deploying contract to Nitro ({rpc})...");
29 | let args = &["0xbeef"];
30 | let address =
31 | stylus_tools::deploy_with_constructor(rpc, DEVNET_PRIVATE_KEY, "12.34", args)?;
32 | println!("Deployed contract to {address}");
33 | let provider = devnode.create_provider().await?;
34 |
35 | // Check balance sent in constructor
36 | let balance = provider.get_balance(address).await?;
37 | assert_eq!(balance, parse_ether("12.34")?);
38 | println!("Got balance: {balance}");
39 |
40 | let contract = IContract::IContractInstance::new(address, provider);
41 |
42 | // Check values set by constructor
43 | let owner = contract.owner().call().await?;
44 | assert_eq!(owner, OWNER);
45 | let number = contract.number().call().await?;
46 | assert_eq!(number, U256::from(0xbeef));
47 |
48 | // Change number and check
49 | let new_number = U256::from(123);
50 | contract.setNumber(new_number).send().await?.watch().await?;
51 | let number = contract.number().call().await?;
52 | assert_eq!(number, new_number);
53 |
54 | Ok(())
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/examples/erc20/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [target.wasm32-unknown-unknown]
2 | rustflags = [
3 | "-C", "link-arg=-zstack-size=32768",
4 | "-C", "target-feature=-reference-types",
5 | ]
6 |
7 | [target.aarch64-apple-darwin]
8 | rustflags = [
9 | "-C", "link-arg=-undefined",
10 | "-C", "link-arg=dynamic_lookup",
11 | ]
12 |
13 | [target.x86_64-apple-darwin]
14 | rustflags = [
15 | "-C", "link-arg=-undefined",
16 | "-C", "link-arg=dynamic_lookup",
17 | ]
18 |
--------------------------------------------------------------------------------
/examples/erc20/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "erc20"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | alloy-primitives = "1.0.1"
8 | alloy-sol-types = "1.0.1"
9 | stylus-sdk = { path = "../../stylus-sdk" }
10 |
11 | [dev-dependencies]
12 | alloy = { version = "1.0.1", features = ["contract", "signer-local", "rpc-types", "sha3-keccak"] }
13 | eyre = "0.6"
14 | tokio = "1.44"
15 | stylus-sdk = { path = "../../stylus-sdk", features = ["stylus-test"] }
16 | stylus-tools = { path = "../../stylus-tools" }
17 |
18 | [features]
19 | export-abi = ["stylus-sdk/export-abi"]
20 | integration-tests = ["stylus-tools/integration-tests"]
21 |
22 | [lib]
23 | crate-type = ["lib", "cdylib"]
24 |
25 | [profile.release]
26 | codegen-units = 1
27 | strip = true
28 | lto = true
29 | panic = "abort"
30 | opt-level = "s"
31 |
32 | [workspace]
33 |
--------------------------------------------------------------------------------
/examples/erc20/node_modules/.yarn-integrity:
--------------------------------------------------------------------------------
1 | {
2 | "systemParams": "darwin-arm64-115",
3 | "modulesFolders": [],
4 | "flags": [],
5 | "linkedModules": [],
6 | "topLevelPatterns": [],
7 | "lockfileEntries": {},
8 | "files": [],
9 | "artifacts": {}
10 | }
--------------------------------------------------------------------------------
/examples/erc20/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "1.87.0"
3 |
--------------------------------------------------------------------------------
/examples/erc20/scripts/.env.example:
--------------------------------------------------------------------------------
1 | RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
2 | TX_EXPLORER_URL=https://sepolia.arbiscan.io/tx/
3 | CONTRACT_ADDRESS=
4 | PRIVATE_KEY=
5 | AMOUNT_TO_MINT=
--------------------------------------------------------------------------------
/examples/erc20/scripts/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
--------------------------------------------------------------------------------
/examples/erc20/scripts/README.md:
--------------------------------------------------------------------------------
1 | # ERC-20 helper scripts
2 |
3 | This folder contains a script to quickly mint an amount of ERC-20 tokens. By default, it mints 1000 tokens from the contract configured in .env.
4 |
5 | ## How to use it
6 |
7 | 1. Install the dependencies
8 |
9 | ```shell
10 | yarn
11 | ```
12 |
13 | 2. Copy and fill the .env file
14 |
15 | ```shell
16 | cp .env.example .env
17 | ```
18 |
19 | 3. Run the minting script
20 |
21 | ```shell
22 | yarn mint
23 | ```
24 |
--------------------------------------------------------------------------------
/examples/erc20/scripts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "erc20-scripts",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "mint": "node src/mint.js"
8 | },
9 | "dependencies": {
10 | "dotenv": "^16.4.5",
11 | "ethers": "^6.13.0"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/examples/erc20/scripts/src/mint.js:
--------------------------------------------------------------------------------
1 | const {
2 | Contract,
3 | Wallet,
4 | JsonRpcProvider,
5 | parseEther,
6 | formatEther,
7 | } = require("ethers");
8 | require("dotenv").config();
9 |
10 | // Initial checks
11 | if (
12 | !process.env.RPC_URL ||
13 | process.env.RPC_URL == "" ||
14 | !process.env.CONTRACT_ADDRESS ||
15 | process.env.CONTRACT_ADDRESS == "" ||
16 | !process.env.PRIVATE_KEY ||
17 | process.env.PRIVATE_KEY == ""
18 | ) {
19 | console.error(
20 | "The following environment variables are needed (set them in .env): RPC_URL, CONTRACT_ADDRESS, PRIVATE_KEY"
21 | );
22 | return;
23 | }
24 |
25 | // ABI of the token (used functions)
26 | const abi = [
27 | "function mint(uint256 value) external",
28 | "function mintTo(address to, uint256 value) external",
29 |
30 | // Read-Only Functions
31 | "function balanceOf(address owner) external view returns (uint256)",
32 | ];
33 |
34 | // Address of the token
35 | const address = process.env.CONTRACT_ADDRESS;
36 | // Transaction Explorer URL
37 | const tx_explorer_url = process.env.TX_EXPLORER_URL;
38 |
39 | // Private key and ethers provider
40 | const walletPrivateKey = process.env.PRIVATE_KEY;
41 | const stylusRpcProvider = new JsonRpcProvider(process.env.RPC_URL);
42 | const signer = new Wallet(walletPrivateKey, stylusRpcProvider);
43 |
44 | // Amount of tokens to mint
45 | const amountToMint = process.env.AMOUNT_TO_MINT || "1000";
46 |
47 | // Main function
48 | const main = async () => {
49 | // Presentation message
50 | console.log(
51 | `Minting ${amountToMint} tokens of the contract ${address} to account ${signer.address}`
52 | );
53 |
54 | // Connecting to the ERC-20 contract
55 | const erc20 = new Contract(address, abi, signer);
56 |
57 | // Current balance of user
58 | const currentBalance = await erc20.balanceOf(signer.address);
59 | console.log(`Current balance: ${formatEther(currentBalance)}`);
60 |
61 | // Minting tokens
62 | const mintTransaction = await erc20.mint(parseEther(amountToMint));
63 | await mintTransaction.wait();
64 | console.log(`Transaction hash: ${mintTransaction.hash}`);
65 | console.log(`View tx on explorer: ${tx_explorer_url}${mintTransaction.hash}`);
66 |
67 | // Final balance of user
68 | const finalBalance = await erc20.balanceOf(signer.address);
69 | console.log(`Final balance: ${formatEther(finalBalance)}`);
70 | };
71 |
72 | main()
73 | .then(() => process.exit(0))
74 | .catch((error) => {
75 | console.error(error);
76 | process.exit(1);
77 | });
78 |
--------------------------------------------------------------------------------
/examples/erc20/scripts/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@adraffy/ens-normalize@1.10.1":
6 | version "1.10.1"
7 | resolved "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz"
8 | integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==
9 |
10 | "@noble/curves@1.2.0":
11 | version "1.2.0"
12 | resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz"
13 | integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==
14 | dependencies:
15 | "@noble/hashes" "1.3.2"
16 |
17 | "@noble/hashes@1.3.2":
18 | version "1.3.2"
19 | resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz"
20 | integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
21 |
22 | "@types/node@18.15.13":
23 | version "18.15.13"
24 | resolved "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz"
25 | integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==
26 |
27 | aes-js@4.0.0-beta.5:
28 | version "4.0.0-beta.5"
29 | resolved "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz"
30 | integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==
31 |
32 | dotenv@^16.4.5:
33 | version "16.4.5"
34 | resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz"
35 | integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
36 |
37 | ethers@^6.13.0:
38 | version "6.13.0"
39 | resolved "https://registry.npmjs.org/ethers/-/ethers-6.13.0.tgz"
40 | integrity sha512-+yyQQQWEntY5UVbCv++guA14RRVFm1rSnO1GoLFdrK7/XRWMoktNgyG9UjwxrQqGBfGyFKknNZ81YpUS2emCgg==
41 | dependencies:
42 | "@adraffy/ens-normalize" "1.10.1"
43 | "@noble/curves" "1.2.0"
44 | "@noble/hashes" "1.3.2"
45 | "@types/node" "18.15.13"
46 | aes-js "4.0.0-beta.5"
47 | tslib "2.4.0"
48 | ws "8.5.0"
49 |
50 | tslib@2.4.0:
51 | version "2.4.0"
52 | resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz"
53 | integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
54 |
55 | ws@8.5.0:
56 | version "8.5.0"
57 | resolved "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz"
58 | integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==
59 |
--------------------------------------------------------------------------------
/examples/erc20/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | // Only run this as a WASM if the export-abi feature is not set.
5 | #![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
6 | extern crate alloc;
7 |
8 | // Modules and imports
9 | pub mod erc20;
10 |
11 | use crate::erc20::{Erc20, Erc20Error, Erc20Params};
12 | use alloy_primitives::{Address, U256};
13 | use stylus_sdk::prelude::*;
14 |
15 | /// Immutable definitions
16 | pub struct StylusTestTokenParams;
17 | impl Erc20Params for StylusTestTokenParams {
18 | const NAME: &'static str = "StylusTestToken";
19 | const SYMBOL: &'static str = "STTK";
20 | const DECIMALS: u8 = 18;
21 | }
22 |
23 | // Define the entrypoint as a Solidity storage object. The sol_storage! macro
24 | // will generate Rust-equivalent structs with all fields mapped to Solidity-equivalent
25 | // storage slots and types.
26 | sol_storage! {
27 | #[entrypoint]
28 | struct StylusTestToken {
29 | // Allows erc20 to access StylusTestToken's storage and make calls
30 | #[borrow]
31 | Erc20 erc20;
32 | }
33 | }
34 |
35 | #[public]
36 | #[inherit(Erc20)]
37 | impl StylusTestToken {
38 | /// Mints tokens
39 | pub fn mint(&mut self, value: U256) -> Result<(), Erc20Error> {
40 | self.erc20.mint(self.vm().msg_sender(), value)?;
41 | Ok(())
42 | }
43 |
44 | /// Mints tokens to another address
45 | pub fn mint_to(&mut self, to: Address, value: U256) -> Result<(), Erc20Error> {
46 | self.erc20.mint(to, value)?;
47 | Ok(())
48 | }
49 |
50 | /// Burns tokens
51 | pub fn burn(&mut self, value: U256) -> Result<(), Erc20Error> {
52 | self.erc20.burn(self.vm().msg_sender(), value)?;
53 | Ok(())
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/examples/erc20/src/main.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | #![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
5 |
6 | #[cfg(not(any(test, feature = "export-abi")))]
7 | #[no_mangle]
8 | pub extern "C" fn main() {}
9 |
10 | #[cfg(feature = "export-abi")]
11 | fn main() {
12 | erc20::print_from_args();
13 | }
14 |
--------------------------------------------------------------------------------
/examples/erc20/tests/erc20_integration_test.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | #[cfg(feature = "integration-tests")]
5 | mod integration_test {
6 | use alloy::{primitives::U256, sol};
7 | use erc20::{erc20::Erc20Params, StylusTestTokenParams};
8 | use eyre::Result;
9 | use stylus_tools::devnet::{addresses::OWNER, Node, DEVNET_PRIVATE_KEY};
10 |
11 | sol! {
12 | #[sol(rpc)]
13 | interface IStylusTestToken is IErc20 {
14 | function name() external pure returns (string memory);
15 | function symbol() external pure returns (string memory);
16 | function decimals() external pure returns (uint8);
17 | function totalSupply() external view returns (uint256);
18 | function balanceOf(address owner) external view returns (uint256);
19 | function transfer(address to, uint256 value) external returns (bool);
20 | function transferFrom(address from, address to, uint256 value) external returns (bool);
21 | function approve(address spender, uint256 value) external returns (bool);
22 | function allowance(address owner, address spender) external view returns (uint256);
23 | function mint(uint256 value) external;
24 | function mintTo(address to, uint256 value) external;
25 | function burn(uint256 value) external;
26 | error InsufficientBalance(address, uint256, uint256);
27 | error InsufficientAllowance(address, address, uint256, uint256);
28 | }
29 | }
30 |
31 | #[tokio::test]
32 | async fn erc20() -> Result<()> {
33 | let devnode = Node::new().await?;
34 | let rpc = devnode.rpc();
35 | println!("Deploying contract to Nitro ({rpc})...");
36 | let address = stylus_tools::deploy(rpc, DEVNET_PRIVATE_KEY)?;
37 | println!("Deployed contract to {address}");
38 | let provider = devnode.create_provider().await?;
39 | let contract = IStylusTestToken::IStylusTestTokenInstance::new(address, provider);
40 |
41 | // Check name
42 | let name = contract.name().call().await?;
43 | assert_eq!(name, StylusTestTokenParams::NAME);
44 | println!("ERC20.name(): {name}");
45 |
46 | // Check symbol
47 | let symbol = contract.symbol().call().await?;
48 | assert_eq!(symbol, StylusTestTokenParams::SYMBOL);
49 | println!("ERC20.symbol(): {symbol}");
50 |
51 | // Mint tokens
52 | let num_tokens = U256::from(1000);
53 | contract.mint(num_tokens).send().await?.watch().await?;
54 |
55 | // Check balance
56 | let balance = contract.balanceOf(OWNER).call().await?;
57 | assert_eq!(balance, num_tokens);
58 |
59 | Ok(())
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/examples/erc20/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 |
--------------------------------------------------------------------------------
/examples/erc721/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [target.wasm32-unknown-unknown]
2 | rustflags = [
3 | "-C", "link-arg=-zstack-size=32768",
4 | "-C", "target-feature=-reference-types",
5 | ]
6 |
7 | [target.aarch64-apple-darwin]
8 | rustflags = [
9 | "-C", "link-arg=-undefined",
10 | "-C", "link-arg=dynamic_lookup",
11 | ]
12 |
13 | [target.x86_64-apple-darwin]
14 | rustflags = [
15 | "-C", "link-arg=-undefined",
16 | "-C", "link-arg=dynamic_lookup",
17 | ]
18 |
--------------------------------------------------------------------------------
/examples/erc721/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "erc721"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | alloy-primitives = "1.0.1"
8 | alloy-sol-types = "1.0.1"
9 | stylus-sdk = { path = "../../stylus-sdk" }
10 |
11 | [dev-dependencies]
12 | alloy = { version = "1.0.1", features = ["contract", "signer-local", "rpc-types", "sha3-keccak"] }
13 | eyre = "0.6"
14 | tokio = "1.44"
15 | stylus-sdk = { path = "../../stylus-sdk", features = ["stylus-test"] }
16 | stylus-tools = { path = "../../stylus-tools" }
17 |
18 | [features]
19 | export-abi = ["stylus-sdk/export-abi"]
20 | integration-tests = ["stylus-tools/integration-tests"]
21 |
22 | [lib]
23 | crate-type = ["lib", "cdylib"]
24 |
25 | [profile.release]
26 | codegen-units = 1
27 | strip = true
28 | lto = true
29 | panic = "abort"
30 | opt-level = "s"
31 |
32 | [workspace]
33 |
--------------------------------------------------------------------------------
/examples/erc721/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "1.87.0"
3 |
--------------------------------------------------------------------------------
/examples/erc721/scripts/.env.example:
--------------------------------------------------------------------------------
1 | RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
2 | TX_EXPLORER_URL=https://sepolia.arbiscan.io/tx/
3 | CONTRACT_ADDRESS=
4 | PRIVATE_KEY=
5 |
--------------------------------------------------------------------------------
/examples/erc721/scripts/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
--------------------------------------------------------------------------------
/examples/erc721/scripts/README.md:
--------------------------------------------------------------------------------
1 | # ERC-721 helper scripts
2 |
3 | This folder contains a script to quickly mint an ERC-721 token.configured in .env.
4 |
5 | ## How to use it
6 |
7 | 1. Install the dependencies
8 |
9 | ```shell
10 | yarn
11 | ```
12 |
13 | 2. Copy and fill the .env file
14 |
15 | ```shell
16 | cp .env.example .env
17 | ```
18 |
19 | 3. Run the minting script
20 |
21 | ```shell
22 | yarn mint
23 | ```
24 |
--------------------------------------------------------------------------------
/examples/erc721/scripts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "erc721-scripts",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "mint": "node src/mint.js"
8 | },
9 | "dependencies": {
10 | "dotenv": "^16.4.5",
11 | "ethers": "^6.13.0"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/examples/erc721/scripts/src/mint.js:
--------------------------------------------------------------------------------
1 | const { Contract, Wallet, JsonRpcProvider } = require("ethers");
2 | require("dotenv").config();
3 |
4 | // Initial checks
5 | if (
6 | !process.env.RPC_URL ||
7 | process.env.RPC_URL == "" ||
8 | !process.env.CONTRACT_ADDRESS ||
9 | process.env.CONTRACT_ADDRESS == "" ||
10 | !process.env.PRIVATE_KEY ||
11 | process.env.PRIVATE_KEY == ""
12 | ) {
13 | console.error(
14 | "The following environment variables are needed (set them in .env): RPC_URL, CONTRACT_ADDRESS, PRIVATE_KEY"
15 | );
16 | return;
17 | }
18 |
19 | // ABI of the token (used functions)
20 | const abi = [
21 | "function mint() external",
22 | "function mintTo(address to) external",
23 |
24 | // Read-Only Functions
25 | "function balanceOf(address owner) external view returns (uint256)",
26 | ];
27 |
28 | // Address of the token
29 | const address = process.env.CONTRACT_ADDRESS;
30 | // Transaction Explorer URL
31 | const tx_explorer_url = process.env.TX_EXPLORER_URL;
32 |
33 | // Private key and ethers provider
34 | const walletPrivateKey = process.env.PRIVATE_KEY;
35 | const stylusRpcProvider = new JsonRpcProvider(process.env.RPC_URL);
36 | const signer = new Wallet(walletPrivateKey, stylusRpcProvider);
37 |
38 | // Main function
39 | const main = async () => {
40 | // Presentation message
41 | console.log(
42 | `Minting an NFT of the contract ${address} to account ${signer.address}`
43 | );
44 |
45 | // Connecting to the ERC-721 contract
46 | const erc721 = new Contract(address, abi, signer);
47 |
48 | // Current balance of user
49 | const currentBalance = await erc721.balanceOf(signer.address);
50 | console.log(`Current balance: ${currentBalance}`);
51 |
52 | // Minting tokens
53 | const mintTransaction = await erc721.mint();
54 | await mintTransaction.wait();
55 | console.log(`Transaction hash: ${mintTransaction.hash}`);
56 | console.log(`View tx on explorer: ${tx_explorer_url}${mintTransaction.hash}`);
57 |
58 | // Final balance of user
59 | const finalBalance = await erc721.balanceOf(signer.address);
60 | console.log(`Final balance: ${finalBalance}`);
61 | };
62 |
63 | main()
64 | .then(() => process.exit(0))
65 | .catch((error) => {
66 | console.error(error);
67 | process.exit(1);
68 | });
69 |
--------------------------------------------------------------------------------
/examples/erc721/scripts/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@adraffy/ens-normalize@1.10.1":
6 | version "1.10.1"
7 | resolved "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz"
8 | integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==
9 |
10 | "@noble/curves@1.2.0":
11 | version "1.2.0"
12 | resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz"
13 | integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==
14 | dependencies:
15 | "@noble/hashes" "1.3.2"
16 |
17 | "@noble/hashes@1.3.2":
18 | version "1.3.2"
19 | resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz"
20 | integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
21 |
22 | "@types/node@18.15.13":
23 | version "18.15.13"
24 | resolved "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz"
25 | integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==
26 |
27 | aes-js@4.0.0-beta.5:
28 | version "4.0.0-beta.5"
29 | resolved "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz"
30 | integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==
31 |
32 | dotenv@^16.4.5:
33 | version "16.4.5"
34 | resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz"
35 | integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
36 |
37 | ethers@^6.13.0:
38 | version "6.13.0"
39 | resolved "https://registry.npmjs.org/ethers/-/ethers-6.13.0.tgz"
40 | integrity sha512-+yyQQQWEntY5UVbCv++guA14RRVFm1rSnO1GoLFdrK7/XRWMoktNgyG9UjwxrQqGBfGyFKknNZ81YpUS2emCgg==
41 | dependencies:
42 | "@adraffy/ens-normalize" "1.10.1"
43 | "@noble/curves" "1.2.0"
44 | "@noble/hashes" "1.3.2"
45 | "@types/node" "18.15.13"
46 | aes-js "4.0.0-beta.5"
47 | tslib "2.4.0"
48 | ws "8.5.0"
49 |
50 | tslib@2.4.0:
51 | version "2.4.0"
52 | resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz"
53 | integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
54 |
55 | ws@8.5.0:
56 | version "8.5.0"
57 | resolved "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz"
58 | integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==
59 |
--------------------------------------------------------------------------------
/examples/erc721/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | // Only run this as a WASM if the export-abi feature is not set.
5 | #![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
6 | extern crate alloc;
7 |
8 | // Modules and imports
9 | pub mod erc721;
10 |
11 | use crate::erc721::{Erc721, Erc721Error, Erc721Params};
12 | use alloy_primitives::{Address, U256};
13 | /// Import the Stylus SDK along with alloy primitive types for use in our program.
14 | use stylus_sdk::prelude::*;
15 |
16 | /// Immutable definitions
17 | pub struct StylusTestNFTParams;
18 | impl Erc721Params for StylusTestNFTParams {
19 | const NAME: &'static str = "StylusTestNFT";
20 | const SYMBOL: &'static str = "STNFT";
21 |
22 | fn token_uri(token_id: U256) -> String {
23 | format!("{}{}{}", "https://my-nft-metadata.com/", token_id, ".json")
24 | }
25 | }
26 |
27 | // Define the entrypoint as a Solidity storage object. The sol_storage! macro
28 | // will generate Rust-equivalent structs with all fields mapped to Solidity-equivalent
29 | // storage slots and types.
30 | sol_storage! {
31 | #[entrypoint]
32 | struct StylusTestNFT {
33 | #[borrow] // Allows erc721 to access StylusTestNFT's storage and make calls
34 | Erc721 erc721;
35 | }
36 | }
37 |
38 | #[public]
39 | #[inherit(Erc721)]
40 | impl StylusTestNFT {
41 | /// Mints an NFT
42 | pub fn mint(&mut self) -> Result<(), Erc721Error> {
43 | let minter = self.vm().msg_sender();
44 | self.erc721.mint(minter)?;
45 | Ok(())
46 | }
47 |
48 | /// Mints an NFT to another address
49 | pub fn mint_to(&mut self, to: Address) -> Result<(), Erc721Error> {
50 | self.erc721.mint(to)?;
51 | Ok(())
52 | }
53 |
54 | /// Burns an NFT
55 | pub fn burn(&mut self, token_id: U256) -> Result<(), Erc721Error> {
56 | // This function checks that msg_sender owns the specified token_id
57 | self.erc721.burn(self.vm().msg_sender(), token_id)?;
58 | Ok(())
59 | }
60 |
61 | /// Total supply
62 | pub fn total_supply(&mut self) -> Result {
63 | Ok(self.erc721.total_supply.get())
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/examples/erc721/src/main.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | #![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
5 |
6 | #[cfg(not(any(test, feature = "export-abi")))]
7 | #[no_mangle]
8 | pub extern "C" fn main() {}
9 |
10 | #[cfg(feature = "export-abi")]
11 | fn main() {
12 | erc721::print_from_args();
13 | }
14 |
--------------------------------------------------------------------------------
/examples/erc721/tests/erc721_integration_test.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | #[cfg(feature = "integration-tests")]
5 | mod integration_test {
6 | use alloy::{primitives::U256, sol};
7 | use erc721::{erc721::Erc721Params, StylusTestNFTParams};
8 | use eyre::Result;
9 | use stylus_tools::devnet::{addresses::OWNER, Node, DEVNET_PRIVATE_KEY};
10 |
11 | sol! {
12 | #[sol(rpc)]
13 | interface IStylusTestNFT {
14 | function name() external pure returns (string memory);
15 | function symbol() external pure returns (string memory);
16 | function tokenURI(uint256 token_id) external view returns (string memory);
17 | function balanceOf(address owner) external view returns (uint256);
18 | function ownerOf(uint256 token_id) external view returns (address);
19 | function safeTransferFrom(address from, address to, uint256 token_id, bytes calldata data) external;
20 | function safeTransferFrom(address from, address to, uint256 token_id) external;
21 | function transferFrom(address from, address to, uint256 token_id) external;
22 | function approve(address approved, uint256 token_id) external;
23 | function setApprovalForAll(address operator, bool approved) external;
24 | function getApproved(uint256 token_id) external returns (address);
25 | function isApprovedForAll(address owner, address operator) external returns (bool);
26 | function supportsInterface(bytes4 _interface) external pure returns (bool);
27 | function mint() external;
28 | function mintTo(address to) external;
29 | function burn(uint256 token_id) external;
30 | function totalSupply() external returns (uint256);
31 | error NotOwner(address, uint256, address);
32 | error NotApproved(address, address, uint256);
33 | error TransferToZero(uint256);
34 | error ReceiverRefused(address, uint256, bytes4);
35 | error InvalidTokenId(uint256);
36 | }
37 | }
38 |
39 | #[tokio::test]
40 | async fn erc721() -> Result<()> {
41 | let devnode = Node::new().await?;
42 | let rpc = devnode.rpc();
43 | println!("Deploying contract to Nitro ({rpc})...");
44 | let address = stylus_tools::deploy(rpc, DEVNET_PRIVATE_KEY)?;
45 | println!("Deployed contract to {address}");
46 | let provider = devnode.create_provider().await?;
47 | let contract = IStylusTestNFT::IStylusTestNFTInstance::new(address, provider);
48 |
49 | // Check name
50 | let name = contract.name().call().await?;
51 | assert_eq!(name, StylusTestNFTParams::NAME);
52 | println!("ERC721.name(): {name}");
53 |
54 | // Check symbol
55 | let symbol = contract.symbol().call().await?;
56 | assert_eq!(symbol, StylusTestNFTParams::SYMBOL);
57 | println!("ERC721.symbol(): {symbol}");
58 |
59 | // Mint NFTs
60 | const NUM_NFTS: usize = 3;
61 | for _ in 0..NUM_NFTS {
62 | contract.mint().send().await?.watch().await?;
63 | }
64 |
65 | // Check balance
66 | let balance = contract.balanceOf(OWNER).call().await?;
67 | assert_eq!(balance, U256::from(NUM_NFTS));
68 |
69 | Ok(())
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/examples/single_call/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [target.wasm32-unknown-unknown]
2 | rustflags = [
3 | "-C", "link-arg=-zstack-size=32768",
4 | "-C", "target-feature=-reference-types",
5 | ]
6 |
7 | [target.aarch64-apple-darwin]
8 | rustflags = [
9 | "-C", "link-arg=-undefined",
10 | "-C", "link-arg=dynamic_lookup",
11 | ]
12 |
13 | [target.x86_64-apple-darwin]
14 | rustflags = [
15 | "-C", "link-arg=-undefined",
16 | "-C", "link-arg=dynamic_lookup",
17 | ]
18 |
--------------------------------------------------------------------------------
/examples/single_call/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | .deployment_key
--------------------------------------------------------------------------------
/examples/single_call/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "stylus-single-call"
3 | version = "0.1.5"
4 | edition = "2021"
5 | license = "MIT OR Apache-2.0"
6 | keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
7 | description = "Stylus single call router contract"
8 |
9 | [dependencies]
10 | alloy-primitives = "1.0.1"
11 | stylus-sdk = { path = "../../stylus-sdk" }
12 |
13 | [dev-dependencies]
14 | alloy = { version = "1.0.1", features = ["contract", "signer-local", "rpc-types", "sha3-keccak"] }
15 | eyre = "0.6"
16 | tokio = "1.44"
17 | stylus-sdk = { path = "../../stylus-sdk", features = ["stylus-test"] }
18 | stylus-tools = { path = "../../stylus-tools" }
19 |
20 | [features]
21 | export-abi = ["stylus-sdk/export-abi"]
22 | integration-tests = ["stylus-tools/integration-tests"]
23 |
24 | [lib]
25 | crate-type = ["lib", "cdylib"]
26 |
27 | [profile.release]
28 | codegen-units = 1
29 | strip = true
30 | lto = true
31 | panic = "abort"
32 | opt-level = "s"
33 |
34 | [workspace]
35 |
--------------------------------------------------------------------------------
/examples/single_call/README.md:
--------------------------------------------------------------------------------
1 | # Stylus SingleCall
2 |
3 | This contract features a single method `execute` which takes two args: a `target` address and `data` bytes. The `execute` function will pass the `data` to the `target`.
4 |
5 | There is an example script showing how to use this contract in the `scripts` folder. See [`scripts/README.md`](scripts/README.md) for further details.
6 |
--------------------------------------------------------------------------------
/examples/single_call/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "1.87.0"
3 |
--------------------------------------------------------------------------------
/examples/single_call/scripts/.env.example:
--------------------------------------------------------------------------------
1 | RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
2 | TX_EXPLORER_URL=https://sepolia.arbiscan.io/tx/
3 | SINGLE_CALL_CONTRACT_ADDRESS=0xb27fc74caf34c5c26d27a7654358017331330cee
4 | COUNTER_CONTRACT_ADDRESS=
5 | PRIVATE_KEY=
6 |
--------------------------------------------------------------------------------
/examples/single_call/scripts/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | cache
4 | artifacts
5 | ignition/deployments
--------------------------------------------------------------------------------
/examples/single_call/scripts/README.md:
--------------------------------------------------------------------------------
1 | # Single Call Stylus Contract
2 |
3 | The purpose of this simple example is to test external contracts by routing calldata through the `SingleCall` contract to a target contract. This can be useful for testing existing Solidity contracts in a Stylus context. As part of this example, we'll deploy a simple Solidity `Counter` contract, and test incrementing the `Counter` by executing via the `SingleCall` contract.
4 |
5 | ## Basic Requirements
6 |
7 | You'll need a developer wallet funded with some Sepolia ETH on the Arbitrum Sepolia testnet.
8 |
9 | ### Public Faucets
10 |
11 | - [Alchemy Arbitrum Sepolia Faucet](https://www.alchemy.com/faucets/arbitrum-sepolia)
12 | - [Quicknode Arbitrum Sepolia Faucet](https://faucet.quicknode.com/arbitrum/sepolia)
13 |
14 | Note: If you already have some Sepolia ETH, but it has not been bridged to the Arbitrum Sepolia layer 2, you can do so using [the official Arbitrum bridge](https://bridge.arbitrum.io/?destinationChain=arbitrum-sepolia&sourceChain=sepolia). This process can take up to 15 minutes.
15 |
16 | ## Deploy the Rust `SingleCall` Contract
17 |
18 | The source code for `SingleCall` contract can be found in the `/src/lib.rs` file. It is a simple contract that contains a single `execute(address,bytes)(bytes)` function signature. It takes a target contract as its first parameter and any arbitrary `bytes` as its second parameter. It will forward the `bytes` you pass in to that contract, and return any `bytes` it receives back.
19 |
20 | A default instance of the `SingleCall` contract has been deployed to `0x5856f06b2924733049d87c261aba77f1f10be2a8` on Arbitrum Sepolia.
21 |
22 | If you need to deploy to another network, then from the root `single_call` directory call:
23 |
24 | ```
25 | cargo stylus deploy -e https://sepolia-rollup.arbitrum.io/rpc --private-key-path=./.path_to_your_key_file
26 | ```
27 |
28 | The `cargo stylus` CLI tool can be found at [cargo-stylus](https://github.com/OffchainLabs/cargo-stylus) if you do not already have it installed. It is not necessary for this demo, however, if you use the pre-deployed contract.
29 |
30 | ## Set Up Your Environment Variables
31 |
32 | From your terminal, run:
33 |
34 | ```
35 | cp .env.example .env
36 | ```
37 |
38 | Now open the `.env` file. You will see that `RPC_URL`, `TX_EXPLORER_URL`, and `SINGLE_CALL_CONTRACT_ADDRESS` has already been populated. We will deploy the `Counter` contract in just a moment, but first, you'll need to populate your `PRIVATE_KEY`.
39 |
40 | **NOTE: DO NOT use a personal account to deploy contracts. Always use a fresh developer wallet that does not contain any real assets for development purposes.**
41 |
42 | Make sure the account you're using has Sepolia ETH on Arbitrum Sepolia. You can check your balance on [Sepolia Arbiscan](https://sepolia.arbiscan.io/).
43 |
44 | ## Deploy the Solidity Counter Contract
45 |
46 | We'll be deploying the Solidity `Counter.sol` contract that can be found in the `external_contracts` folder. It is a simple contract that contains two methods: `setNumber(uint256)` and `increment()`, as well as a getter for the public `number()(uint256)` value.
47 |
48 | You'll need a recent version of [Node.js](https://nodejs.org/en) and [Yarn](https://yarnpkg.com/) to run these scripts. First, install all dependencies by running:
49 |
50 | ```
51 | yarn
52 | ```
53 |
54 | Then deploy `Counter.sol` by running the `deploy_counter.js` script:
55 |
56 | ```
57 | yarn hardhat ignition deploy ignition/modules/deploy_counter.js --network arb_sepolia
58 | ```
59 |
60 | You should see in your console something like:
61 |
62 | ```
63 | ✔ Confirm deploy to network arb_sepolia (421614)? … yes
64 | Compiled 1 Solidity file successfully (evm target: paris).
65 | Hardhat Ignition 🚀
66 |
67 | Deploying [ deploy_counter ]
68 |
69 | Batch #1
70 | Executed deploy_counter#Counter
71 |
72 | Batch #2
73 | Executed deploy_counter#Counter.setNumber
74 |
75 | [ deploy_counter ] successfully deployed 🚀
76 |
77 | Deployed Addresses
78 |
79 | deploy_counter#Counter - 0xfDa82C11DF0Eb7490fFACC0652cbcC36D49327Bd
80 | ```
81 |
82 | Take note of the `Deployed Addresses` value and go ahead and copy and paste that as your `COUNTER_CONTRACT_ADDRESS` variable in your `.env` file.
83 |
84 | ## Incrementing `Counter` via `SingleCall`
85 |
86 | Now you're all set to increment the `Counter` via `SingleCall`. There is a script that shows how to do this in `/src/main.js`. To run the script, simply call:
87 |
88 | ```
89 | yarn run increment
90 | ```
91 |
92 | You should see console output similar to below:
93 |
94 | ```
95 | Incrementing the Counter contract at 0xaAf6112301a19c90feFb251D0567610eA649752D via the SingleCall router at 0xb27fc74caf34c5c26d27a7654358017331330cee
96 | Current count: 42
97 | 0xd09de08a
98 | Transaction hash: 0x35c6d2ea3de188ed6bd5283c49d58cf89fc12e65cece9ad19a62e158e0bc944e
99 | View tx on explorer: https://sepolia.arbiscan.io/tx/0x35c6d2ea3de188ed6bd5283c49d58cf89fc12e65cece9ad19a62e158e0bc944e
100 | Updated count: 43
101 | ✨ Done in 5.48s.
102 | ```
103 |
--------------------------------------------------------------------------------
/examples/single_call/scripts/external_contracts/Counter.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.13;
3 |
4 | contract Counter {
5 | uint256 public number;
6 |
7 | function setNumber(uint256 newNumber) public {
8 | number = newNumber;
9 | }
10 |
11 | function increment() public {
12 | number++;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/examples/single_call/scripts/hardhat.config.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | require("@nomicfoundation/hardhat-ignition-ethers");
3 |
4 | // Initial checks
5 | if (
6 | !process.env.RPC_URL ||
7 | process.env.RPC_URL == "" ||
8 | !process.env.PRIVATE_KEY ||
9 | process.env.PRIVATE_KEY == ""
10 | ) {
11 | console.error(
12 | "The following environment variables are needed (set them in .env): RPC_URL, PRIVATE_KEY"
13 | );
14 | return;
15 | }
16 |
17 | const RPC_URL = process.env.RPC_URL;
18 | const PRIVATE_KEY = process.env.PRIVATE_KEY;
19 |
20 | module.exports = {
21 | solidity: "0.8.18",
22 | paths: {
23 | sources: "./external_contracts",
24 | },
25 | networks: {
26 | arb_sepolia: {
27 | url: RPC_URL,
28 | accounts: [PRIVATE_KEY],
29 | chainId: 421614,
30 | },
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/examples/single_call/scripts/ignition/modules/deploy_counter.js:
--------------------------------------------------------------------------------
1 | const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");
2 |
3 | module.exports = buildModule("deploy_counter", (m) => {
4 | const deployCounter = m.contract("Counter", []);
5 |
6 | m.call(deployCounter, "setNumber", [42]);
7 |
8 | return { deployCounter };
9 | });
10 |
--------------------------------------------------------------------------------
/examples/single_call/scripts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "single_call_scripts",
3 | "version": "1.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "increment": "node src/main.js"
7 | },
8 | "dependencies": {
9 | "@nomicfoundation/hardhat-ethers": "^3.0.6",
10 | "@nomicfoundation/hardhat-ignition": "^0.15.5",
11 | "@nomicfoundation/hardhat-ignition-ethers": "^0.15.5",
12 | "@nomicfoundation/hardhat-verify": "^2.0.8",
13 | "@nomicfoundation/ignition-core": "^0.15.5",
14 | "dotenv": "^16.4.5",
15 | "ethers": "^6.13.1",
16 | "hardhat": "^2.22.5"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/single_call/scripts/src/main.js:
--------------------------------------------------------------------------------
1 | const {
2 | Contract,
3 | Wallet,
4 | JsonRpcProvider,
5 | parseEther,
6 | formatEther,
7 | } = require("ethers");
8 | require("dotenv").config();
9 |
10 | // Initial checks
11 | if (
12 | !process.env.RPC_URL ||
13 | process.env.RPC_URL == "" ||
14 | !process.env.SINGLE_CALL_CONTRACT_ADDRESS ||
15 | process.env.SINGLE_CALL_CONTRACT_ADDRESS == "" ||
16 | !process.env.COUNTER_CONTRACT_ADDRESS ||
17 | process.env.COUNTER_CONTRACT_ADDRESS == "" ||
18 | !process.env.PRIVATE_KEY ||
19 | process.env.PRIVATE_KEY == ""
20 | ) {
21 | console.error(
22 | "The following environment variables are needed (set them in .env): RPC_URL, SINGLE_CALL_CONTRACT_ADDRESS, COUNTER_CONTRACT_ADDRESS, PRIVATE_KEY"
23 | );
24 | return;
25 | }
26 |
27 | // ABI of the SingleCall contract
28 | const ABI_SINGLE_CALL = [
29 | "function execute(address target, bytes data) external returns (bytes)",
30 | ];
31 |
32 | // ABI of the Counter contract
33 | const ABI_COUNTER = [
34 | "function number() external view returns (uint256)",
35 | "function setNumber(uint256 value) external",
36 | "function increment() external",
37 | ];
38 |
39 | // Addresses for the contracts
40 | const SINGLE_CALL_ADDRESS = process.env.SINGLE_CALL_CONTRACT_ADDRESS;
41 | const COUNTER_ADDRESS = process.env.COUNTER_CONTRACT_ADDRESS;
42 |
43 | // Transaction Explorer URL
44 | const TX_EXPLORER_URL = process.env.TX_EXPLORER_URL;
45 |
46 | // Private key and ethers provider
47 | const WALLET_PRIVATE_KEY = process.env.PRIVATE_KEY;
48 | const stylusRpcProvider = new JsonRpcProvider(process.env.RPC_URL);
49 | const signer = new Wallet(WALLET_PRIVATE_KEY, stylusRpcProvider);
50 |
51 | // Main function
52 | const main = async () => {
53 | // // Presentation message
54 | console.log(
55 | `Incrementing the Counter contract at ${COUNTER_ADDRESS} via the SingleCall router at ${SINGLE_CALL_ADDRESS}`
56 | );
57 |
58 | // Connecting to the contracts
59 | const singleCall = new Contract(SINGLE_CALL_ADDRESS, ABI_SINGLE_CALL, signer);
60 | const counter = new Contract(COUNTER_ADDRESS, ABI_COUNTER, signer);
61 |
62 | // Current value for the Counter
63 | const currentCount = await counter.number();
64 | console.log(`Current count: ${currentCount}`);
65 |
66 | // Encode the function call data
67 | const encodedData = counter.interface.encodeFunctionData("increment");
68 | console.log(encodedData);
69 |
70 | // Route the calldata through the SingleCall contract to the Counter contract
71 | const incTransaction = await singleCall.execute(COUNTER_ADDRESS, encodedData);
72 | await incTransaction.wait();
73 |
74 | console.log(`Transaction hash: ${incTransaction.hash}`);
75 | console.log(`View tx on explorer: ${TX_EXPLORER_URL}${incTransaction.hash}`);
76 |
77 | // Check the Counter value again
78 | const updatedCount = await counter.number();
79 | console.log(`Updated count: ${updatedCount}`);
80 | };
81 |
82 | main()
83 | .then(() => process.exit(0))
84 | .catch((error) => {
85 | console.error(error);
86 | process.exit(1);
87 | });
88 |
--------------------------------------------------------------------------------
/examples/single_call/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | #![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
5 | extern crate alloc;
6 |
7 | use stylus_sdk::{abi::Bytes, alloy_primitives::Address, prelude::*};
8 |
9 | #[storage]
10 | #[entrypoint]
11 | pub struct SingleCall;
12 |
13 | #[public]
14 | impl SingleCall {
15 | pub fn execute(&self, target: Address, data: Bytes) -> Bytes {
16 | unsafe {
17 | let result = RawCall::new(self.vm()).call(target, &data);
18 | result.unwrap().into()
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/single_call/src/main.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | #![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
5 |
6 | #[cfg(not(any(test, feature = "export-abi")))]
7 | #[no_mangle]
8 | pub extern "C" fn main() {}
9 |
10 | #[cfg(feature = "export-abi")]
11 | fn main() {
12 | stylus_single_call::print_from_args();
13 | }
14 |
--------------------------------------------------------------------------------
/examples/single_call/tests/single_call_integration_test.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | #[cfg(feature = "integration-tests")]
5 | mod integration_test {
6 | use alloy::{
7 | primitives::{address, Address},
8 | sol,
9 | sol_types::SolCall,
10 | };
11 | use eyre::Result;
12 | use stylus_tools::devnet::{addresses::OWNER, Node, DEVNET_PRIVATE_KEY};
13 |
14 | const ARB_OWNER_PUBLIC: Address = address!("0x000000000000000000000000000000000000006b");
15 |
16 | sol! {
17 | #[sol(rpc)]
18 | interface ISingleCall {
19 | function execute(address target, bytes calldata data) external view returns (bytes memory);
20 | }
21 |
22 | // ArbOwner precompile used for tests
23 | #[sol(rpc)]
24 | interface ArbOwnerPublic {
25 | function getAllChainOwners() external view returns (address[] memory);
26 | }
27 | }
28 |
29 | #[tokio::test]
30 | async fn single_call() -> Result<()> {
31 | let devnode = Node::new().await?;
32 | let rpc = devnode.rpc();
33 | println!("Deploying contract to Nitro ({rpc})...");
34 | let address = stylus_tools::deploy(rpc, DEVNET_PRIVATE_KEY)?;
35 | println!("Deployed contract to {address}");
36 | let provider = devnode.create_provider().await?;
37 | let contract = ISingleCall::ISingleCallInstance::new(address, provider);
38 |
39 | let calldata = ArbOwnerPublic::getAllChainOwnersCall {}.abi_encode();
40 | let owners_raw = contract
41 | .execute(ARB_OWNER_PUBLIC, calldata.into())
42 | .call()
43 | .await?;
44 | let owners =
45 | ArbOwnerPublic::getAllChainOwnersCall::abi_decode_returns_validate(&owners_raw)?;
46 |
47 | assert_eq!(owners, vec![Address::ZERO, OWNER]);
48 |
49 | Ok(())
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/licenses/COPYRIGHT.md:
--------------------------------------------------------------------------------
1 | # Licensing Information
2 |
3 | Copyright 2022-2024 Offchain Labs, Inc.
4 |
5 | Copyright assignment and [DCO sign-off](DCO.txt) is required to contribute to this project.
6 |
7 | Except as otherwise noted (below and/or in individual files), this project is licensed under the Apache License, Version 2.0 ([`LICENSE-APACHE`](Apache-2.0) or http://www.apache.org/licenses/LICENSE-2.0) or the MIT license, ([`LICENSE-MIT`](MIT) or http://opensource.org/licenses/MIT), at your option.
8 |
9 | Each Stylus logo is a service mark of Offchain Labs (collectively, the "Offchain Labs Trademarks"). Offchain Labs reserves all rights in the Offchain Labs Trademarks and nothing herein or in these licenses should be construed as granting, by implication, estoppel, or otherwise, any license or right to use any of Offchain Labs Trademarks without Offchain Labs' prior written permission in each instance. All goodwill generated from the use of Offchain Labs Trademarks will inure to our exclusive benefit.
10 |
--------------------------------------------------------------------------------
/licenses/DCO.txt:
--------------------------------------------------------------------------------
1 | Developer Certificate of Origin
2 | Version 1.1
3 |
4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
5 |
6 | Everyone is permitted to copy and distribute verbatim copies of this
7 | license document, but changing it is not allowed.
8 |
9 |
10 | Developer's Certificate of Origin 1.1
11 |
12 | By making a contribution to this project, I certify that:
13 |
14 | (a) The contribution was created in whole or in part by me and I
15 | have the right to submit it under the open source license
16 | indicated in the file; or
17 |
18 | (b) The contribution is based upon previous work that, to the best
19 | of my knowledge, is covered under an appropriate open source
20 | license and I have the right under that license to submit that
21 | work with modifications, whether created in whole or in part
22 | by me, under the same open source license (unless I am
23 | permitted to submit under a different license), as indicated
24 | in the file; or
25 |
26 | (c) The contribution was provided directly to me by some other
27 | person who certified (a), (b) or (c) and I have not modified
28 | it.
29 |
30 | (d) I understand and agree that this project and the contribution
31 | are public and that a record of the contribution (including all
32 | personal information I submit with it, including my sign-off) is
33 | maintained indefinitely and may be redistributed consistent with
34 | this project or the open source license(s) involved.
35 |
--------------------------------------------------------------------------------
/licenses/MIT:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright 2022-2024 Offchain Labs, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/mini-alloc/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | target = "wasm32-unknown-unknown"
3 |
4 | [target.wasm32-unknown-unknown]
5 | rustflags = [
6 | "-C", "link-arg=-zstack-size=8192",
7 | ]
8 |
--------------------------------------------------------------------------------
/mini-alloc/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "mini-alloc"
3 | keywords = ["wasm", "stylus", "allocator"]
4 | description = "Very simple global allocator"
5 | readme = "README.md"
6 |
7 | authors.workspace = true
8 | edition.workspace = true
9 | license.workspace = true
10 | repository.workspace = true
11 | version.workspace = true
12 |
13 | [dev-dependencies]
14 | wasm-bindgen-test = "0.3.0"
15 |
16 | [dependencies]
17 | cfg-if = "1.0.0"
18 |
--------------------------------------------------------------------------------
/mini-alloc/README.md:
--------------------------------------------------------------------------------
1 | # mini-alloc
2 |
3 | `mini-alloc` is a small and performant allocator optimized for `wasm32` targets like [Arbitrum Stylus][Stylus]. You can use it in your program as follows.
4 | ```rust
5 | #[global_allocator]
6 | static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT;
7 | ```
8 |
9 | ## Benchmarks
10 |
11 | `mini-alloc` implements a minimal bump allocator strategy. It never deallocates memory -- that is, `dealloc` does nothing. It's suitable for cases where binary size is at a premium and it's acceptable to leak all allocations. The simplicity of this model makes it very efficient, as seen in the following benchmarks.
12 |
13 | | | `MiniAlloc` | [`WeeAlloc`][WeeAlloc] | Std Library |
14 | |--------------|-------------|------------------------|----------------|
15 | | alloc | 333 gas | 721 gas | 516 gas |
16 | | alloc_zeroed | 329 gas | 95 million gas | 48 million gas |
17 |
18 | The benchmarks compare the performance of this crate's `edge_cases` test in the [Stylus VM][StylusVM]. Normal allocations are over **2x** cheaper than when using [`WeeAlloc`][WeeAlloc], a common WASM alternative that this crate defaults to when built for non-WASM targets.
19 |
20 | Replacing each instance of `alloc` in the test with `alloc_zeroed` reveals an over **99%** improvement for zero-filled allocations. Unlike [`WeeAlloc`][WeeAlloc] and the standard library, `MiniAlloc` takes advantage of the fact that WASM pages are zero-filled at initialization, and uses fewer of them due to the layout of Rust's memory.
21 |
22 | In the above tests we disable memory expansion costs, which unfairly penelize `WeeAlloc` and the standard library due to their increased resource consumption.
23 |
24 | ## Notice
25 |
26 | `MiniAlloc` should not be used in `wasm32` environments that enable the multithreading proposal. Although `MiniAlloc` implements `Sync` since Rust requires it for the global allocator, this crate should not be used in this way. This should not be a concern in [`Stylus`][Stylus].
27 |
28 | Also, `core::arch::wasm32::memory_grow` must never be called by any code outside this crate.
29 |
30 | ## License
31 |
32 | © 2023-2024 Offchain Labs, Inc.
33 |
34 | This project is licensed under either of
35 |
36 | - [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) ([licenses/Apache-2.0](../licenses/Apache-2.0))
37 | - [MIT license](https://opensource.org/licenses/MIT) ([licenses/MIT](../licenses/MIT))
38 |
39 | at your option.
40 |
41 | The [SPDX](https://spdx.dev) license identifier for this project is `MIT OR Apache-2.0`.
42 |
43 | [Stylus]: https://github.com/OffchainLabs/stylus-sdk-rs
44 | [StylusVM]: https://github.com/OffchainLabs/stylus
45 | [WeeAlloc]: https://github.com/rustwasm/wee_alloc
46 |
--------------------------------------------------------------------------------
/mini-alloc/src/imp.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | use core::{
5 | alloc::{GlobalAlloc, Layout},
6 | arch::wasm32,
7 | num::NonZeroUsize as NonZero,
8 | };
9 |
10 | pub struct MiniAlloc;
11 |
12 | /// This is not a valid implementation of [`Sync`] but is ok in single-threaded WASM.
13 | unsafe impl Sync for MiniAlloc {}
14 |
15 | impl MiniAlloc {
16 | pub const INIT: Self = MiniAlloc;
17 |
18 | /// The WASM page size, or 2^16 bytes.
19 | pub const PAGE_SIZE: usize = 1 << 16;
20 | }
21 |
22 | unsafe impl GlobalAlloc for MiniAlloc {
23 | unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
24 | alloc_impl(layout).unwrap_or(core::ptr::null_mut())
25 | }
26 |
27 | #[inline]
28 | unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
29 | self.alloc(layout)
30 | }
31 |
32 | #[inline]
33 | unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {}
34 | }
35 |
36 | extern "C" {
37 | /// This symbol is created by the LLVM linker.
38 | static __heap_base: u8;
39 | }
40 |
41 | /// Represents the negation of the allocator's bump offset and boundary
42 | ///
43 | /// We store the negation because we can align the negative offset in fewer
44 | /// instructions than the positive offset.
45 | static mut STATE: Option<(NonZero, usize)> = None;
46 |
47 | unsafe fn alloc_impl(layout: Layout) -> Option<*mut u8> {
48 | // Avoid the warning "creating a mutable reference to mutable static is discouraged" by taking
49 | // a raw pointer to STATE and then converting it to a mutable reference.
50 | // We know this is safe because:
51 | // * We are in single-threaded WASM.
52 | // * This is the only place that references STATE.
53 | // * This function is not reentrant.
54 | // See: https://doc.rust-lang.org/nightly/edition-guide/rust-2024/static-mut-references.html#safe-references
55 | let state_ref = &mut *&raw mut STATE;
56 | let (neg_offset, neg_bound) = state_ref.get_or_insert_with(|| {
57 | let heap_base = &__heap_base as *const u8 as usize;
58 | let bound = MiniAlloc::PAGE_SIZE * wasm32::memory_size(0) - 1;
59 | (
60 | NonZero::new_unchecked(heap_base.wrapping_neg()),
61 | bound.wrapping_neg(),
62 | )
63 | });
64 |
65 | let neg_aligned = make_aligned(neg_offset.get(), layout.align());
66 | let next_neg_offset = neg_aligned.checked_sub(layout.size())?;
67 | let bytes_needed = neg_bound.saturating_sub(next_neg_offset + 1);
68 | if bytes_needed != 0 {
69 | let pages_needed = 1 + (bytes_needed - 1) / MiniAlloc::PAGE_SIZE;
70 | if wasm32::memory_grow(0, pages_needed) == usize::MAX {
71 | return None;
72 | }
73 | *neg_bound -= MiniAlloc::PAGE_SIZE * pages_needed;
74 | }
75 | *neg_offset = NonZero::new_unchecked(next_neg_offset);
76 | Some(neg_aligned.wrapping_neg() as *mut u8)
77 | }
78 |
79 | /// Returns `value` rounded down to the next multiple of `align`.
80 | /// Note: `align` must be a power of two, which is guaranteed by [`Layout::align`].
81 | #[inline(always)]
82 | fn make_aligned(value: usize, align: usize) -> usize {
83 | value & align.wrapping_neg()
84 | }
85 |
--------------------------------------------------------------------------------
/mini-alloc/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | #![no_std]
5 |
6 | use cfg_if::cfg_if;
7 |
8 | cfg_if! {
9 | if #[cfg(target_arch = "wasm32")] {
10 | mod imp;
11 | pub use imp::MiniAlloc;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/mini-alloc/tests/misc.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | #![no_std]
5 |
6 | extern crate alloc;
7 |
8 | #[cfg(target_arch = "wasm32")]
9 | use wasm_bindgen_test::*;
10 |
11 | #[cfg(target_arch = "wasm32")]
12 | #[global_allocator]
13 | static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT;
14 |
15 | #[cfg(target_arch = "wasm32")]
16 | #[wasm_bindgen_test]
17 | fn vec_test() {
18 | use alloc::vec::Vec;
19 |
20 | let p1 = Vec::::with_capacity(700);
21 | let p2 = Vec::::with_capacity(65536);
22 | let p3 = Vec::::with_capacity(700000);
23 | let p4 = Vec::::with_capacity(1);
24 | let p5 = Vec::::with_capacity(1);
25 | let p6 = Vec::::with_capacity(1);
26 | assert_eq!(p1.as_ptr() as usize + 700, p2.as_ptr() as usize);
27 | assert_eq!(p2.as_ptr() as usize + 65536, p3.as_ptr() as usize);
28 | assert!((p4.as_ptr() as usize) < p3.as_ptr() as usize + 700004);
29 | assert!((p4.as_ptr() as usize) >= p3.as_ptr() as usize + 700000);
30 | assert_eq!(p4.as_ptr() as usize & 3, 0);
31 | assert_eq!(p4.as_ptr() as usize + 4, p5.as_ptr() as usize);
32 | assert_eq!(p5.as_ptr() as usize + 2, p6.as_ptr() as usize);
33 | }
34 |
35 | #[cfg(target_arch = "wasm32")]
36 | #[wasm_bindgen_test]
37 | fn vec_test_loop() {
38 | use alloc::vec::Vec;
39 |
40 | let mut size = 1usize;
41 | let mut p = Vec::::with_capacity(size);
42 | for _ in 0..22 {
43 | let new_size = size * 2;
44 | let new_p = Vec::::with_capacity(new_size);
45 | assert_eq!(p.as_ptr() as usize + size, new_p.as_ptr() as usize);
46 | size = new_size;
47 | p = new_p;
48 | }
49 | }
50 |
51 | #[cfg(target_arch = "wasm32")]
52 | #[wasm_bindgen_test]
53 | #[should_panic]
54 | fn vec_test_overallocate() {
55 | use alloc::vec::Vec;
56 |
57 | let _ = Vec::::with_capacity(0xFFFFFFFF);
58 | }
59 |
60 | #[cfg(target_arch = "wasm32")]
61 | #[wasm_bindgen_test]
62 | fn edge_cases() {
63 | use alloc::alloc::{alloc, Layout};
64 | use core::arch::wasm32;
65 | use mini_alloc::MiniAlloc;
66 |
67 | const PAGE_SIZE: usize = MiniAlloc::PAGE_SIZE;
68 | const PAGE_LIMIT: usize = 65536;
69 |
70 | fn size() -> usize {
71 | wasm32::memory_size(0) as usize
72 | }
73 |
74 | fn size_bytes() -> usize {
75 | size() * PAGE_SIZE
76 | }
77 |
78 | fn next(size: usize) -> usize {
79 | let align = 1;
80 | let layout = Layout::from_size_align(size, align).unwrap();
81 | unsafe { alloc(layout) as usize }
82 | }
83 |
84 | assert_eq!(size(), 1);
85 |
86 | // check that zero-allocs don't bump
87 | let start = next(0);
88 | assert_eq!(start, next(0));
89 | assert_eq!(start / PAGE_SIZE, 0);
90 | assert_eq!(size(), 1);
91 |
92 | // fill the rest of the page
93 | let rest = size_bytes() - start;
94 | let end = next(rest);
95 | assert_eq!(end / PAGE_SIZE, 0);
96 | assert_eq!(end, start);
97 | assert_eq!(size(), 1);
98 |
99 | // allocate a second page
100 | let first = next(1);
101 | assert_eq!(first / PAGE_SIZE, 1);
102 | assert_eq!(first, PAGE_SIZE);
103 | assert_eq!(size(), 2);
104 |
105 | // fill the rest of the second page
106 | let rest = size_bytes() - (first + 1);
107 | let end = next(rest);
108 | assert_eq!(end, first + 1);
109 | assert_eq!(size(), 2);
110 |
111 | // jump 4 pages
112 | let jump = next(4 * PAGE_SIZE);
113 | assert_eq!(jump, 2 * PAGE_SIZE);
114 | assert_eq!(size(), 6);
115 |
116 | // allocate many pages
117 | let mut rng: usize = 0;
118 | while size() < PAGE_LIMIT / 2 {
119 | rng = rng.wrapping_mul(1664525).wrapping_add(1013904223);
120 |
121 | let rest = usize::MAX - next(0) + 1;
122 | let bytes = match rng % 4 {
123 | 0 => rng % 1024,
124 | 1 => rng % PAGE_SIZE,
125 | 2 => next(size_bytes() - next(0)), // rest of page
126 | _ => rng % (PAGE_SIZE * 200),
127 | };
128 |
129 | let offset = next(bytes.min(rest));
130 |
131 | if offset == size_bytes() {
132 | assert_eq!(bytes, 0);
133 | } else {
134 | assert!(offset < size_bytes());
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "1.87.0"
3 |
--------------------------------------------------------------------------------
/stylus-core/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "stylus-core"
3 | keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
4 | description = "Core definitions of Stylus SDK traits and types that are used by multiple crates in the workspace"
5 | readme = "../README.md"
6 |
7 | authors.workspace = true
8 | edition.workspace = true
9 | license.workspace = true
10 | repository.workspace = true
11 | version.workspace = true
12 |
13 | [dependencies]
14 | alloy-primitives.workspace = true
15 | alloy-sol-types.workspace = true
16 | cfg-if.workspace = true
17 | dyn-clone = "1.0.17"
18 |
19 | [dev-dependencies]
20 |
21 | [features]
22 | default = []
23 | reentrant = []
24 |
--------------------------------------------------------------------------------
/stylus-core/src/calls/errors.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025-2026, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 | //
4 | extern crate alloc;
5 |
6 | use alloc::vec::Vec;
7 | use alloy_sol_types::{Panic, PanicKind, SolError};
8 |
9 | /// Represents error data when a call fails.
10 | #[derive(Debug, PartialEq)]
11 | pub enum Error {
12 | /// Revert data returned by the other contract.
13 | Revert(Vec),
14 | /// Failure to decode the other contract's return data.
15 | AbiDecodingFailed(alloy_sol_types::Error),
16 | }
17 |
18 | impl From for Error {
19 | fn from(err: alloy_sol_types::Error) -> Self {
20 | Error::AbiDecodingFailed(err)
21 | }
22 | }
23 |
24 | /// Encode an error.
25 | ///
26 | /// This is useful so that users can use `Error` as a variant in their error
27 | /// types. It should not be necessary to implement this.
28 | pub trait MethodError {
29 | /// Users should not have to call this.
30 | fn encode(self) -> Vec;
31 | }
32 |
33 | impl MethodError for Error {
34 | #[inline]
35 | fn encode(self) -> Vec {
36 | From::from(self)
37 | }
38 | }
39 |
40 | impl MethodError for T {
41 | #[inline]
42 | fn encode(self) -> Vec {
43 | SolError::abi_encode(&self)
44 | }
45 | }
46 |
47 | impl From for Vec {
48 | fn from(err: Error) -> Vec {
49 | match err {
50 | Error::Revert(data) => data,
51 | #[allow(unused)]
52 | Error::AbiDecodingFailed(err) => Panic::from(PanicKind::Generic).abi_encode(),
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/stylus-core/src/calls/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025-2026, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | use alloy_primitives::U256;
5 |
6 | use crate::TopLevelStorage;
7 |
8 | pub mod errors;
9 |
10 | /// Trait for calling other contracts.
11 | /// Users should rarely implement this trait outside of proc macros.
12 | pub trait CallContext {
13 | /// Amount of gas to supply the call.
14 | /// Note: values are clipped to the amount of gas remaining.
15 | fn gas(&self) -> u64;
16 | }
17 |
18 | /// Trait for calling the `view` or `pure` methods of other contracts.
19 | /// Users should rarely implement this trait outside of proc macros.
20 | pub trait StaticCallContext: CallContext {}
21 |
22 | /// Trait for calling the mutable methods of other contracts.
23 | /// Users should rarely implement this trait outside of proc macros.
24 | ///
25 | /// # Safety
26 | ///
27 | /// The type initializer must be a [`TopLevelStorage`][TLS] to prevent aliasing in cases of reentrancy.
28 | ///
29 | /// [TLS]: stylus_core::storage::TopLevelStorage
30 | pub unsafe trait MutatingCallContext: CallContext {
31 | /// Amount of ETH in wei to give the other contract.
32 | fn value(&self) -> U256;
33 | }
34 |
35 | /// Trait for calling the `write` methods of other contracts.
36 | /// Users should rarely implement this trait outside of proc macros.
37 | ///
38 | /// Note: any implementations of this must return zero for [`MutatingCallContext::value`].
39 | pub trait NonPayableCallContext: MutatingCallContext {}
40 |
41 | /// Enables configurable calls to other contracts.
42 | #[derive(Debug, Clone)]
43 | pub struct Call {
44 | gas: u64,
45 | value: Option,
46 | }
47 |
48 | impl Call {
49 | /// Amount of gas to supply the call.
50 | /// Values greater than the amount provided will be clipped to all gas left.
51 | pub fn gas(self, gas: u64) -> Self {
52 | Self { gas, ..self }
53 | }
54 |
55 | /// Amount of ETH in wei to give the other contract.
56 | /// Note: adding value will prevent calls to non-payable methods.
57 | pub fn value(self, value: U256) -> Call {
58 | Call {
59 | value: Some(value),
60 | gas: self.gas,
61 | }
62 | }
63 | }
64 |
65 | impl CallContext for Call {
66 | fn gas(&self) -> u64 {
67 | self.gas
68 | }
69 | }
70 |
71 | impl StaticCallContext for Call {}
72 |
73 | impl NonPayableCallContext for Call {}
74 |
75 | unsafe impl MutatingCallContext for Call {
76 | fn value(&self) -> U256 {
77 | self.value.unwrap_or_default()
78 | }
79 | }
80 |
81 | impl Default for Call {
82 | fn default() -> Self {
83 | Self::new()
84 | }
85 | }
86 |
87 | impl Call {
88 | pub fn new() -> Self {
89 | Self {
90 | gas: u64::MAX,
91 | value: None,
92 | }
93 | }
94 | }
95 |
96 | impl Call {
97 | pub fn new_mutating(_storage: &mut impl TopLevelStorage) -> Self {
98 | Self {
99 | gas: 0,
100 | value: None,
101 | }
102 | }
103 | }
104 |
105 | impl Call {
106 | pub fn new_payable(_storage: &mut impl TopLevelStorage, value: U256) -> Self {
107 | Self {
108 | gas: 0,
109 | value: Some(value),
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/stylus-core/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024-2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 | #![no_std]
4 |
5 | //! Defines host environment methods Stylus SDK contracts have access to.
6 |
7 | extern crate alloc;
8 |
9 | pub mod calls;
10 | pub mod host;
11 | pub mod storage;
12 |
13 | pub use calls::*;
14 | pub use host::*;
15 | pub use storage::TopLevelStorage;
16 |
--------------------------------------------------------------------------------
/stylus-core/src/storage.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024-2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | /// Trait for top-level storage types, usually implemented by proc macros.
5 | /// Top-level types are special in that their lifetimes track the entirety
6 | /// of all the EVM state-changes throughout a contract invocation.
7 | ///
8 | /// To prevent storage aliasing during reentrancy, you must hold a reference
9 | /// to such a type when making an EVM call. This may change in the future
10 | /// for programs that prevent reentrancy.
11 | ///
12 | /// # Safety
13 | ///
14 | /// The type must be top-level to prevent storage aliasing.
15 | pub unsafe trait TopLevelStorage {}
16 |
--------------------------------------------------------------------------------
/stylus-proc/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "stylus-proc"
3 | keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
4 | description = "Procedural macros for stylus-sdk"
5 | readme = "../README.md"
6 |
7 | authors.workspace = true
8 | edition.workspace = true
9 | license.workspace = true
10 | repository.workspace = true
11 | version.workspace = true
12 |
13 | [dependencies]
14 | alloy-primitives.workspace = true
15 | alloy-sol-types.workspace = true
16 | cfg-if.workspace = true
17 | convert_case.workspace = true
18 | lazy_static.workspace = true
19 | proc-macro-error.workspace = true
20 | proc-macro2.workspace = true
21 | quote.workspace = true
22 | regex.workspace = true
23 | sha3.workspace = true
24 | syn.workspace = true
25 | syn-solidity.workspace = true
26 | trybuild.workspace = true
27 |
28 | [dev-dependencies]
29 | paste.workspace = true
30 | pretty_assertions.workspace = true
31 | prettyplease.workspace = true
32 | stylus-sdk = { workspace = true, features = ["stylus-test"] }
33 | stylus-core.workspace = true
34 |
35 | [features]
36 | default = []
37 | export-abi = ["stylus-sdk/export-abi"]
38 | reentrant = []
39 | stylus-test = ["stylus-sdk/stylus-test"]
40 |
41 | [package.metadata.docs.rs]
42 | features = ["export-abi"]
43 |
44 | [lib]
45 | proc-macro = true
46 |
--------------------------------------------------------------------------------
/stylus-proc/README.md:
--------------------------------------------------------------------------------
1 | # stylus_proc
2 |
3 | Procedural Macros for Stylus SDK
4 |
5 | ## Macro usage
6 |
7 | Macro usage should be done through the [stylus-sdk] crate. Refer to the
8 | [documentation] for additional information and examples.
9 |
10 | ## Development Considerations
11 |
12 | ### Error handling
13 |
14 | The [proc_macro_error] crate is used for error handling to ensure consistency
15 | across rust versions and for convenience.
16 |
17 | Prefer [emit_error!] where possible to allow multiple errors to be displayed to
18 | the user. If an error is reached where compilation must be halted, [abort!]
19 | should be used instead.
20 |
21 | ### Testing
22 |
23 | Procedural macro implementations should be written in a way that returns AST
24 | data structures from the [syn] crate before converting them to
25 | [proc_macro::TokenStream]. This allows the implementation to be unit tested
26 | within its module to ensure the generated code is as expected.
27 |
28 | The [trybuild] crate is used to write test cases which should fail to compile.
29 | These tests are located in [tests/fail/] directory.
30 |
31 | [stylus-sdk]: https://crates.io/crates/stylus-sdk
32 | [documentation]: https://crates.io/crates/stylus-proc
33 | [proc_macro_error]: https://crates.io/crates/proc-macro-error
34 | [emit_error!]: https://docs.rs/proc-macro-error/latest/proc_macro_error/macro.emit_error.html
35 | [abort!]: https://docs.rs/proc-macro-error/latest/proc_macro_error/macro.abort.html
36 | [syn]: https://crates.io/crates/syn
37 | [proc_macro::TokenStream]: https://docs.rs/proc-macro/latest/proc_macro/struct.TokenStream.html
38 | [trybuild]: https://crates.io/crates/trybuild
39 |
--------------------------------------------------------------------------------
/stylus-proc/src/consts.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Constants for name definitions in generated code.
5 | //!
6 | //! Any generated globals or associated items should use a `__stylus` prefix to avoid name
7 | //! collisions.
8 |
9 | use proc_macro2::{Span, TokenStream};
10 | use quote::ToTokens;
11 |
12 | /// Name of the entrypoint function that is generated for struct-based contracts.
13 | pub const STRUCT_ENTRYPOINT_FN: ConstIdent = ConstIdent("__stylus_struct_entrypoint");
14 |
15 | /// Name of the associated function that can be called to assert safe overrides at compile-time.
16 | pub const ASSERT_OVERRIDES_FN: ConstIdent = ConstIdent("__stylus_assert_overrides");
17 |
18 | /// Name of the associated function that can be called to check safe overriding of a single
19 | /// function.
20 | pub const ALLOW_OVERRIDE_FN: ConstIdent = ConstIdent("__stylus_allow_override");
21 |
22 | pub const STYLUS_HOST_FIELD: ConstIdent = ConstIdent("__stylus_host");
23 |
24 | /// Definition of a constant identifier
25 | pub struct ConstIdent(&'static str);
26 |
27 | impl ConstIdent {
28 | pub fn as_ident(&self) -> syn::Ident {
29 | syn::Ident::new(self.0, Span::call_site())
30 | }
31 | }
32 |
33 | impl ToTokens for ConstIdent {
34 | fn to_tokens(&self, tokens: &mut TokenStream) {
35 | self.as_ident().to_tokens(tokens);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/stylus-proc/src/impls/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Generate trait implementations.
5 |
6 | pub mod abi_proxy;
7 |
--------------------------------------------------------------------------------
/stylus-proc/src/imports.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Constants for referencing imports within generated code.
5 | //!
6 | //! These constants use a fully qualified path with dependencies nested within [`stylus_sdk`] to
7 | //! ensure compatibility.
8 | //!
9 | //! Usage:
10 | //! ```compile_fail
11 | //! use crate::imports::alloy_primitives::Address;
12 | //!
13 | //! let _ = quote! {
14 | //! let addr = #Address::random();
15 | //! };
16 | //! ```
17 |
18 | #![allow(non_upper_case_globals)]
19 |
20 | use proc_macro2::TokenStream;
21 | use quote::ToTokens;
22 |
23 | pub mod alloy_primitives {
24 | use crate::imports::ConstPath;
25 |
26 | pub const Address: ConstPath = ConstPath("stylus_sdk::alloy_primitives::Address");
27 | }
28 |
29 | pub mod alloy_sol_types {
30 | use crate::imports::ConstPath;
31 |
32 | pub const SolType: ConstPath = ConstPath("stylus_sdk::alloy_sol_types::SolType");
33 | pub const SolValue: ConstPath = ConstPath("stylus_sdk::alloy_sol_types::SolValue");
34 |
35 | pub mod private {
36 | use crate::imports::ConstPath;
37 |
38 | pub const SolTypeValue: ConstPath =
39 | ConstPath("stylus_sdk::alloy_sol_types::private::SolTypeValue");
40 | }
41 |
42 | pub mod sol_data {
43 | use syn::parse::Parse;
44 |
45 | use crate::imports::ConstPath;
46 |
47 | pub const Address: ConstPath = ConstPath("stylus_sdk::alloy_sol_types::sol_data::Address");
48 |
49 | /// Build path or type to member of the `alloy_sol_types::sol_data` module.
50 | ///
51 | /// This should not be used on user input, as parsing should be expected to succeed.
52 | pub fn join(name: &str) -> T {
53 | syn::parse_str(&format!("stylus_sdk::alloy_sol_types::sol_data::{name}")).unwrap()
54 | }
55 | }
56 | }
57 |
58 | pub mod stylus_sdk {
59 | pub mod abi {
60 | use crate::imports::ConstPath;
61 |
62 | pub const AbiType: ConstPath = ConstPath("stylus_sdk::abi::AbiType");
63 | pub const Router: ConstPath = ConstPath("stylus_sdk::abi::Router");
64 | }
65 | }
66 |
67 | /// Definition of a fully-qualified path for generated code.
68 | pub struct ConstPath(&'static str);
69 |
70 | impl ConstPath {
71 | /// Interpret the path as a [`syn::Type`].
72 | pub fn as_type(&self) -> syn::Type {
73 | syn::parse_str(self.0).unwrap()
74 | }
75 | }
76 |
77 | impl ToTokens for ConstPath {
78 | fn to_tokens(&self, tokens: &mut TokenStream) {
79 | let path: syn::Path = syn::parse_str(self.0).unwrap();
80 | path.to_tokens(tokens);
81 | }
82 | }
83 |
84 | #[cfg(test)]
85 | mod tests {
86 | use syn::parse_quote;
87 |
88 | #[test]
89 | fn test_const_path() {
90 | assert_eq!(
91 | super::alloy_primitives::Address.as_type(),
92 | parse_quote!(stylus_sdk::alloy_primitives::Address),
93 | );
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/stylus-proc/src/macros/derive/abi_type.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | use proc_macro::TokenStream;
5 | use quote::ToTokens;
6 | use syn::{parse_macro_input, parse_quote};
7 |
8 | use crate::imports::stylus_sdk::abi::AbiType;
9 |
10 | /// Implementation of the [`#[derive(AbiType)]`][crate::AbiType] macro.
11 | pub fn derive_abi_type(input: TokenStream) -> TokenStream {
12 | let item = parse_macro_input!(input as syn::ItemStruct);
13 | impl_abi_type(&item).into_token_stream().into()
14 | }
15 |
16 | /// Implement [`stylus_sdk::abi::AbiType`] for the given struct.
17 | ///
18 | /// The name is used for the ABI name to match the
19 | /// [`SolType::SOL_NAME`][alloy_sol_types::SolType::SOL_NAME] generated by the
20 | /// [`sol!`][alloy_sol_types::sol] macro.
21 | fn impl_abi_type(item: &syn::ItemStruct) -> syn::ItemImpl {
22 | let name = &item.ident;
23 | let name_str = name.to_string();
24 | let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
25 |
26 | parse_quote! {
27 | impl #impl_generics #AbiType for #name #ty_generics #where_clause {
28 | type SolType = Self;
29 |
30 | const ABI: stylus_sdk::abi::ConstString = stylus_sdk::abi::ConstString::new(#name_str);
31 | }
32 | }
33 | }
34 |
35 | #[cfg(test)]
36 | mod tests {
37 | use syn::parse_quote;
38 |
39 | use super::impl_abi_type;
40 | use crate::utils::testing::assert_ast_eq;
41 |
42 | #[test]
43 | fn test_impl_abi_type() {
44 | assert_ast_eq(
45 | impl_abi_type(&parse_quote! {
46 | struct Foo
47 | where T: Bar {
48 | a: bool,
49 | b: String,
50 | t: T,
51 | }
52 | }),
53 | parse_quote! {
54 | impl stylus_sdk::abi::AbiType for Foo
55 | where T: Bar {
56 | type SolType = Self;
57 |
58 | const ABI: stylus_sdk::abi::ConstString = stylus_sdk::abi::ConstString::new("Foo");
59 | }
60 | },
61 | )
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/stylus-proc/src/macros/derive/erase.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | use proc_macro::TokenStream;
5 | use quote::ToTokens;
6 | use syn::{parse_macro_input, parse_quote};
7 |
8 | use crate::consts::STYLUS_HOST_FIELD;
9 |
10 | /// Implementation of the [`#[derive(Erase)]`][crate::derive_erase] macro.
11 | pub fn derive_erase(input: TokenStream) -> TokenStream {
12 | let node = parse_macro_input!(input as syn::ItemStruct);
13 | impl_erase(&node).into_token_stream().into()
14 | }
15 |
16 | /// Implement [`stylus_sdk::storage::Erase`] for the given struct.
17 | ///
18 | /// Calls `Erase::erase()` on each of the members of the struct.
19 | fn impl_erase(node: &syn::ItemStruct) -> syn::ItemImpl {
20 | let name = &node.ident;
21 | let (impl_generics, ty_generics, where_clause) = node.generics.split_for_impl();
22 | let filtered_fields = node
23 | .fields
24 | .clone()
25 | .into_iter()
26 | .filter_map(|field| match field.ident {
27 | Some(ident) if ident == STYLUS_HOST_FIELD.as_ident() => None,
28 | _ => field.ident,
29 | });
30 |
31 | parse_quote! {
32 | impl #impl_generics stylus_sdk::storage::Erase for #name #ty_generics #where_clause {
33 | fn erase(&mut self) {
34 | #(
35 | self.#filtered_fields.erase();
36 | )*
37 | }
38 | }
39 | }
40 | }
41 |
42 | #[cfg(test)]
43 | mod tests {
44 | use syn::parse_quote;
45 |
46 | use super::*;
47 | use crate::utils::testing::assert_ast_eq;
48 |
49 | #[test]
50 | fn test_impl_erase() {
51 | assert_ast_eq(
52 | impl_erase(&parse_quote! {
53 | struct Foo {
54 | field1: StorageString,
55 | field2: T,
56 | }
57 | }),
58 | parse_quote! {
59 | impl stylus_sdk::storage::Erase for Foo {
60 | fn erase(&mut self) {
61 | self.field1.erase();
62 | self.field2.erase();
63 | }
64 | }
65 | },
66 | );
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/stylus-proc/src/macros/derive/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Implementations of derive macros.
5 |
6 | pub mod abi_type;
7 | pub mod erase;
8 | pub mod solidity_error;
9 |
--------------------------------------------------------------------------------
/stylus-proc/src/macros/derive/solidity_error/export_abi.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | use syn::parse_quote;
5 |
6 | use super::{DeriveSolidityError, SolidityErrorExtension};
7 |
8 | #[derive(Debug, Default)]
9 | pub struct InnerTypesExtension {
10 | errors: Vec,
11 | }
12 |
13 | impl SolidityErrorExtension for InnerTypesExtension {
14 | type Ast = syn::ItemImpl;
15 |
16 | fn add_variant(&mut self, field: syn::Field) {
17 | self.errors.push(field);
18 | }
19 |
20 | fn codegen(err: &DeriveSolidityError) -> syn::ItemImpl {
21 | let name = &err.name;
22 | let errors = err._ext.errors.iter();
23 | parse_quote! {
24 | impl stylus_sdk::abi::export::internal::InnerTypes for #name {
25 | fn inner_types() -> alloc::vec::Vec {
26 | use alloc::{format, vec};
27 | use core::any::TypeId;
28 | use stylus_sdk::abi::export::internal::InnerType;
29 | use stylus_sdk::alloy_sol_types::SolError;
30 |
31 | vec![
32 | #(
33 | InnerType {
34 | name: format!("error {};", <#errors as SolError>::SIGNATURE.replace(',', ", ")),
35 | id: TypeId::of::<#errors>(),
36 | }
37 | ),*
38 | ]
39 | }
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/stylus-proc/src/macros/derive/solidity_error/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | use cfg_if::cfg_if;
5 | use proc_macro2::TokenStream;
6 | use proc_macro_error::emit_error;
7 | use quote::ToTokens;
8 | use syn::{parse::Nothing, parse_macro_input, parse_quote, Fields};
9 |
10 | cfg_if! {
11 | if #[cfg(feature = "export-abi")] {
12 | mod export_abi;
13 | type Extension = export_abi::InnerTypesExtension;
14 | } else {
15 | type Extension = ();
16 | }
17 | }
18 |
19 | /// Implementation of the [`#[derive(SolidityError]`][crate::SolidityError] macro.
20 | pub fn derive_solidity_error(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
21 | let item = parse_macro_input!(input as syn::ItemEnum);
22 | DeriveSolidityError::from(&item).into_token_stream().into()
23 | }
24 |
25 | #[derive(Debug)]
26 | struct DeriveSolidityError {
27 | name: syn::Ident,
28 | from_impls: Vec,
29 | match_arms: Vec,
30 | _ext: E,
31 | }
32 |
33 | impl DeriveSolidityError {
34 | fn new(name: syn::Ident) -> Self {
35 | Self {
36 | name,
37 | from_impls: Vec::new(),
38 | match_arms: Vec::new(),
39 | _ext: Extension::default(),
40 | }
41 | }
42 |
43 | fn add_variant(&mut self, name: &syn::Ident, field: syn::Field) {
44 | let self_name = &self.name;
45 | let ty = &field.ty;
46 | self.from_impls.push(parse_quote! {
47 | impl From<#ty> for #self_name {
48 | fn from(value: #ty) -> Self {
49 | #self_name::#name(value)
50 | }
51 | }
52 | });
53 | self.match_arms.push(parse_quote! {
54 | #self_name::#name(e) => stylus_sdk::stylus_core::errors::MethodError::encode(e),
55 | });
56 | #[allow(clippy::unit_arg)]
57 | self._ext.add_variant(field);
58 | }
59 |
60 | fn vec_u8_from_impl(&self) -> syn::ItemImpl {
61 | let name = &self.name;
62 | let match_arms = self.match_arms.iter();
63 | parse_quote! {
64 | impl From<#name> for alloc::vec::Vec {
65 | fn from(err: #name) -> Self {
66 | match err {
67 | #(#match_arms)*
68 | }
69 | }
70 | }
71 | }
72 | }
73 | }
74 |
75 | impl From<&syn::ItemEnum> for DeriveSolidityError {
76 | fn from(item: &syn::ItemEnum) -> Self {
77 | let mut output = DeriveSolidityError::new(item.ident.clone());
78 |
79 | for variant in &item.variants {
80 | match &variant.fields {
81 | Fields::Unnamed(e) if variant.fields.len() == 1 => {
82 | let field = e.unnamed.first().unwrap().clone();
83 | output.add_variant(&variant.ident, field);
84 | }
85 | Fields::Unit => {
86 | emit_error!(variant, "variant not a 1-tuple");
87 | }
88 | _ => {
89 | emit_error!(variant.fields, "variant not a 1-tuple");
90 | }
91 | };
92 | }
93 |
94 | output
95 | }
96 | }
97 |
98 | impl ToTokens for DeriveSolidityError {
99 | fn to_tokens(&self, tokens: &mut TokenStream) {
100 | for from_impl in &self.from_impls {
101 | from_impl.to_tokens(tokens);
102 | }
103 | self.vec_u8_from_impl().to_tokens(tokens);
104 | Extension::codegen(self).to_tokens(tokens);
105 | }
106 | }
107 |
108 | trait SolidityErrorExtension: Default {
109 | type Ast: ToTokens;
110 |
111 | fn add_variant(&mut self, field: syn::Field);
112 | fn codegen(err: &DeriveSolidityError) -> Self::Ast;
113 | }
114 |
115 | impl SolidityErrorExtension for () {
116 | type Ast = Nothing;
117 |
118 | fn add_variant(&mut self, _field: syn::Field) {}
119 |
120 | fn codegen(_err: &DeriveSolidityError) -> Self::Ast {
121 | Nothing
122 | }
123 | }
124 |
125 | #[cfg(test)]
126 | mod tests {
127 | use syn::parse_quote;
128 |
129 | use super::DeriveSolidityError;
130 | use crate::utils::testing::assert_ast_eq;
131 |
132 | #[test]
133 | fn test_derive_solidity_error() {
134 | let derived = DeriveSolidityError::from(&parse_quote! {
135 | enum MyError {
136 | Foo(FooError),
137 | Bar(BarError),
138 | }
139 | });
140 | assert_ast_eq(
141 | &derived.from_impls[0],
142 | &parse_quote! {
143 | impl From for MyError {
144 | fn from(value: FooError) -> Self {
145 | MyError::Foo(value)
146 | }
147 | }
148 | },
149 | );
150 | assert_ast_eq(
151 | derived.vec_u8_from_impl(),
152 | parse_quote! {
153 | impl From for alloc::vec::Vec {
154 | fn from(err: MyError) -> Self {
155 | match err {
156 | MyError::Foo(e) => stylus_sdk::stylus_core::errors::MethodError::encode(e),
157 | MyError::Bar(e) => stylus_sdk::stylus_core::errors::MethodError::encode(e),
158 | }
159 | }
160 | }
161 | },
162 | );
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/stylus-proc/src/macros/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | mod derive;
5 | mod entrypoint;
6 | mod public;
7 | mod sol_interface;
8 | mod sol_storage;
9 | mod storage;
10 |
11 | pub use derive::abi_type::derive_abi_type;
12 | pub use derive::erase::derive_erase;
13 | pub use derive::solidity_error::derive_solidity_error;
14 | pub use entrypoint::entrypoint;
15 | pub use public::public;
16 | pub use sol_interface::sol_interface;
17 | pub use sol_storage::sol_storage;
18 | pub use storage::storage;
19 |
--------------------------------------------------------------------------------
/stylus-proc/src/macros/public/attrs.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | use syn::{
5 | parse::{Parse, ParseStream},
6 | punctuated::Punctuated,
7 | Token,
8 | };
9 |
10 | /// Inherit from parent contracts.
11 | ///
12 | /// Used for the `#[inherit(Parent1, Parent2]` attribute.
13 | pub struct Inherit {
14 | pub types: Punctuated,
15 | }
16 |
17 | impl Parse for Inherit {
18 | fn parse(input: ParseStream) -> syn::Result {
19 | Ok(Self {
20 | types: Punctuated::parse_terminated(input)?,
21 | })
22 | }
23 | }
24 |
25 | /// Implement parent trait routes.
26 | ///
27 | /// Used for the `#[implements(Parent1, Parent2)` attribute.
28 | ///
29 | /// The contract must implement whatever traits are specified.
30 | pub struct Implements {
31 | pub types: Punctuated,
32 | }
33 |
34 | impl Parse for Implements {
35 | fn parse(input: ParseStream) -> syn::Result {
36 | Ok(Self {
37 | types: Punctuated::parse_terminated(input)?,
38 | })
39 | }
40 | }
41 |
42 | /// Selector name overloading for public functions.
43 | ///
44 | /// Used for the `#[selector(name = "...")]` attribute.
45 | #[derive(Debug)]
46 | pub struct Selector {
47 | _name: kw::name,
48 | _eq_token: Token![=],
49 | pub value: syn::LitStr,
50 | }
51 |
52 | impl Parse for Selector {
53 | fn parse(input: ParseStream) -> syn::Result {
54 | Ok(Self {
55 | _name: input.parse()?,
56 | _eq_token: input.parse()?,
57 | value: input.parse()?,
58 | })
59 | }
60 | }
61 |
62 | mod kw {
63 | syn::custom_keyword!(name);
64 | }
65 |
--------------------------------------------------------------------------------
/stylus-proc/src/macros/public/overrides.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Ensure that public functions follow safe override rules.
5 |
6 | use proc_macro2::Span;
7 | use syn::parse_quote;
8 |
9 | use super::types::{FnExtension, PublicFn, PublicImpl};
10 | use crate::consts::{ALLOW_OVERRIDE_FN, ASSERT_OVERRIDES_FN};
11 |
12 | impl PublicImpl {
13 | pub fn impl_override_checks(&self) -> syn::ItemImpl {
14 | let Self {
15 | self_ty,
16 | generic_params,
17 | where_clause,
18 | ..
19 | } = self;
20 | let selector_consts = self
21 | .funcs
22 | .iter()
23 | .map(PublicFn::selector_const)
24 | .collect::>();
25 | let override_arms = self.funcs.iter().map(PublicFn::override_arm);
26 | let inheritance_overrides = self.inheritance_overrides();
27 | let override_checks = self.override_checks();
28 | parse_quote! {
29 | impl<#generic_params> #self_ty where #where_clause {
30 | /// Whether or not to allow overriding a selector by a child contract and method with
31 | /// the given purity. This is currently implemented as a hidden function to allow it to
32 | /// be `const`. A trait would be better, but `const` is not currently supported for
33 | /// trait fns.
34 | #[doc(hidden)]
35 | pub const fn #ALLOW_OVERRIDE_FN(selector: u32, purity: stylus_sdk::methods::Purity) -> bool {
36 | use stylus_sdk::function_selector;
37 |
38 | #(#selector_consts)*
39 | if !match selector {
40 | #(#override_arms)*
41 | _ => true
42 | } { return false; }
43 | #(#inheritance_overrides)*
44 | true
45 | }
46 |
47 | /// Check the functions defined in an entrypoint for valid overrides.
48 | #[doc(hidden)]
49 | pub const fn #ASSERT_OVERRIDES_FN() {
50 | use stylus_sdk::function_selector;
51 |
52 | #(#selector_consts)*
53 | #(#override_checks)*
54 | }
55 | }
56 | }
57 | }
58 |
59 | fn inheritance_overrides(&self) -> impl Iterator- + '_ {
60 | self.inheritance.iter().map(|ty| {
61 | parse_quote! {
62 | if !<#ty>::#ALLOW_OVERRIDE_FN(selector, purity) {
63 | return false;
64 | }
65 | }
66 | })
67 | }
68 |
69 | fn override_checks(&self) -> impl Iterator
- + '_ {
70 | self.funcs
71 | .iter()
72 | .map(|func| func.assert_override(&self.self_ty))
73 | .chain(self.inheritance.iter().map(|ty| {
74 | parse_quote! {
75 | <#ty>::#ASSERT_OVERRIDES_FN();
76 | }
77 | }))
78 | }
79 | }
80 |
81 | impl PublicFn {
82 | fn override_arm(&self) -> syn::Arm {
83 | let constant = self.selector_name();
84 | let purity = self.purity.as_path();
85 | parse_quote! {
86 | #[allow(non_upper_case_globals)]
87 | #constant => #purity.allow_override(purity),
88 | }
89 | }
90 |
91 | fn override_error(&self) -> syn::LitStr {
92 | syn::LitStr::new(
93 | &format!(
94 | "function {} cannot be overriden with function marked {:?}",
95 | self.name, self.purity,
96 | ),
97 | Span::mixed_site(),
98 | )
99 | }
100 |
101 | fn assert_override(&self, self_ty: &syn::Type) -> syn::Stmt {
102 | let purity = self.purity.as_path();
103 | let selector_name = self.selector_name();
104 | let error = self.override_error();
105 | parse_quote! {
106 | assert!(<#self_ty>::#ALLOW_OVERRIDE_FN(#selector_name, #purity), #error);
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/stylus-proc/src/macros/sol_storage/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | use proc_macro::TokenStream;
5 | use quote::quote;
6 | use syn::{parse_macro_input, parse_quote, punctuated::Punctuated, Token};
7 |
8 | use proc::{SolidityField, SolidityFields, SolidityStruct, SolidityStructs};
9 |
10 | mod proc;
11 |
12 | pub fn sol_storage(input: TokenStream) -> TokenStream {
13 | let SolidityStructs(decls) = parse_macro_input!(input as SolidityStructs);
14 | let mut out = quote!();
15 |
16 | for decl in decls {
17 | let SolidityStruct {
18 | attrs,
19 | vis,
20 | name,
21 | generics,
22 | fields: SolidityFields(fields),
23 | } = decl;
24 |
25 | let fields: Punctuated<_, Token![,]> = fields
26 | .into_iter()
27 | .map(|SolidityField { attrs, name, ty }| -> syn::Field {
28 | parse_quote! {
29 | #(#attrs)*
30 | pub #name: #ty
31 | }
32 | })
33 | .collect();
34 |
35 | out.extend(quote! {
36 | #(#attrs)*
37 | #[stylus_sdk::stylus_proc::storage]
38 | #vis struct #name #generics {
39 | #fields
40 | }
41 | });
42 | }
43 |
44 | out.into()
45 | }
46 |
--------------------------------------------------------------------------------
/stylus-proc/src/utils/attrs.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Utilities for handling macro attributes.
5 |
6 | use proc_macro2::TokenStream;
7 | use proc_macro_error::emit_error;
8 | use syn::parse::{Nothing, Parse};
9 |
10 | /// Consume any used attributes, leaving unused attributes in the list.
11 | pub fn consume_attr(
12 | attrs: &mut Vec,
13 | ident_str: &'static str,
14 | ) -> Option {
15 | let mut result = None;
16 | for attr in core::mem::take(attrs) {
17 | // skip all other attrs, adding them back to the Vec
18 | if !attr_ident_matches(&attr, ident_str) {
19 | attrs.push(attr);
20 | continue;
21 | }
22 |
23 | if result.is_some() {
24 | emit_error!(attr, "duplicate attribute");
25 | }
26 |
27 | let tokens = get_attr_tokens(&attr).unwrap_or_default();
28 | match syn::parse2(tokens) {
29 | Ok(value) => result = Some(value),
30 | Err(err) => {
31 | emit_error!(err.span(), "{}", err);
32 | }
33 | }
34 | }
35 | result
36 | }
37 |
38 | /// Consume a flag attribute (no input tokens)
39 | pub fn consume_flag(attrs: &mut Vec, ident_str: &'static str) -> bool {
40 | consume_attr::(attrs, ident_str).is_some()
41 | }
42 |
43 | /// Check that an attribute stream is empty.
44 | pub fn check_attr_is_empty(attr: impl Into) {
45 | let attr = attr.into();
46 | if let Err(err) = syn::parse2::(attr) {
47 | emit_error!(err.span(), "{}", err);
48 | }
49 | }
50 |
51 | /// Check if attribute is a simple [`syn::Ident`] and matches a given string
52 | fn attr_ident_matches(attr: &syn::Attribute, value: &'static str) -> bool {
53 | matches!(attr.path().get_ident(), Some(ident) if *ident == value)
54 | }
55 |
56 | /// Get tokens for parsing from a [`syn::Attribute`].
57 | fn get_attr_tokens(attr: &syn::Attribute) -> Option {
58 | if let syn::Meta::List(syn::MetaList { tokens, .. }) = &attr.meta {
59 | Some(tokens.clone())
60 | } else {
61 | None
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/stylus-proc/src/utils/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Macro generation utilities.
5 |
6 | use sha3::{Digest, Keccak256};
7 | use syn::{punctuated::Punctuated, Token};
8 | use syn_solidity::SolIdent;
9 |
10 | pub mod attrs;
11 |
12 | #[cfg(test)]
13 | pub mod testing;
14 |
15 | /// Like [`syn::Generics::split_for_impl`] but for [`syn::ItemImpl`].
16 | ///
17 | /// [`syn::Generics::split_for_impl`] does not work in this case because the `name` of the
18 | /// implemented type is not easy to get, but the type including generics is.
19 | pub fn split_item_impl_for_impl(
20 | node: &syn::ItemImpl,
21 | ) -> (
22 | Punctuated,
23 | syn::Type,
24 | Punctuated,
25 | ) {
26 | let generic_params = node.generics.params.clone();
27 | let self_ty = (*node.self_ty).clone();
28 | let where_clause = node
29 | .generics
30 | .where_clause
31 | .clone()
32 | .map(|c| c.predicates)
33 | .unwrap_or_default();
34 | (generic_params, self_ty, where_clause)
35 | }
36 |
37 | /// Build [function selector](https://solidity-by-example.org/function-selector/) byte array.
38 | pub fn build_selector<'a>(
39 | name: &SolIdent,
40 | params: impl Iterator
- ,
41 | ) -> [u8; 4] {
42 | let mut selector = Keccak256::new();
43 | selector.update(name.to_string());
44 | selector.update("(");
45 | for (i, param) in params.enumerate() {
46 | if i > 0 {
47 | selector.update(",");
48 | }
49 | selector.update(param.to_string());
50 | }
51 | selector.update(")");
52 | let selector = selector.finalize();
53 | [selector[0], selector[1], selector[2], selector[3]]
54 | }
55 |
--------------------------------------------------------------------------------
/stylus-proc/src/utils/testing.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Utilities for testing.
5 |
6 | use quote::ToTokens;
7 |
8 | /// Assert equality of two AST nodes, with pretty diff output for failures.
9 | pub fn assert_ast_eq(left: T, right: T) {
10 | let left = pprint(left);
11 | let right = pprint(right);
12 | pretty_assertions::assert_str_eq!(left, right);
13 | }
14 |
15 | fn pprint(node: T) -> String {
16 | let tokens = node.into_token_stream();
17 | let file = syn::parse2(tokens).unwrap();
18 | prettyplease::unparse(&file)
19 | }
20 |
--------------------------------------------------------------------------------
/stylus-proc/tests/derive_abi_type.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | use stylus_proc::AbiType;
5 | use stylus_sdk::abi::AbiType;
6 | use stylus_sdk::alloy_sol_types::sol;
7 | use stylus_sdk::alloy_sol_types::SolType;
8 |
9 | sol! {
10 | #[derive(Debug, PartialEq, AbiType)]
11 | struct MyStruct {
12 | uint8 bar;
13 | }
14 | }
15 |
16 | #[test]
17 | fn test_abi_type() {
18 | assert_eq!(::ABI.as_str(), "MyStruct");
19 | assert_eq!(
20 | ::ABI.as_str(),
21 | ::SOL_NAME,
22 | );
23 | }
24 |
25 | #[test]
26 | fn test_abi_encode() {
27 | let mut expected = [0u8; 32];
28 | expected[31] = 100;
29 | assert_eq!(
30 | ::abi_encode(&MyStruct { bar: 100 }),
31 | expected,
32 | );
33 | }
34 |
35 | #[test]
36 | fn test_abi_decode() {
37 | let mut input = [0u8; 32];
38 | input[31] = 100;
39 | assert_eq!(
40 | ::abi_decode(&input),
41 | Ok(MyStruct { bar: 100 }),
42 | );
43 | }
44 |
45 | #[test]
46 | fn test_derive_abi_type_failures() {
47 | let t = trybuild::TestCases::new();
48 | t.compile_fail("tests/fail/derive_abi_type/missing_sol_macro.rs");
49 | }
50 |
--------------------------------------------------------------------------------
/stylus-proc/tests/derive_erase.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 | extern crate alloc;
4 |
5 | use stylus_proc::{storage, Erase};
6 | use stylus_sdk::prelude::*;
7 | use stylus_sdk::storage::{StorageType, StorageU256, StorageVec};
8 |
9 | #[storage]
10 | #[derive(Erase)]
11 | pub struct Erasable {
12 | arr: StorageVec,
13 | }
14 |
--------------------------------------------------------------------------------
/stylus-proc/tests/derive_solidity_error.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | extern crate alloc;
5 |
6 | use alloy_primitives::{Address, U256};
7 | use alloy_sol_types::sol;
8 |
9 | use stylus_proc::{public, SolidityError};
10 |
11 | sol! {
12 | error InsufficientBalance(address from, uint256 have, uint256 want);
13 | error InsufficientAllowance(address owner, address spender, uint256 have, uint256 want);
14 | }
15 |
16 | #[derive(SolidityError)]
17 | pub enum Erc20Error {
18 | InsufficientBalance(InsufficientBalance),
19 | InsufficientAllowance(InsufficientAllowance),
20 | }
21 |
22 | struct Contract {}
23 |
24 | #[public]
25 | impl Contract {
26 | /// Test using the defined error in a result value
27 | pub fn fallible_method() -> Result<(), Erc20Error> {
28 | Err(InsufficientBalance {
29 | from: Address::ZERO,
30 | have: U256::ZERO,
31 | want: U256::ZERO,
32 | }
33 | .into())
34 | }
35 | }
36 |
37 | #[test]
38 | fn test_derive_solidity_error_failures() {
39 | let t = trybuild::TestCases::new();
40 | t.compile_fail("tests/fail/derive_solidity_error/invalid_variants.rs");
41 | }
42 |
--------------------------------------------------------------------------------
/stylus-proc/tests/fail/derive_abi_type/missing_sol_macro.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Compilation will fail if the type is not wrapped in the [`sol!`][alloy_sol_types::sol] macro.
5 |
6 | use stylus_proc::AbiType;
7 | use stylus_sdk::storage::StorageBool;
8 |
9 | #[derive(AbiType)]
10 | struct MyStruct {
11 | bar: StorageBool,
12 | }
13 |
14 | fn main() {}
15 |
--------------------------------------------------------------------------------
/stylus-proc/tests/fail/derive_abi_type/missing_sol_macro.stderr:
--------------------------------------------------------------------------------
1 | error[E0277]: the trait bound `MyStruct: SolType` is not satisfied
2 | --> tests/fail/derive_abi_type/missing_sol_macro.rs:9:10
3 | |
4 | 9 | #[derive(AbiType)]
5 | | ^^^^^^^ the trait `SolType` is not implemented for `MyStruct`
6 | |
7 | = help: the following other types implement trait `SolType`:
8 | ()
9 | (T1, T2)
10 | (T1, T2, T3)
11 | (T1, T2, T3, T4)
12 | (T1, T2, T3, T4, T5)
13 | (T1, T2, T3, T4, T5, T6)
14 | (T1, T2, T3, T4, T5, T6, T7)
15 | (T1, T2, T3, T4, T5, T6, T7, T8)
16 | and $N others
17 | note: required by a bound in `stylus_sdk::abi::AbiType::SolType`
18 | --> $WORKSPACE/stylus-sdk/src/abi/mod.rs
19 | |
20 | | type SolType: SolType;
21 | | ^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `AbiType::SolType`
22 | = note: this error originates in the derive macro `AbiType` (in Nightly builds, run with -Z macro-backtrace for more info)
23 |
--------------------------------------------------------------------------------
/stylus-proc/tests/fail/derive_solidity_error/invalid_variants.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | use alloy_sol_types::sol;
5 |
6 | use stylus_proc::SolidityError;
7 |
8 | sol! {
9 | error InsufficientBalance(address from, uint256 have, uint256 want);
10 | error InsufficientAllowance(address owner, address spender, uint256 have, uint256 want);
11 | }
12 |
13 | #[derive(SolidityError)]
14 | enum MyError {
15 | Unit,
16 | Two(InsufficientBalance, InsufficientAllowance),
17 | Named { balance: InsufficientBalance },
18 | }
19 |
20 | fn main() {}
21 |
--------------------------------------------------------------------------------
/stylus-proc/tests/fail/derive_solidity_error/invalid_variants.stderr:
--------------------------------------------------------------------------------
1 | error: variant not a 1-tuple
2 | --> tests/fail/derive_solidity_error/invalid_variants.rs:15:5
3 | |
4 | 15 | Unit,
5 | | ^^^^
6 |
7 | error: variant not a 1-tuple
8 | --> tests/fail/derive_solidity_error/invalid_variants.rs:16:8
9 | |
10 | 16 | Two(InsufficientBalance, InsufficientAllowance),
11 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12 |
13 | error: variant not a 1-tuple
14 | --> tests/fail/derive_solidity_error/invalid_variants.rs:17:11
15 | |
16 | 17 | Named { balance: InsufficientBalance },
17 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
18 |
--------------------------------------------------------------------------------
/stylus-proc/tests/fail/public/constructor.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | extern crate alloc;
5 |
6 | use stylus_proc::public;
7 |
8 | struct Contract {}
9 |
10 | #[public]
11 | impl Contract {
12 | // error: function can be only one of fallback, receive or constructor
13 | #[fallback]
14 | #[receive]
15 | #[constructor]
16 | fn init() {}
17 |
18 | // error: fallback, receive, and constructor can't have custom selector
19 | #[constructor]
20 | #[selector(name = "foo")]
21 | fn constr() {}
22 |
23 | // error: constructor function can only be defined using the corresponding attribute
24 | fn constructor() {}
25 |
26 | // error: constructor function can only be defined using the corresponding attribute
27 | #[receive]
28 | fn constructor() {}
29 |
30 | // error: constructor function can only be defined using the corresponding attribute
31 | fn stylus_constructor() {}
32 |
33 | // error: constructor function can only be defined using the corresponding attribute
34 | #[selector(name = "stylusConstructor")]
35 | fn foo() {}
36 | }
37 |
38 | fn main() {}
39 |
40 |
--------------------------------------------------------------------------------
/stylus-proc/tests/fail/public/constructor.stderr:
--------------------------------------------------------------------------------
1 | error: function can be only one of fallback, receive or constructor
2 | --> tests/fail/public/constructor.rs:16:5
3 | |
4 | 16 | fn init() {}
5 | | ^^
6 |
7 | error: fallback, receive, and constructor can't have custom selector
8 | --> tests/fail/public/constructor.rs:21:5
9 | |
10 | 21 | fn constr() {}
11 | | ^^
12 |
13 | error: constructor function can only be defined using the corresponding attribute
14 | --> tests/fail/public/constructor.rs:24:5
15 | |
16 | 24 | fn constructor() {}
17 | | ^^
18 |
19 | error: constructor function can only be defined using the corresponding attribute
20 | --> tests/fail/public/constructor.rs:28:5
21 | |
22 | 28 | fn constructor() {}
23 | | ^^
24 |
25 | error: constructor function can only be defined using the corresponding attribute
26 | --> tests/fail/public/constructor.rs:31:5
27 | |
28 | 31 | fn stylus_constructor() {}
29 | | ^^
30 |
31 | error: constructor function can only be defined using the corresponding attribute
32 | --> tests/fail/public/constructor.rs:35:5
33 | |
34 | 35 | fn foo() {}
35 | | ^^
36 |
--------------------------------------------------------------------------------
/stylus-proc/tests/fail/public/generated.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Compilation failures after macro generation completes.
5 |
6 | extern crate alloc;
7 |
8 | use stylus_proc::public;
9 |
10 | struct UnsupportedType;
11 |
12 | struct Contract {}
13 |
14 | #[public]
15 | impl Contract {
16 | fn unsupported_input(_arg: UnsupportedType) {}
17 |
18 | fn unsupported_output() -> UnsupportedType {
19 | UnsupportedType
20 | }
21 | }
22 |
23 | fn main() {}
24 |
--------------------------------------------------------------------------------
/stylus-proc/tests/fail/public/generated.stderr:
--------------------------------------------------------------------------------
1 | error[E0277]: the trait bound `UnsupportedType: AbiType` is not satisfied
2 | --> tests/fail/public/generated.rs:16:32
3 | |
4 | 16 | fn unsupported_input(_arg: UnsupportedType) {}
5 | | ^^^^^^^^^^^^^^^ the trait `AbiType` is not implemented for `UnsupportedType`
6 | |
7 | = help: the following other types implement trait `AbiType`:
8 | ()
9 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
10 | (B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
11 | (C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
12 | (D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
13 | (E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
14 | (F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
15 | (G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
16 | and $N others
17 |
18 | error[E0277]: the trait bound `UnsupportedType: AbiType` is not satisfied
19 | --> tests/fail/public/generated.rs:16:26
20 | |
21 | 16 | fn unsupported_input(_arg: UnsupportedType) {}
22 | | ^^^^ the trait `AbiType` is not implemented for `UnsupportedType`
23 | |
24 | = help: the following other types implement trait `AbiType`:
25 | ()
26 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
27 | (B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
28 | (C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
29 | (D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
30 | (E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
31 | (F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
32 | (G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
33 | and $N others
34 | = note: required for `(UnsupportedType,)` to implement `AbiType`
35 |
36 | error[E0277]: the trait bound `UnsupportedType: EncodableReturnType` is not satisfied
37 | --> tests/fail/public/generated.rs:18:32
38 | |
39 | 18 | fn unsupported_output() -> UnsupportedType {
40 | | ^^^^^^^^^^^^^^^ the trait `AbiType` is not implemented for `UnsupportedType`
41 | |
42 | = help: the following other types implement trait `AbiType`:
43 | ()
44 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
45 | (B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
46 | (C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
47 | (D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
48 | (E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
49 | (F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
50 | (G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X)
51 | and $N others
52 | = note: required for `UnsupportedType` to implement `EncodableReturnType`
53 |
--------------------------------------------------------------------------------
/stylus-proc/tests/fail/public/macro_errors.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Compilation should fail for any unsupported attributes or other features.
5 |
6 | extern crate alloc;
7 |
8 | use stylus_proc::public;
9 |
10 | struct Contract {}
11 |
12 | #[public(unsupported)]
13 | impl Contract {
14 | #[payable(unsupported)]
15 | fn test_method() {}
16 | }
17 |
18 | fn main() {}
19 |
--------------------------------------------------------------------------------
/stylus-proc/tests/fail/public/macro_errors.stderr:
--------------------------------------------------------------------------------
1 | error: unexpected token
2 | --> tests/fail/public/macro_errors.rs:12:10
3 | |
4 | 12 | #[public(unsupported)]
5 | | ^^^^^^^^^^^
6 |
7 | error: unexpected token
8 | --> tests/fail/public/macro_errors.rs:14:15
9 | |
10 | 14 | #[payable(unsupported)]
11 | | ^^^^^^^^^^^
12 |
--------------------------------------------------------------------------------
/stylus-proc/tests/fail/sol_interface/generated.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Compilation failures after macro generation completes.
5 |
6 | extern crate alloc;
7 |
8 | use stylus_proc::sol_interface;
9 |
10 | sol_interface! {
11 | interface IService {
12 | #[function_attr]
13 | function makePayment(address user) payable external returns (string);
14 | function getConstant() pure external returns (bytes32);
15 | }
16 |
17 | #[interface_attr]
18 | interface ITree {
19 | // Define more interface methods here
20 | }
21 | }
22 |
23 | fn main() {}
24 |
--------------------------------------------------------------------------------
/stylus-proc/tests/fail/sol_interface/generated.stderr:
--------------------------------------------------------------------------------
1 | error: cannot find attribute `function_attr` in this scope
2 | --> tests/fail/sol_interface/generated.rs:12:11
3 | |
4 | 12 | #[function_attr]
5 | | ^^^^^^^^^^^^^
6 |
7 | error: cannot find attribute `interface_attr` in this scope
8 | --> tests/fail/sol_interface/generated.rs:17:7
9 | |
10 | 17 | #[interface_attr]
11 | | ^^^^^^^^^^^^^^
12 |
--------------------------------------------------------------------------------
/stylus-proc/tests/fail/sol_interface/macro_errors.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Compilation should fail for any unsupported Solidity features.
5 |
6 | use stylus_proc::sol_interface;
7 |
8 | sol_interface! {
9 | #![file_attribute]
10 |
11 | interface IParent {
12 | function makePayment(address user) payable external returns (string);
13 | function getConstant() pure external returns (bytes32);
14 | }
15 |
16 | interface IChild is IParent {
17 | }
18 |
19 | contract TestContract {
20 | }
21 |
22 | function sum(uint[] memory arr) pure returns (uint s) {
23 | for (uint i = 0; i < arr.length; i++)
24 | s += arr[i];
25 | }
26 | }
27 |
28 | fn main() {}
29 |
--------------------------------------------------------------------------------
/stylus-proc/tests/fail/sol_interface/macro_errors.stderr:
--------------------------------------------------------------------------------
1 | error: attribute not supported
2 | --> tests/fail/sol_interface/macro_errors.rs:9:5
3 | |
4 | 9 | #![file_attribute]
5 | | ^^^^^^^^^^^^^^^^^^
6 |
7 | error: inheritance not supported
8 | --> tests/fail/sol_interface/macro_errors.rs:16:22
9 | |
10 | 16 | interface IChild is IParent {
11 | | ^^
12 |
13 | error: not an interface
14 | --> tests/fail/sol_interface/macro_errors.rs:19:14
15 | |
16 | 19 | contract TestContract {
17 | | ^^^^^^^^^^^^
18 |
19 | error: not an interface
20 | --> tests/fail/sol_interface/macro_errors.rs:22:14
21 | |
22 | 22 | function sum(uint[] memory arr) pure returns (uint s) {
23 | | ^^^
24 |
--------------------------------------------------------------------------------
/stylus-proc/tests/mutating_call_context_safety.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Integration test for using call contexts with sol_interface macros to generate
5 | //! cross-contract call bindings.
6 | //!
7 | //! Currently this simply checks that a contract using this macro can compile successfully.
8 |
9 | #![allow(dead_code)]
10 | #![allow(unused_variables)]
11 |
12 | extern crate alloc;
13 |
14 | use alloy_primitives::U256;
15 | use stylus_sdk::prelude::*;
16 |
17 | sol_interface! {
18 | interface IFoo {
19 | function viewFoo() view external;
20 | function mutateFoo() external;
21 | function payFoo() payable external;
22 | }
23 | }
24 |
25 | #[entrypoint]
26 | #[storage]
27 | struct Contract {}
28 |
29 | #[public]
30 | impl Contract {
31 | pub fn mutate(&mut self, methods: IFoo) -> Result<(), Vec> {
32 | let cfg = Call::new_mutating(self);
33 | methods.mutate_foo(self.vm(), cfg).unwrap();
34 | Ok(())
35 | }
36 | pub fn view(&mut self, methods: IFoo) -> Result<(), Vec> {
37 | let cfg = Call::new();
38 | methods.view_foo(self.vm(), cfg).unwrap();
39 | Ok(())
40 | }
41 | #[payable]
42 | pub fn pay(&mut self, methods: IFoo) -> Result<(), Vec> {
43 | let cfg = Call::new_payable(self, U256::from(1));
44 | methods.pay_foo(self.vm(), cfg).unwrap();
45 | Ok(())
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/stylus-proc/tests/public.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Integration test for the `#[public]` macro
5 | //!
6 | //! Currently this simply checks that a contract using this macro can compile successfully.
7 |
8 | extern crate alloc;
9 |
10 | use alloy_primitives::U256;
11 | use stylus_proc::public;
12 | use stylus_sdk::{storage::StorageU256, ArbResult};
13 |
14 | struct Contract {
15 | value: StorageU256,
16 | }
17 |
18 | #[public]
19 | impl Contract {
20 | #[payable]
21 | fn method() {}
22 |
23 | #[fallback]
24 | fn fallback(&mut self, _args: &[u8]) -> ArbResult {
25 | Ok(vec![])
26 | }
27 |
28 | #[receive]
29 | fn receive(&mut self) -> Result<(), Vec> {
30 | Ok(())
31 | }
32 |
33 | #[constructor]
34 | fn constructor(&mut self, value: U256) {
35 | self.value.set(value);
36 | }
37 |
38 | fn value(&self) -> Result> {
39 | Ok(self.value.get())
40 | }
41 | }
42 |
43 | #[test]
44 | fn test_public_failures() {
45 | let t = trybuild::TestCases::new();
46 | #[cfg(not(feature = "export-abi"))]
47 | t.compile_fail("tests/fail/public/generated.rs");
48 | t.compile_fail("tests/fail/public/macro_errors.rs");
49 | t.compile_fail("tests/fail/public/constructor.rs");
50 | }
51 |
--------------------------------------------------------------------------------
/stylus-proc/tests/public_composition.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Integration test for the `#[public]` macro using composition based inheritance.
5 | //!
6 | //! Currently this simply checks that a contract using this macro can compile successfully.
7 |
8 | #![allow(dead_code)]
9 | #![allow(unused_variables)]
10 |
11 | extern crate alloc;
12 |
13 | use stylus_sdk::{
14 | alloy_primitives::{Address, U256},
15 | prelude::*,
16 | storage::{StorageAddress, StorageMap, StorageU256},
17 | };
18 |
19 | #[storage]
20 | #[entrypoint]
21 | struct Contract {
22 | erc20: Erc20,
23 | ownable: Ownable,
24 | }
25 |
26 | #[public]
27 | #[implements(IErc20, IOwnable)]
28 | impl Contract {}
29 |
30 | #[storage]
31 | struct Erc20 {
32 | balances: StorageMap,
33 | total_supply: StorageU256,
34 | }
35 |
36 | trait IErc20 {
37 | fn name(&self) -> String;
38 | fn symbol(&self) -> String;
39 | fn decimals(&self) -> U256;
40 | fn total_supply(&self) -> U256;
41 | fn balance_of(&self, _account: Address) -> U256;
42 | fn transfer(&mut self, _to: Address, _value: U256) -> bool;
43 | // fn transfer_from(&mut self, from: Address, to: Address, value: U256) -> bool;
44 | // fn approve(&mut self, spender: Address, value: U256) -> bool;
45 | // fn allowance(&self, owner: Address, spender: Address) -> U256;
46 | }
47 |
48 | #[public]
49 | impl IErc20 for Contract {
50 | fn name(&self) -> String {
51 | todo!()
52 | }
53 | fn symbol(&self) -> String {
54 | todo!()
55 | }
56 | fn decimals(&self) -> U256 {
57 | todo!()
58 | }
59 | fn total_supply(&self) -> U256 {
60 | todo!()
61 | }
62 | fn balance_of(&self, _account: Address) -> U256 {
63 | todo!()
64 | }
65 | fn transfer(&mut self, _to: Address, _value: U256) -> bool {
66 | todo!()
67 | }
68 | }
69 |
70 | #[storage]
71 | struct Ownable {
72 | owner: StorageAddress,
73 | }
74 |
75 | trait IOwnable {
76 | fn owner(&self) -> Address;
77 | fn transfer_ownership(&mut self, new_owner: Address) -> bool;
78 | fn renounce_ownership(&mut self) -> bool;
79 | }
80 |
81 | #[public]
82 | impl IOwnable for Contract {
83 | fn owner(&self) -> Address {
84 | todo!()
85 | }
86 | fn transfer_ownership(&mut self, new_owner: Address) -> bool {
87 | todo!()
88 | }
89 | fn renounce_ownership(&mut self) -> bool {
90 | todo!()
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/stylus-proc/tests/sol_interface.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Integration test for the `sol_interface!` macro
5 |
6 | use stylus_proc::sol_interface;
7 |
8 | mod inner {
9 | use alloy_sol_types::sol;
10 |
11 | sol! {
12 | struct Foo {
13 | uint256 bar;
14 | }
15 | }
16 | }
17 |
18 | sol_interface! {
19 | #[derive(Debug)]
20 | interface IService {
21 | function makePayment(address user) payable external returns (string);
22 | function getConstant() pure external returns (bytes32);
23 | function getFoo() pure external returns (inner.Foo);
24 | }
25 |
26 | interface ITree {
27 | // Define more interface methods here
28 | }
29 | }
30 |
31 | #[test]
32 | fn sol_interface_failures() {
33 | let t = trybuild::TestCases::new();
34 | t.compile_fail("tests/fail/sol_interface/macro_errors.rs");
35 | t.compile_fail("tests/fail/sol_interface/generated.rs");
36 | }
37 |
--------------------------------------------------------------------------------
/stylus-sdk/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "stylus-sdk"
3 | keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
4 | description = "Rust smart contracts with Arbitrum Stylus"
5 | readme = "../README.md"
6 |
7 | authors.workspace = true
8 | edition.workspace = true
9 | license.workspace = true
10 | repository.workspace = true
11 | version.workspace = true
12 |
13 | [dependencies]
14 | alloy-primitives.workspace = true
15 | alloy-sol-types.workspace = true
16 | cfg-if.workspace = true
17 | clap.workspace = true
18 | derivative.workspace = true
19 | hex = { workspace = true, default-features = false, features = ["alloc"] }
20 | keccak-const.workspace = true
21 | lazy_static.workspace = true
22 |
23 | # export-abi
24 | regex = { workspace = true, optional = true }
25 |
26 | # local deps
27 | mini-alloc = { workspace = true, optional = true }
28 | stylus-proc.workspace = true
29 | stylus-core.workspace = true
30 |
31 | # Ensure these dependencies won't be included in wasm32 target
32 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
33 | rclite = { workspace = true, optional = true }
34 | stylus-test = { workspace = true, optional = true }
35 |
36 | [dev-dependencies]
37 | paste.workspace = true
38 | sha3.workspace = true
39 | alloy-primitives = { workspace = true, default-features = false, features=["tiny-keccak"] }
40 | stylus-sdk = { workspace = true, features = ["stylus-test"] }
41 |
42 | [package.metadata.docs.rs]
43 | features = ["default", "docs", "debug", "export-abi", "stylus-test"]
44 |
45 | [features]
46 | default = ["mini-alloc"]
47 | export-abi = ["debug", "regex", "stylus-proc/export-abi", "alloy-primitives/tiny-keccak"]
48 | debug = []
49 | docs = []
50 | hostio = []
51 | mini-alloc = ["dep:mini-alloc"]
52 | stylus-test = ["dep:stylus-test", "dep:rclite", "stylus-proc/stylus-test"]
53 | reentrant = ["stylus-proc/reentrant", "stylus-core/reentrant", "stylus-test/reentrant"]
54 |
--------------------------------------------------------------------------------
/stylus-sdk/README.md:
--------------------------------------------------------------------------------
1 | ../README.md
--------------------------------------------------------------------------------
/stylus-sdk/src/abi/const_string.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Provides [`ConstString`], a mechanism for string operations in `const` contexts.
5 |
6 | use core::{
7 | fmt::{Debug, Display},
8 | ops::Deref,
9 | };
10 |
11 | /// Maximum length of a [`ConstString`] in bytes.
12 | pub const MAX_CONST_STRING_LENGTH: usize = 1024;
13 |
14 | /// Represents a string with a bounded length at compile time.
15 | /// This allows something approximating string operations in `const` contexts.
16 | #[derive(Clone)]
17 | pub struct ConstString {
18 | /// The signature's text encoding. Must be valid UTF-8.
19 | data: [u8; MAX_CONST_STRING_LENGTH],
20 | /// The length of the string in bytes.
21 | len: usize,
22 | }
23 |
24 | /// Copies data from `source` to `dest` in a `const` context.
25 | /// This function is very inefficient for other purposes.
26 | const fn memcpy(
27 | mut source: &[u8],
28 | mut dest: [u8; N],
29 | mut offset: usize,
30 | ) -> [u8; N] {
31 | if offset > dest.len() {
32 | panic!("out-of-bounds memcpy");
33 | }
34 | while !source.is_empty() {
35 | dest[offset] = source[0];
36 | offset += 1;
37 | (_, source) = source.split_at(1);
38 | }
39 | dest
40 | }
41 |
42 | impl ConstString {
43 | /// Creates a new [`ConstString`] equivalent to the empty string.
44 | pub const fn new(s: &str) -> ConstString {
45 | let mut data = [0u8; MAX_CONST_STRING_LENGTH];
46 | data = memcpy(s.as_bytes(), data, 0);
47 | ConstString { data, len: s.len() }
48 | }
49 |
50 | /// Creates a new [`ConstString`] from a decimal number.
51 | /// For example, the number 42 maps to "42".
52 | pub const fn from_decimal_number(mut number: usize) -> ConstString {
53 | let mut data = [0u8; MAX_CONST_STRING_LENGTH];
54 | let digits = number.checked_ilog10();
55 | let digits = match digits {
56 | // TODO: simplify when `const_precise_live_drops` is stabilized
57 | // https://github.com/rust-lang/rust/issues/73255
58 | Some(digits) => digits as usize + 1,
59 | None => 1,
60 | };
61 |
62 | if digits > MAX_CONST_STRING_LENGTH {
63 | panic!("from_decimal_number: too many digits");
64 | }
65 | let mut position = digits;
66 | while position > 0 {
67 | position -= 1;
68 | data[position] = b'0' + (number % 10) as u8;
69 | number /= 10;
70 | }
71 | Self { data, len: digits }
72 | }
73 |
74 | /// Selects a [`ConstString`] depending on the condition.
75 | pub const fn select(cond: bool, true_value: &str, false_value: &str) -> Self {
76 | match cond {
77 | true => Self::new(true_value),
78 | false => Self::new(false_value),
79 | }
80 | }
81 |
82 | /// Clones a [`ConstString`] in a `const` context.
83 | pub const fn const_clone(&self) -> Self {
84 | Self {
85 | data: self.data,
86 | len: self.len,
87 | }
88 | }
89 |
90 | /// Concatenates two [`ConstString`]'s.
91 | pub const fn concat(&self, other: ConstString) -> ConstString {
92 | let mut new = self.const_clone();
93 | new.data = memcpy(other.as_bytes(), new.data, self.len);
94 | new.len += other.len;
95 | new
96 | }
97 |
98 | /// Converts a [`ConstString`] to a slice.
99 | pub const fn as_bytes(&self) -> &[u8] {
100 | self.data.split_at(self.len).0
101 | }
102 |
103 | /// Converts a [`ConstString`] to an equivalent [`str`].
104 | pub const fn as_str(&self) -> &str {
105 | // # Safety
106 | // A `ConstString` represents a valid, utf8-encoded string
107 | unsafe { core::str::from_utf8_unchecked(self.as_bytes()) }
108 | }
109 | }
110 |
111 | impl Deref for ConstString {
112 | type Target = [u8];
113 |
114 | fn deref(&self) -> &Self::Target {
115 | self.as_bytes()
116 | }
117 | }
118 |
119 | impl Display for ConstString {
120 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
121 | write!(f, "{}", self.as_str())
122 | }
123 | }
124 |
125 | impl Debug for ConstString {
126 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
127 | write!(f, "{:?}", self.as_str())
128 | }
129 | }
130 |
131 | #[cfg(test)]
132 | mod tests {
133 | use super::*;
134 |
135 | #[test]
136 | fn test_from_decimal() {
137 | for i in (0..=100).chain(1000..=1001) {
138 | assert_eq!(ConstString::from_decimal_number(i).as_str(), i.to_string());
139 | }
140 | }
141 |
142 | #[test]
143 | fn test_concat() {
144 | assert_eq!(
145 | ConstString::new("foo")
146 | .concat(ConstString::new("bar"))
147 | .as_str(),
148 | "foobar"
149 | );
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/stylus-sdk/src/abi/export/internal.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! This module provides functions for code generated by `stylus-sdk-proc` for the `export-abi` command.
5 | //! Most users shouldn't call these.
6 |
7 | use core::any::TypeId;
8 |
9 | use alloy_primitives::{Address, FixedBytes, Signed, Uint};
10 |
11 | use crate::abi::Bytes;
12 |
13 | /// Represents a unique Solidity Type.
14 | pub struct InnerType {
15 | /// Full interface string.
16 | pub name: String,
17 | /// Unique identifier for de-duplication when printing interfaces.
18 | pub id: TypeId,
19 | }
20 |
21 | /// Trait for collecting structs and error types.
22 | pub trait InnerTypes {
23 | /// Collect any structs and errors under the type.
24 | /// Empty for primitives.
25 | fn inner_types() -> Vec {
26 | vec![]
27 | }
28 | }
29 |
30 | impl InnerTypes for Result
31 | where
32 | O: InnerTypes,
33 | E: InnerTypes,
34 | {
35 | fn inner_types() -> Vec {
36 | let mut out = O::inner_types();
37 | out.extend(E::inner_types());
38 | out
39 | }
40 | }
41 |
42 | impl InnerTypes for Vec {
43 | fn inner_types() -> Vec {
44 | T::inner_types()
45 | }
46 | }
47 |
48 | impl InnerTypes for [T; N] {
49 | fn inner_types() -> Vec {
50 | T::inner_types()
51 | }
52 | }
53 |
54 | macro_rules! impl_inner {
55 | ($ty:ident $($rest:ident)+) => {
56 | impl_inner!($ty);
57 | impl_inner!($($rest)+);
58 | };
59 | ($ty:ident) => {
60 | impl InnerTypes for $ty {}
61 | };
62 | }
63 |
64 | impl_inner!(bool u8 u16 u32 u64 u128 i8 i16 i32 i64 i128 String Address Bytes);
65 |
66 | impl InnerTypes for Uint {}
67 | impl InnerTypes for Signed {}
68 | impl InnerTypes for FixedBytes {}
69 |
70 | macro_rules! impl_tuple {
71 | () => {
72 | impl InnerTypes for () {}
73 | };
74 | ($first:ident $(, $rest:ident)*) => {
75 | impl<$first: InnerTypes $(, $rest: InnerTypes)*> InnerTypes for ( $first $(, $rest)* , ) {
76 | fn inner_types() -> Vec {
77 | vec![]
78 | }
79 | }
80 |
81 | impl_tuple! { $($rest),* }
82 | };
83 | }
84 |
85 | 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);
86 |
--------------------------------------------------------------------------------
/stylus-sdk/src/abi/export/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Traits for exporting Solidity interfaces.
5 | //!
6 | //! The contents of this module are imported when the `export-abi` feature flag is enabled,
7 | //! which happens automatically during [`cargo stylus export-abi`][cargo].
8 | //!
9 | //! [cargo]: https://github.com/OffchainLabs/cargo-stylus#exporting-solidity-abis
10 |
11 | use clap::{Parser, Subcommand};
12 | use core::fmt;
13 | use lazy_static::lazy_static;
14 | use regex::Regex;
15 |
16 | #[doc(hidden)]
17 | pub mod internal;
18 |
19 | const DEFAULT_LICENSE: &str = "MIT-OR-APACHE-2.0";
20 | const DEFAULT_PRAGMA: &str = "pragma solidity ^0.8.23;";
21 |
22 | /// Export information about the Stylus contract.
23 | #[derive(Parser)]
24 | struct ExportCLI {
25 | #[command(subcommand)]
26 | commands: Option,
27 | }
28 |
29 | #[derive(Subcommand)]
30 | enum ExportCommands {
31 | /// Export the Stylus contract ABI as a Solidity interface.
32 | Abi {
33 | /// Lisense of the generated ABI file.
34 | #[arg(long, default_value = DEFAULT_LICENSE)]
35 | license: String,
36 | /// Solidity pragma line on the generated ABI file.
37 | #[arg(long, default_value = DEFAULT_PRAGMA)]
38 | pragma: String,
39 | },
40 | /// Export the constructor signature.
41 | Constructor,
42 | }
43 |
44 | /// Prints the ABI given the CLI options.
45 | pub fn print_from_args() {
46 | let args = ExportCLI::parse();
47 | match args.commands {
48 | None => {
49 | print_abi::(DEFAULT_LICENSE, DEFAULT_PRAGMA);
50 | }
51 | Some(ExportCommands::Abi { license, pragma }) => {
52 | print_abi::(&license, &pragma);
53 | }
54 | Some(ExportCommands::Constructor) => {
55 | print_constructor_signature::();
56 | }
57 | }
58 | }
59 |
60 | /// Trait for storage types so that users can print a Solidity interface to the console.
61 | /// This is auto-derived via the [`public`] macro when the `export-abi` feature is enabled.
62 | ///
63 | /// [`public`]: stylus-proc::public
64 | pub trait GenerateAbi {
65 | /// The interface's name.
66 | const NAME: &'static str;
67 |
68 | /// How to format the ABI. Analogous to [`Display`](std::fmt::Display).
69 | fn fmt_abi(f: &mut fmt::Formatter<'_>) -> fmt::Result;
70 |
71 | /// How to format the constructor signature. Analogous to [`Display`](std::fmt::Display).
72 | fn fmt_constructor_signature(f: &mut fmt::Formatter<'_>) -> fmt::Result;
73 | }
74 |
75 | /// Type that makes an ABI printable.
76 | struct AbiPrinter(fn(&mut fmt::Formatter<'_>) -> fmt::Result);
77 |
78 | impl fmt::Display for AbiPrinter {
79 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 | self.0(f)
81 | }
82 | }
83 |
84 | /// Prints the full contract ABI to standard out
85 | pub fn print_abi(license: &str, pragma: &str) {
86 | println!("/**");
87 | println!(" * This file was automatically generated by Stylus and represents a Rust program.");
88 | println!(" * For more information, please see [The Stylus SDK](https://github.com/OffchainLabs/stylus-sdk-rs).");
89 | println!(" */");
90 | println!();
91 | println!("// SPDX-License-Identifier: {license}");
92 | println!("{pragma}");
93 | println!();
94 | print!("{}", AbiPrinter(T::fmt_abi));
95 | }
96 |
97 | fn print_constructor_signature() {
98 | print!("{}", AbiPrinter(T::fmt_constructor_signature));
99 | }
100 |
101 | lazy_static! {
102 | static ref UINT_REGEX: Regex = Regex::new(r"^uint(\d+)$").unwrap();
103 | static ref INT_REGEX: Regex = Regex::new(r"^int(\d+)$").unwrap();
104 | static ref BYTES_REGEX: Regex = Regex::new(r"^bytes(\d+)$").unwrap();
105 | }
106 |
107 | /// Prepends the string with an underscore if it is a Solidity keyword.
108 | /// Otherwise, the string is unchanged.
109 | /// Note: also prepends a space when the input is nonempty.
110 | pub fn underscore_if_sol(name: &str) -> String {
111 | let underscore = || format!(" _{name}");
112 |
113 | if let Some(caps) = UINT_REGEX.captures(name) {
114 | let bits: usize = caps[1].parse().unwrap();
115 | if bits % 8 == 0 {
116 | return underscore();
117 | }
118 | }
119 |
120 | if let Some(caps) = INT_REGEX.captures(name) {
121 | let bits: usize = caps[1].parse().unwrap();
122 | if bits % 8 == 0 {
123 | return underscore();
124 | }
125 | }
126 |
127 | if let Some(caps) = BYTES_REGEX.captures(name) {
128 | let bits: usize = caps[1].parse().unwrap();
129 | if bits <= 32 {
130 | return underscore();
131 | }
132 | }
133 |
134 | match name {
135 | "" => "".to_string(),
136 |
137 | // other types
138 | "address" | "bool" | "int" | "uint" => underscore(),
139 |
140 | // other words
141 | "is" | "contract" | "interface" => underscore(),
142 |
143 | // reserved keywords
144 | "after" | "alias" | "apply" | "auto" | "byte" | "case" | "copyof" | "default"
145 | | "define" | "final" | "implements" | "in" | "inline" | "let" | "macro" | "match"
146 | | "mutable" | "null" | "of" | "partial" | "promise" | "reference" | "relocatable"
147 | | "sealed" | "sizeof" | "static" | "supports" | "switch" | "typedef" | "typeof" | "var" => {
148 | underscore()
149 | }
150 | _ => format!(" {name}"),
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/stylus-sdk/src/abi/internal.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! This module provides functions for code generated by `stylus-sdk-proc`.
5 | //! Most users shouldn't call these.
6 |
7 | use crate::{abi::AbiType, console, ArbResult};
8 | use alloc::vec::Vec;
9 | use alloy_primitives::U256;
10 | use alloy_sol_types::SolType;
11 | use core::fmt;
12 |
13 | /// Name used in the constructor storage slot and function selector.
14 | pub const CONSTRUCTOR_BASE_NAME: &str = "stylus_constructor";
15 |
16 | /// The storage slot that specify whether the constructor was executed.
17 | pub const CONSTRUCTOR_EXECUTED_SLOT: U256 = alloy_primitives::U256::from_be_bytes(
18 | keccak_const::Keccak256::new()
19 | .update(CONSTRUCTOR_BASE_NAME.as_bytes())
20 | .finalize(),
21 | );
22 |
23 | pub trait EncodableReturnType {
24 | fn encode(self) -> ArbResult;
25 | }
26 |
27 | impl EncodableReturnType for T
28 | where
29 | T: AbiType + alloy_sol_types::private::SolTypeValue<::SolType>,
30 | {
31 | #[inline(always)]
32 | fn encode(self) -> ArbResult {
33 | // coerce types into a tuple of at least 1 element
34 | Ok(<::SolType>::abi_encode(&self))
35 | }
36 | }
37 |
38 | impl>> EncodableReturnType for Result
39 | where
40 | T: AbiType + alloy_sol_types::private::SolTypeValue<::SolType>,
41 | {
42 | #[inline(always)]
43 | fn encode(self) -> ArbResult {
44 | match self {
45 | Ok(result) => result.encode(),
46 | Err(err) => Err(err.into()),
47 | }
48 | }
49 | }
50 |
51 | #[inline(always)]
52 | pub const fn digest_to_selector(digest: [u8; 32]) -> [u8; 4] {
53 | let mut selector = [0u8; 4];
54 | selector[0] = digest[0];
55 | selector[1] = digest[1];
56 | selector[2] = digest[2];
57 | selector[3] = digest[3];
58 | selector
59 | }
60 |
61 | #[allow(unused)]
62 | pub fn failed_to_decode_arguments(err: alloy_sol_types::Error) {
63 | console!("failed to decode arguments: {err}");
64 | }
65 |
66 | pub trait AbiResult {
67 | type OkType;
68 | }
69 |
70 | impl AbiResult for Result {
71 | type OkType = O;
72 | }
73 |
74 | impl AbiResult for T {
75 | type OkType = T;
76 | }
77 |
78 | pub fn write_solidity_returns(f: &mut fmt::Formatter) -> fmt::Result
79 | where
80 | T::OkType: AbiType,
81 | {
82 | let abi = T::OkType::EXPORT_ABI_RET.as_str();
83 | if abi == "()" {
84 | Ok(())
85 | } else if abi.starts_with('(') {
86 | write!(f, " returns {abi}")
87 | } else {
88 | write!(f, " returns ({abi})")
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/stylus-sdk/src/call/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Call other contracts.
5 | //!
6 | //! There are two primary ways to make calls to other contracts via the Stylus SDK.
7 | //! - [`Call`] with [`sol_interface!`][sol_interface] for richly-typed calls.
8 | //! - [`RawCall`] for `unsafe`, bytes-in bytes-out calls.
9 | //!
10 | //! Additional helpers exist for specific use-cases like [`transfer_eth`].
11 | //!
12 | //! [sol_interface]: crate::prelude::sol_interface
13 |
14 | use alloc::vec::Vec;
15 | use alloy_primitives::Address;
16 |
17 | pub(crate) use raw::CachePolicy;
18 | pub use raw::RawCall;
19 | use stylus_core::{
20 | calls::{errors::Error, MutatingCallContext, StaticCallContext},
21 | Host,
22 | };
23 |
24 | mod raw;
25 |
26 | /// Provides a convenience method to transfer ETH to a given address.
27 | pub mod transfer;
28 |
29 | /// Static calls the contract at the given address.
30 | pub fn static_call(
31 | host: &dyn Host,
32 | context: impl StaticCallContext,
33 | to: Address,
34 | data: &[u8],
35 | ) -> Result, Error> {
36 | host.flush_cache(false); // flush storage to persist changes, but don't invalidate the cache
37 | unsafe {
38 | RawCall::new_static(host)
39 | .gas(context.gas())
40 | .call(to, data)
41 | .map_err(Error::Revert)
42 | }
43 | }
44 |
45 | /// Delegate calls the contract at the given address.
46 | ///
47 | /// # Safety
48 | ///
49 | /// A delegate call must trust the other contract to uphold safety requirements.
50 | /// Though this function clears any cached values, the other contract may arbitrarily change storage,
51 | /// spend ether, and do other things one should never blindly allow other contracts to do.
52 | pub unsafe fn delegate_call(
53 | host: &dyn Host,
54 | context: impl MutatingCallContext,
55 | to: Address,
56 | data: &[u8],
57 | ) -> Result, Error> {
58 | host.flush_cache(true); // clear storage to persist changes, invalidating the cache
59 |
60 | RawCall::new_delegate(host)
61 | .gas(context.gas())
62 | .call(to, data)
63 | .map_err(Error::Revert)
64 | }
65 |
66 | /// Calls the contract at the given address.
67 | pub fn call(
68 | host: &dyn Host,
69 | context: impl MutatingCallContext,
70 | to: Address,
71 | data: &[u8],
72 | ) -> Result, Error> {
73 | host.flush_cache(true); // clear storage to persist changes, invalidating the cache
74 |
75 | unsafe {
76 | RawCall::new_with_value(host, context.value())
77 | .gas(context.gas())
78 | .call(to, data)
79 | .map_err(Error::Revert)
80 | }
81 | }
82 |
83 | #[cfg(test)]
84 | mod test {
85 | use super::*;
86 | use alloy_primitives::{Address, U256};
87 | use stylus_core::CallContext;
88 | use stylus_test::TestVM;
89 |
90 | #[derive(Clone)]
91 | pub struct MyContract;
92 | impl CallContext for MyContract {
93 | fn gas(&self) -> u64 {
94 | 0
95 | }
96 | }
97 | unsafe impl MutatingCallContext for MyContract {
98 | fn value(&self) -> U256 {
99 | U256::from(0)
100 | }
101 | }
102 | impl StaticCallContext for MyContract {}
103 |
104 | #[test]
105 | fn test_calls() {
106 | let vm = TestVM::new();
107 | let contract = MyContract {};
108 | let target = Address::from([2u8; 20]);
109 | let data = vec![1, 2, 3, 4];
110 | let expected_return = vec![5, 6, 7, 8];
111 |
112 | // Mock a regular call.
113 | vm.mock_call(
114 | target,
115 | data.clone(),
116 | U256::ZERO,
117 | Ok(expected_return.clone()),
118 | );
119 |
120 | let response = call(&vm, contract.clone(), target, &data).unwrap();
121 | assert_eq!(response, expected_return);
122 | vm.clear_mocks();
123 |
124 | // Mock a delegate call.
125 | vm.mock_delegate_call(target, data.clone(), Ok(expected_return.clone()));
126 | let response = unsafe { delegate_call(&vm, contract.clone(), target, &data).unwrap() };
127 | assert_eq!(response, expected_return);
128 | vm.clear_mocks();
129 |
130 | // Mock a static call.
131 | vm.mock_static_call(target, data.clone(), Ok(expected_return.clone()));
132 | let response = static_call(&vm, contract.clone(), target, &data).unwrap();
133 | assert_eq!(response, expected_return);
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/stylus-sdk/src/call/transfer.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | use alloc::vec::Vec;
5 | use alloy_primitives::{Address, U256};
6 | use stylus_core::Host;
7 |
8 | use super::RawCall;
9 |
10 | /// Transfers an amount of ETH in wei to the given account.
11 | /// Note that this method will call the other contract, which may in turn call others.
12 | ///
13 | /// All gas is supplied, which the recipient may burn.
14 | /// If this is not desired, the [`call`](super::call) function may be used directly.
15 | ///
16 | /// ```
17 | /// # use stylus_sdk::prelude::*;
18 | /// # use stylus_sdk::stylus_core::host::Host;
19 | /// # use stylus_sdk::call::transfer::transfer_eth;
20 | /// # fn wrap(host: &dyn Host) -> Result<(), Vec> {
21 | /// # let value = alloy_primitives::U256::ZERO;
22 | /// # let recipient = alloy_primitives::Address::ZERO;
23 | /// transfer_eth(host, recipient, value)?; // these two are equivalent
24 | /// call(host, Call::new().value(value), recipient, &[])?; // these two are equivalent
25 | /// # Ok(())
26 | /// # }
27 | /// ```
28 | pub fn transfer_eth(host: &dyn Host, to: Address, amount: U256) -> Result<(), Vec> {
29 | host.flush_cache(true); // clear the storage to persist changes, invalidating the cache
30 | unsafe {
31 | RawCall::new_with_value(host, amount)
32 | .skip_return_data()
33 | .call(to, &[])?;
34 | }
35 | Ok(())
36 | }
37 |
--------------------------------------------------------------------------------
/stylus-sdk/src/crypto.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! VM-accelerated cryptography.
5 | //!
6 | //! See also [`block`](crate::block), [`contract`](crate::contract), [`evm`](crate::evm),
7 | //! [`msg`](crate::msg), and [`tx`](crate::tx).
8 | //!
9 | //! ```no_run
10 | //! use stylus_sdk::crypto;
11 | //! use stylus_sdk::alloy_primitives::address;
12 | //!
13 | //! let preimage = address!("361594F5429D23ECE0A88E4fBE529E1c49D524d8");
14 | //! let hash = crypto::keccak(&preimage);
15 | //! ```
16 |
17 | use alloy_primitives::B256;
18 |
19 | /// Efficiently computes the [`keccak256`] hash of the given preimage.
20 | ///
21 | /// [`keccak256`]: https://en.wikipedia.org/wiki/SHA-3
22 | pub fn keccak>(bytes: T) -> B256 {
23 | alloy_primitives::keccak256(bytes)
24 | }
25 |
--------------------------------------------------------------------------------
/stylus-sdk/src/debug.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Debug-only items for printing to the console.
5 | //!
6 | //! ```no_run
7 | //! use stylus_sdk::console;
8 | //! use stylus_sdk::alloy_primitives::address;
9 | //! extern crate alloc;
10 | //!
11 | //! let arbinaut = address!("361594F5429D23ECE0A88E4fBE529E1c49D524d8");
12 | //! console!("Gm {}", arbinaut); // prints nothing in production
13 | //! ```
14 |
15 | /// Prints a UTF-8 encoded string to the console. Only available in debug mode.
16 | #[cfg(feature = "debug")]
17 | pub fn console_log>(text: T) {
18 | let text = text.as_ref();
19 | unsafe { crate::hostio::log_txt(text.as_ptr(), text.len()) };
20 | }
21 |
22 | /// Prints to the console when executing in a debug environment. Otherwise does nothing.
23 | #[cfg(feature = "debug")]
24 | #[macro_export]
25 | macro_rules! console {
26 | ($($msg:tt)*) => {
27 | $crate::debug::console_log(alloc::format!($($msg)*));
28 | };
29 | }
30 |
31 | /// Prints to the console when executing in a debug environment. Otherwise does nothing.
32 | #[cfg(not(feature = "debug"))]
33 | #[macro_export]
34 | macro_rules! console {
35 | ($($msg:tt)*) => {{}};
36 | }
37 |
--------------------------------------------------------------------------------
/stylus-sdk/src/deploy/mod.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Deploy other contracts.
5 | //!
6 | //! Currently this module only supports low-level contract creation via [`RawDeploy`],
7 | //! but work is being done to introduce high-level deployment patterns.
8 |
9 | pub use raw::RawDeploy;
10 |
11 | mod raw;
12 |
--------------------------------------------------------------------------------
/stylus-sdk/src/deploy/raw.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | use crate::call::CachePolicy;
5 | use alloc::vec::Vec;
6 | use alloy_primitives::{Address, B256, U256};
7 | use stylus_core::Host;
8 |
9 | #[allow(unused_imports)]
10 | #[cfg(feature = "reentrant")]
11 | use crate::storage::StorageCache;
12 |
13 | /// Mechanism for performing raw deploys of other contracts.
14 | #[derive(Clone, Default)]
15 | #[must_use]
16 | pub struct RawDeploy {
17 | salt: Option,
18 | #[allow(unused)]
19 | cache_policy: CachePolicy,
20 | }
21 |
22 | impl RawDeploy {
23 | /// Begin configuring the raw deploy.
24 | pub fn new() -> Self {
25 | Default::default()
26 | }
27 |
28 | /// Configure the deploy to use the salt provided.
29 | /// This will use [`CREATE2`] under the hood to provide a deterministic address.
30 | ///
31 | /// [`CREATE2`]: https://www.evm.codes/#f5
32 | pub fn salt(mut self, salt: B256) -> Self {
33 | self.salt = Some(salt);
34 | self
35 | }
36 |
37 | /// Configure the deploy to use the salt provided.
38 | /// This will use [`CREATE2`] under the hood to provide a deterministic address if [`Some`].
39 | ///
40 | /// [`CREATE2`]: https://www.evm.codes/#f5
41 | pub fn salt_option(mut self, salt: Option) -> Self {
42 | self.salt = salt;
43 | self
44 | }
45 |
46 | /// Write all cached values to persistent storage before the init code.
47 | #[cfg(feature = "reentrant")]
48 | pub fn flush_storage_cache(mut self) -> Self {
49 | self.cache_policy = self.cache_policy.max(CachePolicy::Flush);
50 | self
51 | }
52 |
53 | /// Flush and clear the storage cache before the init code.
54 | #[cfg(feature = "reentrant")]
55 | pub fn clear_storage_cache(mut self) -> Self {
56 | self.cache_policy = CachePolicy::Clear;
57 | self
58 | }
59 |
60 | /// Performs a raw deploy of another contract with the given `endowment` and init `code`.
61 | /// Returns the address of the newly deployed contract, or the error data in case of failure.
62 | ///
63 | /// # Safety
64 | ///
65 | /// Note that the EVM allows init code to make calls to other contracts, which provides a vector for
66 | /// reentrancy. This means that this method may enable storage aliasing if used in the middle of a storage
67 | /// reference's lifetime and if reentrancy is allowed.
68 | ///
69 | /// For extra flexibility, this method does not clear the global storage cache.
70 | pub unsafe fn deploy(
71 | self,
72 | host: &dyn Host,
73 | code: &[u8],
74 | endowment: U256,
75 | ) -> Result> {
76 | #[cfg(feature = "reentrant")]
77 | match self.cache_policy {
78 | CachePolicy::Clear => host.flush_cache(true),
79 | CachePolicy::Flush => host.flush_cache(false),
80 | CachePolicy::DoNothing => {}
81 | }
82 |
83 | let mut contract = Address::default();
84 | let mut revert_data_len: usize = 0;
85 |
86 | let endowment: B256 = endowment.into();
87 | if let Some(salt) = self.salt {
88 | host.create2(
89 | code.as_ptr(),
90 | code.len(),
91 | endowment.as_ptr(),
92 | salt.as_ptr(),
93 | contract.as_mut_ptr(),
94 | &mut revert_data_len as *mut _,
95 | );
96 | } else {
97 | host.create1(
98 | code.as_ptr(),
99 | code.len(),
100 | endowment.as_ptr(),
101 | contract.as_mut_ptr(),
102 | &mut revert_data_len as *mut _,
103 | );
104 | }
105 |
106 | if contract.is_zero() {
107 | return Err(host.read_return_data(0, None));
108 | }
109 | Ok(contract)
110 | }
111 | }
112 |
113 | #[cfg(test)]
114 | mod test {
115 | use super::*;
116 | use alloy_primitives::{Address, U256};
117 | use stylus_test::TestVM;
118 |
119 | #[test]
120 | fn test_deploy() {
121 | let vm = TestVM::new();
122 |
123 | let code = vec![0x60, 0x80, 0x60, 0x40];
124 | let salt = B256::with_last_byte(1);
125 | let deployed_address = Address::from([2u8; 20]);
126 | vm.mock_deploy(code.clone(), Some(salt), Ok(deployed_address));
127 |
128 | let deployer = RawDeploy::new().salt(salt);
129 |
130 | let result = unsafe { deployer.deploy(&vm, &code, U256::ZERO).unwrap() };
131 | assert_eq!(result, deployed_address);
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/stylus-sdk/src/evm.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Affordances for the Ethereum Virtual Machine.
5 |
6 | use alloc::vec::Vec;
7 | use alloy_sol_types::{abi::token::WordToken, SolEvent, TopicList};
8 | use stylus_core::Host;
9 |
10 | /// Emits a typed alloy log.
11 | pub fn log(host: &dyn Host, event: T) {
12 | // According to the alloy docs, encode_topics_raw fails only if the array is too small
13 |
14 | let mut topics = [WordToken::default(); 4];
15 | event.encode_topics_raw(&mut topics).unwrap();
16 |
17 | let count = T::TopicList::COUNT;
18 | let mut bytes = Vec::with_capacity(32 * count);
19 | for topic in &topics[..count] {
20 | bytes.extend_from_slice(topic.as_slice());
21 | }
22 | event.encode_data_to(&mut bytes);
23 | host.emit_log(&bytes, count);
24 | }
25 |
--------------------------------------------------------------------------------
/stylus-sdk/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! The Stylus SDK.
5 | //!
6 | //! The Stylus SDK makes it easy to develop Solidity ABI-equivalent Stylus contracts in Rust.
7 | //! Included is a full suite of types and shortcuts that abstract away the details of Solidity's storage layout,
8 | //! method selectors, affordances, and more, making it easy to *just write Rust*.
9 | //! For a guided exploration of the features, please see the comprehensive [Feature Overview][overview].
10 | //!
11 | //! Some of the features available in the SDK include:
12 | //! - **Generic**, storage-backed Rust types for programming **Solidity-equivalent** smart contracts with optimal
13 | //! storage caching.
14 | //! - Simple macros for writing **language-agnostic** methods and entrypoints.
15 | //! - Automatic export of Solidity interfaces for interoperability across programming languages.
16 | //! - Powerful **primitive types** backed by the feature-rich [Alloy][alloy].
17 | //!
18 | //! Rust programs written with the Stylus SDK can **call and be called** by Solidity smart contracts
19 | //! due to ABI equivalence with Ethereum programming languages. In fact, existing Solidity DEXs can list Rust
20 | //! tokens without modification, and vice versa.
21 | //!
22 | //! [overview]: https://docs.arbitrum.io/stylus/reference/rust-sdk-guide
23 | //! [alloy]: https://docs.rs/alloy-primitives/latest/alloy_primitives/
24 |
25 | #![doc(html_favicon_url = "https://arbitrum.io/assets/stylus/Arbitrum_Stylus-Logomark.png")]
26 | #![doc(html_logo_url = "https://arbitrum.io/assets/stylus/Arbitrum_Stylus-Logomark.png")]
27 | #![warn(missing_docs)]
28 | // Only allow the standard library in tests and for exports
29 | #![cfg_attr(not(any(test, feature = "export-abi")), no_std)]
30 |
31 | /// Use an efficient WASM allocator.
32 | ///
33 | /// If a different custom allocator is desired, disable the `mini-alloc` feature.
34 | #[cfg(all(target_arch = "wasm32", feature = "mini-alloc"))]
35 | #[global_allocator]
36 | static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT;
37 |
38 | extern crate alloc;
39 |
40 | pub use alloy_primitives;
41 | pub use alloy_sol_types;
42 | pub use hex;
43 | pub use keccak_const;
44 | pub use stylus_core;
45 | pub use stylus_proc;
46 |
47 | #[cfg(all(feature = "stylus-test", target_arch = "wasm32"))]
48 | compile_error!("The `stylus-test` feature should not be enabled for wasm32 targets");
49 |
50 | // If the target is a testing environment, we export the stylus test module as the `testing` crate
51 | // for Stylus SDK consumers, to be used as a test framework.
52 | #[cfg(feature = "stylus-test")]
53 | pub use rclite as rc;
54 | #[cfg(feature = "stylus-test")]
55 | pub use stylus_test as testing;
56 |
57 | #[macro_use]
58 | pub mod abi;
59 |
60 | #[macro_use]
61 | pub mod debug;
62 |
63 | pub mod call;
64 | pub mod crypto;
65 | pub mod deploy;
66 | pub mod evm;
67 | pub mod host;
68 | pub mod methods;
69 | pub mod prelude;
70 | pub mod storage;
71 |
72 | mod util;
73 |
74 | #[cfg(feature = "hostio")]
75 | pub mod hostio;
76 |
77 | #[cfg(not(feature = "hostio"))]
78 | mod hostio;
79 |
80 | use alloc::vec::Vec;
81 |
82 | /// Represents a contract invocation outcome.
83 | pub type ArbResult = Result, Vec>;
84 |
--------------------------------------------------------------------------------
/stylus-sdk/src/methods.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md
3 |
4 | //! Types relating to method definitions.
5 |
6 | /// State mutability of a contract fuction. This is currently used for checking whether contracts
7 | /// are allowed to override a function from another contract they inherit from.
8 | /// Users should not need this type outside of proc macros.
9 | #[derive(Debug, Clone, Copy)]
10 | pub enum Purity {
11 | /// No state read/write.
12 | Pure,
13 | /// No state write.
14 | View,
15 | /// Cannot receive Ether.
16 | Write,
17 | /// Everything is allowed.
18 | Payable,
19 | }
20 |
21 | impl Purity {
22 | /// Returns whether a function defined with this purity may be overridden
23 | /// by one with the given purity.
24 | pub const fn allow_override(&self, other: Purity) -> bool {
25 | use Purity::*;
26 | matches!(
27 | (*self, other),
28 | (Payable, Payable)
29 | | (Write, Write)
30 | | (Write, View)
31 | | (Write, Pure)
32 | | (View, View)
33 | | (View, Pure)
34 | | (Pure, Pure)
35 | )
36 | }
37 | }
38 |
39 | #[cfg(test)]
40 | mod tests {
41 | #[test]
42 | fn test_allow_override() {
43 | use super::Purity::*;
44 | assert!(Payable.allow_override(Payable));
45 | assert!(!Payable.allow_override(Write));
46 | assert!(!Payable.allow_override(View));
47 | assert!(!Payable.allow_override(Pure));
48 |
49 | assert!(!Write.allow_override(Payable));
50 | assert!(Write.allow_override(Write));
51 | assert!(Write.allow_override(View));
52 | assert!(Write.allow_override(Pure));
53 |
54 | assert!(!View.allow_override(Payable));
55 | assert!(!View.allow_override(Write));
56 | assert!(View.allow_override(View));
57 | assert!(View.allow_override(Pure));
58 |
59 | assert!(!Pure.allow_override(Payable));
60 | assert!(!Pure.allow_override(Write));
61 | assert!(!Pure.allow_override(View));
62 | assert!(Pure.allow_override(Pure));
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/stylus-sdk/src/prelude.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Common imports for Stylus contracts.
5 | //!
6 | //! Included are all the proc macros and common traits.
7 | //!
8 | //! ```
9 | //! use stylus_sdk::prelude::*;
10 | //! ```
11 |
12 | pub use crate::call::*;
13 | pub use crate::storage::{Erase, SimpleStorageType, StorageType};
14 | pub use crate::stylus_core::*;
15 | pub use crate::stylus_proc::*;
16 |
--------------------------------------------------------------------------------
/stylus-sdk/src/storage/array.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | use super::{Erase, StorageGuard, StorageGuardMut, StorageType};
5 |
6 | use alloy_primitives::U256;
7 | use cfg_if::cfg_if;
8 | use core::marker::PhantomData;
9 | use stylus_core::HostAccess;
10 |
11 | use crate::host::VM;
12 |
13 | /// Accessor for a storage-backed array.
14 | pub struct StorageArray {
15 | slot: U256,
16 | marker: PhantomData
,
17 | __stylus_host: VM,
18 | }
19 |
20 | impl StorageType for StorageArray {
21 | type Wraps<'a>
22 | = StorageGuard<'a, StorageArray>
23 | where
24 | Self: 'a;
25 | type WrapsMut<'a>
26 | = StorageGuardMut<'a, StorageArray>
27 | where
28 | Self: 'a;
29 |
30 | const REQUIRED_SLOTS: usize = Self::required_slots();
31 |
32 | unsafe fn new(slot: U256, offset: u8, host: VM) -> Self {
33 | debug_assert!(offset == 0);
34 | Self {
35 | slot,
36 | marker: PhantomData,
37 | __stylus_host: host,
38 | }
39 | }
40 |
41 | fn load<'s>(self) -> Self::Wraps<'s> {
42 | StorageGuard::new(self)
43 | }
44 |
45 | fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
46 | StorageGuardMut::new(self)
47 | }
48 | }
49 |
50 | impl HostAccess for StorageArray {
51 | fn vm(&self) -> &dyn stylus_core::Host {
52 | cfg_if! {
53 | if #[cfg(not(feature = "stylus-test"))] {
54 | &self.__stylus_host
55 | } else {
56 | self.__stylus_host.host.as_ref()
57 | }
58 | }
59 | }
60 | }
61 |
62 | #[cfg(feature = "stylus-test")]
63 | impl From<&T> for StorageArray
64 | where
65 | T: stylus_core::Host + Clone + 'static,
66 | S: StorageType,
67 | {
68 | fn from(host: &T) -> Self {
69 | unsafe {
70 | Self::new(
71 | U256::ZERO,
72 | 0,
73 | crate::host::VM {
74 | host: alloc::boxed::Box::new(host.clone()),
75 | },
76 | )
77 | }
78 | }
79 | }
80 |
81 | impl StorageArray {
82 | /// Gets the number of elements stored.
83 | ///
84 | /// Although this type will always have the same length, this method is still provided for
85 | /// consistency with [`StorageVec`].
86 | #[allow(clippy::len_without_is_empty)]
87 | pub const fn len(&self) -> usize {
88 | N
89 | }
90 |
91 | /// Gets an accessor to the element at a given index, if it exists.
92 | /// Note: the accessor is protected by a [`StorageGuard`], which restricts
93 | /// its lifetime to that of `&self`.
94 | pub fn getter(&self, index: impl TryInto) -> Option> {
95 | let store = unsafe { self.accessor(index)? };
96 | Some(StorageGuard::new(store))
97 | }
98 |
99 | /// Gets a mutable accessor to the element at a given index, if it exists.
100 | /// Note: the accessor is protected by a [`StorageGuardMut`], which restricts
101 | /// its lifetime to that of `&mut self`.
102 | pub fn setter(&mut self, index: impl TryInto) -> Option> {
103 | let store = unsafe { self.accessor(index)? };
104 | Some(StorageGuardMut::new(store))
105 | }
106 |
107 | /// Gets the underlying accessor to the element at a given index, if it exists.
108 | ///
109 | /// # Safety
110 | ///
111 | /// Enables aliasing.
112 | unsafe fn accessor(&self, index: impl TryInto) -> Option {
113 | let index = index.try_into().ok()?;
114 | if index >= N {
115 | return None;
116 | }
117 | let (slot, offset) = self.index_slot(index);
118 | Some(S::new(slot, offset, self.__stylus_host.clone()))
119 | }
120 |
121 | /// Gets the underlying accessor to the element at a given index, even if out of bounds.
122 | ///
123 | /// # Safety
124 | ///
125 | /// Enables aliasing. UB if out of bounds.
126 | unsafe fn accessor_unchecked(&self, index: usize) -> S {
127 | let (slot, offset) = self.index_slot(index);
128 | S::new(slot, offset, self.__stylus_host.clone())
129 | }
130 |
131 | /// Gets the element at the given index, if it exists.
132 | pub fn get(&self, index: impl TryInto) -> Option> {
133 | let store = unsafe { self.accessor(index)? };
134 | Some(store.load())
135 | }
136 |
137 | /// Gets a mutable accessor to the element at a given index, if it exists.
138 | pub fn get_mut(&mut self, index: impl TryInto) -> Option> {
139 | let store = unsafe { self.accessor(index)? };
140 | Some(store.load_mut())
141 | }
142 |
143 | /// Determines the slot and offset for the element at an index.
144 | fn index_slot(&self, index: usize) -> (U256, u8) {
145 | let width = S::SLOT_BYTES;
146 | let words = S::REQUIRED_SLOTS.max(1);
147 | let density = Self::density();
148 |
149 | let slot = self.slot + U256::from(words * index / density);
150 | let offset = 32 - (width * (1 + index % density)) as u8;
151 | (slot, offset)
152 | }
153 |
154 | /// Number of elements per slot.
155 | const fn density() -> usize {
156 | 32 / S::SLOT_BYTES
157 | }
158 |
159 | /// Required slots for the storage array.
160 | const fn required_slots() -> usize {
161 | let reserved = N * S::REQUIRED_SLOTS;
162 | let density = Self::density();
163 | let packed = N.div_ceil(density);
164 | if reserved > packed {
165 | return reserved;
166 | }
167 | packed
168 | }
169 | }
170 |
171 | impl Erase for StorageArray {
172 | fn erase(&mut self) {
173 | for i in 0..N {
174 | let mut store = unsafe { self.accessor_unchecked(i) };
175 | store.erase()
176 | }
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/stylus-sdk/src/util.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2024, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | /// Returns the minimum number of EVM words needed to store `bytes` bytes.
5 | pub(crate) const fn evm_words(bytes: usize) -> usize {
6 | bytes.div_ceil(32)
7 | }
8 |
9 | /// Pads a length to the next multiple of 32 bytes
10 | pub(crate) const fn evm_padded_length(bytes: usize) -> usize {
11 | evm_words(bytes) * 32
12 | }
13 |
--------------------------------------------------------------------------------
/stylus-test/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "stylus-test"
3 | keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
4 | description = "Testing utilities for Arbitrum Stylus contracts"
5 | readme = "../README.md"
6 |
7 | authors.workspace = true
8 | edition.workspace = true
9 | license.workspace = true
10 | repository.workspace = true
11 | version.workspace = true
12 |
13 | [dependencies]
14 | alloy-primitives.workspace = true
15 | alloy-sol-types.workspace = true
16 | stylus-core.workspace = true
17 | tokio = { version = "1.12.0", features = ["full"] }
18 | alloy-provider = "1.0.1"
19 | url = "2.5.4"
20 |
21 | [dev-dependencies]
22 |
23 | [features]
24 | default = []
25 | reentrant = []
26 |
--------------------------------------------------------------------------------
/stylus-test/src/builder.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025-2026, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Defines a builder struct that can create a [`crate::TestVM`] instance
5 | //! with convenient overrides for unit testing Stylus contracts.
6 |
7 | use std::{collections::HashMap, sync::Arc};
8 |
9 | use alloy_primitives::{Address, B256, U256};
10 | use alloy_provider::{network::Ethereum, RootProvider};
11 | use url::Url;
12 |
13 | use crate::{state::VMState, TestVM};
14 |
15 | /// Builder for constructing a [`crate::TestVM`] used for unit testing Stylus contracts built with the Stylus SDK.
16 | /// Allows for convenient customization of the contract's address, sender address, message value, and RPC
17 | /// URL if state forking is desired. These values and more can still be customized if the builder is not used,
18 | /// by instead invoking the corresponding method on the TestVM struct such as `vm.set_value(value)`.
19 | ///
20 | /// # Example
21 | /// ```
22 | /// use stylus_test::{TestVM, TestVMBuilder};
23 | /// use alloy_primitives::{address, Address, U256};
24 | ///
25 | /// let vm: TestVM = TestVMBuilder::new()
26 | /// .sender(address!("dCE82b5f92C98F27F116F70491a487EFFDb6a2a9"))
27 | /// .contract_address(address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"))
28 | /// .value(U256::from(1))
29 | /// .rpc_url("http://localhost:8547")
30 | /// .build();
31 | /// ```
32 | #[derive(Default)]
33 | pub struct TestVMBuilder {
34 | sender: Option,
35 | value: Option,
36 | contract_address: Option,
37 | rpc_url: Option,
38 | storage: Option>,
39 | provider: Option>>,
40 | block_num: Option,
41 | chain_id: Option,
42 | }
43 |
44 | impl TestVMBuilder {
45 | pub fn new() -> Self {
46 | Self::default()
47 | }
48 | /// Sets the sender address for contract invocations.
49 | pub fn sender(mut self, sender: Address) -> Self {
50 | self.sender = Some(sender);
51 | self
52 | }
53 | /// Sets the block number.
54 | pub fn block_number(mut self, block_num: u64) -> Self {
55 | self.block_num = Some(block_num);
56 | self
57 | }
58 | /// Sets the chain id.
59 | pub fn chain_id(mut self, id: u64) -> Self {
60 | self.chain_id = Some(id);
61 | self
62 | }
63 | /// Sets the storage mapping.
64 | pub fn storage(mut self, mapping: HashMap) -> Self {
65 | self.storage = Some(mapping);
66 | self
67 | }
68 | /// Sets the msg value for contract invocations.
69 | pub fn value(mut self, value: U256) -> Self {
70 | self.value = Some(value);
71 | self
72 | }
73 | /// Sets the contract address.
74 | pub fn contract_address(mut self, address: Address) -> Self {
75 | self.contract_address = Some(address);
76 | self
77 | }
78 | /// Sets the RPC URL to a Stylus-enabled Arbitrum chain for storage forking.
79 | /// If specified, any calls to load storage will be made to the RPC URL at the TestVM's specified
80 | /// contract address.
81 | pub fn rpc_url(mut self, url: &str) -> Self {
82 | let url = match Url::parse(url) {
83 | Ok(url) => url,
84 | Err(e) => {
85 | panic!("Invalid RPC URL specified: {}", e);
86 | }
87 | };
88 | self.rpc_url = Some(url.to_string());
89 | self.provider = Some(Arc::new(RootProvider::new_http(url)));
90 | self
91 | }
92 | /// Returns and TestVM instance from the builder with the specified parameters.
93 | pub fn build(self) -> TestVM {
94 | let mut state = VMState::default();
95 | if let Some(sender) = self.sender {
96 | state.msg_sender = sender;
97 | }
98 | if let Some(value) = self.value {
99 | state.msg_value = value;
100 | }
101 | if let Some(storage) = self.storage {
102 | state.storage = storage;
103 | }
104 | if let Some(block_number) = self.block_num {
105 | state.block_number = block_number;
106 | }
107 | if let Some(id) = self.chain_id {
108 | state.chain_id = id;
109 | }
110 | if let Some(contract_address) = self.contract_address {
111 | state.contract_address = contract_address;
112 | }
113 | state.provider = self.provider;
114 | TestVM::from(state)
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/stylus-test/src/constants.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025-2026, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! Defaults used by the [`crate::TestVM`] for unit testing Stylus contracts.
5 |
6 | use alloy_primitives::{address, Address};
7 |
8 | /// Default sender address used by the [`crate::TestVM`] for unit testing Stylus contracts.
9 | pub const DEFAULT_SENDER: Address = address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF");
10 |
11 | /// Default contract address used by the [`crate::TestVM`] for unit testing Stylus contracts.
12 | pub const DEFAULT_CONTRACT_ADDRESS: Address = address!("dCE82b5f92C98F27F116F70491a487EFFDb6a2a9");
13 |
14 | /// Default chain id used by the [`crate::TestVM`] for unit testing Stylus contracts.
15 | pub const DEFAULT_CHAIN_ID: u64 = 42161; // Arbitrum One.
16 |
17 | /// Default basefee used by the [`crate::TestVM`] for unit testing Stylus contracts.
18 | pub const DEFAULT_BASEFEE: u64 = 1_000_000;
19 |
20 | /// Default block gas limit used by the [`crate::TestVM`] for unit testing Stylus contracts.
21 | pub const DEFAULT_BLOCK_GAS_LIMIT: u64 = 30_000_000;
22 |
--------------------------------------------------------------------------------
/stylus-test/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025-2026, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! The Stylus testing suite.
5 | //!
6 | //! The stylus-test crate makes it easy to unit test all the storage types and contracts that use the
7 | //! Stylus SDK. Included is an implementation of the [`stylus_core::host::Host`] trait that all Stylus
8 | //! contracts have access to for interfacing with their host environment.
9 | //!
10 | //! The mock implementation, named [`crate::TestVM`], can be used to unit test Stylus contracts
11 | //! in native Rust without the need for a real EVM or Arbitrum chain environment. [`crate::TestVM`]
12 | //! allows for mocking of all host functions, including storage, gas, and external calls to assert
13 | //! contract behavior.
14 | //!
15 | //! To be able to unit test Stylus contracts, contracts must access host methods through the [`stylus_core::host:HostAccessor`] trait,
16 | //! which gives all contracts access to a `.vm()` method.
17 |
18 | pub mod builder;
19 | pub mod constants;
20 | pub mod state;
21 | pub mod vm;
22 | pub use builder::*;
23 | pub use vm::*;
24 |
--------------------------------------------------------------------------------
/stylus-test/src/state.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025-2026, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | //! This module defines the internal state of the Stylus test VM.
5 |
6 | use alloy_primitives::{Address, B256, U256};
7 | use alloy_provider::{network::Ethereum, RootProvider};
8 | use std::{collections::HashMap, sync::Arc};
9 |
10 | use crate::constants::{DEFAULT_CHAIN_ID, DEFAULT_CONTRACT_ADDRESS, DEFAULT_SENDER};
11 |
12 | /// Type aliases for the return values of mocked calls and deployments.
13 | type CallReturn = Result, Vec>;
14 | type DeploymentReturn = Result>;
15 | type MockCallWithAddress = (Address, Vec);
16 | type MockCallWithAddressAndValue = (Address, Vec, U256);
17 | type DeploymentWithSalt = (Vec, Option);
18 |
19 | /// Type alias for the RPC provider used in the test VM.
20 | type RPCProvider = Arc>;
21 |
22 | /// Defines the internal state of the Stylus test VM for unit testing.
23 | /// Internally, it tracks information such as mocked calls and their return values,
24 | /// balances of addresses, and the storage of the contract being tested.
25 | #[derive(Debug, Clone)]
26 | pub struct VMState {
27 | pub storage: HashMap,
28 | pub msg_sender: Address,
29 | pub contract_address: Address,
30 | pub chain_id: u64,
31 | pub reentrant: bool,
32 | pub block_number: u64,
33 | pub block_timestamp: u64,
34 | pub tx_origin: Option, // Defaults to msg sender if None.
35 | pub balances: HashMap,
36 | pub code_storage: HashMap>,
37 | pub gas_left: u64,
38 | pub ink_left: u64,
39 | pub msg_value: U256,
40 | pub block_gas_limit: u64,
41 | pub coinbase: Address,
42 | pub block_basefee: U256,
43 | pub tx_gas_price: U256,
44 | pub tx_ink_price: u32,
45 | pub call_returns: HashMap,
46 | pub delegate_call_returns: HashMap,
47 | pub static_call_returns: HashMap,
48 | pub deploy_returns: HashMap,
49 | pub emitted_logs: Vec<(Vec, Vec)>,
50 | pub provider: Option,
51 | pub return_data: Vec,
52 | }
53 |
54 | impl Default for VMState {
55 | fn default() -> Self {
56 | Self {
57 | storage: HashMap::new(),
58 | msg_sender: DEFAULT_SENDER,
59 | contract_address: DEFAULT_CONTRACT_ADDRESS,
60 | chain_id: DEFAULT_CHAIN_ID,
61 | reentrant: false,
62 | block_number: 0,
63 | block_timestamp: 0,
64 | balances: HashMap::new(),
65 | code_storage: HashMap::new(),
66 | gas_left: u64::MAX,
67 | ink_left: u64::MAX,
68 | msg_value: U256::ZERO,
69 | block_basefee: U256::from(1_000_000),
70 | block_gas_limit: 30_000_000,
71 | coinbase: DEFAULT_SENDER,
72 | tx_origin: None,
73 | tx_gas_price: U256::from(1),
74 | tx_ink_price: 1,
75 | call_returns: HashMap::new(),
76 | delegate_call_returns: HashMap::new(),
77 | static_call_returns: HashMap::new(),
78 | deploy_returns: HashMap::new(),
79 | emitted_logs: Vec::new(),
80 | provider: None,
81 | return_data: Vec::new(),
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/stylus-tools/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "stylus-tools"
3 | keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
4 | description = "Tools to compile, deploy and verify Arbitrum Stylus contracts"
5 | readme = "../README.md"
6 |
7 | authors.workspace = true
8 | edition.workspace = true
9 | license.workspace = true
10 | repository.workspace = true
11 | version.workspace = true
12 |
13 | [dependencies]
14 | alloy = { workspace = true, features = ["contract", "signer-local", "rpc-types"] }
15 | eyre.workspace = true
16 | regex.workspace = true
17 | reqwest.workspace = true
18 | testcontainers = { workspace = true, optional = true }
19 | tokio.workspace = true
20 | trybuild.workspace = true
21 |
22 | [features]
23 | integration-tests = ["dep:testcontainers"]
24 |
--------------------------------------------------------------------------------
/stylus-tools/src/cargo_stylus.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | use alloy::primitives::Address;
5 | use eyre::{bail, eyre, Result, WrapErr};
6 | use regex::Regex;
7 | use std::{ffi::OsStr, process::Command};
8 |
9 | /// Deploy the contract in the current directory
10 | pub fn deploy(rpc: &str, key: &str) -> Result {
11 | call_deploy(["-e", rpc, "--private-key", key])
12 | }
13 |
14 | /// Deploy the contract in the current directory passing the arguments to the constructor.
15 | /// This function will fail if the contract doesn't have a constructor.
16 | pub fn deploy_with_constructor(
17 | rpc: &str,
18 | key: &str,
19 | value: &str,
20 | args: &[&str],
21 | ) -> Result {
22 | let mut deploy_args = vec!["-e", rpc, "--private-key", key];
23 | if !value.is_empty() {
24 | deploy_args.push("--constructor-value");
25 | deploy_args.push(value);
26 | }
27 | deploy_args.push("--constructor-args");
28 | deploy_args.extend_from_slice(args);
29 | call_deploy(&deploy_args)
30 | }
31 |
32 | fn call_deploy, S: AsRef>(args: I) -> Result {
33 | let output = Command::new("cargo")
34 | .arg("stylus")
35 | .arg("deploy")
36 | .arg("--no-verify")
37 | .args(args)
38 | .output()
39 | .wrap_err("failed to run cargo deploy")?;
40 | if !output.status.success() {
41 | let err = String::from_utf8(output.stderr).unwrap_or("failed to decode error".to_owned());
42 | bail!("failed to run node: {}", err);
43 | }
44 | let out = String::from_utf8(output.stdout).wrap_err("failed to decode stdout")?;
45 | let out = strip_color(&out);
46 | let address = extract_deployed_address(&out)?;
47 | let address: Address = address
48 | .parse()
49 | .wrap_err("failed to parse deployment address")?;
50 | Ok(address)
51 | }
52 |
53 | fn strip_color(s: &str) -> String {
54 | let re = Regex::new(r"\x1b\[[0-9;]*[ABCDHJKSTfGmsu]").unwrap();
55 | re.replace_all(s, "").into_owned()
56 | }
57 |
58 | fn extract_deployed_address(s: &str) -> Result<&str> {
59 | for line in s.lines() {
60 | if let Some(rest) = line.strip_prefix("deployed code at address: ") {
61 | return Ok(rest);
62 | }
63 | }
64 | Err(eyre!("deployment address not found"))
65 | }
66 |
--------------------------------------------------------------------------------
/stylus-tools/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2025, Offchain Labs, Inc.
2 | // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3 |
4 | #[cfg(feature = "integration-tests")]
5 | pub mod devnet;
6 |
7 | pub mod cargo_stylus;
8 |
9 | pub use cargo_stylus::*;
10 |
--------------------------------------------------------------------------------