├── .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 | Logo 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 | --------------------------------------------------------------------------------