From d9ab9c33e61d5d2db13382758d42a8ec73e8bc0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Wed, 12 Nov 2025 14:31:28 -0800 Subject: [PATCH 01/28] fix title location --- docs/stylus/gentle-introduction.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/stylus/gentle-introduction.mdx b/docs/stylus/gentle-introduction.mdx index 6662a24ab3..20eb3faf4a 100644 --- a/docs/stylus/gentle-introduction.mdx +++ b/docs/stylus/gentle-introduction.mdx @@ -11,8 +11,6 @@ displayed_sidebar: buildAppsSidebar import ImageZoom from '@site/src/components/ImageZoom'; -# A gentle introduction: Stylus - ### In a nutshell: - Stylus lets you write smart contracts in programming languages that compile to WASM, such as **Rust, C, C++, and many others**, allowing you to tap into their ecosystem of libraries and tools. Rich language and tooling support already exist for Rust. You can try the SDK and CLI with the [quickstart](/stylus/quickstart.mdx). From e11335b6342ae1d3fb672121b2ecf571baf5135d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 13 Nov 2025 14:02:30 -0800 Subject: [PATCH 02/28] remove deprecated overview file --- docs/stylus/overview.mdx | 81 ---------------------------------------- 1 file changed, 81 deletions(-) delete mode 100644 docs/stylus/overview.mdx diff --git a/docs/stylus/overview.mdx b/docs/stylus/overview.mdx deleted file mode 100644 index 10b1700705..0000000000 --- a/docs/stylus/overview.mdx +++ /dev/null @@ -1,81 +0,0 @@ ---- -id: stylus-overview -title: Write Stylus Contracts -sidebar_label: Write Stylus contracts -displayed_sidebar: buildAppsSidebar ---- - -import Card from '@site/src/components/Cards/Card'; - -# Write Stylus Contracts - -Let's learn how to write contracts with Stylus! - -
- - - - - - - - - - - - -
From 8d14fb4289ec99d793ec9a7f90608963829b42f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 13 Nov 2025 14:07:58 -0800 Subject: [PATCH 03/28] resize multivm diagram --- docs/stylus/gentle-introduction.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stylus/gentle-introduction.mdx b/docs/stylus/gentle-introduction.mdx index 20eb3faf4a..b6f1695509 100644 --- a/docs/stylus/gentle-introduction.mdx +++ b/docs/stylus/gentle-introduction.mdx @@ -21,7 +21,7 @@ import ImageZoom from '@site/src/components/ImageZoom'; Stylus is an upgrade to Arbitrum Nitro [(ArbOS 32)](/run-arbitrum-node/arbos-releases/arbos32.mdx), the tech stack powering Arbitrum One, Arbitrum Nova, and Arbitrum chains. This upgrade adds a second, coequal virtual machine to the EVM, where EVM contracts continue to behave exactly as they would in Ethereum. We call this paradigm **MultiVM** since **everything is entirely additive.** - + This second virtual machine executes WebAssembly (WASM) rather than EVM bytecode. WASM is a modern binary format popularized by its use in major web standards, browsers, and companies to speed up computation. WASM is built to be fast, portable, and human-readable. It has sandboxed execution environments for security and simplicity. Working with WASM is nothing new for Arbitrum chains. Ever since the [Nitro upgrade](https://medium.com/offchainlabs/arbitrum-nitro-one-small-step-for-l2-one-giant-leap-for-ethereum-bc9108047450), WASM has been a fundamental component of Arbitrum's fully functioning fraud proofs. From 0493ac838600c93044e1babf3e938a7e990e39e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 13 Nov 2025 14:09:45 -0800 Subject: [PATCH 04/28] refactor: integrate markdownlint into format workflow Add lint:markdown:fix to the format script to automatically fix markdown linting issues when running yarn format. This ensures markdown files are both formatted (prettier) and linted (markdownlint) in one command. The new format workflow: 1. Format docs with prettier 2. Format app files with prettier 3. Auto-fix markdown linting issues 4. Verify all files pass prettier check --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4779955263..4bb9a5a5dc 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "find-orphan-pages": "tsx scripts/find-orphan-pages.ts", - "format": "yarn format:docs && yarn format:app && yarn format:check", + "format": "yarn format:docs && yarn format:app && yarn lint:markdown:fix && yarn format:check", "format:app": "prettier --write --config \"./.prettierrc.js\" -- \"./*.{js,json}\" \"src/**/*.{tsx,ts,scss,json,js}\"", "format:docs": "prettier --write --config \"./.prettierrc.js\" -- \"docs/**/*.{md,mdx}\"", "format:check": "prettier --check --config \"./.prettierrc.js\" -- \"./*.{js,json}\" \"src/**/*.{tsx,ts,scss,json,js}\" \"docs/**/*.{md,mdx}\"", From 19480b189600bd76e4d0284615d83fd98b1f10eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 13 Nov 2025 16:02:14 -0800 Subject: [PATCH 05/28] remove deprecated stylus content map --- docs/stylus/stylus-content-map.mdx | 93 ------------------------------ 1 file changed, 93 deletions(-) delete mode 100644 docs/stylus/stylus-content-map.mdx diff --git a/docs/stylus/stylus-content-map.mdx b/docs/stylus/stylus-content-map.mdx deleted file mode 100644 index 3aa5044cc7..0000000000 --- a/docs/stylus/stylus-content-map.mdx +++ /dev/null @@ -1,93 +0,0 @@ ---- -id: stylus-content-map -title: Write Stylus Contracts -sidebar_label: Write Stylus contracts -displayed_sidebar: buildAppsSidebar ---- - -import Card from '@site/src/components/Cards/Card'; - -# Write Stylus Contracts - -Let's learn how to write contracts with Stylus! - -
- - - - - - - - - - - - -
From b25495c97bf17ca891f229748cefe71f84977c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 13 Nov 2025 16:16:40 -0800 Subject: [PATCH 06/28] remove irrelevant link --- docs/stylus/how-tos/adding-support-for-new-languages.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stylus/how-tos/adding-support-for-new-languages.mdx b/docs/stylus/how-tos/adding-support-for-new-languages.mdx index 03f167dc7b..3dd295f010 100644 --- a/docs/stylus/how-tos/adding-support-for-new-languages.mdx +++ b/docs/stylus/how-tos/adding-support-for-new-languages.mdx @@ -13,7 +13,7 @@ displayed_sidebar: buildAppsSidebar This is possible thanks to [WebAssembly](https://www.infoworld.com/article/3291780/what-is-webassembly-the-next-generation-web-platform-explained.html) technology, which all Stylus contracts compile to. Stylus smart contracts live under the **same Ethereum state trie** in Arbitrum nodes, and can fully interoperate with Solidity or Vyper EVM smart contracts. With Stylus, developers can write smart contracts in Rust that talk to Solidity and vice versa without any limitations. -Today, the Stylus testnet also comes with two officially supported [SDKs](/stylus/overview.mdx) for developers to write contracts in the [Rust](../reference/rust-sdk-guide.md) or [C](https://github.com/OffchainLabs/stylus-sdk-c) programming languages. +Today, the Stylus testnet also comes with two officially supported SDKs for developers to write contracts in the [Rust](../reference/rust-sdk-guide.md) or [C](https://github.com/OffchainLabs/stylus-sdk-c) programming languages. However, _anyone_ can add support for new languages in Stylus. **As long as a programming language can compile to WebAssembly**, Stylus will let you use it to write EVM-compatible smart contracts. Note that in order to be deployed onchain, your compiled program must fit under the 24Kb brotli-compressed limit, and should meet Stylus gas metering requirements. From 44bdffa132715953d9b9ac370af4ebf4c1a37cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 13 Nov 2025 16:20:22 -0800 Subject: [PATCH 07/28] remove SBE from stylus content --- sidebars.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sidebars.js b/sidebars.js index 2fdcd5db43..c59cdb9834 100644 --- a/sidebars.js +++ b/sidebars.js @@ -3,8 +3,6 @@ // Use the generated SDK sidebar for API reference const sdkApiSidebar = require('./sdk-sidebar.js'); // Use the generated stylus-by-example sidebars -const stylusByExampleBasicExamples = require('./docs/stylus-by-example/basic_examples/sidebar.js'); -const stylusByExampleApplications = require('./docs/stylus-by-example/applications/sidebar.js'); // Create a custom SDK sidebar that combines manual intro pages with generated API docs const sdkSidebar = { @@ -1217,7 +1215,6 @@ const sidebars = { id: 'stylus/reference/project-structure', label: 'Structure of a Contract', }, - ...stylusByExampleBasicExamples, { type: 'doc', id: 'stylus/how-tos/using-inheritance', @@ -1304,7 +1301,6 @@ const sidebars = { label: 'Examples', collapsed: true, items: [ - ...stylusByExampleApplications, { type: 'link', label: 'Awesome Stylus', From ff150ec52f1259938fe7a4e661c201eb87e7a74f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 13 Nov 2025 18:14:28 -0800 Subject: [PATCH 08/28] first iteration content restructuring --- docs/stylus/reference/contracts.mdx | 22 +++++++++++ .../reference/data-types/compound-types.mdx | 17 +++++++++ .../data-types/conversions-between-types.mdx | 11 ++++++ .../reference/data-types/primitives.mdx | 15 ++++++++ docs/stylus/reference/data-types/storage.mdx | 20 ++++++++++ .../global-variables-and-functions.mdx | 18 +++++++++ sidebars.js | 37 +++++++++++++++++++ 7 files changed, 140 insertions(+) create mode 100644 docs/stylus/reference/contracts.mdx create mode 100644 docs/stylus/reference/data-types/compound-types.mdx create mode 100644 docs/stylus/reference/data-types/conversions-between-types.mdx create mode 100644 docs/stylus/reference/data-types/primitives.mdx create mode 100644 docs/stylus/reference/data-types/storage.mdx create mode 100644 docs/stylus/reference/global-variables-and-functions.mdx diff --git a/docs/stylus/reference/contracts.mdx b/docs/stylus/reference/contracts.mdx new file mode 100644 index 0000000000..771b25cea6 --- /dev/null +++ b/docs/stylus/reference/contracts.mdx @@ -0,0 +1,22 @@ +--- +title: 'Contracts' +description: 'Contracts' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts. +displayed_sidebar: buildAppsSidebar +--- + +6. Contracts + 1. Calling other contracts + 1. Call + 2. Raw call + 3. Delegate call + 4. Static call + 5. sol_interface + 2. Sending ETH + 3. Factory deploy + 4. Function modifiers + 5. Inheritance (temporary) + 6. Constructors (coming soon) diff --git a/docs/stylus/reference/data-types/compound-types.mdx b/docs/stylus/reference/data-types/compound-types.mdx new file mode 100644 index 0000000000..33466eddca --- /dev/null +++ b/docs/stylus/reference/data-types/compound-types.mdx @@ -0,0 +1,17 @@ +--- +title: 'Stylus compound types' +description: 'Stylus Rust SDK compound types' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts. +displayed_sidebar: buildAppsSidebar +--- + +2. Compound types + 1. Tuple + 2. Struct + 3. Array + 4. Vector + 5. Bytes + 6. FixedBytes diff --git a/docs/stylus/reference/data-types/conversions-between-types.mdx b/docs/stylus/reference/data-types/conversions-between-types.mdx new file mode 100644 index 0000000000..f44e822ec2 --- /dev/null +++ b/docs/stylus/reference/data-types/conversions-between-types.mdx @@ -0,0 +1,11 @@ +--- +title: 'Conversions Between Types' +description: 'Conversions Between Types' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts. +displayed_sidebar: buildAppsSidebar +--- + +TBD diff --git a/docs/stylus/reference/data-types/primitives.mdx b/docs/stylus/reference/data-types/primitives.mdx new file mode 100644 index 0000000000..ac1440578c --- /dev/null +++ b/docs/stylus/reference/data-types/primitives.mdx @@ -0,0 +1,15 @@ +--- +title: 'Stylus primitives' +description: 'Stylus Rust SDK primitives' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts. +displayed_sidebar: buildAppsSidebar +--- + +1. Boolean +2. Integers +3. Address (include AddressVM traits from types module) 4. String + 1. Literals + 2. Hex diff --git a/docs/stylus/reference/data-types/storage.mdx b/docs/stylus/reference/data-types/storage.mdx new file mode 100644 index 0000000000..8f49eab97c --- /dev/null +++ b/docs/stylus/reference/data-types/storage.mdx @@ -0,0 +1,20 @@ +--- +title: 'Stylus Rust SDK storage' +description: 'Stylus Rust SDK storage' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts. +displayed_sidebar: buildAppsSidebar +--- + +3. Storage + 1. Bool + 2. Uint + 3. Address + 4. Struct + 5. Array + 6. Map + 7. String + 8. Bytes + 9. FixedBytes diff --git a/docs/stylus/reference/global-variables-and-functions.mdx b/docs/stylus/reference/global-variables-and-functions.mdx new file mode 100644 index 0000000000..dd71e050db --- /dev/null +++ b/docs/stylus/reference/global-variables-and-functions.mdx @@ -0,0 +1,18 @@ +--- +title: 'Global variables and functions' +description: 'Global variables and functions' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts. +displayed_sidebar: buildAppsSidebar +--- + +5. Global variables and functions + 1. block + 2. contract + 3. crypto + 4. evm + 5. msg + 6. tx + 7. debug diff --git a/sidebars.js b/sidebars.js index c59cdb9834..96cc50b096 100644 --- a/sidebars.js +++ b/sidebars.js @@ -1215,6 +1215,43 @@ const sidebars = { id: 'stylus/reference/project-structure', label: 'Structure of a Contract', }, + { + type: 'category', + label: 'Data types', + collapsed: true, + items: [ + { + type: 'doc', + id: 'stylus/reference/data-types/primitives', + label: 'Primitives', + }, + { + type: 'doc', + id: 'stylus/reference/data-types/compound-types', + label: 'Compound types', + }, + { + type: 'doc', + id: 'stylus/reference/data-types/storage', + label: 'Storage', + }, + { + type: 'doc', + id: 'stylus/reference/data-types/conversions-between-types', + label: 'Conversions Between Types', + }, + ], + }, + { + type: 'doc', + id: 'stylus/reference/global-variables-and-functions', + label: 'Global variables and functions', + }, + { + type: 'doc', + id: 'stylus/reference/contracts', + label: 'Contracts', + }, { type: 'doc', id: 'stylus/how-tos/using-inheritance', From 0e121cdead24a02adfbdf1468b664dd10d718390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 18 Nov 2025 11:14:31 -0800 Subject: [PATCH 09/28] tone adjustments and simplification --- docs/stylus/gentle-introduction.mdx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/stylus/gentle-introduction.mdx b/docs/stylus/gentle-introduction.mdx index b6f1695509..1ec4540453 100644 --- a/docs/stylus/gentle-introduction.mdx +++ b/docs/stylus/gentle-introduction.mdx @@ -14,18 +14,18 @@ import ImageZoom from '@site/src/components/ImageZoom'; ### In a nutshell: - Stylus lets you write smart contracts in programming languages that compile to WASM, such as **Rust, C, C++, and many others**, allowing you to tap into their ecosystem of libraries and tools. Rich language and tooling support already exist for Rust. You can try the SDK and CLI with the [quickstart](/stylus/quickstart.mdx). -- Solidity contracts and Stylus contracts are **fully interoperable**. In Solidity, you can call a Rust program and vice versa, thanks to a second, coequal WASM virtual machine. -- Stylus contracts offer significantly **faster execution and lower gas fees** for memory and compute-intensive operations, thanks to the superior efficiency of WASM programs. +- Solidity contracts and Stylus contracts are **fully interoperable**. In Solidity, you can call a Rust program and vice versa. +- Stylus contracts offer **faster execution and lower gas fees** for memory and compute-intensive operations, they also enable complex computation Solidity doesn't support. ### What's Stylus? -Stylus is an upgrade to Arbitrum Nitro [(ArbOS 32)](/run-arbitrum-node/arbos-releases/arbos32.mdx), the tech stack powering Arbitrum One, Arbitrum Nova, and Arbitrum chains. This upgrade adds a second, coequal virtual machine to the EVM, where EVM contracts continue to behave exactly as they would in Ethereum. We call this paradigm **MultiVM** since **everything is entirely additive.** +Stylus is an upgrade to Arbitrum Nitro [(ArbOS 32)](/run-arbitrum-node/arbos-releases/arbos32.mdx), the tech stack powering Arbitrum. This upgrade adds a second, coequal virtual machine to the EVM, where EVM contracts continue to behave exactly as they would in Ethereum. We call this paradigm MultiVM. -This second virtual machine executes WebAssembly (WASM) rather than EVM bytecode. WASM is a modern binary format popularized by its use in major web standards, browsers, and companies to speed up computation. WASM is built to be fast, portable, and human-readable. It has sandboxed execution environments for security and simplicity. Working with WASM is nothing new for Arbitrum chains. Ever since the [Nitro upgrade](https://medium.com/offchainlabs/arbitrum-nitro-one-small-step-for-l2-one-giant-leap-for-ethereum-bc9108047450), WASM has been a fundamental component of Arbitrum's fully functioning fraud proofs. +This second virtual machine executes WebAssembly (WASM) rather than EVM bytecode. WASM is a modern binary format popularized by its use in major web standards, browsers, and companies to speed up computation. WASM is built to be fast, portable, and human-readable. It has sandboxed execution environments for security and simplicity. Working with WASM is nothing new for Arbitrum chains. Ever since the [Nitro upgrade](https://medium.com/offchainlabs/arbitrum-nitro-one-small-step-for-l2-one-giant-leap-for-ethereum-bc9108047450), WASM has been a fundamental component of Arbitrum's fully functioning fraud proofs. -With a WASM VM, any programming language compilable to WASM is within Stylus's scope. While many popular programming languages can compile into WASM, some compilers are more suitable for smart contract development than others, like Rust, C, and C++. Other languages like Go, Sway, Move, and Cairo are also supported. Languages that include their own runtimes, like Python and Javascript, are more complex for Stylus to support, although not impossible. Compared to Solidity, WASM programs are much more efficient for memory-intensive applications. There are many reasons for this, including the decades of compiler development for Rust and C. WASM also has a faster runtime than the EVM, resulting in faster execution. Third-party contributions in the form of libraries for new and existing languages are welcomed! +With a WASM VM, any programming language compilable to WASM is within Stylus's scope. While many popular programming languages can compile into WASM, some compilers are more suitable for smart contract development than others, like Rust, C, and C++. Other languages like Go, Sway, Move, and Cairo are also supported. Languages that include their own runtimes, like Python and Javascript, are more complex for Stylus to support, although not impossible. Compared to Solidity, WASM programs are more efficient for memory-intensive applications. There are many reasons for this, including the decades of compiler development for Rust and C. WASM also has a faster runtime than the EVM, resulting in faster execution. Third-party contributions in the form of libraries for new and existing languages are welcomed! ### Use Cases @@ -40,9 +40,8 @@ While many developers will be drawn to new use cases, rebuilding existing applic - **High-Performance Onchain Logic**: Support memory and compute-intensive applications like onchain games and generative art either by writing all of the application in Stylus or enhance performance of existing Solidity contracts by optimizing specific parts. -- **Endless Possibilities**: Enable innovative use cases such as generative art, compute-heavy - AI models, onchain games, and projects utilizing advanced cryptography, unlocking the full potential - of resource-intensive applications onchain. +- **Innovations**: generative art, compute-heavy + AI models, onchain games, advanced cryptography. ### Getting Started From 42c22bb25522c8e302570894febfe0e47b710311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Wed, 19 Nov 2025 19:37:44 -0800 Subject: [PATCH 10/28] add template files --- docs/stylus/advanced/hostio-exports.mdx | 11 +++++++++++ .../advanced/minimal-entrypoint-contracts.mdx | 11 +++++++++++ .../recommended-libraries.mdx} | 0 docs/stylus/advanced/solidity-differences.mdx | 11 +++++++++++ docs/stylus/concepts/activation.mdx | 11 +++++++++++ docs/stylus/concepts/caching-strategy.mdx | 11 +++++++++++ docs/stylus/concepts/evm-differences.mdx | 15 +++++++++++++++ docs/stylus/concepts/webassembly.mdx | 11 +++++++++++ docs/stylus/how-tos/check-and-deploy.mdx | 11 +++++++++++ .../how-tos/deploying-non-rust-wasm-contracts.mdx | 13 +++++++++++++ docs/stylus/how-tos/exporting-abi.mdx | 11 +++++++++++ .../how-tos/optimizing-wasm-binary-size.mdx | 13 +++++++++++++ 12 files changed, 129 insertions(+) create mode 100644 docs/stylus/advanced/hostio-exports.mdx create mode 100644 docs/stylus/advanced/minimal-entrypoint-contracts.mdx rename docs/stylus/{recommended-libraries.md => advanced/recommended-libraries.mdx} (100%) create mode 100644 docs/stylus/advanced/solidity-differences.mdx create mode 100644 docs/stylus/concepts/activation.mdx create mode 100644 docs/stylus/concepts/caching-strategy.mdx create mode 100644 docs/stylus/concepts/evm-differences.mdx create mode 100644 docs/stylus/concepts/webassembly.mdx create mode 100644 docs/stylus/how-tos/check-and-deploy.mdx create mode 100644 docs/stylus/how-tos/deploying-non-rust-wasm-contracts.mdx create mode 100644 docs/stylus/how-tos/exporting-abi.mdx create mode 100644 docs/stylus/how-tos/optimizing-wasm-binary-size.mdx diff --git a/docs/stylus/advanced/hostio-exports.mdx b/docs/stylus/advanced/hostio-exports.mdx new file mode 100644 index 0000000000..8dc22dfa6b --- /dev/null +++ b/docs/stylus/advanced/hostio-exports.mdx @@ -0,0 +1,11 @@ +--- +title: 'Hostio exports' +description: 'Hostio exports' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers who need to understand how to use hostio exports with Stylus. +displayed_sidebar: buildAppsSidebar +--- + +TBD diff --git a/docs/stylus/advanced/minimal-entrypoint-contracts.mdx b/docs/stylus/advanced/minimal-entrypoint-contracts.mdx new file mode 100644 index 0000000000..211fa3acc5 --- /dev/null +++ b/docs/stylus/advanced/minimal-entrypoint-contracts.mdx @@ -0,0 +1,11 @@ +--- +title: 'Minimal entrypoint contracts' +description: 'Minimal entrypoint contracts' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers who need to understand how to build a minimal entrypoint contract. +displayed_sidebar: buildAppsSidebar +--- + +TBD diff --git a/docs/stylus/recommended-libraries.md b/docs/stylus/advanced/recommended-libraries.mdx similarity index 100% rename from docs/stylus/recommended-libraries.md rename to docs/stylus/advanced/recommended-libraries.mdx diff --git a/docs/stylus/advanced/solidity-differences.mdx b/docs/stylus/advanced/solidity-differences.mdx new file mode 100644 index 0000000000..1acd8509c9 --- /dev/null +++ b/docs/stylus/advanced/solidity-differences.mdx @@ -0,0 +1,11 @@ +--- +title: 'Differences between Solidity and Stylus' +description: 'Solidity and Stylus differences' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers who need to compare Stylus with Solidity. +displayed_sidebar: buildAppsSidebar +--- + +TBD diff --git a/docs/stylus/concepts/activation.mdx b/docs/stylus/concepts/activation.mdx new file mode 100644 index 0000000000..14376d2def --- /dev/null +++ b/docs/stylus/concepts/activation.mdx @@ -0,0 +1,11 @@ +--- +title: 'Activation' +description: 'Activation' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: . +displayed_sidebar: buildAppsSidebar +--- + +TBD diff --git a/docs/stylus/concepts/caching-strategy.mdx b/docs/stylus/concepts/caching-strategy.mdx new file mode 100644 index 0000000000..3bed5bb6fd --- /dev/null +++ b/docs/stylus/concepts/caching-strategy.mdx @@ -0,0 +1,11 @@ +--- +title: 'Caching strategy' +description: 'Caching strategy' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: . +displayed_sidebar: buildAppsSidebar +--- + +TBD diff --git a/docs/stylus/concepts/evm-differences.mdx b/docs/stylus/concepts/evm-differences.mdx new file mode 100644 index 0000000000..720af77c48 --- /dev/null +++ b/docs/stylus/concepts/evm-differences.mdx @@ -0,0 +1,15 @@ +--- +title: 'evm-differences' +description: 'EVM-differences' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers who need to understand how Stylus works with evm-differences. +displayed_sidebar: buildAppsSidebar +--- + +- Size limits +- Ink and gas +- Memory and compute + +TBD diff --git a/docs/stylus/concepts/webassembly.mdx b/docs/stylus/concepts/webassembly.mdx new file mode 100644 index 0000000000..a67c6a44c5 --- /dev/null +++ b/docs/stylus/concepts/webassembly.mdx @@ -0,0 +1,11 @@ +--- +title: 'WebAssembly' +description: 'WebAssembly' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers who need to understand how Stylus works with WebAssembly. +displayed_sidebar: buildAppsSidebar +--- + +TBD diff --git a/docs/stylus/how-tos/check-and-deploy.mdx b/docs/stylus/how-tos/check-and-deploy.mdx new file mode 100644 index 0000000000..be3fe47b9f --- /dev/null +++ b/docs/stylus/how-tos/check-and-deploy.mdx @@ -0,0 +1,11 @@ +--- +title: 'Check and deploy' +description: 'Check and deploy Stylus contracts' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers who need to understand how to check and deploy Stylus contracts. +displayed_sidebar: buildAppsSidebar +--- + +TBD diff --git a/docs/stylus/how-tos/deploying-non-rust-wasm-contracts.mdx b/docs/stylus/how-tos/deploying-non-rust-wasm-contracts.mdx new file mode 100644 index 0000000000..42ff37236b --- /dev/null +++ b/docs/stylus/how-tos/deploying-non-rust-wasm-contracts.mdx @@ -0,0 +1,13 @@ +--- +id: Deploying non-Rust WASM contracts +title: 'Deploying non-Rust WASM contracts' +description: 'Deploying non-Rust WASM contracts' +author: chrisco +sme: chrisco +target_audience: 'Developers who need to deploy non-Rust WASM contracts' +content_type: how-to +sidebar_position: 2 +displayed_sidebar: buildAppsSidebar +--- + +TBD diff --git a/docs/stylus/how-tos/exporting-abi.mdx b/docs/stylus/how-tos/exporting-abi.mdx new file mode 100644 index 0000000000..579319a42e --- /dev/null +++ b/docs/stylus/how-tos/exporting-abi.mdx @@ -0,0 +1,11 @@ +--- +title: 'Exporting ABIs' +description: 'Exporting ABIs' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers who need to understand how to check and deploy Stylus contracts. +displayed_sidebar: buildAppsSidebar +--- + +TBD diff --git a/docs/stylus/how-tos/optimizing-wasm-binary-size.mdx b/docs/stylus/how-tos/optimizing-wasm-binary-size.mdx new file mode 100644 index 0000000000..d8c3c0fab5 --- /dev/null +++ b/docs/stylus/how-tos/optimizing-wasm-binary-size.mdx @@ -0,0 +1,13 @@ +--- +id: Optimizing WASM binary size +title: 'How to optimizing WASM binary size' +description: 'How-to optimize WASM binary size' +author: chrisco +sme: chrisco +target_audience: 'Developers who need to reduce their Stylus binary size' +content_type: how-to +sidebar_position: 2 +displayed_sidebar: buildAppsSidebar +--- + +TBD From b02bc5bb553c5e16d07d5c348dda402acdc6ec80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 20 Nov 2025 11:04:28 -0800 Subject: [PATCH 11/28] add redirect to new recommended libraries location --- vercel.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vercel.json b/vercel.json index baa2930216..7ebb9a0d98 100644 --- a/vercel.json +++ b/vercel.json @@ -426,6 +426,11 @@ "destination": "/(docs/run-arbitrum-node/run-feed-relay/?)", "permanent": false }, + { + "source": "/(docs/stylus/recommended-libraries/?)", + "destination": "/(docs/stylus/advanced/recommended-libraries/?)", + "permanent": false + }, { "source": "/(docs/tx_lifecycle/?)", "destination": "/how-arbitrum-works/deep-dives/transaction-lifecycle", From d62a2050f428ae0bfc96bb227110afb24359a451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 20 Nov 2025 11:19:56 -0800 Subject: [PATCH 12/28] add new articles in sidebars > WIP --- sidebars.js | 241 +++++++++++++++++++++++++--------------------------- 1 file changed, 118 insertions(+), 123 deletions(-) diff --git a/sidebars.js b/sidebars.js index 96cc50b096..193cc4b735 100644 --- a/sidebars.js +++ b/sidebars.js @@ -1195,11 +1195,6 @@ const sidebars = { id: 'stylus/gentle-introduction', label: 'A gentle introduction', }, - { - type: 'doc', - id: 'stylus/quickstart', - label: 'Quickstart', - }, { type: 'category', label: 'Rust SDK', @@ -1252,141 +1247,141 @@ const sidebars = { id: 'stylus/reference/contracts', label: 'Contracts', }, - { - type: 'doc', - id: 'stylus/how-tos/using-inheritance', - label: 'Composition and trait-based routing model', - }, - { - type: 'doc', - id: 'stylus/reference/rust-sdk-guide', - label: 'Advanced features', - }, - { - type: 'doc', - id: 'stylus/recommended-libraries', - label: 'Recommended Rust Crates', - }, - ], - }, - { - type: 'category', - label: 'Rust CLI', - collapsed: true, - items: [ - { - type: 'doc', - id: 'stylus/using-cli', - label: 'Overview', - }, - { - type: 'doc', - id: 'stylus/how-tos/debugging-tx', - label: 'Debug transactions', - }, { type: 'doc', id: 'stylus/how-tos/testing-contracts', - label: 'Testing contracts', + label: 'Writing Tests', }, { - type: 'doc', - id: 'stylus/how-tos/verifying-contracts', - label: 'Verify contracts', - }, - { - type: 'doc', - id: 'stylus/how-tos/caching-contracts', - label: 'Cache contracts', - }, - { - type: 'doc', - id: 'stylus/how-tos/verifying-contracts-arbiscan', - label: 'Verify on Arbiscan', - }, - { - type: 'doc', - id: 'stylus/how-tos/optimizing-binaries', - label: 'Optimize WASM binaries', - }, - ], - }, - { - type: 'html', - value: - 'Run a local dev node', - }, - { - type: 'category', - label: 'Concepts', - collapsed: true, - items: [ - { - type: 'doc', - id: 'stylus/concepts/how-it-works', - label: 'Architecture overview', - }, - { - type: 'doc', - id: 'stylus/concepts/gas-metering', - label: 'Gas metering', - }, - ], - }, - { - type: 'category', - label: 'Examples', - collapsed: true, - items: [ - { - type: 'link', - label: 'Awesome Stylus', - href: 'https://github.com/OffchainLabs/awesome-stylus', + type: 'category', + label: 'Advanced', + collapsed: true, + items: [ + { + type: 'doc', + id: 'stylus/advanced/solidity-differences', + label: 'Solidity differences', + }, + { + type: 'doc', + id: 'stylus/advanced/recommended-libraries', + label: 'Recommended packages', + }, + { + type: 'doc', + id: 'stylus/advanced/minimal-entrypoint-contracts', + label: 'Minimal entrypoint contracts', + }, + { + type: 'doc', + id: 'stylus/advanced/hostio-exports', + label: 'Hostio exports', + }, + ], }, - ], - }, - { - type: 'category', - label: 'Reference', - collapsed: true, - items: [ { type: 'doc', - id: 'stylus/reference/opcode-hostio-pricing', - label: 'Gas & Ink Pricing', + id: 'stylus/troubleshooting-building-stylus', + label: 'Troubleshooting', }, { - type: 'link', - label: 'Stylus by Example', - href: 'https://stylus-by-example.org/', - }, - { - type: 'link', - label: 'Cargo Stylus CLI GitHub', - href: 'https://github.com/OffchainLabs/cargo-stylus', + type: 'category', + label: 'Using the CLI', + collapsed: true, + items: [ + { + type: 'doc', + id: 'stylus/how-tos/check-and-deploy', + label: 'Check and deploy', + }, + { + type: 'doc', + id: 'stylus/how-tos/verifying-contracts', + label: 'Verify contracts', + }, + { + type: 'doc', + id: 'stylus/how-tos/exporting-abi', + label: 'Exporting ABI', + }, + { + type: 'doc', + id: 'stylus/how-tos/debugging-tx', + label: 'Debugging with replay', + }, + { + type: 'doc', + id: 'stylus/how-tos/optimizing-wasm-binary-size', + label: 'Optimizing WASM binary size', + }, + { + type: 'doc', + id: 'stylus/how-tos/deploying-non-rust-wasm-contracts', + label: 'Deploying non-Rust WASM contracts', + }, + ], }, { - type: 'link', - label: 'Rust SDK Crate', - href: 'https://docs.rs/stylus-sdk/latest/stylus_sdk/index.html', + type: 'category', + label: 'WM Concepts', + collapsed: true, + items: [ + { + type: 'doc', + id: 'stylus/concepts/webassembly', + label: 'Check and deploy', + }, + { + type: 'doc', + id: 'stylus/concepts/evm-differences', + label: 'EVM differences', + }, + { + type: 'doc', + id: 'stylus/concepts/activation', + label: 'Activation', + }, + { + type: 'doc', + id: 'stylus/how-tos/caching-contracts', + label: 'Caching Strategy', + }, + { + type: 'doc', + id: 'stylus/how-tos/caching-contracts', + label: 'Caching Strategy', + }, + ], }, { - type: 'link', - label: 'Source Code Repository', - href: 'https://github.com/OffchainLabs/stylus', + type: 'category', + label: 'Reference', + collapsed: true, + items: [ + { + type: 'link', + label: 'Stylus by Example', + href: 'https://stylus-by-example.org/', + }, + { + type: 'link', + label: 'Cargo Stylus CLI GitHub', + href: 'https://github.com/OffchainLabs/cargo-stylus', + }, + { + type: 'link', + label: 'Rust SDK Crate', + href: 'https://docs.rs/stylus-sdk/latest/stylus_sdk/index.html', + }, + { + type: 'link', + label: 'Source Code Repository', + href: 'https://github.com/OffchainLabs/stylus', + }, + ], }, ], }, - { - type: 'doc', - id: 'stylus/how-tos/adding-support-for-new-languages', - label: 'Using other languages', - }, - { - type: 'doc', - id: 'stylus/troubleshooting-building-stylus', - label: 'Troubleshooting', - }, ], }, { From 42b23d56a0a02df50e3f5d70cd7ffa05382c6708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 20 Nov 2025 18:04:47 -0800 Subject: [PATCH 13/28] remove unneeded how-to guide on optimizing WASM binary size --- docs/stylus/how-tos/optimizing-wasm-binary-size.mdx | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 docs/stylus/how-tos/optimizing-wasm-binary-size.mdx diff --git a/docs/stylus/how-tos/optimizing-wasm-binary-size.mdx b/docs/stylus/how-tos/optimizing-wasm-binary-size.mdx deleted file mode 100644 index d8c3c0fab5..0000000000 --- a/docs/stylus/how-tos/optimizing-wasm-binary-size.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -id: Optimizing WASM binary size -title: 'How to optimizing WASM binary size' -description: 'How-to optimize WASM binary size' -author: chrisco -sme: chrisco -target_audience: 'Developers who need to reduce their Stylus binary size' -content_type: how-to -sidebar_position: 2 -displayed_sidebar: buildAppsSidebar ---- - -TBD From fe4ff4ffaa24ba15ec27b32ec807ba7d5eaa5d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 20 Nov 2025 18:05:15 -0800 Subject: [PATCH 14/28] update sidebars.js --- sidebars.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sidebars.js b/sidebars.js index 193cc4b735..6b79fb3ff8 100644 --- a/sidebars.js +++ b/sidebars.js @@ -1311,7 +1311,7 @@ const sidebars = { }, { type: 'doc', - id: 'stylus/how-tos/optimizing-wasm-binary-size', + id: 'stylus/how-tos/optimizing-binaries', label: 'Optimizing WASM binary size', }, { From 21fc192075a973e7b3fc2b90340eceba2dc91e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 20 Nov 2025 18:19:29 -0800 Subject: [PATCH 15/28] fix frontmatter typo in deploying-non-rust-wasm-contracts --- docs/stylus/how-tos/deploying-non-rust-wasm-contracts.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stylus/how-tos/deploying-non-rust-wasm-contracts.mdx b/docs/stylus/how-tos/deploying-non-rust-wasm-contracts.mdx index 42ff37236b..97bd94cd1a 100644 --- a/docs/stylus/how-tos/deploying-non-rust-wasm-contracts.mdx +++ b/docs/stylus/how-tos/deploying-non-rust-wasm-contracts.mdx @@ -1,5 +1,5 @@ --- -id: Deploying non-Rust WASM contracts +id: deploying-non-rust-wasm-contracts title: 'Deploying non-Rust WASM contracts' description: 'Deploying non-Rust WASM contracts' author: chrisco From 382792361a259a5a18e75219ff9768abf79fab13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 20 Nov 2025 18:29:42 -0800 Subject: [PATCH 16/28] fix recommended libraries broken links --- docs/stylus/how-tos/optimizing-binaries.mdx | 2 +- docs/stylus/reference/project-structure.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/stylus/how-tos/optimizing-binaries.mdx b/docs/stylus/how-tos/optimizing-binaries.mdx index d88274f8ad..9924b3f5b6 100644 --- a/docs/stylus/how-tos/optimizing-binaries.mdx +++ b/docs/stylus/how-tos/optimizing-binaries.mdx @@ -46,7 +46,7 @@ Additional WASM-specific tooling exists to shrink binaries. Due to being third p [twiggy](https://github.com/rustwasm/twiggy) is a code size profiler for WASM, it can help you estimate the impact of each added component on your binaries' size. -Our team has also curated a [list of recommended libraries](/stylus/recommended-libraries) that are helpful to Stylus development and optimally sized. +Our team has also curated a [list of recommended libraries](/stylus/advanced/recommended-libraries) that are helpful to Stylus development and optimally sized. ### Frequently asked questions diff --git a/docs/stylus/reference/project-structure.mdx b/docs/stylus/reference/project-structure.mdx index c37969b3d7..1d978c2c51 100644 --- a/docs/stylus/reference/project-structure.mdx +++ b/docs/stylus/reference/project-structure.mdx @@ -159,7 +159,7 @@ Use imported types in your contract: let price = dec!(72.00); ``` -Note, not all Rust crates are compatible with Stylus since they need to be compiled to WASM and used in a blockchain context, which is more limited than a desktop application. For instance, the `rand` crate is not usable, as there is no onchain randomness available to smart contracts. In addition, contracts cannot access functions that use networking or filesystem access features. There is also a need to be mindful of the size of the crates you import, since the default contract size limit is 24KB (compressed). Crates that do not use the standard library (`no_std` crates) tend to work best. See [Using public Rust crates](https://docs.arbitrum.io/stylus/recommended-libraries#using-public-rust-crates) for more important details on using public Rust crates as well as a curated list of crates that tend to work well for smart contract development. +Note, not all Rust crates are compatible with Stylus since they need to be compiled to WASM and used in a blockchain context, which is more limited than a desktop application. For instance, the `rand` crate is not usable, as there is no onchain randomness available to smart contracts. In addition, contracts cannot access functions that use networking or filesystem access features. There is also a need to be mindful of the size of the crates you import, since the default contract size limit is 24KB (compressed). Crates that do not use the standard library (`no_std` crates) tend to work best. See [Using public Rust crates](https://docs.arbitrum.io/stylus/advanced/recommended-libraries#using-public-rust-crates) for more important details on using public Rust crates as well as a curated list of crates that tend to work well for smart contract development. ## Events From cdb1f6f7cc89cb4ac88a20ffa888fc2af17a80c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Fri, 21 Nov 2025 12:37:23 -0800 Subject: [PATCH 17/28] Document Stylus Rust primitives with comprehensive examples Add detailed documentation for all Stylus SDK primitive types including: - Boolean (bool) with usage examples and Solidity mappings - Unsigned integers (u8-u128, U256, Uint) - Signed integers (i8-i128, I256, Signed) - Address type with constants and validation examples - String type with literal and formatting examples - Bytes types (dynamic Bytes and fixed FixedBytes) - Hex literal usage examples - Complete type mapping tables for all primitives - Best practices for each primitive type - Type conversion examples Each section includes: - Practical code examples - Solidity type mappings - Storage size information - ABI signature details - Common usage patterns --- .../reference/data-types/primitives.mdx | 561 +++++++++++++++++- 1 file changed, 556 insertions(+), 5 deletions(-) diff --git a/docs/stylus/reference/data-types/primitives.mdx b/docs/stylus/reference/data-types/primitives.mdx index ac1440578c..28e0d78357 100644 --- a/docs/stylus/reference/data-types/primitives.mdx +++ b/docs/stylus/reference/data-types/primitives.mdx @@ -8,8 +8,559 @@ target_audience: Developers using the Stylus Rust SDK to write and deploy smart displayed_sidebar: buildAppsSidebar --- -1. Boolean -2. Integers -3. Address (include AddressVM traits from types module) 4. String - 1. Literals - 2. Hex +# Primitives + +The Stylus SDK provides full support for Rust primitive types with automatic ABI encoding/decoding and Solidity type mappings. These primitives can be used in contract method signatures, storage, and as function parameters. + +## Boolean (`bool`) + +Booleans in Rust map directly to Solidity's `bool` type. + +### Usage in Contract Methods + +```rust +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + pub fn is_valid(&self) -> bool { + true + } + + pub fn toggle(&mut self, flag: bool) { + // Use the boolean value + if flag { + // Do something + } + } +} +``` + +### Solidity Mapping + +- **Rust type**: `bool` +- **Solidity type**: `bool` +- **Storage size**: 1 byte +- **ABI signature**: `"bool"` + +## Integers + +The Stylus SDK supports both signed and unsigned integers with various bit sizes. All integer types from Rust's standard library and alloy-primitives are supported. + +### Unsigned Integers + +#### Standard Rust Unsigned Integers + +```rust +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + // u8: 8-bit unsigned integer + pub fn get_byte(&self) -> u8 { + 255 + } + + // u16: 16-bit unsigned integer + pub fn get_short(&self) -> u16 { + 65535 + } + + // u32: 32-bit unsigned integer + pub fn get_int(&self) -> u32 { + 4294967295 + } + + // u64: 64-bit unsigned integer + pub fn get_long(&self) -> u64 { + 18446744073709551615 + } + + // u128: 128-bit unsigned integer + pub fn get_u128(&self) -> u128 { + 340282366920938463463374607431768211455 + } +} +``` + +#### Alloy Unsigned Integers + +For larger integers and full compatibility with Solidity's uint types, use `alloy_primitives::Uint`: + +```rust +use alloy_primitives::{U256, Uint}; +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + // U256: 256-bit unsigned integer (most common in Solidity) + pub fn get_balance(&self) -> U256 { + U256::from(1000000) + } + + // Any bit size from 8 to 256 (in multiples of 8) + pub fn get_u160(&self) -> Uint<160, 3> { + Uint::<160, 3>::from(999) + } + + pub fn get_u96(&self) -> Uint<96, 2> { + Uint::<96, 2>::from(123456) + } +} +``` + +### Signed Integers + +#### Standard Rust Signed Integers + +```rust +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + // i8: 8-bit signed integer + pub fn get_signed_byte(&self) -> i8 { + -128 + } + + // i16: 16-bit signed integer + pub fn get_signed_short(&self) -> i16 { + -32768 + } + + // i32: 32-bit signed integer + pub fn get_signed_int(&self) -> i32 { + -2147483648 + } + + // i64: 64-bit signed integer + pub fn get_signed_long(&self) -> i64 { + -9223372036854775808 + } + + // i128: 128-bit signed integer + pub fn get_signed_i128(&self) -> i128 { + -170141183460469231731687303715884105728 + } +} +``` + +#### Alloy Signed Integers + +```rust +use alloy_primitives::{Signed, I256}; +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + // I256: 256-bit signed integer + pub fn get_signed_balance(&self) -> I256 { + I256::try_from(-1000).unwrap() + } + + // Any bit size from 8 to 256 (in multiples of 8) + pub fn get_i160(&self) -> Signed<160, 3> { + Signed::<160, 3>::try_from(-999).unwrap() + } +} +``` + +### Integer Type Mappings + +| Rust Type | Solidity Type | Bit Size | ABI Signature | +| ------------------------- | ------------- | -------- | ------------- | +| `u8` | `uint8` | 8 bits | `"uint8"` | +| `u16` | `uint16` | 16 bits | `"uint16"` | +| `u32` | `uint32` | 32 bits | `"uint32"` | +| `u64` | `uint64` | 64 bits | `"uint64"` | +| `u128` | `uint128` | 128 bits | `"uint128"` | +| `Uint<160, 3>` | `uint160` | 160 bits | `"uint160"` | +| `U256` / `Uint<256, 4>` | `uint256` | 256 bits | `"uint256"` | +| `i8` | `int8` | 8 bits | `"int8"` | +| `i16` | `int16` | 16 bits | `"int16"` | +| `i32` | `int32` | 32 bits | `"int32"` | +| `i64` | `int64` | 64 bits | `"int64"` | +| `i128` | `int128` | 128 bits | `"int128"` | +| `Signed<160, 3>` | `int160` | 160 bits | `"int160"` | +| `I256` / `Signed<256, 4>` | `int256` | 256 bits | `"int256"` | + +**Note**: All Solidity uint/int types from `uint8`/`int8` to `uint256`/`int256` (in 8-bit increments) are supported through `Uint` and `Signed`. + +## Address + +Ethereum addresses are represented by the `Address` type from `alloy_primitives`. + +### Basic Usage + +```rust +use alloy_primitives::Address; +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + pub fn get_owner(&self) -> Address { + Address::ZERO + } + + pub fn is_owner(&self, account: Address) -> bool { + account == self.vm().msg_sender() + } + + pub fn transfer_ownership(&mut self, new_owner: Address) { + // Address validation and logic + if new_owner == Address::ZERO { + // Handle error + } + } +} +``` + +### Address Constants + +```rust +use alloy_primitives::Address; + +// Zero address (0x0000000000000000000000000000000000000000) +let zero = Address::ZERO; + +// Parse from string +let addr = Address::parse_checksummed("0x1234567890123456789012345678901234567890", None).unwrap(); + +// Create from bytes +let bytes: [u8; 20] = [0; 20]; +let addr = Address::from(bytes); +``` + +### Solidity Mapping + +- **Rust type**: `Address` (from `alloy_primitives`) +- **Solidity type**: `address` +- **Storage size**: 20 bytes (160 bits) +- **ABI signature**: `"address"` + +## String + +Rust `String` types map to Solidity `string` type. + +### String Literals + +```rust +use stylus_sdk::prelude::*; +use alloc::string::String; + +#[public] +impl MyContract { + pub fn get_name(&self) -> String { + String::from("MyToken") + } + + pub fn greet(&self, name: String) -> String { + format!("Hello, {}!", name) + } +} +``` + +### Solidity Mapping + +- **Rust type**: `String` (from `alloc::string`) +- **Solidity type**: `string` +- **Storage**: Dynamic (heap-allocated) +- **ABI signature**: `"string"` +- **ABI export**: + - As argument: `"string calldata"` + - As return: `"string memory"` + +**Note**: Strings in Solidity are UTF-8 encoded byte arrays. When using strings in Stylus: + +- Use `alloc::string::String` for owned strings +- Strings are dynamically sized and stored in memory/calldata +- For storage, use `StorageString` (see [Storage Types](./storage.mdx)) + +## Bytes + +The SDK provides two types for working with byte data: + +### 1. Dynamic Bytes (`Bytes`) + +For variable-length byte arrays (Solidity `bytes`): + +```rust +use alloy_primitives::Bytes; +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + pub fn get_data(&self) -> Bytes { + Bytes::from(vec![1, 2, 3, 4]) + } + + pub fn process_data(&mut self, data: Bytes) -> usize { + data.len() + } +} +``` + +### 2. Fixed Bytes (`FixedBytes`) + +For fixed-length byte arrays (Solidity `bytesN`): + +```rust +use alloy_primitives::FixedBytes; +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + // bytes32 (common for hashes) + pub fn get_hash(&self) -> FixedBytes<32> { + FixedBytes::<32>::ZERO + } + + // bytes2 + pub fn get_signature(&self) -> FixedBytes<2> { + FixedBytes::new([0x12, 0x34]) + } + + // Any size from 1 to 32 + pub fn get_bytes8(&self) -> FixedBytes<8> { + FixedBytes::<8>::from([1, 2, 3, 4, 5, 6, 7, 8]) + } +} +``` + +### Common FixedBytes Aliases + +```rust +use alloy_primitives::{B256, B160, B128}; + +// B256 is FixedBytes<32> (bytes32) +let hash: B256 = B256::ZERO; + +// B160 is FixedBytes<20> (bytes20) +let data: B160 = B160::ZERO; + +// B128 is FixedBytes<16> (bytes16) +let value: B128 = B128::ZERO; +``` + +### Bytes Type Mappings + +| Rust Type | Solidity Type | Description | +| ------------------------- | ------------- | -------------------------------- | +| `Bytes` | `bytes` | Dynamic byte array | +| `Vec` | `uint8[]` | Array of bytes (NOT `bytes`!) | +| `FixedBytes` | `bytesN` | Fixed-size byte array (N = 1-32) | +| `B256` / `FixedBytes<32>` | `bytes32` | 32-byte array (hashes) | +| `B160` / `FixedBytes<20>` | `bytes20` | 20-byte array | +| `B128` / `FixedBytes<16>` | `bytes16` | 16-byte array | + +**Important Distinction**: + +- `Vec` maps to Solidity `uint8[]` (array of unsigned integers) +- `Bytes` maps to Solidity `bytes` (dynamic byte array) +- For Solidity `bytes`, always use `alloy_primitives::Bytes` + +### Bytes ABI Encoding + +```rust +// Bytes type +// ABI signature: "bytes" +// As argument: "bytes calldata" +// As return: "bytes memory" + +// FixedBytes type +// ABI signature: "bytesN" where N is 1-32 +// Example: FixedBytes<32> -> "bytes32" +``` + +## Hex String Literals + +When working with hex data, you can use hex literals: + +```rust +use alloy_primitives::{hex, Address, FixedBytes, Bytes}; + +// Hex bytes +let data = hex!("deadbeef"); + +// Address from hex +let addr = Address::from(hex!("1234567890123456789012345678901234567890")); + +// FixedBytes from hex +let hash = FixedBytes::<32>::from(hex!( + "0000000000000000000000000000000000000000000000000000000000000000" +)); + +// Dynamic Bytes from hex +let bytes = Bytes::from(hex!("aabbccdd")); +``` + +## Complete Example + +Here's a comprehensive example showing all primitive types: + +```rust +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] +extern crate alloc; + +use alloc::string::String; +use alloy_primitives::{Address, Bytes, FixedBytes, U256}; +use stylus_sdk::prelude::*; + +sol_storage! { + #[entrypoint] + pub struct PrimitiveExample { + bool initialized; + uint256 count; + address owner; + } +} + +#[public] +impl PrimitiveExample { + // Boolean + pub fn is_initialized(&self) -> bool { + self.initialized.get() + } + + // Unsigned integers (native Rust) + pub fn get_u8(&self) -> u8 { + 255 + } + + pub fn get_u256(&self) -> U256 { + self.count.get() + } + + // Signed integers + pub fn get_signed(&self) -> i32 { + -42 + } + + // Address + pub fn get_owner(&self) -> Address { + self.owner.get() + } + + pub fn set_owner(&mut self, new_owner: Address) { + self.owner.set(new_owner); + } + + // String + pub fn get_name(&self) -> String { + String::from("PrimitiveExample") + } + + // Dynamic bytes + pub fn get_data(&self) -> Bytes { + Bytes::from(vec![1, 2, 3, 4]) + } + + // Fixed bytes + pub fn get_hash(&self) -> FixedBytes<32> { + FixedBytes::<32>::ZERO + } + + // Multiple parameters + pub fn complex_function( + &mut self, + flag: bool, + amount: U256, + recipient: Address, + data: Bytes + ) -> bool { + // Function logic + true + } +} +``` + +## Best Practices + +1. **Use U256 for token amounts**: Solidity commonly uses `uint256` for token balances and amounts. + + ```rust + use alloy_primitives::U256; + + pub fn transfer(&mut self, amount: U256) { + // amount is uint256 in Solidity + } + ``` + +2. **Use Address for account addresses**: Always use `alloy_primitives::Address` for Ethereum addresses. + + ```rust + use alloy_primitives::Address; + + pub fn get_balance(&self, account: Address) -> U256 { + // Query balance + } + ``` + +3. **Use Bytes for dynamic byte data**: For Solidity `bytes`, use `alloy_primitives::Bytes`, not `Vec`. + + ```rust + use alloy_primitives::Bytes; + + pub fn process(&self, data: Bytes) { + // data maps to Solidity bytes + } + ``` + +4. **Use FixedBytes for hashes and signatures**: For fixed-size byte data like hashes. + + ```rust + use alloy_primitives::FixedBytes; + + pub fn verify(&self, hash: FixedBytes<32>) -> bool { + // hash maps to Solidity bytes32 + true + } + ``` + +5. **Check for zero addresses**: Always validate addresses before use. + + ```rust + use alloy_primitives::Address; + + pub fn set_admin(&mut self, admin: Address) { + if admin == Address::ZERO { + // Handle error + } + } + ``` + +## Type Conversion + +### Between Integer Types + +```rust +use alloy_primitives::U256; + +// Native to U256 +let amount: u64 = 1000; +let big_amount = U256::from(amount); + +// U256 to native (with bounds checking) +let big_value = U256::from(1000); +let small_value: u64 = big_value.to::(); +``` + +### Address Conversions + +```rust +use alloy_primitives::Address; + +// From bytes +let bytes: [u8; 20] = [0; 20]; +let addr = Address::from(bytes); + +// To bytes +let addr = Address::ZERO; +let bytes: [u8; 20] = addr.into(); +``` + +## See Also + +- [Compound Types](./compound-types.mdx) - Arrays, tuples, structs +- [Storage Types](./storage.mdx) - Persistent storage for primitives +- [Type Conversions](./conversions-between-types.mdx) - Converting between types From 928ac21470331649537f323893b32e62ec6cfeb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Fri, 21 Nov 2025 12:45:01 -0800 Subject: [PATCH 18/28] add slop primitive data type documentation --- docs/stylus/reference/data-types/primitives.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/stylus/reference/data-types/primitives.mdx b/docs/stylus/reference/data-types/primitives.mdx index 28e0d78357..ee5ce71227 100644 --- a/docs/stylus/reference/data-types/primitives.mdx +++ b/docs/stylus/reference/data-types/primitives.mdx @@ -8,8 +8,6 @@ target_audience: Developers using the Stylus Rust SDK to write and deploy smart displayed_sidebar: buildAppsSidebar --- -# Primitives - The Stylus SDK provides full support for Rust primitive types with automatic ABI encoding/decoding and Solidity type mappings. These primitives can be used in contract method signatures, storage, and as function parameters. ## Boolean (`bool`) From 1a47f7d59dc47a20292e2452e9b3ec2381da5157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Fri, 21 Nov 2025 12:50:18 -0800 Subject: [PATCH 19/28] Document Stylus Rust compound types with comprehensive examples Add detailed documentation for all Stylus SDK compound types including: - Tuples with destructuring and pattern matching examples - Structs using sol! macro with AbiType derivation - Nested structs for complex data structures - Fixed-size arrays with iteration and operations - Vectors (dynamic arrays) with filter, map, and fold operations - Bytes types (dynamic Bytes and fixed FixedBytes) - Complete type mapping tables for all compound types - Best practices for each compound type - Type conversion helpers and iterator patterns - Common patterns (batch operations, pagination) Each section includes: - Practical code examples from SDK examples - Solidity type mappings - ABI signature details - Memory and gas efficiency considerations - Complete working examples with nested structures - Iterator and functional programming patterns --- .../reference/data-types/compound-types.mdx | 846 +++++++++++++++++- 1 file changed, 838 insertions(+), 8 deletions(-) diff --git a/docs/stylus/reference/data-types/compound-types.mdx b/docs/stylus/reference/data-types/compound-types.mdx index 33466eddca..d418445953 100644 --- a/docs/stylus/reference/data-types/compound-types.mdx +++ b/docs/stylus/reference/data-types/compound-types.mdx @@ -3,15 +3,845 @@ title: 'Stylus compound types' description: 'Stylus Rust SDK compound types' author: chrisco sme: chrisco -sidebar_position: 1 +sidebar_position: 2 target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts. displayed_sidebar: buildAppsSidebar --- -2. Compound types - 1. Tuple - 2. Struct - 3. Array - 4. Vector - 5. Bytes - 6. FixedBytes +# Compound Types + +Compound types allow you to group multiple values together in Stylus contracts. The SDK provides full support for tuples, structs, arrays, and vectors with automatic ABI encoding/decoding and Solidity type mappings. + +## Tuples + +Tuples group multiple values of different types together. They map directly to Solidity tuples. + +### Basic Tuples + +```rust +use alloy_primitives::{Address, U256, Bytes}; +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + // Return multiple values as a tuple + pub fn get_data(&self) -> (U256, Address, bool) { + (U256::from(100), Address::ZERO, true) + } + + // Accept tuple as parameter + pub fn process_tuple(&mut self, data: (U256, U256, U256)) -> U256 { + let (a, b, c) = data; + a + b + c + } + + // Nested tuples + pub fn nested(&self) -> ((U256, U256), bool) { + ((U256::from(1), U256::from(2)), true) + } +} +``` + +### Tuple Destructuring + +```rust +use alloy_primitives::U256; +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + pub fn calculate(&self) -> (U256, U256) { + let values = (U256::from(100), U256::from(200)); + + // Destructure the tuple + let (first, second) = values; + + // Return new tuple + (first * U256::from(2), second * U256::from(2)) + } + + // Pattern matching with tuples + pub fn match_tuple(&self, data: (bool, U256)) -> U256 { + match data { + (true, value) => value * U256::from(2), + (false, value) => value, + } + } +} +``` + +### Tuple Type Mappings + +| Rust Type | Solidity Type | ABI Signature | +| ---------------------- | ---------------------------- | ---------------------------- | +| `(U256,)` | `(uint256)` | `"(uint256)"` | +| `(U256, Address)` | `(uint256, address)` | `"(uint256,address)"` | +| `(bool, U256, Bytes)` | `(bool, uint256, bytes)` | `"(bool,uint256,bytes)"` | +| `((U256, U256), bool)` | `((uint256, uint256), bool)` | `"((uint256,uint256),bool)"` | + +**Tuple Limitations**: + +- Tuples support up to 24 elements +- Tuples are always returned as `memory` in Solidity +- Empty tuple `()` represents no return value + +## Structs + +Structs define custom data types with named fields. Use the `sol!` macro to define Solidity-compatible structs. + +### Defining Structs with `sol!` + +```rust +use alloy_primitives::{Address, U256}; +use alloy_sol_types::sol; +use stylus_sdk::prelude::*; + +sol! { + #[derive(Debug, AbiType)] + struct User { + address account; + uint256 balance; + string name; + } + + #[derive(Debug, AbiType)] + struct Token { + string name; + string symbol; + uint8 decimals; + } +} + +#[public] +impl MyContract { + pub fn get_user(&self) -> User { + User { + account: Address::ZERO, + balance: U256::from(1000), + name: "Alice".to_string(), + } + } + + pub fn process_user(&mut self, user: User) -> U256 { + // Access struct fields + user.balance + } + + pub fn get_token_info(&self) -> Token { + Token { + name: "MyToken".to_string(), + symbol: "MTK".to_string(), + decimals: 18, + } + } +} +``` + +### Nested Structs + +Structs can contain other structs, enabling complex data structures: + +```rust +use alloy_primitives::Address; +use alloy_sol_types::sol; +use stylus_sdk::prelude::*; + +sol! { + #[derive(Debug, AbiType)] + struct Dog { + string name; + string breed; + } + + #[derive(Debug, AbiType)] + struct User { + address account; + string name; + Dog[] dogs; + } +} + +#[public] +impl MyContract { + pub fn create_user(&self) -> User { + let dogs = vec![ + Dog { + name: "Rex".to_string(), + breed: "Labrador".to_string(), + }, + Dog { + name: "Max".to_string(), + breed: "Beagle".to_string(), + }, + ]; + + User { + account: Address::ZERO, + name: "Alice".to_string(), + dogs, + } + } + + pub fn get_dog_count(&self, user: User) -> u256 { + user.dogs.len() as u256 + } +} +``` + +### Struct Best Practices + +1. **Always use `#[derive(AbiType)]`** for structs that will be used in contract interfaces: + + ```rust + sol! { + #[derive(Debug, AbiType)] + struct MyData { + uint256 value; + address owner; + } + } + ``` + +2. **Add `Debug` derive** for easier debugging: + + ```rust + sol! { + #[derive(Debug, AbiType)] + struct Config { + bool enabled; + uint256 timeout; + } + } + ``` + +3. **Use descriptive field names** that match Solidity conventions: + ```rust + sol! { + #[derive(Debug, AbiType)] + struct VestingSchedule { + address beneficiary; + uint256 startTime; + uint256 cliffDuration; + uint256 totalAmount; + } + } + ``` + +## Arrays + +Arrays are fixed-size collections of elements. Stylus supports both Rust arrays and Solidity-style arrays. + +### Fixed-Size Arrays + +```rust +use alloy_primitives::U256; +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + // Return a fixed-size array + pub fn get_numbers(&self) -> [U256; 5] { + [ + U256::from(1), + U256::from(2), + U256::from(3), + U256::from(4), + U256::from(5), + ] + } + + // Accept fixed-size array as parameter + pub fn sum_array(&self, numbers: [U256; 5]) -> U256 { + numbers.iter().fold(U256::ZERO, |acc, &x| acc + x) + } + + // Nested arrays + pub fn matrix(&self) -> [[u32; 2]; 3] { + [[1, 2], [3, 4], [5, 6]] + } +} +``` + +### Array Operations + +```rust +use alloy_primitives::{Address, U256}; +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + // Iterate over array + pub fn process_addresses(&self, addresses: [Address; 10]) -> U256 { + let mut count = U256::ZERO; + for addr in addresses.iter() { + if *addr != Address::ZERO { + count += U256::from(1); + } + } + count + } + + // Array of booleans + pub fn check_flags(&self, flags: [bool; 8]) -> bool { + flags.iter().all(|&f| f) + } +} +``` + +### Array Type Mappings + +| Rust Type | Solidity Type | Description | +| --------------------- | -------------- | ------------------------- | +| `[U256; 5]` | `uint256[5]` | 5-element uint256 array | +| `[bool; 10]` | `bool[10]` | 10-element bool array | +| `[Address; 3]` | `address[3]` | 3-element address array | +| `[[u32; 2]; 4]` | `uint32[2][4]` | Nested array (4x2 matrix) | +| `[FixedBytes<32>; 2]` | `bytes32[2]` | 2-element bytes32 array | + +## Vectors + +Vectors are dynamic arrays that can grow or shrink at runtime. They map to Solidity dynamic arrays. + +### Basic Vector Usage + +```rust +use alloy_primitives::{Address, U256, Bytes}; +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + // Return a vector + pub fn get_numbers(&self) -> Vec { + vec![U256::from(1), U256::from(2), U256::from(3)] + } + + // Accept vector as parameter + pub fn sum_vec(&self, numbers: Vec) -> U256 { + numbers.iter().fold(U256::ZERO, |acc, x| acc + *x) + } + + // Vector of addresses + pub fn get_addresses(&self) -> Vec
{ + vec![Address::ZERO, Address::ZERO] + } + + // Vector of bytes + pub fn get_data_list(&self) -> Vec { + vec![ + Bytes::from(vec![1, 2, 3]), + Bytes::from(vec![4, 5, 6]), + ] + } +} +``` + +### Vector Operations + +```rust +use alloy_primitives::U256; +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + // Filter vector + pub fn filter_even(&self, numbers: Vec) -> Vec { + numbers + .into_iter() + .filter(|n| n.byte(0) % 2 == 0) + .collect() + } + + // Map over vector + pub fn double_values(&self, numbers: Vec) -> Vec { + numbers + .into_iter() + .map(|n| n * U256::from(2)) + .collect() + } + + // Find in vector + pub fn contains_value(&self, numbers: Vec, target: U256) -> bool { + numbers.contains(&target) + } + + // Get vector length + pub fn get_length(&self, items: Vec) -> U256 { + U256::from(items.len()) + } +} +``` + +### Vectors of Structs + +```rust +use alloy_primitives::Address; +use alloy_sol_types::sol; +use stylus_sdk::prelude::*; + +sol! { + #[derive(Debug, AbiType)] + struct Transaction { + address from; + address to; + uint256 amount; + } +} + +#[public] +impl MyContract { + pub fn get_transactions(&self) -> Vec { + vec![ + Transaction { + from: Address::ZERO, + to: Address::ZERO, + amount: U256::from(100), + }, + Transaction { + from: Address::ZERO, + to: Address::ZERO, + amount: U256::from(200), + }, + ] + } + + pub fn total_amount(&self, txs: Vec) -> U256 { + txs.iter() + .fold(U256::ZERO, |acc, tx| acc + tx.amount) + } +} +``` + +### Vector Type Mappings + +| Rust Type | Solidity Type | ABI Signature | Storage | +| --------------- | ------------- | --------------------- | ------- | +| `Vec` | `uint256[]` | `"uint256[] memory"` | Dynamic | +| `Vec
` | `address[]` | `"address[] memory"` | Dynamic | +| `Vec` | `bool[]` | `"bool[] memory"` | Dynamic | +| `Vec` | `bytes[]` | `"bytes[] memory"` | Dynamic | +| `Vec` | `MyStruct[]` | `"MyStruct[] memory"` | Dynamic | + +**Important Notes**: + +- Vectors are **always returned as `memory`** in Solidity, never as `calldata` +- `Vec` maps to `uint8[]`, not `bytes` (use `Bytes` for Solidity `bytes`) +- Vectors have dynamic size and consume more gas than fixed arrays + +## Bytes Types + +The SDK provides `Bytes` for dynamic byte arrays and `FixedBytes` for fixed-size byte arrays. + +### Dynamic Bytes (`Bytes`) + +```rust +use alloy_primitives::Bytes; +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + // Return dynamic bytes + pub fn get_data(&self) -> Bytes { + Bytes::from(vec![1, 2, 3, 4, 5]) + } + + // Process bytes + pub fn get_length(&self, data: Bytes) -> usize { + data.len() + } + + // Concatenate bytes + pub fn concat(&self, a: Bytes, b: Bytes) -> Bytes { + let mut result = a.to_vec(); + result.extend_from_slice(&b); + Bytes::from(result) + } +} +``` + +### Fixed Bytes (`FixedBytes`) + +```rust +use alloy_primitives::FixedBytes; +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + // bytes32 (common for hashes) + pub fn get_hash(&self) -> FixedBytes<32> { + FixedBytes::<32>::ZERO + } + + // bytes4 (common for selectors) + pub fn get_selector(&self) -> FixedBytes<4> { + FixedBytes::from([0x12, 0x34, 0x56, 0x78]) + } + + // bytes16 + pub fn get_uuid(&self) -> FixedBytes<16> { + FixedBytes::<16>::from([ + 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, + 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, + ]) + } +} +``` + +## Complete Examples + +### Example 1: Complex Data Structures + +```rust +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] +extern crate alloc; + +use alloc::string::String; +use alloy_primitives::{Address, U256}; +use alloy_sol_types::sol; +use stylus_sdk::prelude::*; + +sol! { + #[derive(Debug, AbiType)] + struct Token { + string name; + string symbol; + uint8 decimals; + uint256 totalSupply; + } + + #[derive(Debug, AbiType)] + struct Balance { + address owner; + uint256 amount; + } +} + +sol_storage! { + #[entrypoint] + pub struct CompoundExample { + uint256 counter; + } +} + +#[public] +impl CompoundExample { + // Return tuple + pub fn get_info(&self) -> (String, U256, bool) { + ("Example".to_string(), U256::from(42), true) + } + + // Return struct + pub fn get_token(&self) -> Token { + Token { + name: "MyToken".to_string(), + symbol: "MTK".to_string(), + decimals: 18, + totalSupply: U256::from(1000000), + } + } + + // Return vector of structs + pub fn get_balances(&self) -> Vec { + vec![ + Balance { + owner: Address::ZERO, + amount: U256::from(100), + }, + Balance { + owner: Address::ZERO, + amount: U256::from(200), + }, + ] + } + + // Accept array + pub fn process_array(&self, data: [U256; 5]) -> U256 { + data.iter().sum() + } + + // Accept vector and struct + pub fn batch_transfer(&mut self, recipients: Vec) -> U256 { + recipients.iter().map(|b| b.amount).sum() + } +} +``` + +### Example 2: Nested Data Structures + +```rust +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] +extern crate alloc; + +use alloc::string::String; +use alloy_primitives::{Address, U256}; +use alloy_sol_types::sol; +use stylus_sdk::prelude::*; + +sol! { + #[derive(Debug, AbiType)] + struct Dog { + string name; + string breed; + } + + #[derive(Debug, AbiType)] + struct User { + address account; + string name; + Dog[] dogs; + } +} + +sol_storage! { + #[entrypoint] + pub struct NestedExample {} +} + +#[public] +impl NestedExample { + pub fn create_user(&self) -> User { + User { + account: Address::ZERO, + name: "Alice".to_string(), + dogs: vec![ + Dog { + name: "Rex".to_string(), + breed: "Labrador".to_string(), + }, + Dog { + name: "Max".to_string(), + breed: "Beagle".to_string(), + }, + ], + } + } + + pub fn get_dog_names(&self, user: User) -> Vec { + user.dogs.into_iter().map(|dog| dog.name).collect() + } + + pub fn count_dogs(&self, users: Vec) -> U256 { + let total: usize = users.iter().map(|u| u.dogs.len()).sum(); + U256::from(total) + } +} +``` + +## Best Practices + +### 1. Choose the Right Type + +```rust +// Use tuples for simple groupings +pub fn get_basics(&self) -> (U256, Address, bool) { /* ... */ } + +// Use structs for complex data with named fields +sol! { + #[derive(Debug, AbiType)] + struct UserProfile { + address account; + string name; + uint256 balance; + bool active; + } +} + +// Use arrays for fixed-size collections +pub fn get_top_five(&self) -> [U256; 5] { /* ... */ } + +// Use vectors for dynamic collections +pub fn get_all_users(&self) -> Vec
{ /* ... */ } +``` + +### 2. Memory Efficiency + +```rust +use alloy_primitives::U256; + +// Prefer fixed arrays when size is known +pub fn fixed_data(&self) -> [U256; 10] { + // More gas-efficient + [U256::ZERO; 10] +} + +// Use vectors only when size varies +pub fn dynamic_data(&self, count: usize) -> Vec { + vec![U256::ZERO; count] +} +``` + +### 3. Struct Naming + +```rust +use alloy_sol_types::sol; + +sol! { + // Good: Clear, descriptive names + #[derive(Debug, AbiType)] + struct TokenMetadata { + string name; + string symbol; + uint8 decimals; + } + + // Avoid: Ambiguous names + #[derive(Debug, AbiType)] + struct Data { + uint256 x; + uint256 y; + } +} +``` + +### 4. Vector vs Array + +```rust +use alloy_primitives::{Address, U256}; + +// Use fixed arrays for known sizes +pub fn get_admins(&self) -> [Address; 3] { + // Three admin addresses + [Address::ZERO; 3] +} + +// Use vectors for variable sizes +pub fn get_users(&self) -> Vec
{ + // Unknown number of users + vec![] +} +``` + +### 5. Nested Structures + +```rust +use alloy_sol_types::sol; + +sol! { + // Good: Reasonable nesting depth + #[derive(Debug, AbiType)] + struct User { + address account; + Profile profile; + } + + #[derive(Debug, AbiType)] + struct Profile { + string name; + uint256 age; + } + + // Avoid: Excessive nesting (gas inefficient) + #[derive(Debug, AbiType)] + struct DeepNesting { + Level1 l1; + } + + #[derive(Debug, AbiType)] + struct Level1 { + Level2 l2; + } + + #[derive(Debug, AbiType)] + struct Level2 { + Level3 l3; + } + + #[derive(Debug, AbiType)] + struct Level3 { + uint256 value; + } +} +``` + +## Type Conversion and Helpers + +### Converting Between Types + +```rust +use alloy_primitives::{U256, Bytes}; + +// Vec to Bytes +let vec: Vec = vec![1, 2, 3]; +let bytes = Bytes::from(vec); + +// Bytes to Vec +let bytes = Bytes::from(vec![1, 2, 3]); +let vec: Vec = bytes.to_vec(); + +// Array to Vec +let arr: [U256; 3] = [U256::from(1), U256::from(2), U256::from(3)]; +let vec: Vec = arr.to_vec(); + +// Vec to array (if size matches) +let vec = vec![U256::from(1), U256::from(2), U256::from(3)]; +let arr: [U256; 3] = vec.try_into().unwrap(); +``` + +### Working with Iterators + +```rust +use alloy_primitives::U256; + +// Map over vector +let numbers = vec![U256::from(1), U256::from(2), U256::from(3)]; +let doubled: Vec = numbers.iter().map(|n| n * U256::from(2)).collect(); + +// Filter vector +let evens: Vec = numbers.into_iter().filter(|n| n.byte(0) % 2 == 0).collect(); + +// Fold/reduce +let sum = numbers.iter().fold(U256::ZERO, |acc, n| acc + n); +``` + +## Common Patterns + +### Batch Operations + +```rust +use alloy_primitives::{Address, U256}; +use alloy_sol_types::sol; +use stylus_sdk::prelude::*; + +sol! { + #[derive(Debug, AbiType)] + struct Transfer { + address to; + uint256 amount; + } +} + +#[public] +impl MyContract { + pub fn batch_transfer(&mut self, transfers: Vec) -> U256 { + let mut total = U256::ZERO; + for transfer in transfers { + // Process each transfer + total += transfer.amount; + } + total + } +} +``` + +### Pagination + +```rust +use alloy_primitives::U256; +use stylus_sdk::prelude::*; + +#[public] +impl MyContract { + pub fn get_page(&self, items: Vec, page: usize, size: usize) -> Vec { + let start = page * size; + let end = start + size; + items.get(start..end.min(items.len())) + .unwrap_or(&[]) + .to_vec() + } +} +``` + +## See Also + +- [Primitives](./primitives.mdx) - Basic types (bool, integers, address, strings) +- [Storage Types](./storage.mdx) - Persistent storage for compound types +- [Type Conversions](./conversions-between-types.mdx) - Converting between types From 69b669f8e94d25f4730998eeca9aed434bc2c368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Fri, 21 Nov 2025 13:11:37 -0800 Subject: [PATCH 20/28] add slop compound types article --- docs/stylus/reference/data-types/compound-types.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/stylus/reference/data-types/compound-types.mdx b/docs/stylus/reference/data-types/compound-types.mdx index d418445953..4de7cedb30 100644 --- a/docs/stylus/reference/data-types/compound-types.mdx +++ b/docs/stylus/reference/data-types/compound-types.mdx @@ -8,8 +8,6 @@ target_audience: Developers using the Stylus Rust SDK to write and deploy smart displayed_sidebar: buildAppsSidebar --- -# Compound Types - Compound types allow you to group multiple values together in Stylus contracts. The SDK provides full support for tuples, structs, arrays, and vectors with automatic ABI encoding/decoding and Solidity type mappings. ## Tuples From a34e1dc8256b16335ee5e440df671f42371b3145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Fri, 21 Nov 2025 13:16:42 -0800 Subject: [PATCH 21/28] Document Stylus Rust storage types with comprehensive examples Add detailed documentation for all Stylus SDK storage types including: - Storage overview and EVM State Trie interoperability - Storage primitives (StorageBool, StorageUint, StorageAddress, FixedBytes) - Integer update operations (wrapping and checked) - Storage collections (StorageVec, StorageArray, StorageMap) - StorageString and StorageBytes for dynamic data - Storage structs with nested structures - Complete type mapping tables - Storage patterns (initialization, counter, access control, registry) - Best practices for storage operations - Storage slots and layout explanation - Complete working token example Each section includes: - Practical code examples from SDK examples - Method signatures and usage patterns - Solidity equivalents and mappings - Gas optimization guidance - Common design patterns - Best practices for efficient storage use --- docs/stylus/reference/data-types/storage.mdx | 1103 +++++++++++++++++- 1 file changed, 1091 insertions(+), 12 deletions(-) diff --git a/docs/stylus/reference/data-types/storage.mdx b/docs/stylus/reference/data-types/storage.mdx index 8f49eab97c..da71a86abb 100644 --- a/docs/stylus/reference/data-types/storage.mdx +++ b/docs/stylus/reference/data-types/storage.mdx @@ -1,20 +1,1099 @@ --- title: 'Stylus Rust SDK storage' -description: 'Stylus Rust SDK storage' +description: 'Stylus Rust SDK storage types' author: chrisco sme: chrisco -sidebar_position: 1 +sidebar_position: 3 target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts. displayed_sidebar: buildAppsSidebar --- -3. Storage - 1. Bool - 2. Uint - 3. Address - 4. Struct - 5. Array - 6. Map - 7. String - 8. Bytes - 9. FixedBytes +# Storage + +Persistent storage in Stylus contracts provides access to the EVM State Trie, the same key-value storage used by Solidity contracts. The SDK provides type-safe storage access through dedicated storage types that prevent aliasing errors at compile time using Rust's borrow checker. + +## Overview + +Stylus contracts share the same persistent storage as Solidity contracts: + +- Both Stylus and Solidity access the **same EVM State Trie** +- Storage is **fully interoperable** between Stylus and Solidity contracts +- Stylus provides **compile-time safety** through Rust's type system +- Storage operations are **cached** for gas efficiency + +### Storage Declaration + +Use the `sol_storage!` macro to define contract storage with Solidity-compatible layout: + +```rust +use stylus_sdk::prelude::*; + +sol_storage! { + #[entrypoint] + pub struct MyContract { + uint256 count; + address owner; + bool initialized; + } +} +``` + +Alternatively, use the `#[storage]` attribute for Rust-style declarations: + +```rust +use stylus_sdk::prelude::*; +use stylus_sdk::storage::*; + +#[storage] +#[entrypoint] +pub struct MyContract { + count: StorageU256, + owner: StorageAddress, + initialized: StorageBool, +} +``` + +## Storage Primitives + +Storage primitives are persistent versions of basic types. + +### Boolean Storage (`StorageBool`) + +Store boolean values in persistent storage: + +```rust +use stylus_sdk::prelude::*; + +sol_storage! { + #[entrypoint] + pub struct Contract { + bool is_initialized; + bool is_paused; + } +} + +#[public] +impl Contract { + pub fn initialize(&mut self) { + self.is_initialized.set(true); + } + + pub fn is_initialized(&self) -> bool { + self.is_initialized.get() + } + + pub fn toggle_pause(&mut self) { + let current = self.is_paused.get(); + self.is_paused.set(!current); + } +} +``` + +### Integer Storage + +Store unsigned and signed integers with various bit sizes: + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::U256; + +sol_storage! { + #[entrypoint] + pub struct Counter { + uint256 count; + uint64 timestamp; + int256 balance; + } +} + +#[public] +impl Counter { + // Unsigned integer operations + pub fn increment(&mut self) { + let current = self.count.get(); + self.count.set(current + U256::from(1)); + } + + pub fn add(&mut self, value: U256) { + let current = self.count.get(); + self.count.set(current + value); + } + + pub fn get_count(&self) -> U256 { + self.count.get() + } + + // Timestamp storage + pub fn set_timestamp(&mut self, ts: u64) { + self.timestamp.set(ts); + } +} +``` + +#### Available Storage Integer Types + +| Storage Type | Primitive Type | Bit Size | Solidity Type | +| ------------- | -------------- | -------- | ------------- | +| `StorageU8` | `U8` | 8 bits | `uint8` | +| `StorageU16` | `U16` | 16 bits | `uint16` | +| `StorageU32` | `U32` | 32 bits | `uint32` | +| `StorageU64` | `U64` | 64 bits | `uint64` | +| `StorageU128` | `U128` | 128 bits | `uint128` | +| `StorageU256` | `U256` | 256 bits | `uint256` | +| `StorageI8` | `I8` | 8 bits | `int8` | +| `StorageI16` | `I16` | 16 bits | `int16` | +| `StorageI32` | `I32` | 32 bits | `int32` | +| `StorageI64` | `I64` | 64 bits | `int64` | +| `StorageI128` | `I128` | 128 bits | `int128` | +| `StorageI256` | `I256` | 256 bits | `int256` | + +#### Integer Update Operations + +`StorageUint` types provide convenient update methods: + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::U256; + +sol_storage! { + #[entrypoint] + pub struct Contract { + uint256 balance; + } +} + +#[public] +impl Contract { + // Wrapping operations (overflow wraps around) + pub fn add_wrapping(&mut self, value: U256) -> U256 { + self.balance.update_wrap_add(value) + } + + pub fn sub_wrapping(&mut self, value: U256) -> U256 { + self.balance.update_wrap_sub(value) + } + + pub fn mul_wrapping(&mut self, value: U256) -> U256 { + self.balance.update_wrap_mul(value) + } + + // Checked operations (return None on overflow) + pub fn add_checked(&mut self, value: U256) -> Option { + self.balance.update_check_add(value) + } + + pub fn sub_checked(&mut self, value: U256) -> Option { + self.balance.update_check_sub(value) + } +} +``` + +### Address Storage (`StorageAddress`) + +Store Ethereum addresses: + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::Address; + +sol_storage! { + #[entrypoint] + pub struct Ownership { + address owner; + address pending_owner; + } +} + +#[public] +impl Ownership { + pub fn get_owner(&self) -> Address { + self.owner.get() + } + + pub fn transfer_ownership(&mut self, new_owner: Address) { + // Validate address + if new_owner == Address::ZERO { + // Handle error + return; + } + + let current_owner = self.owner.get(); + if self.vm().msg_sender() != current_owner { + // Not authorized + return; + } + + self.pending_owner.set(new_owner); + } + + pub fn accept_ownership(&mut self) { + let caller = self.vm().msg_sender(); + if caller != self.pending_owner.get() { + return; + } + + self.owner.set(caller); + self.pending_owner.set(Address::ZERO); + } +} +``` + +### Fixed Bytes Storage + +Store fixed-size byte arrays: + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::FixedBytes; + +sol_storage! { + #[entrypoint] + pub struct Hashes { + bytes32 merkle_root; + bytes32 commitment; + bytes4 selector; + } +} + +#[public] +impl Hashes { + pub fn set_merkle_root(&mut self, root: FixedBytes<32>) { + self.merkle_root.set(root); + } + + pub fn get_merkle_root(&self) -> FixedBytes<32> { + self.merkle_root.get() + } + + pub fn verify_hash(&self, proof: FixedBytes<32>) -> bool { + self.merkle_root.get() == proof + } +} +``` + +#### Available Fixed Bytes Storage Types + +| Storage Type | Bytes | Bits | Solidity Type | +| ------------- | ----- | -------- | ------------- | +| `StorageB8` | 1 | 8 bits | `bytes1` | +| `StorageB16` | 2 | 16 bits | `bytes2` | +| `StorageB32` | 4 | 32 bits | `bytes4` | +| `StorageB64` | 8 | 64 bits | `bytes8` | +| `StorageB128` | 16 | 128 bits | `bytes16` | +| `StorageB160` | 20 | 160 bits | `bytes20` | +| `StorageB224` | 28 | 224 bits | `bytes28` | +| `StorageB256` | 32 | 256 bits | `bytes32` | + +## Storage Collections + +Storage collections provide persistent arrays, vectors, and maps. + +### StorageVec (Dynamic Array) + +Dynamic arrays that can grow and shrink: + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::{Address, U256}; + +sol_storage! { + #[entrypoint] + pub struct TokenList { + address[] holders; + uint256[] balances; + } +} + +#[public] +impl TokenList { + // Add element + pub fn add_holder(&mut self, holder: Address) { + self.holders.push(holder); + } + + // Get element + pub fn get_holder(&self, index: U256) -> Address { + self.holders.get(index).unwrap() + } + + // Get length + pub fn holder_count(&self) -> U256 { + U256::from(self.holders.len()) + } + + // Set element + pub fn set_balance(&mut self, index: U256, balance: U256) { + self.balances.setter(index).unwrap().set(balance); + } + + // Iterate over elements + pub fn total_balance(&self) -> U256 { + let mut total = U256::ZERO; + for i in 0..self.balances.len() { + total += self.balances.get(U256::from(i)).unwrap(); + } + total + } + + // Remove element (erase to zero) + pub fn remove_holder(&mut self, index: U256) { + self.holders.setter(index).unwrap().erase(); + } + + // Clear all elements + pub fn clear_holders(&mut self) { + self.holders.erase(); + } +} +``` + +#### StorageVec Methods + +```rust +// Length operations +fn len(&self) -> usize +fn is_empty(&self) -> bool + +// Access operations +fn get(&self, index: impl TryInto) -> Option +fn getter(&self, index: impl TryInto) -> Option> +fn setter(&mut self, index: impl TryInto) -> Option> + +// Mutation operations +fn push(&mut self, value: T) +fn grow(&mut self) -> StorageGuardMut<'_, T> // Add new element and return mutable reference +fn erase(&mut self) // Clear all elements +``` + +### StorageArray (Fixed Array) + +Fixed-size arrays with compile-time known length: + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::U256; + +sol_storage! { + #[entrypoint] + pub struct FixedData { + uint256[10] values; + address[5] admins; + } +} + +#[public] +impl FixedData { + // Get element + pub fn get_value(&self, index: U256) -> U256 { + self.values.get(index).unwrap() + } + + // Set element + pub fn set_value(&mut self, index: U256, value: U256) { + self.values.setter(index).unwrap().set(value); + } + + // Get array length (compile-time constant) + pub fn array_size(&self) -> U256 { + U256::from(self.values.len()) + } + + // Iterate over array + pub fn sum_values(&self) -> U256 { + let mut sum = U256::ZERO; + for i in 0..self.values.len() { + sum += self.values.get(U256::from(i)).unwrap(); + } + sum + } +} +``` + +### StorageMap (Mapping) + +Key-value storage, equivalent to Solidity `mapping`: + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::{Address, U256}; + +sol_storage! { + #[entrypoint] + pub struct Token { + mapping(address => uint256) balances; + mapping(address => mapping(address => uint256)) allowances; + } +} + +#[public] +impl Token { + // Get value (returns zero if not set) + pub fn balance_of(&self, account: Address) -> U256 { + self.balances.get(account) + } + + // Set value + pub fn set_balance(&mut self, account: Address, amount: U256) { + self.balances.setter(account).set(amount); + } + + // Insert value (same as set) + pub fn mint(&mut self, account: Address, amount: U256) { + let current = self.balances.get(account); + self.balances.insert(account, current + amount); + } + + // Delete value (reset to zero) + pub fn burn(&mut self, account: Address, amount: U256) { + let current = self.balances.get(account); + if current >= amount { + self.balances.setter(account).set(current - amount); + } + } + + // Nested mapping + pub fn allowance(&self, owner: Address, spender: Address) -> U256 { + self.allowances.get(owner).get(spender) + } + + pub fn approve(&mut self, spender: Address, amount: U256) { + let owner = self.vm().msg_sender(); + self.allowances + .setter(owner) + .setter(spender) + .set(amount); + } +} +``` + +#### StorageMap Methods + +```rust +// Read operations +fn get(&self, key: K) -> V // Returns zero-value if not present +fn getter(&self, key: K) -> StorageGuard<'_, V> + +// Write operations +fn setter(&mut self, key: K) -> StorageGuardMut<'_, V> +fn insert(&mut self, key: K, value: V) +fn replace(&mut self, key: K, value: V) -> V // Returns old value +fn take(&mut self, key: K) -> V // Returns value and deletes +fn delete(&mut self, key: K) // Erases entry +``` + +#### Supported Map Key Types + +Any type implementing `StorageKey` can be used as a map key: + +- `Address` +- `U256`, `U160`, and other `Uint` types +- `FixedBytes` +- `Signed` types +- `bool` + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::{Address, U256, FixedBytes}; + +sol_storage! { + #[entrypoint] + pub struct MultiMap { + mapping(address => uint256) by_address; + mapping(uint256 => address) by_id; + mapping(bytes32 => bool) by_hash; + mapping(bool => uint256) by_flag; + } +} +``` + +### StorageString and StorageBytes + +Dynamic string and bytes storage: + +```rust +use stylus_sdk::prelude::*; +use alloc::string::String; + +sol_storage! { + #[entrypoint] + pub struct Metadata { + string name; + string symbol; + bytes data; + } +} + +#[public] +impl Metadata { + // String operations + pub fn get_name(&self) -> String { + self.name.get_string() + } + + pub fn set_name(&mut self, name: String) { + self.name.set_str(name); + } + + pub fn name_length(&self) -> usize { + self.name.len() + } + + pub fn clear_name(&mut self) { + self.name.erase(); + } + + // Bytes operations + pub fn get_data(&self) -> Vec { + self.data.get_bytes() + } + + pub fn set_data(&mut self, data: Vec) { + self.data.set_bytes(data); + } + + pub fn data_length(&self) -> usize { + self.data.len() + } +} +``` + +## Storage Structs + +Define custom storage types with nested structures: + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::{Address, U256}; + +// Storage struct definition +#[storage] +pub struct UserInfo { + balance: StorageU256, + is_active: StorageBool, + timestamp: StorageU64, +} + +sol_storage! { + #[entrypoint] + pub struct UserRegistry { + mapping(address => UserInfo) users; + uint256 total_users; + } +} + +#[public] +impl UserRegistry { + pub fn register_user(&mut self, user: Address) { + let mut user_info = self.users.setter(user); + user_info.balance.set(U256::ZERO); + user_info.is_active.set(true); + user_info.timestamp.set(self.vm().block_timestamp()); + + let count = self.total_users.get(); + self.total_users.set(count + U256::from(1)); + } + + pub fn get_balance(&self, user: Address) -> U256 { + self.users.get(user).balance.get() + } + + pub fn update_balance(&mut self, user: Address, amount: U256) { + self.users.setter(user).balance.set(amount); + } + + pub fn is_active(&self, user: Address) -> bool { + self.users.get(user).is_active.get() + } +} +``` + +### Nested Storage Structs + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::Address; + +#[storage] +pub struct Dog { + name: StorageString, + breed: StorageString, +} + +#[storage] +pub struct User { + name: StorageString, + dogs: StorageVec, +} + +sol_storage! { + #[entrypoint] + pub struct Registry { + mapping(address => User) users; + } +} + +#[public] +impl Registry { + pub fn add_dog(&mut self, owner: Address, name: String, breed: String) { + let mut user = self.users.setter(owner); + let mut dog = user.dogs.grow(); + dog.name.set_str(name); + dog.breed.set_str(breed); + } + + pub fn get_dog_count(&self, owner: Address) -> usize { + self.users.get(owner).dogs.len() + } + + pub fn get_dog_name(&self, owner: Address, index: usize) -> String { + self.users + .get(owner) + .dogs + .get(index) + .unwrap() + .name + .get_string() + } +} +``` + +## Storage Patterns + +### Initialization Pattern + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::{Address, U256}; + +sol_storage! { + #[entrypoint] + pub struct Contract { + bool initialized; + address owner; + uint256 value; + } +} + +#[public] +impl Contract { + #[constructor] + pub fn constructor(&mut self, initial_value: U256) { + self.owner.set(self.vm().msg_sender()); + self.value.set(initial_value); + self.initialized.set(true); + } + + fn only_initialized(&self) { + if !self.initialized.get() { + // Revert: not initialized + } + } + + pub fn get_value(&self) -> U256 { + self.only_initialized(); + self.value.get() + } +} +``` + +### Counter Pattern + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::U256; + +sol_storage! { + #[entrypoint] + pub struct Counter { + uint256 count; + mapping(address => uint256) user_counts; + } +} + +#[public] +impl Counter { + pub fn increment(&mut self) { + let current = self.count.get(); + self.count.set(current + U256::from(1)); + } + + pub fn increment_by(&mut self, amount: U256) { + let current = self.count.get(); + self.count.set(current + amount); + } + + pub fn increment_user(&mut self) { + let user = self.vm().msg_sender(); + let current = self.user_counts.get(user); + self.user_counts.insert(user, current + U256::from(1)); + } + + pub fn get_count(&self) -> U256 { + self.count.get() + } + + pub fn get_user_count(&self, user: Address) -> U256 { + self.user_counts.get(user) + } +} +``` + +### Access Control Pattern + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::Address; + +sol_storage! { + #[entrypoint] + pub struct AccessControl { + address owner; + mapping(address => bool) admins; + mapping(address => bool) users; + } +} + +#[public] +impl AccessControl { + #[constructor] + pub fn constructor(&mut self) { + let sender = self.vm().msg_sender(); + self.owner.set(sender); + self.admins.insert(sender, true); + } + + fn only_owner(&self) { + if self.vm().msg_sender() != self.owner.get() { + // Revert: not owner + } + } + + fn only_admin(&self) { + let sender = self.vm().msg_sender(); + if !self.admins.get(sender) { + // Revert: not admin + } + } + + pub fn add_admin(&mut self, admin: Address) { + self.only_owner(); + self.admins.insert(admin, true); + } + + pub fn remove_admin(&mut self, admin: Address) { + self.only_owner(); + self.admins.delete(admin); + } + + pub fn add_user(&mut self, user: Address) { + self.only_admin(); + self.users.insert(user, true); + } + + pub fn is_admin(&self, account: Address) -> bool { + self.admins.get(account) + } + + pub fn is_user(&self, account: Address) -> bool { + self.users.get(account) + } +} +``` + +### Registry Pattern + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::{Address, U256}; + +#[storage] +pub struct Record { + owner: StorageAddress, + created_at: StorageU64, + updated_at: StorageU64, + active: StorageBool, +} + +sol_storage! { + #[entrypoint] + pub struct Registry { + mapping(bytes32 => Record) records; + mapping(address => bytes32[]) user_records; + uint256 total_records; + } +} + +#[public] +impl Registry { + pub fn create_record(&mut self, id: FixedBytes<32>) { + let now = self.vm().block_timestamp(); + let owner = self.vm().msg_sender(); + + let mut record = self.records.setter(id); + record.owner.set(owner); + record.created_at.set(now); + record.updated_at.set(now); + record.active.set(true); + + // Add to user's record list + self.user_records.setter(owner).push(id); + + // Increment total + let total = self.total_records.get(); + self.total_records.set(total + U256::from(1)); + } + + pub fn get_record_owner(&self, id: FixedBytes<32>) -> Address { + self.records.get(id).owner.get() + } + + pub fn is_active(&self, id: FixedBytes<32>) -> bool { + self.records.get(id).active.get() + } + + pub fn deactivate(&mut self, id: FixedBytes<32>) { + let owner = self.records.get(id).owner.get(); + if owner != self.vm().msg_sender() { + // Not authorized + return; + } + + self.records.setter(id).active.set(false); + } +} +``` + +## Best Practices + +### 1. Use Appropriate Storage Types + +```rust +// Good: Use StorageU256 for counters +sol_storage! { + pub struct Counter { + uint256 count; + } +} + +// Good: Use StorageMap for lookups +sol_storage! { + pub struct Balances { + mapping(address => uint256) balances; + } +} + +// Good: Use StorageVec for dynamic lists +sol_storage! { + pub struct Users { + address[] user_list; + } +} +``` + +### 2. Minimize Storage Operations + +```rust +// Bad: Multiple storage reads +pub fn bad_example(&self) -> U256 { + let a = self.value.get(); + let b = self.value.get(); // Unnecessary read + a + b +} + +// Good: Single storage read +pub fn good_example(&self) -> U256 { + let value = self.value.get(); + value + value +} +``` + +### 3. Use Batch Operations + +```rust +// Good: Batch updates in a single transaction +pub fn update_multiple(&mut self, values: Vec) { + for (i, value) in values.iter().enumerate() { + self.data.setter(U256::from(i)).unwrap().set(*value); + } +} +``` + +### 4. Check Before Delete + +```rust +// Good: Verify before deletion +pub fn remove_user(&mut self, user: Address) { + if self.users.get(user) { + self.users.delete(user); + // Update related storage + } +} +``` + +### 5. Use Erase for Gas Refunds + +```rust +// Good: Clear storage for gas refunds +pub fn clear_data(&mut self) { + self.data.erase(); // Refunds gas +} +``` + +## Storage Slots and Layout + +Stylus uses the same storage layout as Solidity: + +- Each storage slot is **32 bytes** (256 bits) +- Variables are **packed** when possible to save space +- Arrays and mappings use **computed slots** via hashing + +```rust +sol_storage! { + pub struct Packed { + uint128 a; // Slot 0 (first 16 bytes) + uint128 b; // Slot 0 (last 16 bytes) + uint256 c; // Slot 1 (full 32 bytes) + bool d; // Slot 2 (1 byte) + address e; // Slot 2 (20 bytes, packed with d) + } +} +``` + +### Custom Storage Slots + +You can specify custom storage slots for specific use cases: + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::U256; + +#[storage] +#[entrypoint] +pub struct CustomSlots { + // Default slot allocation + value: StorageU256, + + // Custom slot (advanced usage) + // Note: Requires manual slot management +} +``` + +## Complete Example + +Here's a comprehensive example demonstrating various storage types: + +```rust +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] +extern crate alloc; + +use alloc::string::String; +use alloy_primitives::{Address, FixedBytes, U256}; +use stylus_sdk::prelude::*; + +#[storage] +pub struct TokenMetadata { + name: StorageString, + symbol: StorageString, + decimals: StorageU8, +} + +sol_storage! { + #[entrypoint] + pub struct Token { + // Primitives + uint256 total_supply; + bool paused; + address owner; + + // Collections + mapping(address => uint256) balances; + mapping(address => mapping(address => uint256)) allowances; + address[] holders; + + // Nested struct + TokenMetadata metadata; + } +} + +#[public] +impl Token { + #[constructor] + pub fn constructor(&mut self, name: String, symbol: String) { + self.owner.set(self.vm().msg_sender()); + self.metadata.name.set_str(name); + self.metadata.symbol.set_str(symbol); + self.metadata.decimals.set(18); + self.paused.set(false); + } + + pub fn total_supply(&self) -> U256 { + self.total_supply.get() + } + + pub fn balance_of(&self, account: Address) -> U256 { + self.balances.get(account) + } + + pub fn transfer(&mut self, to: Address, amount: U256) -> bool { + if self.paused.get() { + return false; + } + + let from = self.vm().msg_sender(); + let from_balance = self.balances.get(from); + + if from_balance < amount { + return false; + } + + self.balances.insert(from, from_balance - amount); + + let to_balance = self.balances.get(to); + self.balances.insert(to, to_balance + amount); + + true + } + + pub fn approve(&mut self, spender: Address, amount: U256) -> bool { + let owner = self.vm().msg_sender(); + self.allowances.setter(owner).insert(spender, amount); + true + } + + pub fn allowance(&self, owner: Address, spender: Address) -> U256 { + self.allowances.get(owner).get(spender) + } + + pub fn pause(&mut self) { + if self.vm().msg_sender() != self.owner.get() { + return; + } + self.paused.set(true); + } + + pub fn unpause(&mut self) { + if self.vm().msg_sender() != self.owner.get() { + return; + } + self.paused.set(false); + } + + pub fn name(&self) -> String { + self.metadata.name.get_string() + } + + pub fn symbol(&self) -> String { + self.metadata.symbol.get_string() + } + + pub fn decimals(&self) -> u8 { + self.metadata.decimals.get() + } +} +``` + +## See Also + +- [Primitives](./primitives.mdx) - Basic types used in storage +- [Compound Types](./compound-types.mdx) - Complex types in storage +- [Type Conversions](./conversions-between-types.mdx) - Converting between types From 29555c6e29a3445e3645b5dc6f4d8aa0675ac0e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Fri, 21 Nov 2025 13:22:26 -0800 Subject: [PATCH 22/28] fix slop storage article --- docs/stylus/reference/data-types/storage.mdx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/stylus/reference/data-types/storage.mdx b/docs/stylus/reference/data-types/storage.mdx index da71a86abb..82461b39fa 100644 --- a/docs/stylus/reference/data-types/storage.mdx +++ b/docs/stylus/reference/data-types/storage.mdx @@ -8,18 +8,16 @@ target_audience: Developers using the Stylus Rust SDK to write and deploy smart displayed_sidebar: buildAppsSidebar --- -# Storage - Persistent storage in Stylus contracts provides access to the EVM State Trie, the same key-value storage used by Solidity contracts. The SDK provides type-safe storage access through dedicated storage types that prevent aliasing errors at compile time using Rust's borrow checker. ## Overview Stylus contracts share the same persistent storage as Solidity contracts: -- Both Stylus and Solidity access the **same EVM State Trie** -- Storage is **fully interoperable** between Stylus and Solidity contracts -- Stylus provides **compile-time safety** through Rust's type system -- Storage operations are **cached** for gas efficiency +- Both Stylus and Solidity access the same EVM State Trie +- Storage is fully interoperable between Stylus and Solidity contracts +- Stylus provides compile-time safety through Rust's type system +- Storage operations are cached for gas efficiency ### Storage Declaration From 43251884e6037867a1fc55bb34b8b9e2289e9296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Fri, 21 Nov 2025 13:31:02 -0800 Subject: [PATCH 23/28] Document Stylus Rust contracts Add comprehensive documentation for Stylus contracts covering: - Contract basics and minimal contract structure - Storage definition with sol_storage! and #[storage] - #[entrypoint] macro and TopLevelStorage trait - #[public] macro for exposing methods - State mutability inference (view, write, pure) - Constructor with #[constructor] attribute - Method attributes: #[payable], #[receive], #[fallback], #[selector] - Trait-based contract composition with #[implements] - Accessing VM context via self.vm() - Events definition and emission with sol! macro - External contract calls with sol_interface! - Call configurations (view, mutating, payable, gas) - Low-level calls (call, static_call, RawCall) - Error handling with SolidityError - Complete ERC-20 example demonstrating all features - Best practices for state mutability, validation, errors, events, access control, gas optimization, CEI pattern, and type-safe interfaces All code examples are based on actual SDK implementation from stylus-sdk-rs repository. --- docs/stylus/reference/contracts.mdx | 1122 ++++++++++++++++++++++++++- 1 file changed, 1107 insertions(+), 15 deletions(-) diff --git a/docs/stylus/reference/contracts.mdx b/docs/stylus/reference/contracts.mdx index 771b25cea6..9348a417e1 100644 --- a/docs/stylus/reference/contracts.mdx +++ b/docs/stylus/reference/contracts.mdx @@ -1,22 +1,1114 @@ --- -title: 'Contracts' -description: 'Contracts' +title: 'Stylus contracts' +description: 'Stylus Rust SDK contracts, methods, and lifecycle' author: chrisco sme: chrisco -sidebar_position: 1 +sidebar_position: 2 target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts. displayed_sidebar: buildAppsSidebar --- -6. Contracts - 1. Calling other contracts - 1. Call - 2. Raw call - 3. Delegate call - 4. Static call - 5. sol_interface - 2. Sending ETH - 3. Factory deploy - 4. Function modifiers - 5. Inheritance (temporary) - 6. Constructors (coming soon) +The Stylus SDK provides a complete framework for writing smart contracts in Rust that are fully compatible with Solidity contracts on Arbitrum chains. Contracts written with Stylus compile to WebAssembly and share the same EVM state trie as Solidity contracts, enabling seamless interoperability. + +## Contract Basics + +A Stylus contract consists of three main components: + +1. **Storage Definition**: Defines the contract's persistent state +2. **Entrypoint**: Marks the main contract struct that handles incoming calls +3. **Public Methods**: Functions exposed to external callers via the `#[public]` macro + +### Minimal Contract + +Here's the simplest possible Stylus contract: + +```rust +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] +extern crate alloc; + +use stylus_sdk::prelude::*; + +#[storage] +#[entrypoint] +pub struct HelloWorld; + +#[public] +impl HelloWorld { + fn user_main(_input: Vec) -> ArbResult { + Ok(Vec::new()) + } +} +``` + +This contract: + +- Uses `#[storage]` to define the contract struct (empty in this case) +- Uses `#[entrypoint]` to mark it as the contract's entry point +- Uses `#[public]` to expose the `user_main` function +- Returns `ArbResult`, which is `Result, Vec>` + +## Storage Definition + +Stylus contracts use the `sol_storage!` macro or `#[storage]` attribute to define persistent storage that maps directly to Solidity storage slots. + +### Using `sol_storage!` (Solidity-style) + +The `sol_storage!` macro lets you define storage using Solidity syntax: + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::{Address, U256}; + +sol_storage! { + #[entrypoint] + pub struct Counter { + uint256 count; + address owner; + mapping(address => uint256) balances; + } +} +``` + +This creates a contract with: + +- A `count` field of type `StorageU256` +- An `owner` field of type `StorageAddress` +- A `balances` mapping from `Address` to `StorageU256` + +### Using `#[storage]` (Rust-style) + +Alternatively, use the `#[storage]` attribute with explicit storage types: + +```rust +use stylus_sdk::prelude::*; +use stylus_sdk::storage::{StorageU256, StorageAddress, StorageMap}; +use alloy_primitives::{Address, U256}; + +#[storage] +#[entrypoint] +pub struct Counter { + count: StorageU256, + owner: StorageAddress, + balances: StorageMap, +} +``` + +Both approaches produce identical storage layouts and are fully interoperable with Solidity contracts using the same storage structure. + +## The `#[entrypoint]` Macro + +The `#[entrypoint]` macro marks a struct as the contract's main entry point. It automatically implements the `TopLevelStorage` trait, which enables: + +- Routing incoming calls to public methods +- Managing contract storage +- Handling reentrancy protection (unless the `reentrant` feature is enabled) + +**Key requirements:** + +- Exactly one struct per contract must have `#[entrypoint]` +- The struct must also have `#[storage]` or be defined in `sol_storage!` +- The entrypoint struct represents the contract's root storage + +**Example:** + +```rust +sol_storage! { + #[entrypoint] + pub struct MyContract { + uint256 value; + } +} +``` + +The `#[entrypoint]` macro generates: + +1. An implementation of `TopLevelStorage` for the struct +2. A `user_entrypoint` function that Stylus calls when the contract receives a transaction +3. Method routing logic to dispatch calls to `#[public]` methods + +## Public Methods with `#[public]` + +The `#[public]` macro exposes Rust methods as external contract functions callable from Solidity, other Stylus contracts, or external callers. + +### Basic Public Methods + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::U256; + +sol_storage! { + #[entrypoint] + pub struct Calculator { + uint256 result; + } +} + +#[public] +impl Calculator { + // View function (read-only) + pub fn get_result(&self) -> U256 { + self.result.get() + } + + // Write function (mutates state) + pub fn set_result(&mut self, value: U256) { + self.result.set(value); + } + + // Pure function (no state access) + pub fn add(a: U256, b: U256) -> U256 { + a + b + } +} +``` + +### State Mutability + +The SDK automatically infers state mutability from the method signature: + +| Signature | Mutability | Solidity Equivalent | Description | +| ----------- | ---------- | ------------------- | --------------------- | +| `&self` | `view` | `view` | Read contract state | +| `&mut self` | Write | (default) | Modify contract state | +| Neither | `pure` | `pure` | No state access | + +**Examples:** + +```rust +#[public] +impl MyContract { + // View: can read state, cannot modify + pub fn balance_of(&self, account: Address) -> U256 { + self.balances.get(account) + } + + // Write: can read and modify state + pub fn transfer(&mut self, to: Address, amount: U256) { + let sender = self.vm().msg_sender(); + let balance = self.balances.get(sender); + self.balances.setter(sender).set(balance - amount); + self.balances.setter(to).set(self.balances.get(to) + amount); + } + + // Pure: no state access at all + pub fn calculate_fee(amount: U256) -> U256 { + amount * U256::from(3) / U256::from(100) + } +} +``` + +## Constructor + +The `#[constructor]` attribute marks a function that runs once during contract deployment. + +### Basic Constructor + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::{Address, U256}; + +sol_storage! { + #[entrypoint] + pub struct Token { + address owner; + uint256 total_supply; + } +} + +#[public] +impl Token { + #[constructor] + pub fn constructor(&mut self, initial_supply: U256) { + let deployer = self.vm().msg_sender(); + self.owner.set(deployer); + self.total_supply.set(initial_supply); + } + + pub fn owner(&self) -> Address { + self.owner.get() + } +} +``` + +### Constructor Features + +**Payable Constructor:** + +```rust +#[public] +impl Token { + #[constructor] + #[payable] + pub fn constructor(&mut self, initial_supply: U256) { + // Contract can receive ETH during deployment + let received = self.vm().msg_value(); + self.owner.set(self.vm().msg_sender()); + self.total_supply.set(initial_supply); + } +} +``` + +**Important Notes:** + +- The constructor name can be anything (doesn't have to be `constructor`) +- Only one constructor per contract +- Constructor runs exactly once when the contract is deployed +- Use `tx_origin()` instead of `msg_sender()` when deploying via a factory contract + +## Method Attributes + +### `#[payable]` + +Marks a function as able to receive ETH: + +```rust +#[public] +impl PaymentProcessor { + #[payable] + pub fn deposit(&mut self) -> U256 { + let sender = self.vm().msg_sender(); + let amount = self.vm().msg_value(); + + let current = self.balances.get(sender); + self.balances.setter(sender).set(current + amount); + + amount + } + + // Non-payable function will revert if ETH is sent + pub fn withdraw(&mut self, amount: U256) { + // Will revert if msg.value > 0 + let sender = self.vm().msg_sender(); + let balance = self.balances.get(sender); + self.balances.setter(sender).set(balance - amount); + } +} +``` + +**Important:** Without `#[payable]`, sending ETH to a function causes a revert. + +### `#[receive]` + +Handles plain ETH transfers without calldata (equivalent to Solidity's `receive()` function): + +```rust +use alloy_sol_types::sol; + +sol! { + event EtherReceived(address indexed sender, uint256 amount); +} + +#[public] +impl Wallet { + #[receive] + #[payable] + pub fn receive(&mut self) -> Result<(), Vec> { + let sender = self.vm().msg_sender(); + let amount = self.vm().msg_value(); + + let balance = self.balances.get(sender); + self.balances.setter(sender).set(balance + amount); + + self.vm().log(EtherReceived { sender, amount }); + Ok(()) + } +} +``` + +**Notes:** + +- Must be combined with `#[payable]` +- Called when the contract receives ETH without calldata +- Only one `#[receive]` function per contract +- Must have signature: `fn name(&mut self) -> Result<(), Vec>` + +### `#[fallback]` + +Handles calls to non-existent functions or as a fallback for ETH transfers: + +```rust +use alloy_sol_types::sol; + +sol! { + event FallbackCalled(address indexed sender, bytes4 selector, uint256 value); +} + +#[public] +impl Contract { + #[fallback] + #[payable] + pub fn fallback(&mut self, calldata: &[u8]) -> ArbResult { + let sender = self.vm().msg_sender(); + let value = self.vm().msg_value(); + + // Extract function selector if present + let selector = if calldata.len() >= 4 { + [calldata[0], calldata[1], calldata[2], calldata[3]] + } else { + [0; 4] + }; + + self.vm().log(FallbackCalled { + sender, + selector: selector.into(), + value, + }); + + Ok(vec![]) + } +} +``` + +**Fallback is called when:** + +1. A function call doesn't match any existing function signature +2. Plain ETH transfer when no `#[receive]` function exists +3. The contract receives calldata but no function matches + +**Notes:** + +- Must have signature: `fn name(&mut self, calldata: &[u8]) -> ArbResult` +- Can optionally include `#[payable]` to accept ETH +- Only one `#[fallback]` function per contract + +### `#[selector]` + +Customizes the Solidity function selector: + +```rust +#[public] +impl Token { + // Use a custom name in the ABI + #[selector(name = "balanceOf")] + pub fn get_balance(&self, account: Address) -> U256 { + self.balances.get(account) + } + + // Explicitly set the 4-byte selector + #[selector(bytes = "0x70a08231")] + pub fn balance_of_custom(&self, account: Address) -> U256 { + self.balances.get(account) + } +} +``` + +This is useful for: + +- Matching existing Solidity interfaces exactly +- Avoiding naming conflicts +- Implementing multiple methods with the same name but different selectors + +## Contract Composition and Inheritance + +Stylus supports two patterns for code reuse: trait-based composition (new, preferred) and struct inheritance (legacy). + +### Trait-Based Composition (Preferred) + +Define reusable functionality as traits and implement them on your contract: + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::{Address, U256}; + +// Define interface traits +#[public] +trait IOwnable { + fn owner(&self) -> Address; + fn transfer_ownership(&mut self, new_owner: Address) -> bool; +} + +#[public] +trait IErc20 { + fn name(&self) -> String; + fn symbol(&self) -> String; + fn balance_of(&self, account: Address) -> U256; + fn transfer(&mut self, to: Address, value: U256) -> bool; +} + +// Define storage components +#[storage] +struct Ownable { + owner: StorageAddress, +} + +#[storage] +struct Erc20 { + balances: StorageMap, +} + +// Compose into main contract +#[storage] +#[entrypoint] +struct MyToken { + ownable: Ownable, + erc20: Erc20, +} + +// Declare which interfaces this contract implements +#[public] +#[implements(IOwnable, IErc20)] +impl MyToken {} + +// Implement each trait +#[public] +impl IOwnable for MyToken { + fn owner(&self) -> Address { + self.ownable.owner.get() + } + + fn transfer_ownership(&mut self, new_owner: Address) -> bool { + let caller = self.vm().msg_sender(); + if caller != self.ownable.owner.get() { + return false; + } + self.ownable.owner.set(new_owner); + true + } +} + +#[public] +impl IErc20 for MyToken { + fn name(&self) -> String { + "MyToken".into() + } + + fn symbol(&self) -> String { + "MTK".into() + } + + fn balance_of(&self, account: Address) -> U256 { + self.erc20.balances.get(account) + } + + fn transfer(&mut self, to: Address, value: U256) -> bool { + let from = self.vm().msg_sender(); + let from_balance = self.erc20.balances.get(from); + if from_balance < value { + return false; + } + self.erc20.balances.setter(from).set(from_balance - value); + let to_balance = self.erc20.balances.get(to); + self.erc20.balances.setter(to).set(to_balance + value); + true + } +} +``` + +**Benefits:** + +- Clear separation of concerns +- Explicit interface declarations +- Type-safe composition +- Easy to test components independently +- Compatible with Solidity interface standards + +### Accessing VM Context + +All public methods can access blockchain context via `self.vm()`: + +```rust +#[public] +impl MyContract { + pub fn get_caller_info(&self) -> (Address, U256, U256) { + let vm = self.vm(); + ( + vm.msg_sender(), // Caller's address + vm.msg_value(), // ETH sent with call + vm.block_number(), // Current block number + ) + } +} +``` + +See the [Global Variables and Functions](./global-variables-and-functions.mdx) documentation for a complete list of available VM methods. + +## Events + +Events allow contracts to log data to the blockchain, enabling off-chain monitoring and indexing. + +### Defining Events + +Use the `sol!` macro to define events with Solidity-compatible signatures: + +```rust +use alloy_sol_types::sol; +use alloy_primitives::{Address, U256}; + +sol! { + // Up to 3 parameters can be indexed + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + event DataUpdated(string indexed key, bytes data); +} +``` + +**Indexed parameters:** + +- Allow filtering events by that parameter +- Limited to 3 indexed parameters per event +- Indexed parameters are stored in log topics, not data + +### Emitting Events + +Use `self.vm().log()` to emit events: + +```rust +#[public] +impl Token { + pub fn transfer(&mut self, to: Address, value: U256) -> bool { + let from = self.vm().msg_sender(); + + // Transfer logic... + let from_balance = self.balances.get(from); + if from_balance < value { + return false; + } + self.balances.setter(from).set(from_balance - value); + self.balances.setter(to).set(self.balances.get(to) + value); + + // Emit event + self.vm().log(Transfer { from, to, value }); + + true + } +} +``` + +### Raw Log Emission + +For advanced use cases, emit raw logs directly: + +```rust +use alloy_primitives::FixedBytes; + +#[public] +impl Contract { + pub fn emit_raw_log(&self) { + let user = Address::from([0x22; 20]); + let balance = U256::from(1000); + + // Topics (up to 4, must be FixedBytes<32>) + let topics = &[user.into_word()]; + + // Data (arbitrary bytes) + let mut data: Vec = vec![]; + data.extend_from_slice(&balance.to_be_bytes::<32>()); + + self.vm().raw_log(topics, &data).unwrap(); + } +} +``` + +## External Contract Calls + +Stylus contracts can call other contracts (Solidity or Stylus) using typed interfaces or raw calls. + +### Defining Contract Interfaces + +Use `sol_interface!` to define interfaces for external contracts: + +```rust +use stylus_sdk::prelude::*; + +sol_interface! { + interface IToken { + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); + } + + interface IOracle { + function getPrice() external view returns (uint256); + } +} +``` + +### Calling External Contracts + +#### View Calls (Read-Only) + +```rust +use stylus_sdk::call::Call; + +#[public] +impl MyContract { + pub fn get_token_balance(&self, token: IToken, account: Address) -> U256 { + // Call::new() for view calls (no state mutation) + let config = Call::new(); + token.balance_of(self.vm(), config, account).unwrap() + } +} +``` + +#### Mutating Calls + +```rust +#[public] +impl MyContract { + pub fn transfer_tokens(&mut self, token: IToken, to: Address, amount: U256) -> bool { + // Call::new_mutating(self) for state-changing calls + let config = Call::new_mutating(self); + token.transfer(self.vm(), config, to, amount).unwrap() + } +} +``` + +#### Payable Calls + +```rust +#[public] +impl MyContract { + #[payable] + pub fn forward_payment(&mut self, recipient: IPaymentProcessor) -> Result<(), Vec> { + // Forward received ETH to another contract + let value = self.vm().msg_value(); + let config = Call::new_payable(self, value); + + recipient.process_payment(self.vm(), config)?; + Ok(()) + } +} +``` + +#### Configuring Gas + +```rust +#[public] +impl MyContract { + pub fn call_with_limited_gas(&mut self, token: IToken, to: Address) -> bool { + let config = Call::new_mutating(self) + .gas(self.vm().evm_gas_left() / 2); // Use half remaining gas + + token.transfer(self.vm(), config, to, U256::from(100)).unwrap() + } +} +``` + +### Low-Level Calls + +For maximum flexibility, use raw calls: + +```rust +use stylus_sdk::call::{call, static_call, RawCall}; + +#[public] +impl MyContract { + // Low-level call (state-changing) + pub fn execute_call(&mut self, target: Address, calldata: Vec) -> Result, Vec> { + let config = Call::new_mutating(self) + .gas(self.vm().evm_gas_left()); + + call(self.vm(), config, target, &calldata) + } + + // Static call (read-only) + pub fn execute_static_call(&self, target: Address, calldata: Vec) -> Result, Vec> { + static_call(self.vm(), Call::new(), target, &calldata) + } + + // Unsafe raw call with advanced options + pub fn execute_raw_call(&mut self, target: Address, calldata: Vec) -> Result, Vec> { + unsafe { + RawCall::new_delegate(self.vm()) + .gas(2100) + .limit_return_data(0, 32) + .flush_storage_cache() + .call(target, &calldata) + } + } +} +``` + +**Call Types:** + +- `call()`: State-changing call to another contract +- `static_call()`: Read-only call (equivalent to Solidity `staticcall`) +- `RawCall`: Low-level unsafe calls with fine-grained control + +## Error Handling + +Stylus contracts can define and return custom errors using Solidity-compatible error types. + +### Defining Errors + +```rust +use alloy_sol_types::sol; + +sol! { + error Unauthorized(); + error InsufficientBalance(address from, uint256 have, uint256 want); + error InvalidAddress(address addr); +} + +#[derive(SolidityError)] +pub enum TokenError { + Unauthorized(Unauthorized), + InsufficientBalance(InsufficientBalance), + InvalidAddress(InvalidAddress), +} +``` + +### Using Errors in Methods + +```rust +#[public] +impl Token { + pub fn transfer(&mut self, to: Address, amount: U256) -> Result { + let from = self.vm().msg_sender(); + + if to == Address::ZERO { + return Err(TokenError::InvalidAddress(InvalidAddress { addr: to })); + } + + let balance = self.balances.get(from); + if balance < amount { + return Err(TokenError::InsufficientBalance(InsufficientBalance { + from, + have: balance, + want: amount, + })); + } + + self.balances.setter(from).set(balance - amount); + self.balances.setter(to).set(self.balances.get(to) + amount); + + Ok(true) + } +} +``` + +**Error handling notes:** + +- Errors automatically encode as Solidity-compatible error data +- Use `Result` where `E` implements `SolidityError` +- Error data includes the error signature and parameters +- Compatible with Solidity `try/catch` blocks + +## Complete Example + +Here's a complete ERC-20-style token contract demonstrating all major features: + +```rust +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] +extern crate alloc; + +use alloy_primitives::{Address, U256}; +use alloy_sol_types::sol; +use stylus_sdk::prelude::*; + +// Define events +sol! { + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); +} + +// Define errors +sol! { + error InsufficientBalance(address from, uint256 have, uint256 want); + error InsufficientAllowance(address owner, address spender, uint256 have, uint256 want); + error Unauthorized(); +} + +#[derive(SolidityError)] +pub enum TokenError { + InsufficientBalance(InsufficientBalance), + InsufficientAllowance(InsufficientAllowance), + Unauthorized(Unauthorized), +} + +// Define storage +sol_storage! { + #[entrypoint] + pub struct SimpleToken { + mapping(address => uint256) balances; + mapping(address => mapping(address => uint256)) allowances; + uint256 total_supply; + address owner; + } +} + +#[public] +impl SimpleToken { + // Constructor + #[constructor] + pub fn constructor(&mut self, initial_supply: U256) { + let deployer = self.vm().msg_sender(); + self.owner.set(deployer); + self.balances.setter(deployer).set(initial_supply); + self.total_supply.set(initial_supply); + + self.vm().log(Transfer { + from: Address::ZERO, + to: deployer, + value: initial_supply, + }); + } + + // View functions + pub fn balance_of(&self, account: Address) -> U256 { + self.balances.get(account) + } + + pub fn allowance(&self, owner: Address, spender: Address) -> U256 { + self.allowances.getter(owner).get(spender) + } + + pub fn total_supply(&self) -> U256 { + self.total_supply.get() + } + + pub fn owner(&self) -> Address { + self.owner.get() + } + + // Write functions + pub fn transfer(&mut self, to: Address, value: U256) -> Result { + let from = self.vm().msg_sender(); + self._transfer(from, to, value)?; + Ok(true) + } + + pub fn approve(&mut self, spender: Address, value: U256) -> bool { + let owner = self.vm().msg_sender(); + self.allowances.setter(owner).setter(spender).set(value); + + self.vm().log(Approval { owner, spender, value }); + true + } + + pub fn transfer_from( + &mut self, + from: Address, + to: Address, + value: U256 + ) -> Result { + let spender = self.vm().msg_sender(); + + // Check allowance + let current_allowance = self.allowances.getter(from).get(spender); + if current_allowance < value { + return Err(TokenError::InsufficientAllowance(InsufficientAllowance { + owner: from, + spender, + have: current_allowance, + want: value, + })); + } + + // Update allowance + self.allowances.setter(from).setter(spender).set(current_allowance - value); + + // Transfer + self._transfer(from, to, value)?; + Ok(true) + } + + // Owner-only functions + pub fn mint(&mut self, to: Address, value: U256) -> Result<(), TokenError> { + if self.vm().msg_sender() != self.owner.get() { + return Err(TokenError::Unauthorized(Unauthorized {})); + } + + self.balances.setter(to).set(self.balances.get(to) + value); + self.total_supply.set(self.total_supply.get() + value); + + self.vm().log(Transfer { + from: Address::ZERO, + to, + value, + }); + + Ok(()) + } + + // Internal helper function + fn _transfer(&mut self, from: Address, to: Address, value: U256) -> Result<(), TokenError> { + let from_balance = self.balances.get(from); + if from_balance < value { + return Err(TokenError::InsufficientBalance(InsufficientBalance { + from, + have: from_balance, + want: value, + })); + } + + self.balances.setter(from).set(from_balance - value); + self.balances.setter(to).set(self.balances.get(to) + value); + + self.vm().log(Transfer { from, to, value }); + Ok(()) + } +} +``` + +## Best Practices + +### 1. Use Appropriate State Mutability + +```rust +// Good: Read-only functions use &self +pub fn get_balance(&self, account: Address) -> U256 { + self.balances.get(account) +} + +// Good: State-changing functions use &mut self +pub fn set_balance(&mut self, account: Address, balance: U256) { + self.balances.setter(account).set(balance); +} +``` + +### 2. Validate Inputs Early + +```rust +pub fn transfer(&mut self, to: Address, amount: U256) -> Result { + // Validate inputs first + if to == Address::ZERO { + return Err(TokenError::InvalidAddress(InvalidAddress { addr: to })); + } + + if amount == U256::ZERO { + return Ok(true); // Nothing to transfer + } + + // Then proceed with logic + let from = self.vm().msg_sender(); + // ... +} +``` + +### 3. Use Custom Errors + +```rust +// Good: Descriptive custom errors +pub fn withdraw(&mut self, amount: U256) -> Result<(), VaultError> { + let balance = self.balances.get(self.vm().msg_sender()); + if balance < amount { + return Err(VaultError::InsufficientBalance(InsufficientBalance { + have: balance, + want: amount, + })); + } + // ... +} + +// Avoid: Generic Vec errors +pub fn withdraw(&mut self, amount: U256) -> Result<(), Vec> { + // Less informative +} +``` + +### 4. Emit Events for State Changes + +```rust +pub fn update_value(&mut self, new_value: U256) { + let old_value = self.value.get(); + self.value.set(new_value); + + // Always emit events for important state changes + self.vm().log(ValueUpdated { + old_value, + new_value, + }); +} +``` + +### 5. Access Control Patterns + +```rust +// Good: Clear access control checks +pub fn admin_function(&mut self) -> Result<(), TokenError> { + if self.vm().msg_sender() != self.owner.get() { + return Err(TokenError::Unauthorized(Unauthorized {})); + } + // Admin logic... + Ok(()) +} + +// Consider: Reusable modifier-like helper +impl Token { + fn only_owner(&self) -> Result<(), TokenError> { + if self.vm().msg_sender() != self.owner.get() { + return Err(TokenError::Unauthorized(Unauthorized {})); + } + Ok(()) + } + + pub fn admin_function(&mut self) -> Result<(), TokenError> { + self.only_owner()?; + // Admin logic... + Ok(()) + } +} +``` + +### 6. Gas-Efficient Storage Access + +```rust +// Good: Read once, use multiple times +pub fn complex_calculation(&self, account: Address) -> U256 { + let balance = self.balances.get(account); // Read once + let result = balance * U256::from(2) + balance / U256::from(10); + result +} + +// Avoid: Multiple reads of same storage slot +pub fn inefficient_calculation(&self, account: Address) -> U256 { + self.balances.get(account) * U256::from(2) + self.balances.get(account) / U256::from(10) +} +``` + +### 7. Check Effects Interactions Pattern + +```rust +// Good: Check-Effects-Interactions pattern +pub fn withdraw(&mut self, amount: U256) -> Result<(), VaultError> { + let caller = self.vm().msg_sender(); + + // Checks + let balance = self.balances.get(caller); + if balance < amount { + return Err(VaultError::InsufficientBalance(InsufficientBalance { + have: balance, + want: amount, + })); + } + + // Effects (update state BEFORE external calls) + self.balances.setter(caller).set(balance - amount); + + // Interactions (external calls last) + // self.transfer_eth(caller, amount)?; + + Ok(()) +} +``` + +### 8. Use Type-Safe Interfaces for External Calls + +```rust +// Good: Use sol_interface! for type safety +sol_interface! { + interface IToken { + function transfer(address to, uint256 amount) external returns (bool); + } +} + +pub fn call_token(&mut self, token: IToken, to: Address, amount: U256) -> bool { + let config = Call::new_mutating(self); + token.transfer(self.vm(), config, to, amount).unwrap() +} + +// Avoid: Raw calls unless necessary +pub fn raw_call(&mut self, token: Address, to: Address, amount: U256) -> Vec { + // Less type-safe, more error-prone + let config = Call::new_mutating(self); + let calldata = /* manually construct */; + call(self.vm(), config, token, &calldata).unwrap() +} +``` + +## See Also + +- [Primitives](./data-types/primitives.mdx) - Basic data types +- [Compound Types](./data-types/compound-types.mdx) - Arrays, structs, tuples +- [Storage Types](./data-types/storage.mdx) - Persistent storage +- [Global Variables and Functions](./global-variables-and-functions.mdx) - VM context methods From c3627884b6fba8532546701df25abe196189c2a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Fri, 21 Nov 2025 14:08:31 -0800 Subject: [PATCH 24/28] touch up on slop contracts article --- docs/stylus/reference/contracts.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stylus/reference/contracts.mdx b/docs/stylus/reference/contracts.mdx index 9348a417e1..3a3bc982f1 100644 --- a/docs/stylus/reference/contracts.mdx +++ b/docs/stylus/reference/contracts.mdx @@ -8,7 +8,7 @@ target_audience: Developers using the Stylus Rust SDK to write and deploy smart displayed_sidebar: buildAppsSidebar --- -The Stylus SDK provides a complete framework for writing smart contracts in Rust that are fully compatible with Solidity contracts on Arbitrum chains. Contracts written with Stylus compile to WebAssembly and share the same EVM state trie as Solidity contracts, enabling seamless interoperability. +Stylus smart contracts are fully compatible with Solidity contracts on Arbitrum chains. They compile to WebAssembly and share the same EVM state trie as Solidity contracts, enabling seamless interoperability. ## Contract Basics From 83995ab61c8e9e4824ff423a1f3dc5dbca592a73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Fri, 21 Nov 2025 14:12:39 -0800 Subject: [PATCH 25/28] Document Stylus Rust global variables and functions Add comprehensive documentation for global variables and functions accessible via self.vm() in Stylus contracts: - Message context: msg_sender(), msg_value(), msg_reentrant() - Transaction context: tx_origin(), tx_gas_price(), tx_ink_price() - Block context: block_number(), block_timestamp(), block_basefee(), block_coinbase(), block_gas_limit() - Chain context: chain_id() - Account information: contract_address(), balance(), code(), code_size(), code_hash() - Gas and metering: evm_gas_left(), evm_ink_left(), ink_to_gas(), gas_to_ink() - Cryptographic functions: crypto::keccak(), native_keccak256() - Event logging: log(), raw_log() - Storage operations: storage_load_bytes32(), flush_cache() - Memory management: pay_for_memory_grow() Each section includes: - Method signatures - Solidity equivalents - Practical code examples - Important notes and caveats - Arbitrum-specific behavior details Complete example contract demonstrating usage of all major VM methods, plus a summary table of all available methods. All code examples based on actual SDK implementation from stylus-sdk-rs repository. --- .../global-variables-and-functions.mdx | 896 +++++++++++++++++- 1 file changed, 886 insertions(+), 10 deletions(-) diff --git a/docs/stylus/reference/global-variables-and-functions.mdx b/docs/stylus/reference/global-variables-and-functions.mdx index dd71e050db..4b45b9e45a 100644 --- a/docs/stylus/reference/global-variables-and-functions.mdx +++ b/docs/stylus/reference/global-variables-and-functions.mdx @@ -1,18 +1,894 @@ --- title: 'Global variables and functions' -description: 'Global variables and functions' +description: 'Stylus Rust SDK global variables and functions for blockchain context access' author: chrisco sme: chrisco -sidebar_position: 1 +sidebar_position: 3 target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts. displayed_sidebar: buildAppsSidebar --- -5. Global variables and functions - 1. block - 2. contract - 3. crypto - 4. evm - 5. msg - 6. tx - 7. debug +Stylus contracts access blockchain context and utilities through the VM (Virtual Machine) interface via `self.vm()`. This provides access to message context, block information, transaction details, account data, cryptographic functions, and gas metering. + +## Accessing the VM + +All public contract methods have access to the VM context through `self.vm()`: + +```rust +use stylus_sdk::prelude::*; +use alloy_primitives::{Address, U256}; + +#[public] +impl MyContract { + pub fn get_context_info(&self) -> (Address, U256, u64) { + let vm = self.vm(); + ( + vm.msg_sender(), // Caller's address + vm.msg_value(), // ETH sent with call + vm.block_number(), // Current block number + ) + } +} +``` + +The VM provides methods organized into several categories: + +## Message Context (`msg`) + +Methods for accessing information about the current call. + +### `msg_sender()` + +Gets the address of the account that called the program. + +```rust +pub fn msg_sender(&self) -> Address +``` + +**Equivalent to**: Solidity's `msg.sender` + +**Example:** + +```rust +#[public] +impl Token { + pub fn transfer(&mut self, to: Address, amount: U256) -> bool { + let from = self.vm().msg_sender(); + // Transfer from the caller to recipient + self._transfer(from, to, amount) + } +} +``` + +**Important Notes:** + +- For normal L2-to-L2 transactions, behaves like EVM's `CALLER` opcode +- For L1-to-L2 retryable ticket transactions, the top-level sender's address will be [aliased](https://developer.arbitrum.io/arbos/l1-to-l2-messaging#address-aliasing) +- In delegate calls, returns the original caller (not the delegating contract) + +### `msg_value()` + +Gets the ETH value in wei sent to the program. + +```rust +pub fn msg_value(&self) -> U256 +``` + +**Equivalent to**: Solidity's `msg.value` + +**Example:** + +```rust +#[public] +impl PaymentContract { + #[payable] + pub fn deposit(&mut self) -> U256 { + let amount = self.vm().msg_value(); + let sender = self.vm().msg_sender(); + + let balance = self.balances.get(sender); + self.balances.setter(sender).set(balance + amount); + + amount + } +} +``` + +**Note:** Only functions marked with `#[payable]` can receive ETH. Non-payable functions will revert if `msg_value() > 0`. + +### `msg_reentrant()` + +Checks whether the current call is reentrant. + +```rust +pub fn msg_reentrant(&self) -> bool +``` + +**Example:** + +```rust +#[public] +impl Vault { + pub fn withdraw(&mut self, amount: U256) { + if self.vm().msg_reentrant() { + // Handle reentrancy + panic!("Reentrant call detected"); + } + // Withdrawal logic... + } +} +``` + +**Note:** By default, Stylus contracts prevent reentrancy unless the `reentrant` feature is enabled. + +## Transaction Context (`tx`) + +Methods for accessing information about the current transaction. + +### `tx_origin()` + +Gets the top-level sender of the transaction. + +```rust +pub fn tx_origin(&self) -> Address +``` + +**Equivalent to**: Solidity's `tx.origin` + +**Example:** + +```rust +#[public] +impl Factory { + #[constructor] + pub fn constructor(&mut self) { + // Use tx_origin when deploying via a factory + let deployer = self.vm().tx_origin(); + self.owner.set(deployer); + } +} +``` + +**Important:** Returns the original EOA (Externally Owned Account) that initiated the transaction, even through multiple contract calls. + +### `tx_gas_price()` + +Gets the gas price in wei per gas, which on Arbitrum chains equals the basefee. + +```rust +pub fn tx_gas_price(&self) -> U256 +``` + +**Equivalent to**: Solidity's `tx.gasprice` + +**Example:** + +```rust +#[public] +impl Analytics { + pub fn record_gas_price(&mut self) { + let price = self.vm().tx_gas_price(); + self.gas_prices.push(price); + } +} +``` + +### `tx_ink_price()` + +Gets the price of ink in EVM gas basis points. + +```rust +pub fn tx_ink_price(&self) -> u32 +``` + +**Description:** Stylus uses "ink" as its unit of computation. This method returns the conversion rate from ink to gas. See [Ink and Gas](https://docs.arbitrum.io/stylus/concepts/gas-metering) for more information. + +**Example:** + +```rust +#[public] +impl Contract { + pub fn get_ink_price(&self) -> u32 { + self.vm().tx_ink_price() + } +} +``` + +## Block Context (`block`) + +Methods for accessing information about the current block. + +### `block_number()` + +Gets a bounded estimate of the L1 block number at which the Sequencer sequenced the transaction. + +```rust +pub fn block_number(&self) -> u64 +``` + +**Equivalent to**: Solidity's `block.number` + +**Example:** + +```rust +#[public] +impl TimeLock { + pub fn lock_until(&mut self, blocks: u64) { + let unlock_block = self.vm().block_number() + blocks; + self.unlock_block.set(U256::from(unlock_block)); + } + + pub fn can_unlock(&self) -> bool { + let current = self.vm().block_number(); + let unlock = self.unlock_block.get().try_into().unwrap_or(u64::MAX); + current >= unlock + } +} +``` + +**Note:** See [Block Numbers and Time](https://developer.arbitrum.io/time) for more information on how this value is determined on Arbitrum. + +### `block_timestamp()` + +Gets a bounded estimate of the Unix timestamp at which the Sequencer sequenced the transaction. + +```rust +pub fn block_timestamp(&self) -> u64 +``` + +**Equivalent to**: Solidity's `block.timestamp` + +**Example:** + +```rust +#[public] +impl Auction { + pub fn place_bid(&mut self, amount: U256) { + let now = self.vm().block_timestamp(); + let deadline = self.deadline.get().try_into().unwrap_or(0); + + if now > deadline { + panic!("Auction ended"); + } + + // Process bid... + } +} +``` + +**Note:** See [Block Numbers and Time](https://developer.arbitrum.io/time) for more information on how this value is determined on Arbitrum. + +### `block_basefee()` + +Gets the basefee of the current block. + +```rust +pub fn block_basefee(&self) -> U256 +``` + +**Equivalent to**: Solidity's `block.basefee` + +**Example:** + +```rust +#[public] +impl FeeTracker { + pub fn current_basefee(&self) -> U256 { + self.vm().block_basefee() + } +} +``` + +### `block_coinbase()` + +Gets the coinbase of the current block. + +```rust +pub fn block_coinbase(&self) -> Address +``` + +**Equivalent to**: Solidity's `block.coinbase` + +**Important:** On Arbitrum chains, this is the L1 batch poster's address, which differs from Ethereum where the validator determines the coinbase. + +**Example:** + +```rust +#[public] +impl Contract { + pub fn get_batch_poster(&self) -> Address { + self.vm().block_coinbase() + } +} +``` + +### `block_gas_limit()` + +Gets the gas limit of the current block. + +```rust +pub fn block_gas_limit(&self) -> u64 +``` + +**Equivalent to**: Solidity's `block.gaslimit` + +**Example:** + +```rust +#[public] +impl Contract { + pub fn check_gas_limit(&self) -> bool { + let limit = self.vm().block_gas_limit(); + limit > 30_000_000 + } +} +``` + +## Chain Context + +Methods for accessing chain-specific information. + +### `chain_id()` + +Gets the unique chain identifier of the Arbitrum chain. + +```rust +pub fn chain_id(&self) -> u64 +``` + +**Equivalent to**: Solidity's `block.chainid` + +**Example:** + +```rust +#[public] +impl MultiChain { + pub fn verify_chain(&self, expected_chain: u64) -> bool { + self.vm().chain_id() == expected_chain + } +} +``` + +**Common Arbitrum Chain IDs:** + +- Arbitrum One: 42161 +- Arbitrum Nova: 42170 +- Arbitrum Sepolia (testnet): 421614 + +## Account Information + +Methods for querying account details. + +### `contract_address()` + +Gets the address of the current program. + +```rust +pub fn contract_address(&self) -> Address +``` + +**Equivalent to**: Solidity's `address(this)` + +**Example:** + +```rust +#[public] +impl Contract { + pub fn this_address(&self) -> Address { + self.vm().contract_address() + } + + pub fn this_balance(&self) -> U256 { + let addr = self.vm().contract_address(); + self.vm().balance(addr) + } +} +``` + +### `balance(address)` + +Gets the ETH balance in wei of the account at the given address. + +```rust +pub fn balance(&self, account: Address) -> U256 +``` + +**Equivalent to**: Solidity's `address.balance` + +**Example:** + +```rust +#[public] +impl BalanceChecker { + pub fn get_balance(&self, account: Address) -> U256 { + self.vm().balance(account) + } + + pub fn has_sufficient_balance(&self, account: Address, required: U256) -> bool { + self.vm().balance(account) >= required + } +} +``` + +### `code(address)` + +Gets the code from the account at the given address. + +```rust +pub fn code(&self, account: Address) -> Vec +``` + +**Equivalent to**: Solidity's `address.code` (similar to `EXTCODECOPY` opcode) + +**Example:** + +```rust +#[public] +impl Contract { + pub fn is_contract(&self, account: Address) -> bool { + self.vm().code(account).len() > 0 + } +} +``` + +### `code_size(address)` + +Gets the size of the code in bytes at the given address. + +```rust +pub fn code_size(&self, account: Address) -> usize +``` + +**Equivalent to**: Solidity's `EXTCODESIZE` opcode + +**Example:** + +```rust +#[public] +impl Contract { + pub fn get_code_size(&self, account: Address) -> usize { + self.vm().code_size(account) + } +} +``` + +### `code_hash(address)` + +Gets the code hash of the account at the given address. + +```rust +pub fn code_hash(&self, account: Address) -> B256 +``` + +**Equivalent to**: Solidity's `EXTCODEHASH` opcode + +**Example:** + +```rust +#[public] +impl Contract { + pub fn verify_code(&self, account: Address, expected_hash: B256) -> bool { + self.vm().code_hash(account) == expected_hash + } +} +``` + +**Note:** The code hash of an account without code will be the empty hash: `keccak("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`. + +## Gas and Metering + +Methods for accessing gas and ink metering information. + +### `evm_gas_left()` + +Gets the amount of gas left after paying for the cost of this hostio. + +```rust +pub fn evm_gas_left(&self) -> u64 +``` + +**Equivalent to**: Solidity's `gasleft()` + +**Example:** + +```rust +use stylus_sdk::call::Call; + +#[public] +impl Contract { + pub fn complex_operation(&mut self, target: ITarget) { + let gas_before = self.vm().evm_gas_left(); + + // Use half the remaining gas for external call + let config = Call::new_mutating(self) + .gas(gas_before / 2); + + target.do_work(self.vm(), config); + } +} +``` + +### `evm_ink_left()` + +Gets the amount of ink remaining after paying for the cost of this hostio. + +```rust +pub fn evm_ink_left(&self) -> u64 +``` + +**Description:** Returns remaining computation units in "ink". See [Ink and Gas](https://docs.arbitrum.io/stylus/concepts/gas-metering) for more information on Stylus's compute pricing. + +**Example:** + +```rust +#[public] +impl Contract { + pub fn check_ink(&self) -> u64 { + self.vm().evm_ink_left() + } +} +``` + +### `ink_to_gas(ink)` + +Computes the units of gas per a specified amount of ink. + +```rust +pub fn ink_to_gas(&self, ink: u64) -> u64 +``` + +**Example:** + +```rust +#[public] +impl Contract { + pub fn convert_ink_to_gas(&self, ink: u64) -> u64 { + self.vm().ink_to_gas(ink) + } +} +``` + +### `gas_to_ink(gas)` + +Computes the units of ink per a specified amount of gas. + +```rust +pub fn gas_to_ink(&self, gas: u64) -> u64 +``` + +**Example:** + +```rust +#[public] +impl Contract { + pub fn convert_gas_to_ink(&self, gas: u64) -> u64 { + self.vm().gas_to_ink(gas) + } +} +``` + +## Cryptographic Functions + +The SDK provides cryptographic utilities through the `crypto` module and VM methods. + +### `keccak()` + +Efficiently computes the keccak256 hash of the given preimage. + +```rust +use stylus_sdk::crypto; + +pub fn keccak>(bytes: T) -> B256 +``` + +**Equivalent to**: Solidity's `keccak256()` + +**Example:** + +```rust +use stylus_sdk::crypto; +use alloy_primitives::{Address, FixedBytes, U256}; + +#[public] +impl Contract { + pub fn hash_data(&self, data: Vec) -> FixedBytes<32> { + crypto::keccak(data) + } + + pub fn verify_hash(&self, data: Vec, expected: FixedBytes<32>) -> bool { + crypto::keccak(data) == expected + } + + // Hash multiple values together + pub fn hash_packed(&self, addr: Address, amount: U256) -> FixedBytes<32> { + let packed = [ + addr.as_ref(), + &amount.to_be_bytes_vec(), + ].concat(); + crypto::keccak(packed) + } +} +``` + +### `native_keccak256()` + +VM method for computing keccak256 hash (alternative to `crypto::keccak`). + +```rust +pub fn native_keccak256(&self, input: &[u8]) -> B256 +``` + +**Example:** + +```rust +#[public] +impl Contract { + pub fn hash_via_vm(&self, data: Vec) -> B256 { + self.vm().native_keccak256(&data) + } +} +``` + +**Note:** `crypto::keccak()` is the recommended approach as it's more ergonomic. + +## Event Logging + +Methods for emitting events to the blockchain. + +### `log(event)` + +Emits a typed Solidity event. + +```rust +pub fn log(&self, event: T) +``` + +**Example:** + +```rust +use alloy_sol_types::sol; + +sol! { + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); +} + +#[public] +impl Token { + pub fn transfer(&mut self, to: Address, value: U256) -> bool { + let from = self.vm().msg_sender(); + + // Transfer logic... + self._transfer(from, to, value); + + // Emit event + self.vm().log(Transfer { from, to, value }); + + true + } +} +``` + +### `raw_log(topics, data)` + +Emits a raw log with custom topics and data. + +```rust +pub fn raw_log(&self, topics: &[B256], data: &[u8]) -> Result<(), &'static str> +``` + +**Example:** + +```rust +use alloy_primitives::{B256, FixedBytes}; + +#[public] +impl Contract { + pub fn emit_custom_log(&self) { + let topic = B256::from([1u8; 32]); + let topics = &[topic]; + let data = b"custom data"; + + self.vm().raw_log(topics, data).unwrap(); + } +} +``` + +**Note:** Maximum of 4 topics allowed. The first topic is typically the event signature hash. + +## Storage Operations + +Methods for interacting with contract storage. + +### `storage_load_bytes32(key)` + +Reads a 32-byte value from permanent storage. + +```rust +pub fn storage_load_bytes32(&self, key: U256) -> B256 +``` + +**Equivalent to**: Solidity's `SLOAD` opcode + +**Note:** Storage is cached for efficiency. Use the SDK's storage types instead of direct storage access. + +### `flush_cache(clear)` + +Persists dirty values in the storage cache to the EVM state trie. + +```rust +pub fn flush_cache(&self, clear: bool) +``` + +**Parameters:** + +- `clear`: If `true`, drops the cache entirely after flushing + +**Note:** Typically handled automatically by the SDK. Manual cache flushing is rarely needed. + +## Memory Management + +### `pay_for_memory_grow(pages)` + +Pays for memory growth in WASM pages. + +```rust +pub fn pay_for_memory_grow(&self, pages: u16) +``` + +**Note:** The `#[entrypoint]` macro handles this automatically. Manual calls are not recommended and will unproductively consume gas. + +## Complete Example + +Here's a comprehensive example using various global variables and functions: + +```rust +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] +extern crate alloc; + +use alloy_primitives::{Address, U256, FixedBytes}; +use alloy_sol_types::sol; +use stylus_sdk::{crypto, prelude::*}; + +sol! { + event Action( + address indexed caller, + uint256 value, + uint256 timestamp, + bytes32 data_hash + ); +} + +sol_storage! { + #[entrypoint] + pub struct ContextExample { + mapping(address => uint256) balances; + uint256 total_deposits; + uint256 creation_block; + address owner; + } +} + +#[public] +impl ContextExample { + #[constructor] + pub fn constructor(&mut self) { + // Use tx_origin for factory deployments + self.owner.set(self.vm().tx_origin()); + self.creation_block.set(U256::from(self.vm().block_number())); + } + + #[payable] + pub fn deposit(&mut self, data: Vec) { + // Message context + let caller = self.vm().msg_sender(); + let amount = self.vm().msg_value(); + + // Block context + let timestamp = self.vm().block_timestamp(); + let block_num = self.vm().block_number(); + + // Require minimum value + if amount < U256::from(1000) { + panic!("Insufficient deposit"); + } + + // Update balances + let balance = self.balances.get(caller); + self.balances.setter(caller).set(balance + amount); + self.total_deposits.set(self.total_deposits.get() + amount); + + // Hash the data + let data_hash = crypto::keccak(&data); + + // Emit event + self.vm().log(Action { + caller, + value: amount, + timestamp: U256::from(timestamp), + data_hash, + }); + } + + pub fn get_contract_info(&self) -> (Address, U256, u64, u64) { + ( + self.vm().contract_address(), // Contract's address + self.vm().balance(self.vm().contract_address()), // Contract's balance + self.vm().chain_id(), // Chain ID + self.vm().block_number(), // Current block + ) + } + + pub fn verify_signature(&self, message: Vec, expected_hash: FixedBytes<32>) -> bool { + let hash = crypto::keccak(message); + hash == expected_hash + } + + pub fn is_owner(&self, account: Address) -> bool { + account == self.owner.get() + } + + pub fn time_since_creation(&self) -> u64 { + let current_block = self.vm().block_number(); + let creation_block: u64 = self.creation_block.get().try_into().unwrap_or(0); + current_block.saturating_sub(creation_block) + } +} +``` + +## Summary of Available Methods + +### Message Context + +- `msg_sender()` → `Address` - Caller's address +- `msg_value()` → `U256` - ETH sent with call +- `msg_reentrant()` → `bool` - Is reentrant call + +### Transaction Context + +- `tx_origin()` → `Address` - Original transaction sender +- `tx_gas_price()` → `U256` - Gas price +- `tx_ink_price()` → `u32` - Ink price + +### Block Context + +- `block_number()` → `u64` - Block number +- `block_timestamp()` → `u64` - Block timestamp +- `block_basefee()` → `U256` - Base fee +- `block_coinbase()` → `Address` - Batch poster +- `block_gas_limit()` → `u64` - Gas limit + +### Chain Context + +- `chain_id()` → `u64` - Chain identifier + +### Account Information + +- `contract_address()` → `Address` - This contract's address +- `balance(Address)` → `U256` - Account balance +- `code(Address)` → `Vec` - Account code +- `code_size(Address)` → `usize` - Code size +- `code_hash(Address)` → `B256` - Code hash + +### Gas and Metering + +- `evm_gas_left()` → `u64` - Remaining gas +- `evm_ink_left()` → `u64` - Remaining ink +- `ink_to_gas(u64)` → `u64` - Convert ink to gas +- `gas_to_ink(u64)` → `u64` - Convert gas to ink + +### Cryptographic Functions + +- `crypto::keccak(bytes)` → `B256` - Keccak256 hash +- `native_keccak256(&[u8])` → `B256` - Keccak256 via VM + +### Event Logging + +- `log(event)` - Emit typed event +- `raw_log(&[B256], &[u8])` - Emit raw log + +## See Also + +- [Contracts](./contracts.mdx) - Contract structure and methods +- [Primitives](./data-types/primitives.mdx) - Basic data types +- [Storage Types](./data-types/storage.mdx) - Persistent storage From 36ae71af98ab15f3f020a22ab18364da40e46380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Fri, 21 Nov 2025 14:48:06 -0800 Subject: [PATCH 26/28] Document Stylus Rust minimal entrypoint contracts Add comprehensive documentation for minimal entrypoint contracts covering: - ArbResult type and its usage in contract methods - user_entrypoint function structure and generated code - Router trait implementation and routing logic - TopLevelStorage trait and safety requirements - Step-by-step guide to building contracts without macros - Function selector computation and implementation - Complete working minimal contract example - Comparison with high-level macro approach - Advanced use cases and debugging tips Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../advanced/minimal-entrypoint-contracts.mdx | 639 +++++++++++++++++- 1 file changed, 636 insertions(+), 3 deletions(-) diff --git a/docs/stylus/advanced/minimal-entrypoint-contracts.mdx b/docs/stylus/advanced/minimal-entrypoint-contracts.mdx index 211fa3acc5..e291439ec7 100644 --- a/docs/stylus/advanced/minimal-entrypoint-contracts.mdx +++ b/docs/stylus/advanced/minimal-entrypoint-contracts.mdx @@ -1,11 +1,644 @@ --- title: 'Minimal entrypoint contracts' -description: 'Minimal entrypoint contracts' +description: 'Understanding low-level Stylus contract entrypoints and the Router trait' author: chrisco sme: chrisco sidebar_position: 1 -target_audience: Developers who need to understand how to build a minimal entrypoint contract. +target_audience: Developers who need to understand how to build a minimal entrypoint contract without high-level macros. displayed_sidebar: buildAppsSidebar --- -TBD +This guide explains the low-level mechanics of Stylus contract entrypoints, helping you understand what happens behind the `#[entrypoint]` and `#[public]` macros. This knowledge is useful for advanced use cases, debugging, and building custom contract frameworks. + +## Overview + +A Stylus contract at its core consists of: + +1. **`user_entrypoint` function**: The WASM export that Stylus calls +2. **Router implementation**: Routes function selectors to method implementations +3. **TopLevelStorage trait**: Marks the contract's root storage type +4. **ArbResult type**: Represents success/failure with encoded return data + +## Understanding `ArbResult` + +`ArbResult` is the fundamental return type for Stylus contract methods: + +```rust +pub type ArbResult = Result, Vec>; +``` + +- `Ok(Vec)` - Success with ABI-encoded return data +- `Err(Vec)` - Revert with ABI-encoded error data + +**Example:** + +```rust +use stylus_sdk::ArbResult; + +// Success with no return data +fn no_return() -> ArbResult { + Ok(Vec::new()) +} + +// Success with encoded data +fn return_value() -> ArbResult { + let value: u32 = 42; + Ok(value.to_le_bytes().to_vec()) +} + +// Revert with error data +fn revert_with_error() -> ArbResult { + Err(b"InsufficientBalance".to_vec()) +} +``` + +## The `user_entrypoint` Function + +The `user_entrypoint` function is the WASM export that Stylus calls when a transaction invokes the contract. The `#[entrypoint]` macro generates this function automatically. + +### Generated Structure + +When you use `#[entrypoint]`, the macro generates: + +```rust +#[no_mangle] +pub extern "C" fn user_entrypoint(len: usize) -> usize { + let host = stylus_sdk::host::VM { + host: stylus_sdk::host::WasmVM{} + }; + + // Reentrancy check (unless reentrant feature enabled) + if host.msg_reentrant() { + return 1; // revert + } + + // Ensure pay_for_memory_grow is referenced + // (costs 8700 ink, less than 1 gas) + host.pay_for_memory_grow(0); + + // Read calldata + let input = host.read_args(len); + + // Call the router + let (data, status) = match router_entrypoint::(input, host.clone()) { + Ok(data) => (data, 0), // Success + Err(data) => (data, 1), // Revert + }; + + // Persist storage changes + host.flush_cache(false); + + // Write return data + host.write_result(&data); + + status +} +``` + +### Key Points + +- **Signature**: `extern "C" fn user_entrypoint(len: usize) -> usize` +- **Input**: `len` is the length of calldata to read +- **Output**: `0` for success, `1` for revert +- **Side effects**: Reads calldata, writes return data, flushes storage cache + +## The `Router` Trait + +The `Router` trait defines how function calls are dispatched to method implementations. + +### Trait Definition + +```rust +pub trait Router +where + S: TopLevelStorage + BorrowMut + ValueDenier, + I: ?Sized, +{ + type Storage; + + /// Route a function call by selector + fn route(storage: &mut S, selector: u32, input: &[u8]) -> Option; + + /// Handle receive (plain ETH transfers, no calldata) + fn receive(storage: &mut S) -> Option>>; + + /// Handle fallback (unknown selectors or no receive) + fn fallback(storage: &mut S, calldata: &[u8]) -> Option; + + /// Handle constructor + fn constructor(storage: &mut S, calldata: &[u8]) -> Option; +} +``` + +### Routing Logic + +The `router_entrypoint` function implements the routing logic: + +```rust +pub fn router_entrypoint(input: Vec, host: VM) -> ArbResult +where + R: Router, + S: StorageType + TopLevelStorage + BorrowMut + ValueDenier, +{ + let mut storage = unsafe { S::new(U256::ZERO, 0, host) }; + + // No calldata - try receive, then fallback + if input.is_empty() { + if let Some(res) = R::receive(&mut storage) { + return res.map(|_| Vec::new()); + } + if let Some(res) = R::fallback(&mut storage, &[]) { + return res; + } + return Err(Vec::new()); // No receive or fallback + } + + // Extract selector (first 4 bytes) + if input.len() >= 4 { + let selector = u32::from_be_bytes(input[..4].try_into().unwrap()); + + // Check for constructor + if selector == CONSTRUCTOR_SELECTOR { + if let Some(res) = R::constructor(&mut storage, &input[4..]) { + return res; + } + } + // Try to route to a method + else if let Some(res) = R::route(&mut storage, selector, &input[4..]) { + return res; + } + } + + // Try fallback + if let Some(res) = R::fallback(&mut storage, &input) { + return res; + } + + Err(Vec::new()) // Unknown selector and no fallback +} +``` + +## The `TopLevelStorage` Trait + +The `TopLevelStorage` trait marks types that represent the contract's root storage. + +### Trait Definition + +```rust +pub unsafe trait TopLevelStorage {} +``` + +### Purpose + +- Prevents storage aliasing during reentrancy +- Lifetime tracks all EVM state changes during contract invocation +- Must hold a reference when making external calls +- Automatically implemented by `#[entrypoint]` + +### Safety + +The trait is `unsafe` because: + +- Type must truly be top-level to prevent storage aliasing +- Incorrectly implementing this trait can lead to undefined behavior + +## Building a Minimal Contract + +Here's a minimal contract without using the high-level macros: + +### Step 1: Define Storage + +```rust +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] +extern crate alloc; + +use alloc::vec::Vec; +use stylus_sdk::{ + abi::{Router, ArbResult}, + storage::StorageType, + host::VM, + alloy_primitives::U256, +}; + +// Mark as top-level storage (normally done by #[entrypoint]) +pub struct MyContract; + +unsafe impl stylus_core::storage::TopLevelStorage for MyContract {} + +impl StorageType for MyContract { + type Wraps<'a> = &'a Self where Self: 'a; + type WrapsMut<'a> = &'a mut Self where Self: 'a; + + unsafe fn new(_slot: U256, _offset: u8, _host: VM) -> Self { + MyContract + } + + fn load<'s>(self) -> Self::Wraps<'s> { + &self + } + + fn load_mut<'s>(self) -> Self::WrapsMut<'s> { + &mut self + } +} +``` + +### Step 2: Implement Router + +```rust +impl Router for MyContract { + type Storage = MyContract; + + fn route(_storage: &mut MyContract, selector: u32, _input: &[u8]) -> Option { + // Simple example: one method with selector 0x12345678 + match selector { + 0x12345678 => Some(Ok(Vec::new())), + _ => None, // Unknown selector + } + } + + fn receive(_storage: &mut MyContract) -> Option>> { + None // No receive function + } + + fn fallback(_storage: &mut MyContract, _calldata: &[u8]) -> Option { + None // No fallback function + } + + fn constructor(_storage: &mut MyContract, _calldata: &[u8]) -> Option { + None // No constructor + } +} +``` + +### Step 3: Define Entrypoint + +```rust +#[no_mangle] +pub extern "C" fn user_entrypoint(len: usize) -> usize { + let host = VM { host: stylus_sdk::host::WasmVM{} }; + + // Reentrancy check + if host.msg_reentrant() { + return 1; + } + + // Reference pay_for_memory_grow + host.pay_for_memory_grow(0); + + // Read input + let input = host.read_args(len); + + // Route the call + let (data, status) = match stylus_sdk::abi::router_entrypoint::(input, host.clone()) { + Ok(data) => (data, 0), + Err(data) => (data, 1), + }; + + // Flush storage + host.flush_cache(false); + + // Write result + host.write_result(&data); + + status +} +``` + +## Function Selectors + +Function selectors are 4-byte identifiers computed from the function signature. + +### Computing Selectors + +```rust +use stylus_sdk::function_selector; + +// Manual computation +const MY_FUNCTION: [u8; 4] = function_selector!("myFunction"); + +// With parameters +const TRANSFER: [u8; 4] = function_selector!("transfer", Address, U256); + +// Constructor selector +const CONSTRUCTOR_SELECTOR: u32 = + u32::from_be_bytes(function_selector!("constructor")); +``` + +### Using in Router + +```rust +impl Router for MyContract { + type Storage = MyContract; + + fn route(_storage: &mut MyContract, selector: u32, input: &[u8]) -> Option { + const GET_VALUE: u32 = u32::from_be_bytes(function_selector!("getValue")); + const SET_VALUE: u32 = u32::from_be_bytes(function_selector!("setValue", U256)); + + match selector { + GET_VALUE => { + // Return encoded U256 value + let value = U256::from(42); + Some(Ok(value.to_be_bytes::<32>().to_vec())) + } + SET_VALUE => { + // Decode input and set value + if input.len() >= 32 { + // Process set_value logic + Some(Ok(Vec::new())) + } else { + Some(Err(Vec::new())) + } + } + _ => None, + } + } + + fn receive(_storage: &mut MyContract) -> Option>> { + None + } + + fn fallback(_storage: &mut MyContract, _calldata: &[u8]) -> Option { + None + } + + fn constructor(_storage: &mut MyContract, _calldata: &[u8]) -> Option { + None + } +} +``` + +## Implementing Special Functions + +### Receive Function + +Handles plain ETH transfers (no calldata): + +```rust +fn receive(storage: &mut MyContract) -> Option>> { + // Access msg_value via storage.vm().msg_value() + // Must return Ok(()) for success + Some(Ok(())) +} +``` + +### Fallback Function + +Handles unknown selectors or when no receive is defined: + +```rust +fn fallback(storage: &mut MyContract, calldata: &[u8]) -> Option { + // Can access full calldata + // Return Some to handle, None to revert + Some(Ok(Vec::new())) +} +``` + +### Constructor + +Called once during deployment with `CONSTRUCTOR_SELECTOR`: + +```rust +fn constructor(storage: &mut MyContract, calldata: &[u8]) -> Option { + // Initialize contract state + // calldata contains constructor parameters + Some(Ok(Vec::new())) +} +``` + +## Complete Minimal Example + +Here's a complete working minimal contract: + +```rust +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] +extern crate alloc; + +use alloc::vec::Vec; +use core::borrow::BorrowMut; +use stylus_sdk::{ + abi::Router, + alloy_primitives::U256, + host::VM, + storage::StorageType, + ArbResult, + function_selector, +}; +use stylus_core::{storage::TopLevelStorage, ValueDenier}; + +// Contract storage +pub struct MinimalContract; + +// Mark as top-level storage +unsafe impl TopLevelStorage for MinimalContract {} + +// Implement StorageType +impl StorageType for MinimalContract { + type Wraps<'a> = &'a Self where Self: 'a; + type WrapsMut<'a> = &'a mut Self where Self: 'a; + + unsafe fn new(_slot: U256, _offset: u8, _host: VM) -> Self { + MinimalContract + } + + fn load<'s>(self) -> Self::Wraps<'s> { + &self + } + + fn load_mut<'s>(self) -> Self::WrapsMut<'s> { + &mut self + } +} + +// Implement ValueDenier (for non-payable check) +impl ValueDenier for MinimalContract { + fn deny_value(&self, _method_name: &str) -> Result<(), Vec> { + Ok(()) // Allow all for simplicity + } +} + +// Implement BorrowMut +impl BorrowMut for MinimalContract { + fn borrow_mut(&mut self) -> &mut MinimalContract { + self + } +} + +// Implement Router +impl Router for MinimalContract { + type Storage = MinimalContract; + + fn route(_storage: &mut MinimalContract, selector: u32, _input: &[u8]) -> Option { + const HELLO: u32 = u32::from_be_bytes(function_selector!("hello")); + + match selector { + HELLO => Some(Ok(Vec::new())), + _ => None, + } + } + + fn receive(_storage: &mut MinimalContract) -> Option>> { + None + } + + fn fallback(_storage: &mut MinimalContract, _calldata: &[u8]) -> Option { + Some(Ok(Vec::new())) // Accept all unknown calls + } + + fn constructor(_storage: &mut MinimalContract, _calldata: &[u8]) -> Option { + Some(Ok(Vec::new())) + } +} + +// Define user_entrypoint +#[no_mangle] +pub extern "C" fn user_entrypoint(len: usize) -> usize { + let host = VM { host: stylus_sdk::host::WasmVM{} }; + + if host.msg_reentrant() { + return 1; + } + + host.pay_for_memory_grow(0); + + let input = host.read_args(len); + let (data, status) = match stylus_sdk::abi::router_entrypoint::(input, host.clone()) { + Ok(data) => (data, 0), + Err(data) => (data, 1), + }; + + host.flush_cache(false); + host.write_result(&data); + status +} +``` + +## Why Use High-Level Macros? + +While minimal contracts are educational, the `#[entrypoint]` and `#[public]` macros provide: + +1. **Automatic selector generation** from method names +2. **Type-safe parameter encoding/decoding** using Alloy types +3. **Solidity ABI export** for interoperability +4. **Storage trait implementations** with caching +5. **Error handling** with `Result` types +6. **Payable checks** for ETH-receiving functions +7. **Reentrancy protection** by default + +**Recommended approach:** + +```rust +// Use macros for production contracts +#[storage] +#[entrypoint] +pub struct MyContract { + value: StorageU256, +} + +#[public] +impl MyContract { + pub fn get_value(&self) -> U256 { + self.value.get() + } + + pub fn set_value(&mut self, value: U256) { + self.value.set(value); + } +} +``` + +This generates all the low-level code automatically while providing a clean, type-safe interface. + +## Advanced Use Cases + +### Custom Routing Logic + +Implement custom routing for multi-contract systems: + +```rust +impl Router for MultiContract { + type Storage = MultiContract; + + fn route(storage: &mut MultiContract, selector: u32, input: &[u8]) -> Option { + // Route to different modules based on selector range + match selector { + 0x00000000..=0x0fffffff => ModuleA::route(storage, selector, input), + 0x10000000..=0x1fffffff => ModuleB::route(storage, selector, input), + _ => None, + } + } + + // ... other methods +} +``` + +### Custom Entrypoint Logic + +Add custom logic before/after routing: + +```rust +#[no_mangle] +pub extern "C" fn user_entrypoint(len: usize) -> usize { + let host = VM { host: stylus_sdk::host::WasmVM{} }; + + // Custom pre-processing + let start_gas = host.evm_gas_left(); + + // Standard entrypoint logic + if host.msg_reentrant() { + return 1; + } + + host.pay_for_memory_grow(0); + let input = host.read_args(len); + + let (data, status) = match stylus_sdk::abi::router_entrypoint::(input, host.clone()) { + Ok(data) => (data, 0), + Err(data) => (data, 1), + }; + + // Custom post-processing + let gas_used = start_gas - host.evm_gas_left(); + // Log or handle gas usage + + host.flush_cache(false); + host.write_result(&data); + status +} +``` + +## Debugging Tips + +### Enable Debug Mode + +```rust +#[cfg(feature = "debug")] +use stylus_sdk::console; + +fn route(storage: &mut MyContract, selector: u32, input: &[u8]) -> Option { + #[cfg(feature = "debug")] + console!("Selector: {:08x}", selector); + + // Routing logic... +} +``` + +### Check Selector Computation + +```rust +#[test] +fn test_selectors() { + use stylus_sdk::function_selector; + + let hello = u32::from_be_bytes(function_selector!("hello")); + assert_eq!(hello, 0x19ff1d21); + + // Compare with Solidity: bytes4(keccak256("hello()")) +} +``` + +## See Also + +- [Contracts](../reference/contracts.mdx) - High-level contract development +- [Global Variables](../reference/global-variables-and-functions.mdx) - VM context methods +- [Storage Types](../reference/data-types/storage.mdx) - Persistent storage From 5c118b0e8a3b212e887365e3672bc610ada8349d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Fri, 21 Nov 2025 18:06:21 -0800 Subject: [PATCH 27/28] Document Stylus Rust contract activation Add comprehensive documentation for Stylus contract activation covering: - Two-step deployment and activation process - WASM compilation and processing pipeline - Deployment bytecode structure and EVM initcode - Activation fee calculation and configuration - ArbWasm precompile interface and functions - cargo-stylus CLI commands and workflows - Constructor support and arguments - Contract lifecycle and expiration management - Code reuse optimization via codehash - Multi-contract deployment patterns - Programmatic deployment examples - Troubleshooting common activation errors - Best practices for production deployment Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docs/stylus/concepts/activation.mdx | 783 +++++++++++++++++++++++++++- 1 file changed, 780 insertions(+), 3 deletions(-) diff --git a/docs/stylus/concepts/activation.mdx b/docs/stylus/concepts/activation.mdx index 14376d2def..dce723a561 100644 --- a/docs/stylus/concepts/activation.mdx +++ b/docs/stylus/concepts/activation.mdx @@ -1,11 +1,788 @@ --- title: 'Activation' -description: 'Activation' +description: 'Understanding Stylus contract deployment and activation' author: chrisco sme: chrisco sidebar_position: 1 -target_audience: . +target_audience: Developers deploying Stylus contracts to Arbitrum chains. displayed_sidebar: buildAppsSidebar --- -TBD +Stylus contracts undergo a two-step process to become executable on Arbitrum chains: **deployment** and **activation**. This guide explains both steps, the distinction between them, and how to manage the activation process. + +## Overview + +Unlike traditional EVM contracts that become immediately executable after deployment, Stylus contracts require an additional activation step: + +1. **Deployment**: Stores the compressed WASM bytecode on-chain at a contract address +2. **Activation**: Converts the bytecode into an executable Stylus program by registering it with the ArbWasm precompile + +**Why two steps?** + +- **Gas optimization**: Activation involves one-time processing and caching that would be expensive to repeat on every call +- **Code reuse**: Multiple contracts can share the same activated codehash, reducing activation costs +- **Version management**: Allows the chain to track which Stylus protocol version a contract targets + +## Deployment vs Activation + +| Aspect | Deployment | Activation | +| --------------------- | ------------------------------ | ------------------------------ | +| **Purpose** | Store compressed WASM on-chain | Register program with ArbWasm | +| **Transaction count** | 1 transaction | 1 transaction (separate) | +| **Cost type** | Standard EVM deployment gas | Data fee (WASM-specific cost) | +| **When required** | Always - stores the code | Always - makes code executable | +| **Reversible** | No | No (but can expire) | +| **Who can call** | Anyone with funds | Anyone (after deployment) | +| **Can be skipped** | No | No (unless already activated) | + +### Contract States + +A Stylus contract can be in one of these states: + +```rust +pub enum ContractStatus { + /// Contract already exists on-chain and is activated + Active { code: Vec }, + + /// Contract is deployed but not yet activated + /// Ready to activate with the given data fee + Ready { code: Vec, fee: U256 }, +} +``` + +## The Activation Process + +### Step 1: Build and Process WASM + +Before deployment, your Rust contract is compiled and processed: + +```bash +cargo stylus check +``` + +This performs: + +1. **Compile Rust to WASM**: Using `wasm32-unknown-unknown` target +2. **Process WASM binary**: + - Remove dangling references + - Add project hash metadata + - Strip unnecessary custom sections +3. **Brotli compression**: Maximum compression (level 11) +4. **Add EOF prefix**: `EFF00000` (identifies Stylus programs) +5. **Size validation**: Compressed code must be ≤ 24KB + +**WASM Processing Pipeline**: + +``` +Raw WASM Binary + ↓ +Remove dangling references + ↓ +Add project_hash metadata + ↓ +Strip user custom sections + ↓ +Brotli compress (level 11) + ↓ +Add EOF prefix (EFF00000) + ↓ +Final compressed code (≤ 24KB) +``` + +### Step 2: Deploy the Contract + +Deployment creates a transaction that stores your processed WASM on-chain: + +```bash +cargo stylus deploy \ + --private-key-path=key.txt \ + --endpoint="https://sepolia-rollup.arbitrum.io/rpc" +``` + +**What happens during deployment:** + +1. **Generate deployment bytecode**: Create EVM initcode with embedded compressed WASM +2. **Estimate gas**: Calculate deployment transaction gas cost +3. **Send deployment transaction**: To Stylus deployer contract +4. **Extract contract address**: From transaction receipt + +**Deployment Bytecode Structure**: + +``` +EVM Initcode Prelude (43 bytes): +┌─────────────────────────────────────┐ +│ 0x7f PUSH32 │ Push code length +│ 0x80 DUP1 │ Duplicate length +│ 0x60 PUSH1 │ Push prelude length +│ 0x60 PUSH1 0x00 │ Push 0 +│ 0x39 CODECOPY │ Copy code to memory +│ 0x60 PUSH1 0x00 │ Push 0 +│ 0xf3 RETURN │ Return code +│ 0x00 │ Stylus version +└─────────────────────────────────────┘ + ↓ + +``` + +### Step 3: Calculate Activation Fee + +Before activating, the data fee must be calculated: + +```rust +// Simulated via state overrides (no transaction sent) +let data_fee = calculate_activation_fee(contract_address); + +// Apply bump percentage for safety (default: 20%) +let final_fee = data_fee * (1 + bump_percent / 100); +``` + +**Data fee calculation**: + +- Uses state override simulation to estimate fee +- No actual transaction sent during estimation +- Configurable bump percentage protects against variance (default: 20%) +- Fee is paid in ETH when activating + +### Step 4: Activate the Contract + +Activation registers your contract with the ArbWasm precompile: + +```bash +# Automatic activation (default) +cargo stylus deploy --private-key-path=key.txt + +# Or manual activation +cargo stylus activate \ + --address=0x1234... \ + --private-key-path=key.txt +``` + +**What happens during activation:** + +1. **Call ArbWasm precompile**: At address `0x0000000000000000000000000000000000000071` +2. **Send activation transaction**: + ```solidity + ArbWasm.activateProgram{value: dataFee}(contractAddress) + ``` +3. **ArbWasm processes the code**: + - Validates WASM format + - Checks against protocol version + - Stores activation metadata + - Emits `ProgramActivated` event +4. **Returns activation info**: + ```solidity + returns (uint16 version, uint256 actualDataFee) + ``` + +## Using cargo-stylus + +The `cargo-stylus` CLI tool simplifies the deployment and activation workflow. + +### Basic Deployment (Automatic Activation) + +By default, `cargo stylus deploy` handles both steps: + +```bash +cargo stylus deploy \ + --private-key-path=wallet.txt \ + --endpoint="https://sepolia-rollup.arbitrum.io/rpc" +``` + +**Output**: + +``` +Building contract... +Compressing WASM... +Deploying contract to 0x1234567890abcdef... +Deployment transaction: 0xabcd... +Contract deployed at: 0x1234567890abcdef +Activating contract... +Activation transaction: 0xef12... +Contract activated successfully! +``` + +### Deploy Without Activation + +To deploy but skip activation: + +```bash +cargo stylus deploy \ + --private-key-path=wallet.txt \ + --no-activate +``` + +This is useful when: + +- You want to inspect the contract before activating +- Someone else will handle activation +- You're testing deployment workflows + +### Manual Activation + +Activate a previously deployed contract: + +```bash +cargo stylus activate \ + --address=0x1234567890abcdef \ + --private-key-path=wallet.txt \ + --endpoint="https://sepolia-rollup.arbitrum.io/rpc" +``` + +### Check Contract Status + +Before deploying, check if a contract with the same code is already activated: + +```bash +cargo stylus check \ + --endpoint="https://sepolia-rollup.arbitrum.io/rpc" +``` + +**Possible outcomes**: + +1. **Code already activated**: You can reuse the existing deployment +2. **Ready to activate**: Shows estimated data fee +3. **Validation errors**: Displays issues that must be fixed + +## Deployment with Constructors + +If your contract has a constructor, provide arguments during deployment: + +```rust +#[public] +impl MyContract { + #[constructor] + pub fn constructor(&mut self, initial_value: U256, owner: Address) { + self.value.set(initial_value); + self.owner.set(owner); + } +} +``` + +**Deploy with constructor arguments**: + +```bash +cargo stylus deploy \ + --private-key-path=wallet.txt \ + --constructor-args 42 0x1234567890abcdef1234567890abcdef12345678 +``` + +**With payable constructor**: + +```rust +#[constructor] +#[payable] +pub fn constructor(&mut self) { + let value = self.vm().msg_value(); + self.initial_balance.set(value); +} +``` + +```bash +cargo stylus deploy \ + --private-key-path=wallet.txt \ + --constructor-value=1000000000000000000 # 1 ETH in wei +``` + +## The ArbWasm Precompile + +Activation is handled by the ArbWasm precompile at address `0x0000000000000000000000000000000000000071`. + +### Key Functions + +#### activateProgram + +Activates a deployed Stylus contract: + +```solidity +function activateProgram( + address program +) external payable returns (uint16 version, uint256 dataFee); +``` + +**Parameters**: + +- `program`: Contract address containing WASM bytecode + +**Payment**: + +- Must send `value` equal to the calculated data fee (in wei) + +**Returns**: + +- `version`: Stylus protocol version the program was activated against +- `dataFee`: Actual fee paid for activation + +**Example (via cast)**: + +```bash +cast send 0x0000000000000000000000000000000000000071 \ + "activateProgram(address)" \ + 0x1234567890abcdef \ + --value 100000000000000000 \ + --private-key=$PRIVATE_KEY +``` + +#### codehashVersion + +Check if a codehash is activated and get its version: + +```solidity +function codehashVersion(bytes32 codehash) external view returns (uint16 version); +``` + +**Reverts if**: + +- Code is not activated +- Program needs upgrade +- Program has expired + +#### programTimeLeft + +Get remaining time before a program expires: + +```solidity +function programTimeLeft(address program) external view returns (uint64 timeLeft); +``` + +Returns seconds until expiration (default: ~1 year from activation). + +#### codehashKeepalive + +Extend a program's expiration time: + +```solidity +function codehashKeepalive(bytes32 codehash) external payable returns (uint64 expirySeconds); +``` + +Resets the expiration timer, preventing program deactivation. + +### ArbWasm Errors + +Activation can fail with these errors: + +```solidity +error ProgramNotWasm(); +// The deployed bytecode is not valid WASM + +error ProgramNotActivated(); +// Contract exists but hasn't been activated + +error ProgramNeedsUpgrade(uint16 version, uint16 stylusVersion); +// Program version incompatible with current Stylus version + +error ProgramExpired(uint64 ageInSeconds); +// Program has expired and must be reactivated + +error ProgramInsufficientValue(uint256 have, uint256 want); +// Sent data fee is less than required +``` + +## Gas and Fee Optimization + +### Estimating Costs + +Get cost estimates before deploying: + +```bash +# Estimate deployment gas +cargo stylus deploy --estimate-gas + +# Check activation fee +cargo stylus check # Shows estimated data fee +``` + +### Fee Bump Configuration + +Protect against fee variance with configurable bump percentage: + +```bash +# Default: 20% bump +cargo stylus deploy --private-key-path=wallet.txt + +# Custom bump percentage +# (Note: Use programmatically via stylus-tools library) +``` + +**In code** (using stylus-tools): + +```rust +use stylus_tools::core::activation::ActivationConfig; + +let config = ActivationConfig { + data_fee_bump_percent: 25, // 25% safety margin +}; +``` + +### Code Reuse Optimization + +If your contract's codehash matches an already-activated contract: + +```bash +cargo stylus check +``` + +**Output if already activated**: + +``` +Checking contract... +✓ Contract with this codehash is already activated! +Version: 1 +No activation needed - you can deploy without activating. +``` + +You can deploy the contract normally, and it will automatically use the existing activation. + +### Contract Caching + +After activation, contracts can be cached for cheaper calls: + +```solidity +// ArbWasmCache precompile (0x0000000000000000000000000000000000000072) +function cacheProgram(address program) external payable returns (uint256); +``` + +**Benefits**: + +- Reduces gas costs for subsequent contract calls +- One-time caching fee +- Shared across all contracts with same codehash + +## Advanced Activation Patterns + +### Multi-Contract Deployment + +When deploying multiple instances of the same contract: + +```bash +# First deployment: full deploy + activate +cargo stylus deploy --private-key-path=wallet.txt +# Contract 1: 0xaaaa... (activated) + +# Subsequent deployments: deploy only (reuses activation) +cargo stylus deploy --private-key-path=wallet.txt --no-activate +# Contract 2: 0xbbbb... (uses existing activation) + +cargo stylus deploy --private-key-path=wallet.txt --no-activate +# Contract 3: 0xcccc... (uses existing activation) +``` + +All three contracts share the same codehash and activation, saving on data fees. + +### Programmatic Deployment + +Using the stylus-tools library directly: + +```rust +use stylus_tools::core::{ + deployment::{deploy, DeploymentConfig}, + activation::{activate_contract, ActivationConfig, data_fee}, + check::{check_contract, ContractStatus}, +}; +use alloy::providers::{Provider, WalletProvider}; + +async fn deploy_and_activate( + provider: &impl Provider + WalletProvider, +) -> Result> { + let contract = /* build contract */; + + // Step 1: Check if already activated + let config = CheckConfig::default(); + match check_contract(&contract, None, &config, provider).await? { + ContractStatus::Active { .. } => { + println!("Already activated!"); + // Deploy without activation + } + ContractStatus::Ready { code, fee } => { + println!("Ready to activate. Data fee: {}", fee); + // Continue with deployment + activation + } + } + + // Step 2: Deploy + let deploy_config = DeploymentConfig { + no_activate: false, + ..Default::default() + }; + + deploy(&contract, &deploy_config, provider).await?; + + // Contract address returned from deployment + Ok(contract_address) +} +``` + +### Custom Deployer Contracts + +Use a custom deployer contract instead of the default: + +```bash +cargo stylus deploy \ + --private-key-path=wallet.txt \ + --deployer-address=0x... \ + --deployer-salt=0x0000000000000000000000000000000000000000000000000000000000000001 +``` + +This is useful for: + +- CREATE2 deterministic addresses +- Custom deployment logic +- Factory patterns + +## Contract Lifecycle + +### Activation Lifecycle + +``` +Deployed → Activated → [Active] → [Keepalive] → [Expired] + ↑ ↓ + └──────────┘ + (Periodic keepalive) +``` + +### Expiration and Keepalive + +Programs automatically expire after ~1 year (configurable by chain): + +```solidity +// Check time remaining +uint64 timeLeft = ArbWasm.programTimeLeft(contractAddress); + +// Extend expiration +ArbWasm.codehashKeepalive{value: keepaliveFee}(codehash); +``` + +**Why expiration?** + +- Prevents abandoned contracts from consuming ArbOS resources +- Encourages active maintenance +- Allows protocol upgrades + +**Keepalive strategy**: + +- Monitor `programTimeLeft()` periodically +- Call `codehashKeepalive()` before expiration +- Automated scripts can handle this + +### Reactivation After Expiry + +If a program expires: + +```bash +# Reactivate the existing deployment +cargo stylus activate --address=0x... +``` + +The contract code remains on-chain; only the activation state was cleared. + +## Troubleshooting + +### Common Activation Errors + +#### "Program not activated" + +**Cause**: Trying to call a deployed but not activated contract + +**Solution**: + +```bash +cargo stylus activate --address=0x... +``` + +#### "Insufficient value" + +**Cause**: Data fee sent is less than required + +**Solution**: + +- Check current data fee: `cargo stylus check` +- Increase fee bump percentage +- Ensure sufficient ETH balance + +#### "Program not WASM" + +**Cause**: Deployed bytecode is not valid Stylus WASM + +**Solution**: + +- Verify you deployed the correct contract +- Rebuild and redeploy: `cargo stylus deploy` + +#### "Program needs upgrade" + +**Cause**: Contract was activated against an old Stylus version + +**Solution**: + +- Recompile with latest SDK +- Redeploy and reactivate + +#### "Program expired" + +**Cause**: Contract hasn't been kept alive and expired + +**Solution**: + +```bash +# Reactivate the contract +cargo stylus activate --address=0x... +``` + +### Debugging Activation + +Enable verbose output: + +```bash +# Check detailed status +cargo stylus check --verbose + +# Deploy with verbose logging +RUST_LOG=debug cargo stylus deploy --private-key-path=wallet.txt +``` + +### Verifying Activation Status + +Check if a contract is activated: + +```bash +# Via cargo-stylus +cargo stylus check --address=0x... + +# Via cast (calling ArbWasm) +cast call 0x0000000000000000000000000000000000000071 \ + "codehashVersion(bytes32)" \ + $(cast keccak $(cast code 0x...)) +``` + +## Best Practices + +### 1. Always Check Before Deploying + +```bash +cargo stylus check +``` + +This prevents deploying duplicate code and wasting gas. + +### 2. Use Automatic Activation + +Unless you have specific reasons to split deployment and activation, use the default behavior: + +```bash +cargo stylus deploy # Deploys AND activates +``` + +### 3. Test on Testnet First + +Deploy to Arbitrum Sepolia before mainnet: + +```bash +cargo stylus deploy \ + --endpoint="https://sepolia-rollup.arbitrum.io/rpc" \ + --private-key-path=testnet-key.txt +``` + +### 4. Monitor Contract Expiration + +Set up monitoring for production contracts: + +```solidity +uint64 timeLeft = ArbWasm.programTimeLeft(contractAddress); +if (timeLeft < 30 days) { + // Send alert or trigger keepalive +} +``` + +### 5. Document Activation Details + +Track activation information: + +- Contract address +- Activation transaction hash +- Stylus version +- Data fee paid +- Activation timestamp + +### 6. Keep SDK Updated + +Use the latest Stylus SDK version: + +```toml +[dependencies] +stylus-sdk = "0.10.0-beta.1" +``` + +Older versions may become incompatible with chain upgrades. + +### 7. Handle Constructor Arguments Carefully + +Type-check constructor arguments: + +```bash +# Incorrect (will fail) +cargo stylus deploy --constructor-args "hello" 123 + +# Correct (matches constructor signature) +cargo stylus deploy --constructor-args 0x1234... 42 +``` + +## Complete Example + +Here's a full deployment workflow: + +```bash +# 1. Create new Stylus project +cargo stylus new my-token +cd my-token + +# 2. Build and verify locally +cargo build --release --target wasm32-unknown-unknown +cargo stylus check + +# 3. Test on Arbitrum Sepolia +export SEPOLIA_ENDPOINT="https://sepolia-rollup.arbitrum.io/rpc" +export PRIVATE_KEY_PATH="./sepolia-key.txt" + +cargo stylus deploy \ + --endpoint=$SEPOLIA_ENDPOINT \ + --private-key-path=$PRIVATE_KEY_PATH \ + --constructor-args "MyToken" "MTK" 18 + +# 4. Verify deployment +cargo stylus check \ + --endpoint=$SEPOLIA_ENDPOINT \ + --address=0x... # Address from step 3 + +# 5. Deploy to mainnet +export MAINNET_ENDPOINT="https://arb1.arbitrum.io/rpc" +export MAINNET_KEY_PATH="./mainnet-key.txt" + +cargo stylus deploy \ + --endpoint=$MAINNET_ENDPOINT \ + --private-key-path=$MAINNET_KEY_PATH \ + --constructor-args "MyToken" "MTK" 18 + +# 6. Cache the contract (optional, for gas optimization) +cast send 0x0000000000000000000000000000000000000072 \ + "cacheProgram(address)" \ + 0x... # Your contract address \ + --value 10000000000000000 \ + --rpc-url=$MAINNET_ENDPOINT \ + --private-key=$MAINNET_PRIVATE_KEY +``` + +## Summary + +- **Two-step process**: Deployment stores code, activation makes it executable +- **cargo-stylus handles both**: Use `deploy` for automatic activation +- **Data fee required**: Activation costs ETH (separate from deployment gas) +- **Code reuse**: Identical contracts share activation, saving costs +- **Expiration**: Programs expire after ~1 year without keepalive +- **ArbWasm precompile**: All activation goes through address `0x71` +- **Check first**: Use `cargo stylus check` to avoid duplicate activations + +## See Also + +- [Contracts](../reference/contracts.mdx) - Writing Stylus contracts +- [Global Variables and Functions](../reference/global-variables-and-functions.mdx) - VM interface methods +- [cargo-stylus CLI Reference](../reference/cargo-stylus-reference.mdx) - Complete CLI documentation +- [ArbWasm Precompile](../reference/precompiles.mdx#arbwasm) - Detailed precompile reference From 8b2a6e602316a6248f90f8309d854deae3844fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Fri, 21 Nov 2025 18:13:54 -0800 Subject: [PATCH 28/28] fix boilerplate links --- docs/stylus/concepts/activation.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/stylus/concepts/activation.mdx b/docs/stylus/concepts/activation.mdx index dce723a561..f8b0762318 100644 --- a/docs/stylus/concepts/activation.mdx +++ b/docs/stylus/concepts/activation.mdx @@ -784,5 +784,5 @@ cast send 0x0000000000000000000000000000000000000072 \ - [Contracts](../reference/contracts.mdx) - Writing Stylus contracts - [Global Variables and Functions](../reference/global-variables-and-functions.mdx) - VM interface methods -- [cargo-stylus CLI Reference](../reference/cargo-stylus-reference.mdx) - Complete CLI documentation -- [ArbWasm Precompile](../reference/precompiles.mdx#arbwasm) - Detailed precompile reference + +