diff --git a/docs/build-decentralized-apps/01-quickstart-solidity-remix.mdx b/docs/build-decentralized-apps/01-quickstart-solidity-remix.mdx index 2ea5737f97..d6558844ad 100644 --- a/docs/build-decentralized-apps/01-quickstart-solidity-remix.mdx +++ b/docs/build-decentralized-apps/01-quickstart-solidity-remix.mdx @@ -5,7 +5,7 @@ author: symbolpunk user_story: As a web2 developer, I want to onboard into Arbitrum by building and deploying my first smart contract, and knowing how to build a web widget interacting with it. content_type: quickstart slug: /build-decentralized-apps/quickstart-solidity-remix -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- import { VanillaAdmonition } from '@site/src/components/VanillaAdmonition/'; diff --git a/docs/build-decentralized-apps/02-how-to-estimate-gas.mdx b/docs/build-decentralized-apps/02-how-to-estimate-gas.mdx index eb7fb114f4..4d376b9c0b 100644 --- a/docs/build-decentralized-apps/02-how-to-estimate-gas.mdx +++ b/docs/build-decentralized-apps/02-how-to-estimate-gas.mdx @@ -3,7 +3,7 @@ title: 'How to estimate gas in Arbitrum' description: Learn how to estimate gas before submitting transactions. author: TucksonDev content_type: how-to -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- :::info Looking for Stylus guidance? diff --git a/docs/build-decentralized-apps/03-public-chains.mdx b/docs/build-decentralized-apps/03-public-chains.mdx index bbc0b796a6..66d941d7b9 100644 --- a/docs/build-decentralized-apps/03-public-chains.mdx +++ b/docs/build-decentralized-apps/03-public-chains.mdx @@ -3,7 +3,7 @@ title: 'Arbitrum chains overview' description: A high level description of the Arbitrum chains available user_story: As a developer, I want to understand the different Arbitrum chains and how they relate to each other. content_type: concept -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- import { AddressExplorerLink as AEL } from '@site/src/components/AddressExplorerLink'; diff --git a/docs/build-decentralized-apps/04-cross-chain-messaging.mdx b/docs/build-decentralized-apps/04-cross-chain-messaging.mdx index f73d98e710..c581bb7732 100644 --- a/docs/build-decentralized-apps/04-cross-chain-messaging.mdx +++ b/docs/build-decentralized-apps/04-cross-chain-messaging.mdx @@ -3,7 +3,7 @@ title: 'Cross-chain messaging overview' description: Learn about cross-chain messaging in Arbitrum user_story: As a developer, I want to understand how cross-chain messaging works in Arbitrum. content_type: concept -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- The Arbitrum protocol and related tooling makes it easy for developers to build cross-chain applications; i.e., applications that involve sending messages from Ethereum to an Arbitrum chain, and/or from an Arbitrum chain to Ethereum. diff --git a/docs/build-decentralized-apps/arbitrum-vs-ethereum/01-comparison-overview.mdx b/docs/build-decentralized-apps/arbitrum-vs-ethereum/01-comparison-overview.mdx index 316fa8fdbc..595f405270 100644 --- a/docs/build-decentralized-apps/arbitrum-vs-ethereum/01-comparison-overview.mdx +++ b/docs/build-decentralized-apps/arbitrum-vs-ethereum/01-comparison-overview.mdx @@ -6,7 +6,7 @@ author: jose-franco sme: jose-franco target_audience: developers who want to build on Arbitrum content_type: concept -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- Arbitrum's design is to be as compatible and consistent with Ethereum as possible, from its high-level RPCs to its low-level bytecode and everything in between. Decentralized app developers with experience building on Ethereum will likely find that little to no new specific knowledge is required to build on Arbitrum. diff --git a/docs/build-decentralized-apps/arbitrum-vs-ethereum/02-block-numbers-and-time.mdx b/docs/build-decentralized-apps/arbitrum-vs-ethereum/02-block-numbers-and-time.mdx index a2b547e314..bc43a680e6 100644 --- a/docs/build-decentralized-apps/arbitrum-vs-ethereum/02-block-numbers-and-time.mdx +++ b/docs/build-decentralized-apps/arbitrum-vs-ethereum/02-block-numbers-and-time.mdx @@ -6,7 +6,7 @@ author: dzgoldman, jose-franco sme: jose-franco target_audience: developers who want to build on Arbitrum content_type: concept -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- :::info block number vs `block.number` diff --git a/docs/build-decentralized-apps/arbitrum-vs-ethereum/03-rpc-methods.mdx b/docs/build-decentralized-apps/arbitrum-vs-ethereum/03-rpc-methods.mdx index 3c6314fc56..91fdf6f8ed 100644 --- a/docs/build-decentralized-apps/arbitrum-vs-ethereum/03-rpc-methods.mdx +++ b/docs/build-decentralized-apps/arbitrum-vs-ethereum/03-rpc-methods.mdx @@ -5,7 +5,7 @@ description: This concept page provides information about the differences betwee target_audience: developers who want to build on Arbitrum author: dzgoldman content_type: concept -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- Although the majority of RPC methods follow the same behavior as in Ethereum, some methods may produce a different result or add more information when used on an Arbitrum chain. This page covers the differences in response body fields you'll find when calling RPC methods on an Arbitrum chain vs on Ethereum. diff --git a/docs/build-decentralized-apps/arbitrum-vs-ethereum/04-solidity-support.mdx b/docs/build-decentralized-apps/arbitrum-vs-ethereum/04-solidity-support.mdx index b5842e3010..9ed06b6a16 100644 --- a/docs/build-decentralized-apps/arbitrum-vs-ethereum/04-solidity-support.mdx +++ b/docs/build-decentralized-apps/arbitrum-vs-ethereum/04-solidity-support.mdx @@ -5,7 +5,7 @@ description: This concept page provides information about the differences betwee target_audience: developers who want to build on Arbitrum author: dzgoldman content_type: concept -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- Arbitrum chains are Ethereum-compatible and, therefore, allow you to trustlessly deploy Solidity smart contracts, as well as contracts written in Vyper or any other language that compiles to EVM bytecode. However, when calling certain properties and functions on a Solidity smart contract, there are some differences between the result you'd obtain if that contract were on Ethereum and the result on Arbitrum. diff --git a/docs/build-decentralized-apps/custom-gas-token-sdk.mdx b/docs/build-decentralized-apps/custom-gas-token-sdk.mdx index f100dac3cc..77c8f2c3f6 100644 --- a/docs/build-decentralized-apps/custom-gas-token-sdk.mdx +++ b/docs/build-decentralized-apps/custom-gas-token-sdk.mdx @@ -5,7 +5,7 @@ author: Mehdi Salehi sme: Mehdi Salehi target_audience: 'Developers deploying and maintaining Arbitrum chains.' sidebar_position: 2 -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- Arbitrum SDK is a TypeScript library for client-side interactions with Arbitrum. It provides common helper functionality as well as access to the underlying smart contract interfaces. diff --git a/docs/build-decentralized-apps/nodeinterface/01-overview.mdx b/docs/build-decentralized-apps/nodeinterface/01-overview.mdx index fb39528454..b402199a1b 100644 --- a/docs/build-decentralized-apps/nodeinterface/01-overview.mdx +++ b/docs/build-decentralized-apps/nodeinterface/01-overview.mdx @@ -3,7 +3,7 @@ title: 'NodeInterface overview' description: A high level description of what the NodeInterface is and how it works user_story: As a developer, I want to understand what the NodeInterface is and how it works. content_type: concept -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- diff --git a/docs/build-decentralized-apps/nodeinterface/02-reference.mdx b/docs/build-decentralized-apps/nodeinterface/02-reference.mdx index 023bcc2db5..1be9333d80 100644 --- a/docs/build-decentralized-apps/nodeinterface/02-reference.mdx +++ b/docs/build-decentralized-apps/nodeinterface/02-reference.mdx @@ -3,7 +3,7 @@ title: 'NodeInterface reference' description: A reference page of the NodeInterface available on Arbitrum chains user_story: As a developer, I want to understand the specific methods available in the NodeInterface content_type: reference -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- The Arbitrum Nitro software includes a special `NodeInterface` contract available at address `0xc8` that is only accessible via RPCs (it's not actually deployed onchain, and thus can't be called by smart contracts). This reference page documents the specific calls available in the `NodeInterface`. For a more conceptual description of what it is and how it works, please refer to the [`NodeInterface` conceptual page](/build-decentralized-apps/nodeinterface/01-overview.mdx). diff --git a/docs/build-decentralized-apps/oracles/01-overview.mdx b/docs/build-decentralized-apps/oracles/01-overview.mdx index a4228fbe40..6f323431f6 100644 --- a/docs/build-decentralized-apps/oracles/01-overview.mdx +++ b/docs/build-decentralized-apps/oracles/01-overview.mdx @@ -5,7 +5,7 @@ description: A high level description of what oracles are user_story: As a developer, I want to understand what oracles are and how they work. content_type: concept sidebar_label: Oracles -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- import ImageZoom from '@site/src/components/ImageZoom'; diff --git a/docs/build-decentralized-apps/precompiles/01-overview.mdx b/docs/build-decentralized-apps/precompiles/01-overview.mdx index 6b87a8c694..caad074ccd 100644 --- a/docs/build-decentralized-apps/precompiles/01-overview.mdx +++ b/docs/build-decentralized-apps/precompiles/01-overview.mdx @@ -3,7 +3,7 @@ title: 'Precompiles overview' description: A high level description of what precompiles are and how they work user_story: As a developer, I want to understand what precompiles are and how they work. content_type: concept -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- Precompiles are predefined smart contracts that have special addresses and provide specific functionality which is executed not at the EVM bytecode level, but natively by the Arbitrum client itself. Precompiles are primarily used to introduce specific functions that would be computationally expensive if executed in EVM bytecode, and functions that facilitate the interaction between the parent chain and the child chain. By having them natively in the Arbitrum client, they can be optimized for performance. diff --git a/docs/build-decentralized-apps/precompiles/02-reference.mdx b/docs/build-decentralized-apps/precompiles/02-reference.mdx index 9a02c4fac4..d7ade097b3 100644 --- a/docs/build-decentralized-apps/precompiles/02-reference.mdx +++ b/docs/build-decentralized-apps/precompiles/02-reference.mdx @@ -3,7 +3,7 @@ title: 'Precompiles reference' description: A reference page of all precompiles available on Arbitrum chains user_story: As a developer, I want to understand the most useful precompiles available on Arbitrum chains and how to use them. content_type: reference -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- ArbOS provides child chain-specific precompiles with methods smart contracts can call the same way they can solidity functions. This reference page exhaustively documents the specific calls ArbOS makes available through precompiles. For a more conceptual description of what precompiles are and how they work, please refer to the [precompiles conceptual page](/build-decentralized-apps/precompiles/01-overview.mdx). diff --git a/docs/build-decentralized-apps/reference/01-node-providers.mdx b/docs/build-decentralized-apps/reference/01-node-providers.mdx index 1a6016b0cd..a3fd4af1d4 100644 --- a/docs/build-decentralized-apps/reference/01-node-providers.mdx +++ b/docs/build-decentralized-apps/reference/01-node-providers.mdx @@ -3,7 +3,7 @@ title: 'RPC endpoints and providers' description: Find available RPC endpoints and providers in the ecosystem reader_audience: developers who want to build on Arbitrum content_type: overview -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- import ArbitrumRpcEndpoints from '../../partials/_reference-arbitrum-rpc-endpoints-partial.mdx'; diff --git a/docs/build-decentralized-apps/reference/02-contract-addresses.mdx b/docs/build-decentralized-apps/reference/02-contract-addresses.mdx index 01c4f451dd..5e91a09c55 100644 --- a/docs/build-decentralized-apps/reference/02-contract-addresses.mdx +++ b/docs/build-decentralized-apps/reference/02-contract-addresses.mdx @@ -6,7 +6,7 @@ author: anegg0 sme: anegg0 user_story: As a current or prospective Arbitrum user I need to know to what addresses Arbitrum contracts have been deployed. content_type: reference -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- import ArbitrumContractAddresses from '../../partials/_reference-arbitrum-contract-addresses-partial.mdx'; diff --git a/docs/build-decentralized-apps/reference/03-chain-params.mdx b/docs/build-decentralized-apps/reference/03-chain-params.mdx index 221f35769a..fa5926041e 100644 --- a/docs/build-decentralized-apps/reference/03-chain-params.mdx +++ b/docs/build-decentralized-apps/reference/03-chain-params.mdx @@ -3,7 +3,7 @@ title: 'Chain parameters' description: Information about important system parameters for public Arbitrum chains user_story: As a developer, I want to understand the system parameters for the public Arbitrum chains. content_type: overview -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- | Param | Description | Arbitrum One | Arbitrum Nova | Arb Sepolia | diff --git a/docs/build-decentralized-apps/reference/04-development-frameworks.mdx b/docs/build-decentralized-apps/reference/04-development-frameworks.mdx index 8b7ac1eccb..f67a2ade1f 100644 --- a/docs/build-decentralized-apps/reference/04-development-frameworks.mdx +++ b/docs/build-decentralized-apps/reference/04-development-frameworks.mdx @@ -3,7 +3,7 @@ title: 'Development frameworks' description: An overview of popular development frameworks that exist in the Arbitrum ecosystem user_story: As a developer, I want to understand the popular development frameworks that exist in the Arbitrum ecosystem. content_type: overview -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- import KnowMoreToolsBox from '../../for-devs/partials/_know-more-tools-box-partial.mdx'; diff --git a/docs/build-decentralized-apps/reference/05-web3-libraries-tools.mdx b/docs/build-decentralized-apps/reference/05-web3-libraries-tools.mdx index 103f5582f1..3915f5f3db 100644 --- a/docs/build-decentralized-apps/reference/05-web3-libraries-tools.mdx +++ b/docs/build-decentralized-apps/reference/05-web3-libraries-tools.mdx @@ -3,7 +3,7 @@ title: 'Web3 libraries and tools' description: An overview of some popular Web3 libraries that help developers interact with the Ethereum and Arbitrum blockchains. user_story: As a developer, I want to understand what Web3 libraries and tools are available in the Ethereum and Arbitrum ecosystems. content_type: overview -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- import KnowMoreToolsBox from '../../for-devs/partials/_know-more-tools-box-partial.mdx'; diff --git a/docs/build-decentralized-apps/reference/06-monitoring-tools-block-explorers.mdx b/docs/build-decentralized-apps/reference/06-monitoring-tools-block-explorers.mdx index 2b59378770..23c3a82c61 100644 --- a/docs/build-decentralized-apps/reference/06-monitoring-tools-block-explorers.mdx +++ b/docs/build-decentralized-apps/reference/06-monitoring-tools-block-explorers.mdx @@ -3,7 +3,7 @@ title: 'Monitoring tools and block explorers' description: An overview of popular monitoring tools and block explorers that exist in the Arbitrum ecosystem user_story: As a developer, I want to understand what monitoring tools and block explorers are available in the Arbitrum ecosystem. content_type: overview -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- import KnowMoreToolsBox from '../../for-devs/partials/_know-more-tools-box-partial.mdx'; diff --git a/docs/build-decentralized-apps/reference/07-debugging-tools.mdx b/docs/build-decentralized-apps/reference/07-debugging-tools.mdx index 22fe44b703..17a64fb0f5 100644 --- a/docs/build-decentralized-apps/reference/07-debugging-tools.mdx +++ b/docs/build-decentralized-apps/reference/07-debugging-tools.mdx @@ -3,7 +3,7 @@ title: 'Debugging tools' description: An overview of popular debugging tools that exist in the Arbitrum ecosystem user_story: As a developer, I want to understand what debugging tools are available in the Arbitrum ecosystem. content_type: overview -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- import KnowMoreToolsBox from '../../for-devs/partials/_know-more-tools-box-partial.mdx'; diff --git a/docs/build-decentralized-apps/reference/08-mainnet-risks.mdx b/docs/build-decentralized-apps/reference/08-mainnet-risks.mdx index 0431279295..219e3bae3f 100644 --- a/docs/build-decentralized-apps/reference/08-mainnet-risks.mdx +++ b/docs/build-decentralized-apps/reference/08-mainnet-risks.mdx @@ -2,7 +2,7 @@ title: 'Arbitrum: Understanding the risks' description: 'Understand the risks associated with cutting-edge software development' author: dzgoldman -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- # Arbitrum: Understanding the risks diff --git a/docs/build-decentralized-apps/token-bridging/01-overview.mdx b/docs/build-decentralized-apps/token-bridging/01-overview.mdx index 139e5b1ccd..0992179ad7 100644 --- a/docs/build-decentralized-apps/token-bridging/01-overview.mdx +++ b/docs/build-decentralized-apps/token-bridging/01-overview.mdx @@ -5,7 +5,7 @@ author: dzgoldman user_story: As a developer, I want to understand how the token bridge works and what options exist to bridge assets between layers. content_type: overview sidebar_position: 1 -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- Token bridging is a fundamental aspect of any Layer 2 (child chain) protocol. Arbitrum uses its ability to pass messages between parent and child chains (see [Cross-chain messaging](/build-decentralized-apps/04-cross-chain-messaging.mdx)) to enable projects to move assets between Ethereum and an Arbitrum chain trustlessly, and vice versa. Any asset and asset type in principle can be bridged, including `ETH`, `ERC-20` tokens, and `ERC-721` tokens, among others. diff --git a/docs/build-decentralized-apps/token-bridging/02-token-bridge-ether.mdx b/docs/build-decentralized-apps/token-bridging/02-token-bridge-ether.mdx index d6f4803f34..7a99151a64 100644 --- a/docs/build-decentralized-apps/token-bridging/02-token-bridge-ether.mdx +++ b/docs/build-decentralized-apps/token-bridging/02-token-bridge-ether.mdx @@ -5,7 +5,7 @@ author: dzgoldman user_story: As a developer, I want to understand how bridging ether works on Arbitrum content_type: concept sidebar_position: 2 -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- import ImageZoom from '@site/src/components/ImageZoom'; diff --git a/docs/build-decentralized-apps/token-bridging/03-token-bridge-erc20.mdx b/docs/build-decentralized-apps/token-bridging/03-token-bridge-erc20.mdx index cbaa2cfe8a..99a1eaa60c 100644 --- a/docs/build-decentralized-apps/token-bridging/03-token-bridge-erc20.mdx +++ b/docs/build-decentralized-apps/token-bridging/03-token-bridge-erc20.mdx @@ -5,7 +5,7 @@ author: dzgoldman user_story: As a developer, I want to understand how ERC-20 token bridging works on Arbitrum, and the architecture of the token bridge. content_type: concept sidebar_position: 3 -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- import ImageZoom from '@site/src/components/ImageZoom'; diff --git a/docs/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/01-get-started.mdx b/docs/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/01-get-started.mdx index 93ed153406..b35bd528c2 100644 --- a/docs/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/01-get-started.mdx +++ b/docs/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/01-get-started.mdx @@ -3,7 +3,7 @@ title: 'Get started with token bridging' description: Learn the different options available to bridge tokens programmatically user_story: As a developer, I want to understand how to bridge tokens between Ethereum and Arbitrum. content_type: overview -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- Token bridging is a fundamental aspect of any child chain protocol. It allows projects to quickly integrate with the Arbitrum ecosystem by leveraging their existing parent chain tokens. diff --git a/docs/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/02-how-to-bridge-tokens-standard.mdx b/docs/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/02-how-to-bridge-tokens-standard.mdx index 3412398745..7679ab82d1 100644 --- a/docs/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/02-how-to-bridge-tokens-standard.mdx +++ b/docs/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/02-how-to-bridge-tokens-standard.mdx @@ -3,7 +3,7 @@ title: "Bridge tokens via Arbitrum's standard `ERC-20` gateway" description: Learn how to programmatically bridge tokens between Ethereum and Arbitrum using Arbitrum’s standard ER-C20 gateway user_story: As a developer, I want to understand how to bridge tokens between Ethereum and Arbitrum using the standard ER-C20 gateway. content_type: how-to -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- In this how-to, you’ll learn how to bridge your own token between Ethereum (parent chain) and Arbitrum (child chain), using [Arbitrum’s standard `ERC20` gateway](/build-decentralized-apps/token-bridging/03-token-bridge-erc20.mdx#default-standard-bridging). For alternative ways of bridging tokens, don’t forget to check out this [overview](/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/01-get-started.mdx). diff --git a/docs/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/03-how-to-bridge-tokens-generic-custom.mdx b/docs/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/03-how-to-bridge-tokens-generic-custom.mdx index 81c8acd8f9..c1bf69c54a 100644 --- a/docs/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/03-how-to-bridge-tokens-generic-custom.mdx +++ b/docs/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/03-how-to-bridge-tokens-generic-custom.mdx @@ -3,7 +3,7 @@ title: 'Bridge tokens via Arbitrum’s generic-custom gateway' description: Learn how to use the generic-custom gateway to bridge tokens programmatically user_story: As a developer, I want to understand how to bridge tokens between Ethereum and Arbitrum using the generic-custom gateway content_type: how-to -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- In this how-to, you’ll learn how to bridge your own token between Ethereum (parent chain) and Arbitrum (child chain), using [Arbitrum’s generic-custom gateway](/build-decentralized-apps/token-bridging/03-token-bridge-erc20.mdx#the-arbitrum-generic-custom-gateway). For alternative ways of bridging tokens, don’t forget to check out this [overview](/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/01-get-started.mdx). diff --git a/docs/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/04-how-to-bridge-tokens-custom-gateway.mdx b/docs/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/04-how-to-bridge-tokens-custom-gateway.mdx index 29b7fbb608..3ad4a6ea84 100644 --- a/docs/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/04-how-to-bridge-tokens-custom-gateway.mdx +++ b/docs/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/04-how-to-bridge-tokens-custom-gateway.mdx @@ -3,7 +3,7 @@ title: 'How to bridge tokens via a custom gateway' description: Learn how to set up a custom gateway using Arbitrum's Token Bridge to bridge tokens programmatically reader_audience: developers who want to build on Ethereum/Arbitrum and bridge tokens between layers content_type: how-to -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildSoliditySidebar --- :::caution Do you really need a custom gateway? diff --git a/docs/stylus/advanced/hostio-exports.mdx b/docs/stylus/advanced/hostio-exports.mdx new file mode 100644 index 0000000000..80210af745 --- /dev/null +++ b/docs/stylus/advanced/hostio-exports.mdx @@ -0,0 +1,878 @@ +--- +title: 'Hostio exports' +description: 'Learn about low-level hostio functions for direct VM access in Stylus smart contracts' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers who need to understand how to use hostio exports with Stylus. +displayed_sidebar: buildStylusSidebar +--- + +Hostio (Host I/O) exports are low-level functions that provide direct access to the Stylus VM runtime. These functions are WebAssembly imports that allow Stylus programs to interact with the blockchain environment, similar to how EVM opcodes work in Solidity. + +## Overview + +Hostio functions are the foundational layer that powers all Stylus smart contract operations. While most developers will use the higher-level SDK abstractions, understanding hostio functions is valuable for: + +- Performance optimization through direct VM access +- Implementing custom low-level operations +- Understanding gas costs and execution flow +- Debugging and troubleshooting contract behavior + +:::info +Most developers should use the high-level SDK wrappers instead of calling hostio functions directly. The SDK provides safe, ergonomic interfaces that handle memory management and error checking automatically. +::: + +## How hostio works + +Hostio functions are WebAssembly imports defined in the `vm_hooks` module. When a Stylus program is compiled to WASM, these functions are linked at runtime by the Arbitrum VM: + +```rust +#[link(wasm_import_module = "vm_hooks")] +extern "C" { + pub fn msg_sender(sender: *mut u8); + pub fn block_number() -> u64; + // ... more functions +} +``` + +During execution, calls to these functions are intercepted by the Stylus VM, which implements the actual functionality using the underlying ArbOS infrastructure. + +## Function categories + +Hostio functions are organized into several categories based on their purpose. + +### Account operations + +Query information about accounts on the blockchain. + +#### `account_balance` + +Gets the ETH balance of an account in wei. Equivalent to EVM's `BALANCE` opcode. + +```rust +pub fn account_balance(address: *const u8, dest: *mut u8); +``` + +**Parameters:** + +- `address`: Pointer to 20-byte address +- `dest`: Pointer to write 32-byte balance value + +**Usage:** + +```rust +use stylus_sdk::alloy_primitives::{Address, U256}; + +unsafe { + let addr = Address::from([0x11; 20]); + let mut balance_bytes = [0u8; 32]; + hostio::account_balance(addr.as_ptr(), balance_bytes.as_mut_ptr()); + let balance = U256::from_be_bytes(balance_bytes); +} +``` + +#### `account_code` + +Gets a subset of code from an account. Equivalent to EVM's `EXTCODECOPY` opcode. + +```rust +pub fn account_code( + address: *const u8, + offset: usize, + size: usize, + dest: *mut u8 +) -> usize; +``` + +**Returns:** Number of bytes actually written + +#### `account_code_size` + +Gets the size of code at an address. Equivalent to EVM's `EXTCODESIZE` opcode. + +```rust +pub fn account_code_size(address: *const u8) -> usize; +``` + +#### `account_codehash` + +Gets the code hash of an account. Equivalent to EVM's `EXTCODEHASH` opcode. + +```rust +pub fn account_codehash(address: *const u8, dest: *mut u8); +``` + +:::note +Empty accounts return the keccak256 hash of empty bytes: `c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470` +::: + +### Storage operations + +Interact with persistent contract storage. + +#### `storage_load_bytes32` + +Reads a 32-byte value from storage. Equivalent to EVM's `SLOAD` opcode. + +```rust +pub fn storage_load_bytes32(key: *const u8, dest: *mut u8); +``` + +**Parameters:** + +- `key`: Pointer to 32-byte storage key +- `dest`: Pointer to write 32-byte value + +#### `storage_cache_bytes32` + +Writes a 32-byte value to the storage cache. Equivalent to EVM's `SSTORE` opcode. + +```rust +pub fn storage_cache_bytes32(key: *const u8, value: *const u8); +``` + +:::warning +Values are cached and must be persisted using `storage_flush_cache` before they're permanently written to storage. +::: + +#### `storage_flush_cache` + +Persists cached storage values to the EVM state trie. Equivalent to multiple `SSTORE` operations. + +```rust +pub fn storage_flush_cache(clear: bool); +``` + +**Parameters:** + +- `clear`: Whether to drop the cache entirely after flushing + +**Storage caching benefits:** + +The Stylus VM implements storage caching for improved performance: + +- Repeated reads of the same key cost less gas +- Writes are batched for efficiency +- Cache is automatically managed by the SDK + +### Block information + +Access information about the current block. + +#### `block_basefee` + +Gets the basefee of the current block. Equivalent to EVM's `BASEFEE` opcode. + +```rust +pub fn block_basefee(basefee: *mut u8); +``` + +#### `block_chainid` + +Gets the chain identifier. Equivalent to EVM's `CHAINID` opcode. + +```rust +pub fn chainid() -> u64; +``` + +#### `block_coinbase` + +Gets the coinbase (block producer address). On Arbitrum, this is the L1 batch poster's address. + +```rust +pub fn block_coinbase(coinbase: *mut u8); +``` + +#### `block_gas_limit` + +Gets the gas limit of the current block. Equivalent to EVM's `GASLIMIT` opcode. + +```rust +pub fn block_gas_limit() -> u64; +``` + +#### `block_number` + +Gets a bounded estimate of the L1 block number when the transaction was sequenced. + +```rust +pub fn block_number() -> u64; +``` + +:::info +See [Arbitrum block numbers and time](https://developer.arbitrum.io/time) for more information on how block numbers work on Arbitrum chains. +::: + +#### `block_timestamp` + +Gets a bounded estimate of the Unix timestamp when the transaction was sequenced. + +```rust +pub fn block_timestamp() -> u64; +``` + +### Transaction and message context + +Access information about the current transaction and call context. + +#### `msg_sender` + +Gets the address of the caller. Equivalent to EVM's `CALLER` opcode. + +```rust +pub fn msg_sender(sender: *mut u8); +``` + +**Parameters:** + +- `sender`: Pointer to write 20-byte address + +:::note +For L1-to-L2 retryable ticket transactions, addresses are aliased. See [address aliasing documentation](https://developer.arbitrum.io/arbos/l1-to-l2-messaging#address-aliasing). +::: + +#### `msg_value` + +Gets the ETH value sent with the call in wei. Equivalent to EVM's `CALLVALUE` opcode. + +```rust +pub fn msg_value(value: *mut u8); +``` + +#### `msg_reentrant` + +Checks if the current call is reentrant. + +```rust +pub fn msg_reentrant() -> bool; +``` + +#### `tx_gas_price` + +Gets the gas price in wei per gas. On Arbitrum, this equals the basefee. Equivalent to EVM's `GASPRICE` opcode. + +```rust +pub fn tx_gas_price(gas_price: *mut u8); +``` + +#### `tx_origin` + +Gets the top-level sender of the transaction. Equivalent to EVM's `ORIGIN` opcode. + +```rust +pub fn tx_origin(origin: *mut u8); +``` + +#### `tx_ink_price` + +Gets the price of ink in EVM gas basis points. See [Ink and Gas](https://docs.arbitrum.io/stylus/concepts/gas-metering) for more information. + +```rust +pub fn tx_ink_price() -> u32; +``` + +### Contract calls + +Make calls to other contracts. + +#### `call_contract` + +Calls another contract with optional value and gas limit. Equivalent to EVM's `CALL` opcode. + +```rust +pub fn call_contract( + contract: *const u8, + calldata: *const u8, + calldata_len: usize, + value: *const u8, + gas: u64, + return_data_len: *mut usize +) -> u8; +``` + +**Parameters:** + +- `contract`: Pointer to 20-byte contract address +- `calldata`: Pointer to calldata bytes +- `calldata_len`: Length of calldata +- `value`: Pointer to 32-byte value in wei (use 0 for no value) +- `gas`: Gas to supply (use `u64::MAX` for all available gas) +- `return_data_len`: Pointer to store length of return data + +**Returns:** `0` on success, non-zero on failure + +**Gas rules:** + +- Follows the 63/64 rule (at most 63/64 of available gas is forwarded) +- Includes callvalue stipend when value is sent + +**Usage:** + +```rust +use stylus_sdk::call::RawCall; + +unsafe { + let result = RawCall::new(self.vm()) + .gas(100_000) + .value(U256::from(1_000_000)) + .call(contract_address, &calldata)?; +} +``` + +#### `delegate_call_contract` + +Delegate calls another contract. Equivalent to EVM's `DELEGATECALL` opcode. + +```rust +pub fn delegate_call_contract( + contract: *const u8, + calldata: *const u8, + calldata_len: usize, + gas: u64, + return_data_len: *mut usize +) -> u8; +``` + +:::warning +Delegate calls execute code in the context of the current contract. Be extremely careful when delegate calling to untrusted contracts as they have full access to your storage. +::: + +#### `static_call_contract` + +Static calls another contract (read-only). Equivalent to EVM's `STATICCALL` opcode. + +```rust +pub fn static_call_contract( + contract: *const u8, + calldata: *const u8, + calldata_len: usize, + gas: u64, + return_data_len: *mut usize +) -> u8; +``` + +### Contract deployment + +Deploy new contracts. + +#### `create1` + +Deploys a contract using CREATE. Equivalent to EVM's `CREATE` opcode. + +```rust +pub fn create1( + code: *const u8, + code_len: usize, + endowment: *const u8, + contract: *mut u8, + revert_data_len: *mut usize +); +``` + +**Parameters:** + +- `code`: Pointer to initialization code (EVM bytecode) +- `code_len`: Length of initialization code +- `endowment`: Pointer to 32-byte value to send +- `contract`: Pointer to write deployed contract address (20 bytes) +- `revert_data_len`: Pointer to store revert data length on failure + +**Deployment rules:** + +- Init code must be EVM bytecode +- Deployed code can be Stylus (WASM) if it starts with `0xEFF000` header +- Address is determined by sender and nonce +- On failure, address will be zero + +#### `create2` + +Deploys a contract using CREATE2. Equivalent to EVM's `CREATE2` opcode. + +```rust +pub fn create2( + code: *const u8, + code_len: usize, + endowment: *const u8, + salt: *const u8, + contract: *mut u8, + revert_data_len: *mut usize +); +``` + +**Parameters:** + +- `salt`: Pointer to 32-byte salt value + +**Address calculation:** + +- Address is deterministic based on sender, salt, and init code hash +- Allows for pre-computed addresses + +### Events and logging + +Emit events to the blockchain. + +#### `emit_log` + +Emits an EVM log with topics and data. Equivalent to EVM's `LOG0`-`LOG4` opcodes. + +```rust +pub fn emit_log(data: *const u8, len: usize, topics: usize); +``` + +**Parameters:** + +- `data`: Pointer to event data (first bytes should be 32-byte aligned topics) +- `len`: Total length of data including topics +- `topics`: Number of topics (0-4) + +:::warning +Requesting more than 4 topics will cause a revert. +::: + +**Higher-level usage:** + +```rust +sol! { + event Transfer(address indexed from, address indexed to, uint256 value); +} + +// Emit using the SDK +self.vm().log(Transfer { + from: sender, + to: recipient, + value: amount, +}); +``` + +### Gas and ink metering + +Monitor execution costs. + +#### `evm_gas_left` + +Gets the amount of gas remaining. Equivalent to EVM's `GAS` opcode. + +```rust +pub fn evm_gas_left() -> u64; +``` + +#### `evm_ink_left` + +Gets the amount of ink remaining. Stylus-specific metering unit. + +```rust +pub fn evm_ink_left() -> u64; +``` + +:::info +Ink is Stylus's compute pricing unit. See [Ink and Gas](https://docs.arbitrum.io/stylus/concepts/gas-metering) for conversion between ink and gas. +::: + +#### `pay_for_memory_grow` + +Pays for WASM memory growth. Automatically called when allocating new pages. + +```rust +pub fn pay_for_memory_grow(pages: u16); +``` + +:::note +The `entrypoint!` macro handles importing this hostio. Manual calls will unproductively consume gas. +::: + +### Cryptography + +Cryptographic operations. + +#### `native_keccak256` + +Efficiently computes keccak256 hash. Equivalent to EVM's `SHA3` opcode. + +```rust +pub fn native_keccak256(bytes: *const u8, len: usize, output: *mut u8); +``` + +**Parameters:** + +- `bytes`: Pointer to input data +- `len`: Length of input data +- `output`: Pointer to write 32-byte hash + +**Higher-level usage:** + +```rust +use stylus_sdk::crypto::keccak; + +let hash = keccak(b"hello world"); +``` + +### Calldata operations + +Read and write calldata and return data. + +#### `read_args` + +Reads the program calldata. Equivalent to EVM's `CALLDATACOPY` opcode. + +```rust +pub fn read_args(dest: *mut u8); +``` + +:::note +This reads the entirety of the call's calldata. +::: + +#### `read_return_data` + +Copies bytes from the last call or deployment return result. Equivalent to EVM's `RETURNDATACOPY` opcode. + +```rust +pub fn read_return_data(dest: *mut u8, offset: usize, size: usize) -> usize; +``` + +**Parameters:** + +- `dest`: Destination buffer +- `offset`: Offset in return data to start copying from +- `size`: Number of bytes to copy + +**Returns:** Number of bytes actually written + +**Behavior:** + +- Does not revert if out of bounds +- Copies overlapping portion only + +#### `return_data_size` + +Gets the length of the last return result. Equivalent to EVM's `RETURNDATASIZE` opcode. + +```rust +pub fn return_data_size() -> usize; +``` + +#### `write_result` + +Writes the final return data for the current call. + +```rust +pub fn write_result(data: *const u8, len: usize); +``` + +**Behavior:** + +- Does not cause program to exit +- If not called, return data will be empty +- Program exits naturally when entrypoint returns + +#### `contract_address` + +Gets the address of the current program. Equivalent to EVM's `ADDRESS` opcode. + +```rust +pub fn contract_address(address: *mut u8); +``` + +### Debug and console + +Debug-only functions for development. + +#### `log_txt` + +Prints UTF-8 text to console. Only available in debug mode. + +```rust +pub fn log_txt(text: *const u8, len: usize); +``` + +#### `log_i32` / `log_i64` + +Prints integers to console. Only available in debug mode. + +```rust +pub fn log_i32(value: i32); +pub fn log_i64(value: i64); +``` + +#### `log_f32` / `log_f64` + +Prints floating-point numbers to console. Only available in debug mode with floating point enabled. + +```rust +pub fn log_f32(value: f32); +pub fn log_f64(value: f64); +``` + +**Higher-level usage:** + +```rust +use stylus_sdk::console; + +console!("Value: {}", value); // Prints in debug mode, no-op in production +``` + +## Safety considerations + +All hostio functions are marked `unsafe` because they: + +1. **Operate on raw pointers**: Require correct memory management +2. **Lack bounds checking**: Can cause undefined behavior if pointers are invalid +3. **Have side effects**: Can modify contract state or make external calls +4. **May revert**: Some operations can cause the transaction to revert + +### Safe usage patterns + +**Always validate inputs:** + +```rust +// Bad: unchecked pointer usage +unsafe { + hostio::msg_sender(ptr); // ptr might be invalid +} + +// Good: use safe wrappers +let sender = self.vm().msg_sender(); +``` + +**Use SDK wrappers:** + +```rust +// Bad: direct hostio call +unsafe { + let mut balance = [0u8; 32]; + hostio::account_balance(addr.as_ptr(), balance.as_mut_ptr()); +} + +// Good: use SDK wrapper +use stylus_sdk::evm; +let balance = evm::balance(addr); +``` + +**Handle return values:** + +```rust +// Check return status from calls +let status = unsafe { + hostio::call_contract( + contract.as_ptr(), + calldata.as_ptr(), + calldata.len(), + value.as_ptr(), + gas, + &mut return_len, + ) +}; + +if status != 0 { + // Handle call failure +} +``` + +## Higher-level wrappers + +The Stylus SDK provides safe, ergonomic wrappers around hostio functions: + +### Storage operations + +```rust +// Instead of direct hostio: +unsafe { + hostio::storage_load_bytes32(key.as_ptr(), dest.as_mut_ptr()); +} + +// Use storage types: +use stylus_sdk::storage::StorageU256; + +#[storage] +pub struct Contract { + value: StorageU256, +} + +let value = self.value.get(); // Safe, ergonomic +``` + +### Contract calls + +```rust +// Instead of direct hostio: +unsafe { + hostio::call_contract(/* many parameters */); +} + +// Use RawCall or typed interfaces: +use stylus_sdk::call::RawCall; + +let result = unsafe { + RawCall::new(self.vm()) + .gas(100_000) + .call(contract, &calldata)? +}; +``` + +### VM context + +```rust +// Instead of direct hostio: +unsafe { + let mut sender = [0u8; 20]; + hostio::msg_sender(sender.as_mut_ptr()); +} + +// Use VM accessor: +let sender = self.vm().msg_sender(); +let value = self.vm().msg_value(); +let timestamp = self.vm().block_timestamp(); +``` + +## Feature flags + +Hostio behavior changes based on feature flags: + +### `export-abi` + +When enabled, hostio functions are stubbed and return `unimplemented!()`. Used for ABI generation. + +### `stylus-test` + +When enabled, hostio functions panic with an error message. Use `TestVM` for testing instead. + +### `debug` + +When enabled, console logging functions become available. In production, console functions are no-ops. + +## Performance considerations + +### Direct hostio vs SDK wrappers + +- **Direct hostio**: Slightly lower overhead, requires manual memory management +- **SDK wrappers**: Minimal overhead (often zero-cost abstractions), much safer + +**Recommendation:** Use SDK wrappers unless profiling shows a specific performance bottleneck. + +### Storage caching + +The Stylus VM automatically caches storage operations: + +```rust +// First read: full SLOAD cost +let value1 = storage.value.get(); + +// Subsequent reads: reduced cost from cache +let value2 = storage.value.get(); + +// Writes are cached until flush +storage.value.set(new_value); // Cached + +// Cache is flushed automatically at call boundaries +``` + +### Gas vs ink + +Stylus uses "ink" for fine-grained gas metering: + +- **Ink**: WASM execution cost in Stylus-specific units +- **Gas**: Standard EVM gas units +- Conversion happens automatically + +Most developers don't need to think about ink vs gas distinction. + +## Common patterns + +### Check-effects-interactions pattern + +```rust +#[public] +impl MyContract { + pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), Vec> { + // Checks + let sender = self.vm().msg_sender(); + let balance = self.balances.get(sender); + if balance < amount { + return Err(b"Insufficient balance".to_vec()); + } + + // Effects + self.balances.setter(sender).set(balance - amount); + self.balances.setter(to).set(self.balances.get(to) + amount); + + // Interactions (if any) + Ok(()) + } +} +``` + +### Efficient event logging + +```rust +sol! { + event DataUpdated(bytes32 indexed key, uint256 value); +} + +// SDK handles hostio::emit_log internally +self.vm().log(DataUpdated { + key: key_hash, + value: new_value, +}); +``` + +### Gas-limited external calls + +```rust +use stylus_sdk::call::RawCall; + +// Limit gas to prevent griefing +let result = unsafe { + RawCall::new(self.vm()) + .gas(50_000) // Fixed gas limit + .call(untrusted_contract, &calldata) +}; + +match result { + Ok(data) => { /* process return data */ }, + Err(_) => { /* handle failure gracefully */ }, +} +``` + +## Testing with hostio + +Hostio functions are not available in the test environment. Use `TestVM` instead: + +```rust +#[cfg(test)] +mod tests { + use super::*; + use stylus_sdk::testing::*; + + #[test] + fn test_function() { + let vm = TestVM::default(); + let mut contract = MyContract::from(&vm); + + // VM functions work in tests + let sender = vm.msg_sender(); // Works + + // Direct hostio would panic + // unsafe { hostio::msg_sender(...) } // Would panic + } +} +``` + +## Resources + +- [Stylus VM specification](https://github.com/OffchainLabs/stylus) +- [EVM opcodes reference](https://www.evm.codes/) +- [Arbitrum block numbers and time](https://developer.arbitrum.io/time) +- [Ink and gas metering](https://docs.arbitrum.io/stylus/concepts/gas-metering) +- [stylus-sdk-rs source](https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/stylus-sdk/src/hostio.rs) + +## Best practices + +1. **Use SDK wrappers**: Prefer high-level abstractions over direct hostio calls +2. **Validate inputs**: Always check pointers and sizes before unsafe operations +3. **Handle errors**: Check return values from call operations +4. **Test thoroughly**: Use `TestVM` for comprehensive testing +5. **Profile first**: Only optimize to direct hostio if profiling shows it's necessary +6. **Document unsafe code**: Always document why `unsafe` is necessary +7. **Minimize unsafe blocks**: Keep `unsafe` blocks as small as possible diff --git a/docs/stylus/advanced/minimal-entrypoint-contracts.mdx b/docs/stylus/advanced/minimal-entrypoint-contracts.mdx new file mode 100644 index 0000000000..d3d530ba7f --- /dev/null +++ b/docs/stylus/advanced/minimal-entrypoint-contracts.mdx @@ -0,0 +1,644 @@ +--- +title: '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 without high-level macros. +displayed_sidebar: buildStylusSidebar +--- + +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 diff --git a/docs/stylus/recommended-libraries.md b/docs/stylus/advanced/recommended-libraries.mdx similarity index 96% rename from docs/stylus/recommended-libraries.md rename to docs/stylus/advanced/recommended-libraries.mdx index 11f5485db1..213c1b47e3 100644 --- a/docs/stylus/recommended-libraries.md +++ b/docs/stylus/advanced/recommended-libraries.mdx @@ -1,11 +1,9 @@ --- id: recommended-libraries -title: Recommended Libraries -sidebar_label: Use Rust Crates +title: Recommended Libraries (Rust crates) +sidebar_label: Recommended Libraries --- -# Recommended libraries - ## Using public Rust crates Rust provides a package registry at [crates.io](https://crates.io/), which lets developers conveniently access a plethora of open source libraries to utilize as dependencies in their code. Stylus Rust contracts can take advantage of these crates to simplify their development workflow. diff --git a/docs/stylus/advanced/solidity-differences.mdx b/docs/stylus/advanced/solidity-differences.mdx new file mode 100644 index 0000000000..984ab66bc4 --- /dev/null +++ b/docs/stylus/advanced/solidity-differences.mdx @@ -0,0 +1,742 @@ +--- +title: 'Differences between Solidity and Stylus' +description: 'Learn the key differences between writing smart contracts in Solidity versus Stylus Rust' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers who need to compare Stylus with Solidity. +displayed_sidebar: buildStylusSidebar +--- + +Stylus introduces a new paradigm for writing smart contracts on Arbitrum using Rust and other WebAssembly-compatible languages. While Stylus contracts maintain full interoperability with Solidity contracts, there are important differences in how you structure and write code. This guide helps Solidity developers understand these differences. + +## Language and syntax + +### Contract structure + +**Solidity:** + +```solidity +contract MyContract { + uint256 private value; + address public owner; + + constructor(uint256 initialValue) { + value = initialValue; + owner = msg.sender; + } + + function setValue(uint256 newValue) public { + value = newValue; + } +} +``` + +**Stylus (Rust):** + +```rust +use stylus_sdk::prelude::*; +use stylus_sdk::alloy_primitives::{Address, U256}; + +#[storage] +#[entrypoint] +pub struct MyContract { + value: StorageU256, + owner: StorageAddress, +} + +#[public] +impl MyContract { + #[constructor] + pub fn constructor(&mut self, initial_value: U256) { + self.value.set(initial_value); + self.owner.set(self.vm().msg_sender()); + } + + pub fn set_value(&mut self, new_value: U256) { + self.value.set(new_value); + } +} +``` + +### Key structural differences + +1. **Attributes over keywords**: Stylus uses Rust attributes (`#[storage]`, `#[entrypoint]`, `#[public]`) instead of Solidity keywords +2. **Explicit storage types**: Storage variables use special types like `StorageU256`, `StorageAddress` +3. **Getter/setter pattern**: Storage access requires explicit `.get()` and `.set()` calls +4. **Module system**: Rust uses `mod` and `use` for imports instead of `import` + +## Function visibility and state mutability + +### Visibility + +**Solidity:** + +```solidity +function publicFunc() public {} +function externalFunc() external {} +function internalFunc() internal {} +function privateFunc() private {} +``` + +**Stylus:** + +```rust +#[public] +impl MyContract { + // Public external functions + pub fn public_func(&self) {} + + // Internal functions (not in #[public] block) + fn internal_func(&self) {} + + // Private functions + fn private_func(&self) {} +} +``` + +In Stylus: + +- Functions in `#[public]` blocks are externally callable +- Regular `pub fn` outside `#[public]` blocks are internal +- Non-pub functions are private to the module + +### State mutability + +**Solidity:** + +```solidity +function viewFunc() public view returns (uint256) {} +function pureFunc() public pure returns (uint256) {} +function payableFunc() public payable {} +``` + +**Stylus:** + +```rust +#[public] +impl MyContract { + // View function (immutable reference) + pub fn view_func(&self) -> U256 { + self.value.get() + } + + // Pure function (no self reference) + pub fn pure_func(a: U256, b: U256) -> U256 { + a + b + } + + // Payable function + #[payable] + pub fn payable_func(&mut self) { + // Can receive Ether + } + + // Write function (mutable reference) + pub fn write_func(&mut self) { + self.value.set(U256::from(42)); + } +} +``` + +State mutability in Stylus is determined by: + +- `&self` → View (read-only) +- `&mut self` → Write (can modify storage) +- No `self` → Pure (no storage access) +- `#[payable]` → Can receive Ether + +## Constructors + +**Solidity:** + +```solidity +constructor(uint256 initialValue) { + value = initialValue; +} +``` + +**Stylus:** + +```rust +#[public] +impl MyContract { + #[constructor] + pub fn constructor(&mut self, initial_value: U256) { + self.value.set(initial_value); + } +} +``` + +Key differences: + +- Use `#[constructor]` attribute +- Constructor name is always `constructor` +- Can be marked `#[payable]` if needed +- Called only once during deployment +- Each contract struct can have only one constructor + +## Modifiers + +Solidity modifiers don't exist in Stylus. Instead, use regular Rust patterns. + +**Solidity:** + +```solidity +modifier onlyOwner() { + require(msg.sender == owner, "Not owner"); + _; +} + +function sensitiveFunction() public onlyOwner { + // Function logic +} +``` + +**Stylus:** + +```rust +impl MyContract { + fn only_owner(&self) -> Result<(), Vec> { + if self.owner.get() != self.vm().msg_sender() { + return Err(b"Not owner".to_vec()); + } + Ok(()) + } +} + +#[public] +impl MyContract { + pub fn sensitive_function(&mut self) -> Result<(), Vec> { + self.only_owner()?; + // Function logic + Ok(()) + } +} +``` + +Or using custom errors: + +```rust +sol! { + error Unauthorized(); +} + +#[derive(SolidityError)] +pub enum MyErrors { + Unauthorized(Unauthorized), +} + +impl MyContract { + fn only_owner(&self) -> Result<(), MyErrors> { + if self.owner.get() != self.vm().msg_sender() { + return Err(MyErrors::Unauthorized(Unauthorized {})); + } + Ok(()) + } +} +``` + +## Fallback and receive functions + +**Solidity:** + +```solidity +receive() external payable { + // Handle plain Ether transfers +} + +fallback() external payable { + // Handle unmatched calls +} +``` + +**Stylus:** + +```rust +#[public] +impl MyContract { + #[receive] + #[payable] + pub fn receive(&mut self) -> Result<(), Vec> { + // Handle plain Ether transfers + Ok(()) + } + + #[fallback] + #[payable] + pub fn fallback(&mut self, calldata: &[u8]) -> ArbResult { + // Handle unmatched calls + Ok(Vec::new()) + } +} +``` + +Key differences: + +- Use `#[receive]` and `#[fallback]` attributes +- Receive function takes no parameters +- Fallback function receives calldata as a parameter +- Both return `Result` types + +## Events + +**Solidity:** + +```solidity +event Transfer(address indexed from, address indexed to, uint256 value); + +function transfer() public { + emit Transfer(msg.sender, recipient, amount); +} +``` + +**Stylus:** + +```rust +sol! { + event Transfer(address indexed from, address indexed to, uint256 value); +} + +#[public] +impl MyContract { + pub fn transfer(&mut self, recipient: Address, amount: U256) { + self.vm().log(Transfer { + from: self.vm().msg_sender(), + to: recipient, + value: amount, + }); + } +} +``` + +Key differences: + +- Define events in `sol!` macro +- Emit using `self.vm().log()` +- Up to 3 parameters can be indexed +- Can also use `raw_log()` for custom logging + +## Error handling + +**Solidity:** + +```solidity +error InsufficientBalance(uint256 requested, uint256 available); + +function withdraw(uint256 amount) public { + if (balance < amount) { + revert InsufficientBalance(amount, balance); + } +} +``` + +**Stylus:** + +```rust +sol! { + error InsufficientBalance(uint256 requested, uint256 available); +} + +#[derive(SolidityError)] +pub enum MyErrors { + InsufficientBalance(InsufficientBalance), +} + +#[public] +impl MyContract { + pub fn withdraw(&mut self, amount: U256) -> Result<(), MyErrors> { + let balance = self.balance.get(); + if balance < amount { + return Err(MyErrors::InsufficientBalance(InsufficientBalance { + requested: amount, + available: balance, + })); + } + Ok(()) + } +} +``` + +Key differences: + +- Define errors in `sol!` macro +- Create error enum with `#[derive(SolidityError)]` +- Return `Result` +- Use Rust's `?` operator for error propagation + +## Inheritance + +**Solidity:** + +```solidity +contract Base { + function baseFoo() public virtual {} +} + +contract Derived is Base { + function baseFoo() public override {} +} +``` + +**Stylus:** + +```rust +#[public] +trait IBase { + fn base_foo(&self); +} + +#[storage] +struct Base {} + +#[public] +impl IBase for Base { + fn base_foo(&self) { + // Implementation + } +} + +#[storage] +#[entrypoint] +struct Derived { + base: Base, +} + +#[public] +#[implements(IBase)] +impl Derived {} + +#[public] +impl IBase for Derived { + fn base_foo(&self) { + // Override implementation + } +} +``` + +Key differences: + +- Use Rust traits for interfaces +- Composition through storage fields +- Use `#[implements()]` to expose inherited interfaces +- No `virtual` or `override` keywords + +## Storage + +### Storage slots + +**Solidity:** + +```solidity +uint256 public value; +mapping(address => uint256) public balances; +uint256[] public items; +``` + +**Stylus:** + +```rust +#[storage] +pub struct MyContract { + value: StorageU256, + balances: StorageMap, + items: StorageVec, +} +``` + +### Storage access + +**Solidity:** + +```solidity +value = 42; +uint256 x = value; +balances[user] = 100; +``` + +**Stylus:** + +```rust +self.value.set(U256::from(42)); +let x = self.value.get(); +self.balances.setter(user).set(U256::from(100)); +``` + +Key differences: + +- Explicit storage types (`Storage*`) +- Must use `.get()` and `.set()` +- Maps use `.setter()` for write access +- Storage layout is compatible with Solidity + +## Constants and immutables + +**Solidity:** + +```solidity +uint256 public constant MAX_SUPPLY = 1000000; +address public immutable OWNER; + +constructor() { + OWNER = msg.sender; +} +``` + +**Stylus:** + +```rust +const MAX_SUPPLY: u64 = 1000000; + +#[storage] +#[entrypoint] +pub struct MyContract { + owner: StorageAddress, // Set in constructor +} + +#[public] +impl MyContract { + #[constructor] + pub fn constructor(&mut self) { + self.owner.set(self.vm().msg_sender()); + } +} +``` + +Key differences: + +- Use Rust `const` for constants +- No direct equivalent to `immutable` (use storage set once in constructor) +- Constants can be defined outside structs + +## Type system + +### Integer types + +| Solidity | Stylus (Rust) | Notes | +| -------------------- | ---------------------- | ------------------------------------- | +| `uint8` to `uint256` | `u8` to `u128`, `U256` | Native Rust types or Alloy primitives | +| `int8` to `int256` | `i8` to `i128`, `I256` | Signed integers | +| `address` | `Address` | 20-byte addresses | +| `bytes` | `Bytes` | Dynamic bytes | +| `bytesN` | `FixedBytes` | Fixed-size bytes | +| `string` | `String` | UTF-8 strings | + +### Arrays and mappings + +| Solidity | Stylus (Rust) | Notes | +| ----------------------------- | ------------------------------------------------------------ | -------------- | +| `uint256[]` | `Vec` (memory)
`StorageVec` (storage) | Dynamic arrays | +| `uint256[5]` | `[U256; 5]` | Fixed arrays | +| `mapping(address => uint256)` | `StorageMap` | Key-value maps | + +## Global variables and functions + +### Block and transaction properties + +| Solidity | Stylus (Rust) | +| ----------------- | ----------------------------- | +| `msg.sender` | `self.vm().msg_sender()` | +| `msg.value` | `self.vm().msg_value()` | +| `msg.data` | Access through calldata | +| `tx.origin` | `self.vm().tx_origin()` | +| `tx.gasprice` | `self.vm().tx_gas_price()` | +| `block.number` | `self.vm().block_number()` | +| `block.timestamp` | `self.vm().block_timestamp()` | +| `block.basefee` | `self.vm().block_basefee()` | +| `block.coinbase` | `self.vm().block_coinbase()` | + +### Cryptographic functions + +| Solidity | Stylus (Rust) | +| ----------------- | ---------------------------------- | +| `keccak256(data)` | `self.vm().native_keccak256(data)` | +| `sha256(data)` | Use external crate | +| `ecrecover(...)` | Use `crypto::recover()` | + +## External calls + +**Solidity:** + +```solidity +(bool success, bytes memory data) = address.call{value: amount}(data); +``` + +**Stylus:** + +```rust +use stylus_sdk::call::RawCall; + +let result = unsafe { + RawCall::new(self.vm()) + .value(amount) + .call(address, &data) +}; +``` + +Key differences: + +- Use `RawCall` for raw calls +- Calls are `unsafe` in Rust +- Use type-safe interfaces when possible via `sol_interface!` + +## Contract deployment + +**Solidity:** + +```solidity +new MyContract{value: amount}(arg1, arg2); +``` + +**Stylus:** + +```rust +use stylus_sdk::deploy::RawDeploy; + +let contract_address = unsafe { + RawDeploy::new(self.vm()) + .value(amount) + .deploy(&bytecode, salt)? +}; +``` + +## Assembly + +**Solidity:** + +```solidity +assembly { + let x := mload(0x40) + sstore(0, x) +} +``` + +**Stylus:** + +Stylus does not support inline assembly. Instead: + +- Use hostio functions for low-level operations +- Use Rust's `unsafe` blocks when necessary +- Direct memory manipulation through safe Rust APIs + +## Features not in Stylus + +1. **No inline assembly**: Use hostio or safe Rust instead +2. **No `selfdestruct`**: Deprecated in Ethereum, not available in Stylus +3. **No `delegatecall` from storage**: Available but requires careful use +4. **No modifier syntax**: Use regular functions +5. **No multiple inheritance complexity**: Use trait-based composition + +## Features unique to Stylus + +1. **Rust's type system**: Strong compile-time guarantees +2. **Zero-cost abstractions**: No overhead for safe code patterns +3. **Cargo ecosystem**: Access to thousands of Rust crates +4. **Memory safety**: Rust's borrow checker prevents common bugs +5. **Better performance**: Wasm execution can be more efficient +6. **Testing framework**: Use Rust's built-in testing with `TestVM` + +## Memory and gas costs + +### Memory management + +- **Solidity**: Automatic memory management with gas costs for allocation +- **Stylus**: Manual control with Rust's ownership system, more efficient memory usage + +### Gas efficiency + +Stylus programs typically use less gas than equivalent Solidity: + +- More efficient Wasm execution +- Better compiler optimizations +- Fine-grained control over allocations + +## Development workflow + +### Compilation + +**Solidity:** + +```shell +solc --bin --abi MyContract.sol +``` + +**Stylus:** + +```shell +cargo stylus build +``` + +### Testing + +**Solidity:** + +```javascript +// Hardhat or Foundry tests +``` + +**Stylus:** + +```rust +#[cfg(test)] +mod tests { + use super::*; + use stylus_sdk::testing::*; + + #[test] + fn test_function() { + let vm = TestVM::default(); + let mut contract = MyContract::from(&vm); + // Test logic + } +} +``` + +### Deployment + +Both use similar deployment processes but Stylus requires an activation step for new programs. + +## Interoperability + +Stylus and Solidity contracts can fully interact: + +- Stylus can call Solidity contracts +- Solidity can call Stylus contracts +- Same ABI encoding/decoding +- Share storage layout compatibility + +Example calling Solidity from Stylus: + +```rust +sol_interface! { + interface IToken { + function transfer(address to, uint256 amount) external returns (bool); + } +} + +#[public] +impl MyContract { + pub fn call_token(&self, token: Address, recipient: Address, amount: U256) -> Result> { + let token_contract = IToken::new(token); + let result = token_contract.transfer(self.vm(), recipient, amount)?; + Ok(result) + } +} +``` + +## Best practices for transitioning + +1. **Think in Rust patterns**: Don't translate Solidity directly, use idiomatic Rust +2. **Leverage the type system**: Use Rust's types to prevent bugs at compile time +3. **Use composition over inheritance**: Prefer traits and composition +4. **Handle errors explicitly**: Use `Result` types and the `?` operator +5. **Write tests in Rust**: Take advantage of `TestVM` for unit testing +6. **Read existing examples**: Study the stylus-sdk-rs examples directory +7. **Start small**: Convert simple contracts first to learn the patterns + +## Resources + +- [Stylus SDK Documentation](https://docs.arbitrum.io/stylus/reference/stylus-sdk) +- [stylus-sdk-rs Repository](https://github.com/OffchainLabs/stylus-sdk-rs) +- [Example Contracts](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/examples) +- [Rust Book](https://doc.rust-lang.org/book/) for learning Rust diff --git a/docs/stylus/concepts/activation.mdx b/docs/stylus/concepts/activation.mdx new file mode 100644 index 0000000000..24c8edf9ed --- /dev/null +++ b/docs/stylus/concepts/activation.mdx @@ -0,0 +1,788 @@ +--- +title: 'Activation' +description: 'Understanding Stylus contract deployment and activation' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers deploying Stylus contracts to Arbitrum chains. +displayed_sidebar: buildStylusSidebar +--- + +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 + + diff --git a/docs/stylus/concepts/evm-differences.mdx b/docs/stylus/concepts/evm-differences.mdx new file mode 100644 index 0000000000..e844c7a6ee --- /dev/null +++ b/docs/stylus/concepts/evm-differences.mdx @@ -0,0 +1,638 @@ +--- +title: 'EVM and WASM VM differences' +description: 'Understand the key differences between the traditional EVM and Nitro WASM VM for Stylus contracts' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers who need to understand how Stylus works with EVM differences. +displayed_sidebar: buildStylusSidebar +--- + +Arbitrum Nitro supports two execution environments: the traditional Ethereum Virtual Machine (EVM) for Solidity contracts and a WebAssembly (WASM) VM for Stylus contracts. While both environments are fully interoperable and share the same state, they differ significantly in their execution models, performance characteristics, and developer experience. + +## Execution model + +### EVM: Stack-based architecture + +The EVM uses a stack-based execution model: + +- **Operations**: Work with values on a stack (PUSH, POP, ADD, etc.) +- **Opcodes**: 256 predefined opcodes with fixed gas costs +- **Memory**: Linear, byte-addressable memory that grows dynamically +- **Storage**: 256-bit word-based key-value store +- **Call depth**: Limited to 1024 levels + +**Example EVM execution:** + +``` +PUSH1 0x02 // Push 2 onto stack +PUSH1 0x03 // Push 3 onto stack +ADD // Pop 2 values, push sum (5) +``` + +### WASM: Register-based architecture + +The Stylus WASM VM uses a register-based execution model: + +- **Operations**: Work with virtual registers and local variables +- **Instructions**: Thousands of WASM instructions with fine-grained metering +- **Memory**: Linear memory with explicit grow operations +- **Storage**: Same 256-bit storage as EVM (shared state) +- **Call depth**: Same 1024 limit for compatibility + +**Example WASM execution:** + +```wasm +(local.get 0) ;; Read from local variable 0 +(local.get 1) ;; Read from local variable 1 +(i32.add) ;; Add and store result +(local.set 2) ;; Store in local variable 2 +``` + +## Memory model + +### EVM memory + +- **Dynamic expansion**: Memory grows in 32-byte chunks +- **Gas cost**: Quadratic growth (memory expansion gets expensive) +- **Access pattern**: Byte-level addressing +- **Limit**: Practical limit around 15 MB due to gas costs + +### WASM memory + +- **Page-based**: Memory grows in 64 KB pages (WASM standard) +- **Gas cost**: Linear cost per page through `pay_for_memory_grow` +- **Access pattern**: Direct memory load/store instructions +- **Limit**: Can grow much larger efficiently + +**Memory growth in Stylus:** + +```rust +// The entrypoint macro automatically handles pay_for_memory_grow +#[entrypoint] +pub struct MyContract { + // Large data structures are more practical in WASM + data: StorageVec, +} + +// Nitro automatically inserts pay_for_memory_grow calls +// when allocating new pages +let large_vector = vec![0u8; 100_000]; // Efficient in WASM +``` + +:::note +The Stylus SDK's `entrypoint!` macro includes a no-op call to `pay_for_memory_grow` to ensure the function is referenced. Nitro then automatically inserts actual calls when memory allocation occurs. +::: + +## Gas metering: Ink and gas + +### EVM gas metering + +- **Unit**: Gas (standard Ethereum unit) +- **Granularity**: Per opcode (e.g., ADD = 3 gas, SSTORE = 20,000 gas) +- **Measurement**: Coarse-grained +- **Refunds**: Available for storage deletions + +### Stylus ink metering + +Stylus introduces "ink" as a fine-grained metering unit: + +- **Unit**: Ink (Stylus-specific, converted to gas) +- **Granularity**: Per WASM instruction (more fine-grained) +- **Measurement**: Precise tracking of WASM execution costs +- **Conversion**: Ink → Gas conversion happens automatically + +**Ink to gas conversion:** + +```rust +// Check remaining ink +let ink_left = evm_ink_left(); + +// Check remaining gas +let gas_left = evm_gas_left(); + +// Get ink price (in gas basis points) +let ink_price = tx_ink_price(); + +// Conversion formula: +// gas = ink * ink_price / 10000 +``` + +**Why ink?** + +1. **Precision**: WASM instructions have varying costs that don't map cleanly to EVM gas +2. **Efficiency**: Fine-grained metering allows for more accurate pricing +3. **Performance**: Enables cheaper execution for compute-heavy operations +4. **Flexibility**: Ink prices can be adjusted without changing contract code + +**Gas cost comparison:** + +| Operation | EVM Gas | Stylus Gas | Improvement | +| ------------------- | --------- | ---------- | --------------- | +| Basic arithmetic | 3-5 | ~1-2 | 2-3x cheaper | +| Memory operations | Variable | Efficient | 10-100x cheaper | +| Complex computation | Expensive | Cheap | 10-100x cheaper | +| Storage operations | Same | Same | Equal | +| External calls | Same | Same | Equal | + +## Instruction sets + +### EVM opcodes + +- **Count**: ~140 opcodes +- **Categories**: Arithmetic, logic, storage, flow control, system +- **Size**: 1 byte per opcode +- **Examples**: + - `ADD`, `MUL`, `SUB`, `DIV` (arithmetic) + - `SLOAD`, `SSTORE` (storage) + - `CALL`, `DELEGATECALL` (calls) + - `SHA3` (hashing) + +### WASM instructions + +- **Count**: Hundreds of instructions +- **Categories**: Numeric, memory, control flow, function calls +- **Size**: Variable encoding (1-5 bytes) +- **Examples**: + - `i32.add`, `i64.mul`, `f64.div` (numeric) + - `memory.grow`, `memory.size` (memory) + - `call`, `call_indirect` (functions) + - Hostio imports (system operations) + +**WASM advantages:** + +- More expressive instruction set +- Better compiler optimization targets +- Efficient handling of complex data structures +- Native support for 32-bit and 64-bit operations + +## Size limits + +### EVM contracts + +- **Maximum size**: 24,576 bytes (24 KB) of deployed bytecode +- **Limit reason**: Block gas limit and deployment costs +- **Workaround**: Contract splitting, proxies + +### Stylus contracts + +- **Initial limit**: 24 KB (same as EVM for compatibility) +- **Compressed size**: Can be larger before compression +- **Future**: Limit may be increased as WASM tooling improves +- **Practical size**: Stylus programs are often smaller due to efficient compilation + +**Size optimization:** + +```rust +// Stylus contracts benefit from: +// 1. Rust's zero-cost abstractions +// 2. Dead code elimination by wasm-opt +// 3. Efficient WASM encoding + +#[no_std] // Opt out of standard library for smaller binaries +extern crate alloc; + +// Only the code actually used is included +use stylus_sdk::prelude::*; +``` + +## Storage model + +Both EVM and WASM contracts use the **same storage system**: + +- **Format**: 256-bit key-value store +- **Compatibility**: EVM and WASM contracts can share storage +- **Costs**: SLOAD and SSTORE costs are identical +- **Caching**: Stylus VM implements storage caching for efficiency + +### Storage caching in Stylus + +```rust +use stylus_sdk::prelude::*; + +#[storage] +pub struct Counter { + count: StorageU256, +} + +#[public] +impl Counter { + pub fn increment(&mut self) { + // First read: full SLOAD cost + let current = self.count.get(); + + // Write is cached + self.count.set(current + U256::from(1)); + + // Additional reads in same call are cheaper (cached) + let new_value = self.count.get(); + + // Cache is automatically flushed at call boundary + } +} +``` + +**Cache benefits:** + +1. **Reduced gas costs**: Repeated reads are cheaper +2. **Better performance**: Fewer state trie accesses +3. **Automatic management**: SDK handles cache flushing +4. **Compatibility**: Refund logic matches EVM exactly + +## Performance characteristics + +### Compute operations + +| Category | EVM | Stylus WASM | Winner | +| ------------------- | --------- | ----------- | ------------ | +| Integer arithmetic | Moderate | Fast | WASM (10x+) | +| Loops | Expensive | Cheap | WASM (100x+) | +| Memory copying | Expensive | Cheap | WASM (10x+) | +| Hashing (keccak256) | Native | Native | Equal | +| Cryptography | Limited | Efficient | WASM | +| String operations | Expensive | Cheap | WASM (100x+) | + +### Storage operations + +| Operation | EVM | Stylus WASM | Winner | +| --------------- | ---------- | ----------- | ------ | +| SLOAD | 2,100 gas | 2,100 gas | Equal | +| SSTORE (new) | 20,000 gas | 20,000 gas | Equal | +| SSTORE (update) | 5,000 gas | 5,000 gas | Equal | +| Storage refunds | Standard | Standard | Equal | +| Cached reads | No | Yes | WASM | + +### External interactions + +| Operation | EVM | Stylus WASM | Winner | +| -------------------- | ------------- | ------------- | ------ | +| Contract calls | ~700 gas base | ~700 gas base | Equal | +| Cross-language calls | N/A | Efficient | WASM | +| Event emission | Same cost | Same cost | Equal | +| Value transfers | Same cost | Same cost | Equal | + +## Call semantics + +### Interoperability + +Both environments support seamless interoperability: + +```rust +// Stylus calling Solidity +sol_interface! { + interface IERC20 { + function transfer(address to, uint256 amount) external returns (bool); + } +} + +#[public] +impl MyContract { + pub fn call_evm_contract(&self, token: Address) -> Result> { + let erc20 = IERC20::new(token); + let result = erc20.transfer(self.vm(), recipient, amount)?; + Ok(result) + } +} +``` + +```solidity +// Solidity calling Stylus +interface IStylusContract { + function computeHash(bytes calldata data) external view returns (bytes32); +} + +contract EvmContract { + function useStylus(address stylusAddr, bytes calldata data) public view returns (bytes32) { + return IStylusContract(stylusAddr).computeHash(data); + } +} +``` + +### Call costs + +- **Same call overhead**: Both directions have similar base costs +- **ABI encoding**: Identical for both +- **Gas forwarding**: Follows 63/64 rule in both cases +- **Return data**: Handled consistently + +## Contract lifecycle + +### Deployment + +**EVM contracts:** + +1. Submit init code (constructor bytecode) +2. EVM executes init code +3. Returns runtime bytecode +4. Bytecode stored onchain + +**Stylus contracts:** + +1. Compile Rust → WASM +2. Submit WASM code +3. **Activation step**: One-time compilation to native code +4. Activated programs cached for efficiency +5. WASM code stored onchain + +**Activation benefits:** + +```shell +# Deploy and activate a Stylus program +cargo stylus deploy --private-key $PRIVATE_KEY + +# Activation happens once +# Subsequent calls use cached native code +``` + +- **One-time cost**: Pay activation gas once +- **Future savings**: All executions use optimized native code +- **Upgradeability**: Re-activation needed for upgrades + +### Execution flow + +**EVM contracts:** + +``` +Transaction → EVM → Opcode interpretation → State changes +``` + +**Stylus contracts:** + +``` +Transaction → WASM VM → Native code execution → State changes + ↓ + Hostio calls for state access +``` + +## Developer experience + +### EVM development + +**Languages**: Solidity, Vyper, Huff + +**Tools**: + +- Hardhat, Foundry for testing +- Remix for quick development +- Ethers.js/Web3.js for interaction + +**Debugging**: + +- Revert messages +- Events for tracing +- Stack traces limited + +### Stylus development + +**Languages**: Rust, C, C++ (any WASM-compatible language) + +**Tools**: + +- `cargo stylus` for deployment +- Standard Rust tooling (cargo, rustc) +- `TestVM` for unit testing +- Rust analyzer for IDE support + +**Debugging**: + +- Full Rust error messages +- Compile-time safety checks +- `console!` macro for debug builds +- Stack traces in development + +**Development comparison:** + +| Aspect | EVM | Stylus | Notes | +| --------------- | -------------- | ------------------- | ------------------------------------- | +| Type safety | Runtime | Compile-time | Rust catches errors before deployment | +| Memory safety | Manual | Automatic | Rust's borrow checker | +| Testing | External tools | Built-in Rust tests | `#[test]` functions work natively | +| Iteration speed | Slower | Faster | No need to redeploy for tests | +| Learning curve | Moderate | Steeper | Rust has more concepts | +| Maturity | Very mature | Growing | Solidity has more resources | + +## Feature compatibility + +### Supported features + +Both EVM and Stylus support: + +✅ Contract calls and delegate calls +✅ Value transfers +✅ Event emission +✅ Storage operations +✅ Block and transaction properties +✅ Cryptographic functions (keccak256) +✅ Contract creation (CREATE, CREATE2) +✅ Revert and error handling +✅ Reentrancy guards + +### EVM-specific features not in WASM + +❌ Inline assembly (use hostio or Rust instead) +❌ `selfdestruct` (deprecated in Ethereum anyway) +❌ Solidity modifiers (use Rust functions) +❌ Multiple inheritance (use traits and composition) + +### Stylus-specific features not in EVM + +✅ Access to Rust ecosystem (crates) +✅ Efficient memory management +✅ Zero-cost abstractions +✅ Compile-time guarantees +✅ Native testing support +✅ Better optimization opportunities + +## State sharing + +EVM and WASM contracts share the same blockchain state: + +```rust +// Stylus contract can read EVM contract storage +#[storage] +pub struct Bridge { + evm_contract: StorageAddress, +} + +#[public] +impl Bridge { + pub fn read_evm_storage(&self, key: U256) -> U256 { + // Both VMs use the same storage layout + // Can read storage written by EVM contracts + storage_load_bytes32(key) + } +} +``` + +**Shared state:** + +- Account balances +- Contract storage +- Contract code +- Transaction history +- Block data + +## Gas economics + +### Cost structure + +**EVM contract execution:** + +``` +Total cost = Base transaction cost (21,000 gas) + + Input data cost (~16 gas/byte) + + Execution cost (opcode gas) + + Storage cost (SLOAD/SSTORE) +``` + +**Stylus contract execution:** + +``` +Total cost = Base transaction cost (21,000 gas) + + Input data cost (~16 gas/byte) + + Execution cost (ink → gas conversion) + + Storage cost (same as EVM) +``` + +### When to use each + +**Use EVM (Solidity) when:** + +- Quick prototyping needed +- Simple contracts with minimal computation +- Team expertise in Solidity +- Extensive storage operations (cost is equal) +- Maximum ecosystem compatibility + +**Use Stylus (Rust) when:** + +- Compute-intensive operations +- Complex algorithms or data structures +- Need for memory safety guarantees +- Existing Rust codebase to port +- Optimizing for gas efficiency +- Cryptographic operations +- String/byte manipulation + +## Best practices + +### For EVM contracts + +1. **Minimize storage operations**: Use memory when possible +2. **Optimize loops**: Keep iterations minimal +3. **Pack storage**: Use smaller types when possible +4. **Avoid complex math**: Basic operations only +5. **Use libraries**: Leverage audited code + +### For Stylus contracts + +1. **Leverage Rust's safety**: Let the compiler catch bugs +2. **Use iterators**: More efficient than manual loops +3. **Profile before optimizing**: Use cargo-stylus tools +4. **Test thoroughly**: Use Rust's built-in test framework +5. **Consider binary size**: Use `#[no_std]` if needed +6. **Batch operations**: Take advantage of cheap compute + +### Hybrid approach + +Many projects can benefit from both: + +```rust +// Compute-heavy logic in Stylus +#[public] +impl ComputeEngine { + pub fn complex_calculation(&self, data: Vec) -> Vec { + // Efficient loops and data processing + data.iter() + .map(|x| expensive_computation(*x)) + .collect() + } +} +``` + +```solidity +// Coordination and state management in Solidity +contract Coordinator { + IComputeEngine public engine; // Stylus contract + + function process(uint256[] calldata data) public { + uint256[] memory results = engine.complex_calculation(data); + // Store results, emit events, etc. + } +} +``` + +## Migration considerations + +### From Solidity to Stylus + +**What stays the same:** + +- Contract addresses +- Storage layout +- ABIs and interfaces +- Gas for storage operations +- Event signatures + +**What changes:** + +- Programming language (Solidity → Rust) +- Execution engine (EVM → WASM) +- Gas costs for compute (usually cheaper) +- Development workflow +- Testing approach + +**Migration strategy:** + +1. Start with compute-heavy functions +2. Maintain same ABI for compatibility +3. Test extensively with existing contracts +4. Monitor gas costs in production +5. Gradually migrate more functionality + +## Future developments + +### EVM evolution + +- EIP improvements +- New opcodes +- Gas repricing +- EOF (EVM Object Format) + +### Stylus evolution + +- Support for more languages +- SIMD instructions +- Floating point operations +- Larger contract size limits +- Further gas optimizations +- Enhanced debugging tools + +## Resources + +- [Stylus documentation](https://docs.arbitrum.io/stylus) +- [Ink and gas metering](https://docs.arbitrum.io/stylus/concepts/gas-metering) +- [WASM specification](https://webassembly.github.io/spec/) +- [EVM opcodes reference](https://www.evm.codes/) +- [Stylus SDK repository](https://github.com/OffchainLabs/stylus-sdk-rs) + +## Summary + +The WASM VM in Arbitrum Nitro represents a significant evolution in smart contract execution: + +**Key advantages of WASM:** + +- 10-100x cheaper for compute operations +- More expressive programming languages +- Better memory management +- Compile-time safety guarantees +- Access to mature language ecosystems + +**Key advantages of EVM:** + +- Mature tooling and ecosystem +- Familiar to existing developers +- No activation cost +- Decades of collective knowledge + +Both execution environments coexist harmoniously on Arbitrum, allowing developers to choose the best tool for each use case while maintaining full interoperability. diff --git a/docs/stylus/concepts/gas-metering.mdx b/docs/stylus/concepts/gas-metering.mdx index 9354556719..c10a902f0d 100644 --- a/docs/stylus/concepts/gas-metering.mdx +++ b/docs/stylus/concepts/gas-metering.mdx @@ -5,7 +5,7 @@ author: rachel-bousfield sme: rachel-bousfield target_audience: 'Developers deploying smart contracts using Stylus.' sidebar_position: 3 -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildStylusSidebar --- **Gas and ink** are the pricing primitives that are used to determine the cost of handling specific opcodes and host I/Os on Stylus. For an overview of specific opcode and host I/O costs, see [Gas and ink costs](/stylus/reference/opcode-hostio-pricing). diff --git a/docs/stylus/concepts/webassembly.mdx b/docs/stylus/concepts/webassembly.mdx new file mode 100644 index 0000000000..07d296b3ae --- /dev/null +++ b/docs/stylus/concepts/webassembly.mdx @@ -0,0 +1,400 @@ +--- +title: 'WebAssembly in Nitro' +description: 'Understanding WebAssembly compilation, deployment, and execution in Arbitrum Nitro' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers who need to understand how Stylus works with WebAssembly. +displayed_sidebar: buildStylusSidebar +--- + +WebAssembly (WASM) is a binary instruction format that enables high-performance execution of programs in the Nitro virtual machine. This guide explains how WASM works in the context of Arbitrum Nitro and Stylus smart contract development. + +## What is WebAssembly? + +WebAssembly is a portable, size-efficient binary format designed for safe execution at near-native speeds. Key characteristics include: + +- **Binary format**: Compact representation that's faster to parse than text-based formats +- **Stack-based VM**: Simple execution model with operand stack +- **Sandboxed execution**: Memory-safe by design with explicit bounds checking +- **Language-agnostic**: Can be targeted by many programming languages (Rust, C, C++, etc.) + +## Why WebAssembly in Nitro? + +Nitro uses WebAssembly as its execution environment for several reasons: + +1. **Performance**: WASM compiles to native machine code for fast execution +2. **Security**: Sandboxed environment prevents unauthorized access +3. **Portability**: Same bytecode runs identically across all nodes +4. **Language flexibility**: Developers can use Rust, C, C++, or any language that compiles to WASM +5. **Determinism**: Guaranteed identical execution across all validators + +## WASM Compilation Target + +Stylus contracts are compiled to the `wasm32-unknown-unknown` target, which means: + +- **32-bit addressing**: Uses 32-bit pointers and memory addresses +- **Unknown OS**: No operating system dependencies +- **Unknown environment**: Minimal runtime assumptions (no std by default) + +The `.cargo/config.toml` file in Stylus projects configures the WASM target: + +```toml +[target.wasm32-unknown-unknown] +rustflags = [ + "-C", "link-arg=-zstack-size=32768", # 32KB stack + "-C", "target-feature=-reference-types", # Disable reference types + "-C", "target-feature=+bulk-memory", # Enable bulk memory operations +] +``` + +### Compilation flags + +- **Stack size**: Limited to 32KB to ensure bounded memory usage +- **Bulk memory**: Enables efficient `memory.copy` and `memory.fill` operations +- **No reference types**: Keeps the WASM simpler and more compatible + +## WASM Binary Structure + +A Stylus WASM module consists of several sections: + +### Exports + +Every Stylus contract exports a `user_entrypoint` function: + +```rust +#[no_mangle] +pub extern "C" fn user_entrypoint(len: usize) -> usize { + // Entry point for all contract calls + // len: size of calldata in bytes + // returns: size of output data in bytes +} +``` + +This function is automatically generated by the `#[entrypoint]` macro and serves as the single entry point for all contract interactions. + +### Imports + +WASM modules import low-level functions from the `vm_hooks` module: + +```rust +// Example hostio imports +extern "C" { + fn storage_load_bytes32(key: *const u8, dest: *mut u8); + fn storage_store_bytes32(key: *const u8, value: *const u8); + fn msg_sender(sender: *mut u8); + fn block_timestamp() -> u64; + // ... and many more +} +``` + +These imported functions (called "hostio" functions) provide access to blockchain state and functionality. + +### Memory + +WASM modules use linear memory, which is: + +- **Contiguous**: Single continuous address space starting at 0 +- **Growable**: Can expand at runtime (in 64KB pages) +- **Isolated**: Each contract has its own memory space + +Memory growth is explicitly metered: + +```rust +// Exported function that must exist +#[no_mangle] +pub extern "C" fn pay_for_memory_grow(pages: u16) { + // Called before memory.grow to charge for new pages + // Each page is 64KB +} +``` + +### Custom sections + +WASM supports custom sections for metadata: + +```rust +// Example: Add version information +#[link_section = ".custom.stylus-version"] +static VERSION: [u8; 5] = *b"0.1.0"; +``` + +Custom sections can store: + +- Contract version +- Source code hashes +- Compiler metadata +- ABI information + +## Compression and Deployment + +Before deployment, Stylus contracts undergo compression: + +### Brotli compression + +```rust +// From stylus-tools/src/utils/wasm.rs +pub fn brotli_compress(wasm: impl Read, compression_level: u32) -> io::Result> { + let mut compressed = Vec::new(); + let mut encoder = brotli::CompressorWriter::new(&mut compressed, 4096, compression_level, 22); + io::copy(&mut wasm, &mut encoder)?; + encoder.flush()?; + Ok(compressed) +} +``` + +Brotli compression typically reduces WASM size by 50-70%. + +### 0xEFF000 prefix + +Compressed WASM is prefixed with `0xEFF000` to identify it as a Stylus program: + +```rust +pub fn add_prefix(compressed_wasm: impl IntoIterator, prefix: &str) -> Vec { + let prefix_bytes = hex::decode(prefix.strip_prefix("0x").unwrap_or(prefix)).unwrap(); + prefix_bytes.into_iter().chain(compressed_wasm).collect() +} +``` + +This prefix allows the Nitro VM to distinguish Stylus contracts from EVM bytecode. + +## Contract Activation + +After deployment, contracts must be **activated** before execution: + +### Activation process + +1. **Initial deployment**: Contract code is stored on-chain (compressed) +2. **Activation call**: Special transaction invokes `activateProgram` +3. **Decompression**: Brotli-compressed WASM is decompressed +4. **Validation**: WASM is checked for: + - Valid structure + - Required exports (`user_entrypoint`) + - Allowed imports (only `vm_hooks`) + - Memory constraints +5. **Compilation**: WASM is compiled to native machine code +6. **Caching**: Compiled code is cached for future executions + +### One-time cost + +Activation incurs a one-time gas cost but provides benefits: + +- **Fast execution**: Native code runs 10-100x faster than interpreted +- **Persistent cache**: Compilation happens once, benefits all future calls +- **Optimizations**: Native compiler applies target-specific optimizations + +### Verification + +The activation process checks for the `pay_for_memory_grow` function to verify correct entrypoint setup: + +```rust +// From activation.rs +if !wasm::has_entrypoint(&wasm)? { + bail!("WASM is missing the entrypoint export"); +} +``` + +## Development Workflow + +### 1. Write Rust code + +```rust +use stylus_sdk::{alloy_primitives::U256, prelude::*}; + +#[entrypoint] +#[storage] +pub struct Counter { + count: StorageU256, +} + +#[public] +impl Counter { + pub fn increment(&mut self) { + let count = self.count.get() + U256::from(1); + self.count.set(count); + } +} +``` + +### 2. Compile to WASM + +```bash +cargo stylus build +``` + +This runs: + +```bash +cargo build \ + --lib \ + --locked \ + --release \ + --target wasm32-unknown-unknown \ + --target-dir target/wasm32-unknown-unknown/release +``` + +### 3. Optimize (optional) + +```bash +wasm-opt target/wasm32-unknown-unknown/release/my_contract.wasm \ + -O3 \ + --strip-debug \ + -o optimized.wasm +``` + +Optimization can reduce size by an additional 10-30%. + +### 4. Deploy and activate + +```bash +# Deploy compressed WASM +cargo stylus deploy --private-key=$PRIVATE_KEY + +# Activation happens automatically +``` + +## Size Limitations + +Nitro imposes limits on WASM contract size: + +| Limit | Value | Reason | +| --------------------- | ---------------------- | --------------------------------- | +| **Uncompressed size** | ~3-4 MB | Memory and processing constraints | +| **Compressed size** | 24 KB (initial) | Ethereum transaction size limit | +| **Compressed size** | 128 KB (with EIP-4844) | Larger blob transactions | + +To stay within limits: + +- Use `#[no_std]` to avoid standard library bloat +- Strip debug symbols with `--strip-debug` +- Enable aggressive optimization (`-O3`) +- Minimize dependencies +- Use compact data structures + +## Memory Model + +### Linear memory layout + +``` +0x00000000 ┌─────────────────┐ + │ Stack │ 32 KB fixed size +0x00008000 ├─────────────────┤ + │ Heap/Data │ Grows upward + │ │ + │ (Available) │ + │ │ +0xFFFFFFFF └─────────────────┘ +``` + +### Memory operations + +```rust +// Bulk memory operations (enabled by target config) +unsafe { + // Fast memory copy + core::ptr::copy_nonoverlapping(src, dst, len); + + // Fast memory fill + core::ptr::write_bytes(ptr, value, len); +} +``` + +The `bulk-memory` feature flag enables efficient WASM instructions like `memory.copy` and `memory.fill`. + +## Advanced: WASM Instructions + +Stylus uses WASM MVP (Minimum Viable Product) instructions plus bulk-memory operations: + +### Arithmetic + +- `i32.add`, `i32.sub`, `i32.mul`, `i32.div_s`, `i32.div_u` +- `i64.add`, `i64.sub`, `i64.mul`, `i64.div_s`, `i64.div_u` + +### Memory access + +- `i32.load`, `i32.store` (32-bit load/store) +- `i64.load`, `i64.store` (64-bit load/store) +- `memory.grow` (expand memory) +- `memory.copy` (bulk copy, requires flag) +- `memory.fill` (bulk fill, requires flag) + +### Control flow + +- `call`, `call_indirect` (function calls) +- `if`, `else`, `block`, `loop` (structured control flow) +- `br`, `br_if` (branching) + +### Not supported + +- ❌ Floating point operations (f32, f64) +- ❌ SIMD operations +- ❌ Reference types +- ❌ Multiple memories +- ❌ Threads + +## Best Practices + +### 1. Minimize binary size + +```rust +// Use #[no_std] when possible +#![no_std] +extern crate alloc; + +// Avoid large dependencies +// Prefer: alloy-primitives +// Avoid: serde_json, regex (unless necessary) +``` + +### 2. Optimize memory usage + +```rust +// Stack allocate when possible +let small_buffer = [0u8; 32]; + +// Heap allocate only when necessary +let large_buffer = vec![0u8; 1024]; +``` + +### 3. Profile before optimizing + +```bash +# Check binary size +ls -lh target/wasm32-unknown-unknown/release/*.wasm + +# Analyze with twiggy +cargo install twiggy +twiggy top target/wasm32-unknown-unknown/release/my_contract.wasm +``` + +### 4. Test locally + +```bash +# Use cargo-stylus for local testing +cargo stylus check +cargo stylus export-abi +``` + +### 5. Validate before deployment + +```rust +// Ensure entrypoint exists +#[entrypoint] +#[storage] +pub struct MyContract { /* ... */ } + +// Verify required exports +#[no_mangle] +pub extern "C" fn pay_for_memory_grow(pages: u16) { + // Generated automatically by SDK +} +``` + +## Resources + +- [WebAssembly specification](https://webassembly.github.io/spec/) +- [Rust WASM target documentation](https://doc.rust-lang.org/rustc/platform-support/wasm32-unknown-unknown.html) +- [Stylus SDK repository](https://github.com/OffchainLabs/stylus-sdk-rs) +- [Cargo Stylus CLI tool](https://github.com/OffchainLabs/cargo-stylus) +- [WASM binary toolkit (wabt)](https://github.com/WebAssembly/wabt) +- [Binaryen optimization tools](https://github.com/WebAssembly/binaryen) diff --git a/docs/stylus/gentle-introduction.mdx b/docs/stylus/gentle-introduction.mdx index 6662a24ab3..f4e9b8b3d4 100644 --- a/docs/stylus/gentle-introduction.mdx +++ b/docs/stylus/gentle-introduction.mdx @@ -6,28 +6,26 @@ author: amarrazza sme: amarrazza target_audience: 'Developers who want to build on Arbitrum using popular programming languages, like Rust' sidebar_position: 1 -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildStylusSidebar --- 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). -- 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 @@ -42,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 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..527e09fd95 100644 --- a/docs/stylus/how-tos/adding-support-for-new-languages.mdx +++ b/docs/stylus/how-tos/adding-support-for-new-languages.mdx @@ -6,14 +6,14 @@ sme: rauljordan target_audience: 'Developers deploying smart contracts using Stylus' content_type: how-to sidebar_position: 1 -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildStylusSidebar --- [Arbitrum Stylus](../gentle-introduction.mdx) is a new technology developed for Arbitrum chains which gives smart contract developers superpowers. With Stylus, developers can write EVM-compatible smart contracts in many different programming languages, and reap massive performance gains. Stylus slashes fees, with performance gains ranging from 10-70x, and memory efficiency gains as high as 100-500x. 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. diff --git a/docs/stylus/how-tos/caching-contracts.mdx b/docs/stylus/how-tos/caching-contracts.mdx index 92c3a75094..cd55d7d5c7 100644 --- a/docs/stylus/how-tos/caching-contracts.mdx +++ b/docs/stylus/how-tos/caching-contracts.mdx @@ -5,7 +5,7 @@ description: 'A conceptual overview of the Stylus caching strategy and CacheMana sme: mahsa-moosavi target_audience: 'Developers deploying smart contracts using Stylus.' sidebar_position: 3 -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildStylusSidebar --- Stylus is designed for fast computation and efficiency. However, 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..ea35051def --- /dev/null +++ b/docs/stylus/how-tos/check-and-deploy.mdx @@ -0,0 +1,781 @@ +--- +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: buildStylusSidebar +--- + +# Check and Deploy Stylus Contracts + +This guide explains how to validate and deploy Stylus smart contracts using the `cargo stylus` CLI tool. The process involves two main steps: checking that your contract is valid, and deploying it to an Arbitrum chain. + +## Prerequisites + +Before checking or deploying contracts, ensure you have: + +1. **Rust toolchain** installed (see [rustup.rs](https://rustup.rs)) +2. **WebAssembly target** added: + ```shell + rustup target add wasm32-unknown-unknown + ``` +3. **cargo-stylus CLI** installed: + ```shell + cargo install cargo-stylus + ``` +4. **RPC endpoint** for the target chain (see [testnet information](https://docs.arbitrum.io/stylus/reference/testnet-information)) +5. **Funded account** with ETH for gas and activation fees + +## Overview: The Two-Step Process + +Deploying a Stylus contract involves two distinct steps: + +1. **Deployment**: Upload the compressed WASM bytecode to the chain, assigning it an address +2. **Activation**: Trigger onchain compilation to native code and cache it for fast execution + +The `cargo stylus check` command validates your contract before deployment, and `cargo stylus deploy` handles both steps automatically. + +## Checking Contracts + +The `cargo stylus check` command validates that your contract can be deployed and activated without actually sending a transaction. + +### What check does + +1. **Compiles** your Rust code to WASM with `wasm32-unknown-unknown` target +2. **Compresses** the WASM using brotli compression +3. **Validates** the WASM structure: + - Required exports (`user_entrypoint`) + - Allowed imports (only `vm_hooks`) + - Memory constraints + - Size limits (24KB compressed) +4. **Simulates activation** using `eth_call` to verify onchain compatibility +5. **Estimates data fee** required for activation + +### Basic usage + +```shell +# Check the current project against Arbitrum Sepolia (default) +cargo stylus check +``` + +### Common options + +```shell +# Check against a specific network +cargo stylus check \ + --endpoint="https://arb1.arbitrum.io/rpc" + +# Check a specific WASM file +cargo stylus check \ + --wasm-file=./target/wasm32-unknown-unknown/release/my_contract.wasm + +# Check with a specific contract address +cargo stylus check \ + --contract-address=0x1234567890123456789012345678901234567890 +``` + +### Success output + +When your contract passes validation: + +```shell +Finished release [optimized] target(s) in 1.88s +Reading WASM file at target/wasm32-unknown-unknown/release/my_contract.wasm +Compressed WASM size: 3 KB +Contract succeeded Stylus onchain activation checks with Stylus version: 1 +wasm data fee: 0.0001 ETH (originally 0.00008 ETH with 20% bump) +``` + +### Failure output + +If validation fails, you'll see detailed error information: + +```shell +Reading WASM file at target/wasm32-unknown-unknown/release/bad_contract.wasm +Compressed WASM size: 55 B +Stylus checks failed: contract predeployment check failed + +Caused by: + binary exports reserved symbol stylus_ink_left + +Location: + prover/src/binary.rs:493:9 +``` + +Common validation errors include: + +- **Missing entrypoint**: Contract lacks `#[entrypoint]` attribute +- **Invalid exports**: Contract exports reserved symbols +- **Size limit exceeded**: Compressed WASM exceeds 24KB +- **Invalid imports**: Contract imports functions outside `vm_hooks` +- **Memory violations**: Incorrect memory handling or growth + +## Deploying Contracts + +The `cargo stylus deploy` command compiles, deploys, and activates your contract in a single operation. + +### What deploy does + +1. **Compiles and checks** the contract (same as `cargo stylus check`) +2. **Deploys bytecode**: Sends transaction to upload compressed WASM to the chain +3. **Activates contract**: Calls `activateProgram` precompile to compile to native code +4. **Verifies success**: Confirms both transactions completed successfully + +### Basic deployment + +```shell +# Deploy to Arbitrum Sepolia (default testnet) +cargo stylus deploy \ + --private-key-path=./key.txt +``` + +### Deployment with gas estimation + +Before deploying, estimate the gas required: + +```shell +cargo stylus deploy \ + --private-key-path=./key.txt \ + --estimate-gas +``` + +Output: + +```shell +Compressed WASM size: 3 KB +Deploying contract to address 0x457b1ba688e9854bdbed2f473f7510c476a3da09 +Estimated gas: 12756792 +wasm data fee: 0.0001 ETH +``` + +### Full deployment + +Once estimation looks correct, deploy for real: + +```shell +cargo stylus deploy \ + --private-key-path=./key.txt +``` + +Output shows both transactions: + +```shell +Compressed WASM size: 3 KB +Deploying contract to address 0x457b1ba688e9854bdbed2f473f7510c476a3da09 +Estimated gas: 12756792 +Submitting tx... +Confirmed tx 0x42db...7311, gas used 11657164 + +Activating contract at address 0x457b1ba688e9854bdbed2f473f7510c476a3da09 +Estimated gas: 14251759 +Submitting tx... +Confirmed tx 0x0bdb...3307, gas used 14204908 +``` + +### Deployment options + +#### Network selection + +```shell +# Deploy to Arbitrum One (mainnet) +cargo stylus deploy \ + --endpoint="https://arb1.arbitrum.io/rpc" \ + --private-key-path=./key.txt + +# Deploy to Arbitrum Sepolia (testnet) +cargo stylus deploy \ + --endpoint="https://sepolia-rollup.arbitrum.io/rpc" \ + --private-key-path=./key.txt +``` + +#### Private key management + +```shell +# From file (recommended) +cargo stylus deploy \ + --private-key-path=./key.txt + +# From environment (WARNING: exposes key to shell history) +cargo stylus deploy \ + --private-key=$PRIVATE_KEY + +# From keystore file +cargo stylus deploy \ + --keystore-path=./keystore.json \ + --keystore-password-path=./password.txt +``` + +#### Gas price control + +```shell +# Set custom gas price (in gwei) +cargo stylus deploy \ + --private-key-path=./key.txt \ + --max-fee-per-gas-gwei=0.05 +``` + +#### Deploy without activation + +For advanced use cases, deploy bytecode without immediate activation: + +```shell +cargo stylus deploy \ + --private-key-path=./key.txt \ + --no-activate +``` + +Later, activate separately: + +```shell +cargo stylus activate \ + --address=0x457b1ba688e9854bdbed2f473f7510c476a3da09 \ + --private-key-path=./key.txt +``` + +#### Constructor arguments + +Deploy contracts with constructor arguments: + +```shell +cargo stylus deploy \ + --private-key-path=./key.txt \ + --constructor-args "Hello" "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" 42 +``` + +Send ETH to payable constructor: + +```shell +cargo stylus deploy \ + --private-key-path=./key.txt \ + --constructor-value=0.1 \ + --constructor-args "InitialValue" +``` + +#### Reproducible builds + +For contract verification, use Docker-based reproducible builds: + +```shell +# Default: uses Docker for reproducibility +cargo stylus deploy \ + --private-key-path=./key.txt + +# Specify cargo-stylus version +cargo stylus deploy \ + --private-key-path=./key.txt \ + --cargo-stylus-version=0.5.0 +``` + +Skip Docker for local development (non-reproducible): + +```shell +cargo stylus deploy \ + --private-key-path=./key.txt \ + --no-verify +``` + +## Understanding Activation + +Activation is the process of compiling WASM to native machine code onchain. + +### Why activation is required + +- **Performance**: Native code executes 10-100x faster than interpreted WASM +- **Validation**: Ensures WASM is well-formed and follows all constraints +- **Caching**: Compiled code is cached for all future contract calls + +### Activation process + +1. **Decompress**: Brotli-compressed WASM is decompressed +2. **Validate**: WASM structure is checked for correctness +3. **Compile**: WASM is compiled to native machine code +4. **Cache**: Compiled code is stored in the activation cache +5. **Charge fee**: Data fee based on WASM size is charged + +### Data fee calculation + +The data fee depends on the size of your WASM: + +```rust +// From activation.rs +pub async fn data_fee( + code: impl Into, + address: Address, + config: &ActivationConfig, + provider: &impl Provider, +) -> Result { + let result = arbwasm + .activateProgram(address) + .call() + .await?; + + let data_fee = result.dataFee; + let bump = config.data_fee_bump_percent; // Default 20% + let adjusted = bump_data_fee(data_fee, bump); + + Ok(adjusted) +} +``` + +By default, the fee is bumped by 20% to account for gas price fluctuations. + +### Activation errors + +Common activation failures: + +#### Missing entrypoint + +``` +Error: Contract could not be activated as it is missing an entrypoint. +Please ensure that your contract has an #[entrypoint] defined on your main struct +``` + +**Solution**: Add `#[entrypoint]` to your main storage struct: + +```rust +#[entrypoint] +#[storage] +pub struct MyContract { + // ... +} +``` + +#### Insufficient funds + +``` +Error: not enough funds in account 0x... to pay for data fee +balance 0.0001 ETH < 0.0005 ETH +``` + +**Solution**: Fund your account with more ETH. Get testnet ETH from faucets: + +- [Arbitrum Sepolia faucet](https://faucet.quicknode.com/arbitrum/sepolia) + +#### Invalid WASM + +``` +Error: contract activation failed: failed to parse contract +Caused by: binary exports reserved symbol stylus_ink_left +``` + +**Solution**: Ensure you're using the latest `stylus-sdk` version and following SDK conventions. + +## Deployment Workflows + +### Development workflow + +For rapid iteration during development: + +```shell +# 1. Check frequently during development +cargo stylus check + +# 2. Deploy to testnet with no-verify for speed +cargo stylus deploy \ + --endpoint="https://sepolia-rollup.arbitrum.io/rpc" \ + --private-key-path=./key.txt \ + --no-verify + +# 3. Test the deployed contract +# (use your testing framework) + +# 4. Iterate and redeploy as needed +``` + +### Production workflow + +For production deployments: + +```shell +# 1. Final check against mainnet +cargo stylus check \ + --endpoint="https://arb1.arbitrum.io/rpc" + +# 2. Estimate costs +cargo stylus deploy \ + --endpoint="https://arb1.arbitrum.io/rpc" \ + --private-key-path=./key.txt \ + --estimate-gas + +# 3. Deploy with reproducible build (for verification) +cargo stylus deploy \ + --endpoint="https://arb1.arbitrum.io/rpc" \ + --private-key-path=./key.txt \ + --cargo-stylus-version=0.5.0 + +# 4. Verify the deployed contract +cargo stylus verify \ + --endpoint="https://arb1.arbitrum.io/rpc" \ + --deployment-tx=0x... +``` + +### Multi-contract deployment + +Deploy multiple contracts from a workspace: + +```shell +# Check all contracts in workspace +cargo stylus check + +# Deploy specific contract +cargo stylus deploy \ + --contract=my-token \ + --private-key-path=./key.txt + +# Deploy all contracts (must have no-arg constructors) +cargo stylus deploy \ + --private-key-path=./key.txt +``` + +## Checking Existing Deployments + +### Check if contract is activated + +```rust +// From check.rs +let codehash = processed.codehash(); +if Contract::exists(codehash, &provider).await? { + return Ok(ContractStatus::Active { + code: processed.code, + }); +} +``` + +Use `cargo stylus check` with `--contract-address` to verify an existing deployment: + +```shell +cargo stylus check \ + --contract-address=0x457b1ba688e9854bdbed2f473f7510c476a3da09 +``` + +### Re-activation + +If a contract is already deployed but not activated, activate it: + +```shell +cargo stylus activate \ + --address=0x457b1ba688e9854bdbed2f473f7510c476a3da09 \ + --private-key-path=./key.txt +``` + +## Best Practices + +### 1. Always check before deploying + +```shell +# ✅ Good: Check first +cargo stylus check +cargo stylus deploy --private-key-path=./key.txt + +# ❌ Bad: Deploy without checking +cargo stylus deploy --private-key-path=./key.txt +``` + +### 2. Use gas estimation + +```shell +# ✅ Good: Estimate first +cargo stylus deploy --private-key-path=./key.txt --estimate-gas +# Review the output, then deploy for real +cargo stylus deploy --private-key-path=./key.txt + +# ❌ Bad: Deploy without estimation +cargo stylus deploy --private-key-path=./key.txt +``` + +### 3. Secure private key handling + +```shell +# ✅ Good: Use key file +echo $PRIVATE_KEY > /tmp/key.txt +chmod 600 /tmp/key.txt +cargo stylus deploy --private-key-path=/tmp/key.txt +rm /tmp/key.txt + +# ⚠️ Risky: Expose key in command line +cargo stylus deploy --private-key=$PRIVATE_KEY +``` + +### 4. Test on testnet first + +```shell +# ✅ Good: Test on Sepolia first +cargo stylus deploy \ + --endpoint="https://sepolia-rollup.arbitrum.io/rpc" \ + --private-key-path=./key.txt + +# After testing succeeds, deploy to mainnet +cargo stylus deploy \ + --endpoint="https://arb1.arbitrum.io/rpc" \ + --private-key-path=./key.txt + +# ❌ Bad: Deploy directly to mainnet +cargo stylus deploy \ + --endpoint="https://arb1.arbitrum.io/rpc" \ + --private-key-path=./key.txt +``` + +### 5. Use reproducible builds for verification + +```shell +# ✅ Good: Reproducible build for mainnet +cargo stylus deploy \ + --endpoint="https://arb1.arbitrum.io/rpc" \ + --private-key-path=./key.txt \ + --cargo-stylus-version=0.5.0 + +# Then verify on Arbiscan +cargo stylus verify \ + --endpoint="https://arb1.arbitrum.io/rpc" \ + --deployment-tx=0x... + +# ⚠️ OK for development: Skip Docker +cargo stylus deploy \ + --endpoint="https://sepolia-rollup.arbitrum.io/rpc" \ + --private-key-path=./key.txt \ + --no-verify +``` + +### 6. Monitor contract size + +```shell +# Check compressed size +cargo stylus check + +# If size is close to 24KB limit: +# - Use #[no_std] +# - Remove unused dependencies +# - Enable aggressive optimizations +# - Strip debug symbols +``` + +### 7. Verify data fee is reasonable + +```shell +# Check data fee before deploying +cargo stylus deploy --private-key-path=./key.txt --estimate-gas + +# Output shows: +# wasm data fee: 0.0001 ETH (originally 0.00008 ETH with 20% bump) + +# If fee seems high: +# - Optimize WASM size +# - Check network congestion +# - Verify contract correctness +``` + +## Troubleshooting + +### Size limit errors + +**Error**: Compressed WASM exceeds 24KB + +**Solutions**: + +1. Use `#[no_std]` to eliminate standard library: + + ```rust + #![no_std] + extern crate alloc; + ``` + +2. Remove unused dependencies from `Cargo.toml`: + + ```toml + [dependencies] + stylus-sdk = "0.5" + # Remove unnecessary crates + ``` + +3. Enable size optimizations in `Cargo.toml`: + + ```toml + [profile.release] + opt-level = "z" + lto = true + strip = true + ``` + +4. Use `wasm-opt` for additional optimization: + ```shell + wasm-opt -Oz -o optimized.wasm input.wasm + cargo stylus deploy --wasm-file=optimized.wasm --private-key-path=./key.txt + ``` + +### Activation failures + +**Error**: Transaction reverted during activation + +**Solutions**: + +1. Verify entrypoint exists: + + ```rust + #[entrypoint] + #[storage] + pub struct MyContract { /* ... */ } + ``` + +2. Check WASM validity: + + ```shell + cargo stylus check --wasm-file=./target/wasm32-unknown-unknown/release/my_contract.wasm + ``` + +3. Ensure sufficient funds: + + ```shell + # Check balance + cast balance $YOUR_ADDRESS --rpc-url $RPC_URL + + # Get testnet ETH if needed + # Visit faucet.quicknode.com/arbitrum/sepolia + ``` + +### RPC errors + +**Error**: Connection timeout or RPC error + +**Solutions**: + +1. Verify endpoint URL: + + ```shell + curl -X POST $RPC_URL \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' + ``` + +2. Try alternative endpoints: + + ```shell + # Arbitrum One + --endpoint="https://arb1.arbitrum.io/rpc" + --endpoint="https://arbitrum-one.publicnode.com" + + # Arbitrum Sepolia + --endpoint="https://sepolia-rollup.arbitrum.io/rpc" + --endpoint="https://arbitrum-sepolia.blockpi.network/v1/rpc/public" + ``` + +3. Check network status: + - [Arbitrum Status](https://arbiscan.io/) + - [Chainlist](https://chainlist.org/) + +### Build errors + +**Error**: Compilation fails + +**Solutions**: + +1. Update dependencies: + + ```shell + cargo update + ``` + +2. Clear build cache: + + ```shell + cargo clean + ``` + +3. Verify Rust toolchain: + + ```shell + rustup update + rustup target add wasm32-unknown-unknown + ``` + +4. Check SDK version compatibility: + ```toml + [dependencies] + stylus-sdk = "0.5" # Use latest stable version + ``` + +## Non-Rust WASM Deployment + +Deploy WASM from any language (C, C++, etc.): + +```shell +# Deploy raw WASM file +cargo stylus deploy \ + --wasm-file=./my_contract.wasm \ + --private-key-path=./key.txt + +# Deploy WebAssembly Text (WAT) file +cargo stylus deploy \ + --wasm-file=./my_contract.wat \ + --private-key-path=./key.txt +``` + +Example WAT file structure: + +```wasm +(module + (memory 0 0) + (export "memory" (memory 0)) + (func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) + )) +``` + +## Command Reference + +### cargo stylus check + +**Syntax**: + +```shell +cargo stylus check [OPTIONS] +``` + +**Common options**: + +- `--endpoint=`: RPC endpoint (default: Arbitrum Sepolia) +- `--wasm-file=`: Check specific WASM file +- `--contract-address=
`: Target contract address + +### cargo stylus deploy + +**Syntax**: + +```shell +cargo stylus deploy [OPTIONS] +``` + +**Common options**: + +- `--endpoint=`: RPC endpoint +- `--private-key-path=`: Private key file +- `--estimate-gas`: Only estimate gas +- `--no-activate`: Deploy without activation +- `--no-verify`: Skip Docker reproducible build +- `--constructor-args `: Constructor arguments +- `--constructor-value=`: ETH sent to constructor +- `--max-fee-per-gas-gwei=`: Custom gas price + +### cargo stylus activate + +**Syntax**: + +```shell +cargo stylus activate --address=
[OPTIONS] +``` + +**Options**: + +- `--address=
`: Deployed contract address (required) +- `--private-key-path=`: Private key file +- `--estimate-gas`: Only estimate gas + +## Resources + +- [Stylus quickstart guide](https://docs.arbitrum.io/stylus/stylus-quickstart) +- [Cargo Stylus repository](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/cargo-stylus) +- [Testnet information](https://docs.arbitrum.io/stylus/reference/testnet-information) +- [Contract verification guide](https://docs.arbitrum.io/stylus/how-tos/verifying-contracts) +- [Optimizing WASM size](https://github.com/OffchainLabs/cargo-stylus/blob/main/OPTIMIZING_BINARIES.md) +- [Valid WASM requirements](https://github.com/OffchainLabs/cargo-stylus/blob/main/VALID_WASM.md) diff --git a/docs/stylus/how-tos/debugging-tx.mdx b/docs/stylus/how-tos/debugging-tx.mdx index c47cb69116..2cba8db919 100644 --- a/docs/stylus/how-tos/debugging-tx.mdx +++ b/docs/stylus/how-tos/debugging-tx.mdx @@ -7,7 +7,7 @@ sme: mahsamoosavi target_audience: 'Developers deploying smart contracts using Stylus' content_type: how-to sidebar_position: 2 -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildStylusSidebar --- Debugging smart contracts can be challenging, especially when dealing with complex transactions. The `cargo-stylus` crate simplifies the debugging process by allowing developers to replay Stylus transactions. This tool leverages GDB to provide an interactive debugging experience, enabling developers to set breakpoints, inspect state changes, and trace the execution flow step-by-step. This capability is crucial for identifying and resolving issues, ensuring that smart contracts function correctly and efficiently. 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..bf17673f71 --- /dev/null +++ b/docs/stylus/how-tos/deploying-non-rust-wasm-contracts.mdx @@ -0,0 +1,805 @@ +--- +id: deploying-non-rust-wasm-contracts +title: 'Deploying non-Rust WASM contracts' +description: 'Deploy WebAssembly contracts from C, C++, and other languages to Arbitrum Stylus' +author: chrisco +sme: chrisco +target_audience: 'Developers who need to deploy non-Rust WASM contracts' +content_type: how-to +sidebar_position: 3 +displayed_sidebar: buildStylusSidebar +--- + +# Deploying Non-Rust WASM Contracts + +While Rust provides the best developer experience for Stylus, any language that compiles to WebAssembly can be deployed. This guide explains how to deploy WASM contracts written in C, C++, or even pure WebAssembly Text (WAT). + +## Overview + +Stylus accepts any valid WebAssembly module that meets its requirements. You can: + +- Write contracts in **C or C++** using the Stylus C SDK +- Use **WebAssembly Text (WAT)** for direct bytecode control +- Compile from **any language** that targets `wasm32-unknown-unknown` +- Deploy **pre-compiled WASM** binaries directly + +The key is using the `--wasm-file` flag with `cargo stylus` commands to bypass Rust compilation. + +## Why Use Non-Rust Languages? + +Different languages excel at different tasks: + +| Language | Best For | Use Cases | +| ------------------ | -------------------------------------- | -------------------------------------------------- | +| **C/C++** | Low-level control, cryptography | Hash functions, signature verification, algorithms | +| **WAT** | Learning, debugging, minimal contracts | Simple logic, educational examples | +| **AssemblyScript** | TypeScript developers | Web3 integration with familiar syntax | +| **Other** | Specific requirements | Domain-specific computations | + +### When to choose non-Rust + +- **Existing codebase**: Port existing C/C++ cryptography libraries +- **Performance-critical**: Hand-optimized assembly-like control +- **Minimal size**: Ultra-compact contracts for specific operations +- **Team expertise**: Leverage existing C/C++ knowledge + +### When to stick with Rust + +- **Full-featured contracts**: Complex DeFi, NFTs, governance +- **Type safety**: Strong guarantees and tooling +- **Ecosystem**: Rich library support and examples +- **Productivity**: Higher-level abstractions and macros + +## WASM Requirements + +All WASM modules deployed to Stylus must meet these requirements: + +### Required exports + +```wasm +(export "user_entrypoint" (func $user_entrypoint)) +(export "memory" (memory 0)) +``` + +The `user_entrypoint` function: + +- **Signature**: `(param i32) (result i32)` +- **Parameter**: Length of input calldata in bytes +- **Returns**: Length of output data in bytes + +### Allowed imports + +Only functions from the `vm_hooks` module are permitted: + +```wasm +(import "vm_hooks" "msg_sender" (func $msg_sender (param i32))) +(import "vm_hooks" "storage_load_bytes32" (func $storage_load (param i32 i32))) +(import "vm_hooks" "storage_store_bytes32" (func $storage_store (param i32 i32))) +``` + +See the [hostio exports documentation](/stylus/advanced/hostio-exports) for the complete list of available VM hooks. + +### Memory requirements + +- Linear memory must be exported as `"memory"` +- Memory growth must be explicitly paid for +- Initial memory size should be minimal (often `0 0`) +- Maximum memory is limited by gas costs + +### Compilation target + +- **Target triple**: `wasm32-unknown-unknown` +- **No standard library**: WASM runs in a sandboxed environment +- **No floating point**: Not yet supported by Stylus +- **No SIMD**: Not yet supported by Stylus +- **No reference types**: Disabled for compatibility + +## WebAssembly Text (WAT) + +WAT provides direct control over WASM bytecode using human-readable text format. + +### Minimal contract + +The simplest valid Stylus contract: + +```wasm +(module + ;; Export linear memory + (memory 0 0) + (export "memory" (memory 0)) + + ;; Required entrypoint + ;; Takes calldata length, returns output length + (func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) ;; Return 0 bytes + )) +``` + +Save as `minimal.wat` and deploy: + +```shell +cargo stylus deploy --wasm-file=minimal.wat --private-key-path=./key.txt +``` + +### Echo contract + +Returns input data unchanged: + +```wasm +(module + (memory 1 1) + (export "memory" (memory 0)) + + ;; Import VM hook to read calldata + (import "vm_hooks" "read_args" (func $read_args (param i32))) + + (func (export "user_entrypoint") (param $args_len i32) (result i32) + ;; Read calldata into memory at offset 0 + (call $read_args (i32.const 0)) + + ;; Return the same length (echo) + (local.get $args_len) + )) +``` + +### Storage counter + +Increment a value in storage: + +```wasm +(module + (memory 1 1) + (export "memory" (memory 0)) + + ;; Import storage operations + (import "vm_hooks" "storage_load_bytes32" + (func $storage_load (param i32 i32))) + (import "vm_hooks" "storage_store_bytes32" + (func $storage_store (param i32 i32))) + + (func (export "user_entrypoint") (param $args_len i32) (result i32) + ;; Load current value from storage slot 0 + (call $storage_load + (i32.const 0) ;; key pointer + (i32.const 32)) ;; value destination + + ;; Increment the value at memory[32] + (i32.store (i32.const 32) + (i32.add + (i32.load (i32.const 32)) + (i32.const 1))) + + ;; Store back to storage + (call $storage_store + (i32.const 0) ;; key pointer + (i32.const 32)) ;; value pointer + + ;; Return 0 bytes of output + (i32.const 0) + )) +``` + +### Checking WAT contracts + +Validate before deploying: + +```shell +cargo stylus check --wasm-file=counter.wat +``` + +Output shows validation results: + +``` +Reading WASM file at counter.wat +Compressed WASM size: 142 B +Contract succeeded Stylus onchain activation checks with Stylus version: 1 +``` + +## C/C++ Development + +The [Stylus C SDK](https://github.com/OffchainLabs/stylus-sdk-c) enables C/C++ smart contract development. + +### Installation + +Install the C SDK: + +```shell +git clone https://github.com/OffchainLabs/stylus-sdk-c.git +cd stylus-sdk-c +``` + +Install dependencies: + +```shell +# macOS +brew install llvm binaryen wabt + +# Ubuntu/Debian +sudo apt-get install clang lld wasm-ld binaryen wabt +``` + +### Project structure + +Basic C project layout: + +``` +my-contract/ +├── Makefile +├── src/ +│ └── main.c +└── include/ + └── stylus_sdk.h +``` + +### Simple C contract + +```c +// main.c +#include "stylus_sdk.h" + +// Storage slot for counter +static uint8_t counter_slot[32] = {0}; + +// Main entrypoint +int main(int argc, char *argv[]) { + // Load counter from storage + uint8_t value[32]; + storage_load_bytes32(counter_slot, value); + + // Increment + value[31]++; + + // Store back + storage_store_bytes32(counter_slot, value); + + return 0; +} +``` + +### C SDK features + +The C SDK provides: + +```c +// Account operations +void msg_sender(uint8_t *sender); +void tx_origin(uint8_t *origin); +void contract_address(uint8_t *addr); + +// Storage operations +void storage_load_bytes32(uint8_t *key, uint8_t *dest); +void storage_store_bytes32(uint8_t *key, uint8_t *value); + +// Block information +uint64_t block_timestamp(void); +uint64_t block_number(void); +void block_basefee(uint8_t *basefee); + +// Call operations +void call_contract( + uint8_t *contract, + uint8_t *calldata, + uint32_t calldata_len, + uint8_t *value, + uint32_t gas, + uint8_t *return_data_len +); + +// And many more... +``` + +### Building C contracts + +Create a Makefile: + +```makefile +CLANG = clang +WASM_LD = wasm-ld +WASM_OPT = wasm-opt + +CFLAGS = -target wasm32 -nostdlib -O3 +LDFLAGS = -no-entry --export=user_entrypoint --export=memory + +SRC = src/main.c +OUT = build/contract.wasm +OUT_OPT = build/contract-opt.wasm + +all: $(OUT_OPT) + +$(OUT): $(SRC) + mkdir -p build + $(CLANG) $(CFLAGS) -c $(SRC) -o build/main.o + $(WASM_LD) $(LDFLAGS) build/main.o -o $(OUT) + +$(OUT_OPT): $(OUT) + $(WASM_OPT) -Oz $(OUT) -o $(OUT_OPT) + +clean: + rm -rf build + +deploy: $(OUT_OPT) + cargo stylus deploy --wasm-file=$(OUT_OPT) \ + --private-key-path=$$PRIVATE_KEY_PATH + +check: $(OUT_OPT) + cargo stylus check --wasm-file=$(OUT_OPT) +``` + +Build and deploy: + +```shell +make +make check +make deploy +``` + +### C cryptography example + +Verifying a signature: + +```c +#include "stylus_sdk.h" +#include + +// Verify ECDSA signature +int verify_signature( + uint8_t *message_hash, + uint8_t *signature, + uint8_t *public_key +) { + uint8_t recovered[65]; + + // Recover signer from signature + if (ecrecover(message_hash, signature, recovered) != 0) { + return -1; // Recovery failed + } + + // Compare with expected public key + if (memcmp(recovered + 1, public_key, 64) == 0) { + return 0; // Valid signature + } + + return -1; // Invalid signature +} + +int main(int argc, char *argv[]) { + uint8_t msg_hash[32]; + uint8_t sig[65]; + uint8_t pubkey[64]; + + // Read inputs from calldata + read_args(0); + memcpy(msg_hash, memory, 32); + memcpy(sig, memory + 32, 65); + memcpy(pubkey, memory + 97, 64); + + // Verify + int result = verify_signature(msg_hash, sig, pubkey); + + // Write result + memory[0] = (result == 0) ? 1 : 0; + write_result(memory, 1); + + return 0; +} +``` + +## AssemblyScript Contracts + +AssemblyScript is TypeScript-like language that compiles to WebAssembly. + +### Installation + +```shell +npm install -g assemblyscript +npm install @assemblyscript/loader +``` + +### Simple AssemblyScript contract + +```typescript +// contract.ts + +// Import Stylus VM hooks +@external("vm_hooks", "msg_sender") +declare function msg_sender(ptr: usize): void; + +@external("vm_hooks", "storage_load_bytes32") +declare function storage_load(key: usize, dest: usize): void; + +@external("vm_hooks", "storage_store_bytes32") +declare function storage_store(key: usize, value: usize): void; + +// Storage key +const COUNTER_KEY: StaticArray = [0, 0, 0, 0, /* ... 32 zeros ... */]; + +// Entrypoint +export function user_entrypoint(args_len: i32): i32 { + // Load counter + let value = new StaticArray(32); + storage_load( + changetype(COUNTER_KEY), + changetype(value) + ); + + // Increment + value[31]++; + + // Store + storage_store( + changetype(COUNTER_KEY), + changetype(value) + ); + + return 0; // No output +} +``` + +### Compile AssemblyScript + +```shell +asc contract.ts \ + --target release \ + --exportRuntime \ + --exportTable \ + -o contract.wasm +``` + +### Deploy AssemblyScript contract + +```shell +cargo stylus deploy \ + --wasm-file=contract.wasm \ + --private-key-path=./key.txt +``` + +## Deployment Workflow + +### 1. Prepare your WASM + +Ensure your WASM module meets requirements: + +```shell +# Check WASM structure with wasm-objdump +wasm-objdump -x contract.wasm | grep -A 5 "Export\|Import" + +# Should show: +# Export[0]: +# - func[0] +# - memory[0] +# Import[0]: +# - module="vm_hooks" func=... +``` + +### 2. Optimize the WASM + +Reduce size with wasm-opt: + +```shell +wasm-opt -Oz contract.wasm -o contract-opt.wasm +``` + +### 3. Check before deploying + +Validate the contract: + +```shell +cargo stylus check --wasm-file=contract-opt.wasm +``` + +### 4. Deploy + +Deploy to testnet: + +```shell +cargo stylus deploy \ + --wasm-file=contract-opt.wasm \ + --private-key-path=./key.txt \ + --endpoint="https://sepolia-rollup.arbitrum.io/rpc" +``` + +### 5. Verify deployment + +Check deployment succeeded: + +```shell +# Output shows: +Compressed WASM size: 245 B +Deploying contract to address 0x... +Confirmed tx 0x... +Activating contract at address 0x... +Confirmed tx 0x... +``` + +## Best Practices + +### 1. Minimize binary size + +```shell +# ✅ Good: Optimize aggressively +wasm-opt -Oz input.wasm -o output.wasm + +# Use wasm-strip to remove symbols +wasm-strip output.wasm + +# Check final size +ls -lh output.wasm +``` + +### 2. Test with cargo stylus check + +```shell +# ✅ Good: Always check before deploying +cargo stylus check --wasm-file=contract.wasm + +# Test on testnet first +cargo stylus deploy \ + --wasm-file=contract.wasm \ + --private-key-path=./key.txt \ + --endpoint="https://sepolia-rollup.arbitrum.io/rpc" +``` + +### 3. Use standard memory layout + +```c +// ✅ Good: Predictable memory layout +uint8_t calldata[1024]; // 0-1023: Input data +uint8_t storage[32]; // 1024-1055: Storage scratch +uint8_t output[256]; // 1056-1311: Output buffer + +// ❌ Bad: Unpredictable allocations +uint8_t *data = malloc(size); // No malloc in WASM! +``` + +### 4. Handle calldata properly + +```wasm +;; ✅ Good: Read calldata into memory +(call $read_args (i32.const 0)) + +;; Process the data +(call $process_calldata (local.get $args_len)) + +;; ❌ Bad: Assume calldata location +(i32.load (i32.const 0)) ;; Calldata not automatically loaded +``` + +### 5. Export all required functions + +```wasm +;; ✅ Good: Export entrypoint and memory +(export "user_entrypoint" (func $main)) +(export "memory" (memory 0)) + +;; ❌ Bad: Missing exports +(export "main" (func $main)) ;; Wrong name! +``` + +### 6. Use VM hooks correctly + +```c +// ✅ Good: Proper VM hook usage +uint8_t sender[20]; +msg_sender(sender); + +// ✅ Good: Check return values +uint8_t success; +call_contract(addr, data, len, value, gas, &success); +if (!success) { + revert("Call failed"); +} + +// ❌ Bad: Ignoring errors +call_contract(addr, data, len, value, gas, NULL); +``` + +### 7. Mind the size limit + +```shell +# Check compressed size +cargo stylus check --wasm-file=contract.wasm + +# Should show: +# Compressed WASM size: < 24 KB + +# If too large: +# - Remove debug symbols +# - Enable aggressive optimization +# - Minimize code and data sections +``` + +## Troubleshooting + +### Missing entrypoint + +**Error**: `WASM is missing the entrypoint export` + +**Solution**: Ensure `user_entrypoint` is exported: + +```wasm +;; WAT +(func (export "user_entrypoint") (param i32) (result i32) + ;; Implementation +) + +// C +int user_entrypoint(int argc) __attribute__((export_name("user_entrypoint"))); +``` + +### Invalid imports + +**Error**: `contract imports unauthorized function` + +**Solution**: Only import from `vm_hooks`: + +```wasm +;; ✅ Allowed +(import "vm_hooks" "msg_sender" (func $msg_sender (param i32))) + +;; ❌ Not allowed +(import "env" "print" (func $print (param i32))) +``` + +### Memory not exported + +**Error**: `WASM must export memory` + +**Solution**: Export linear memory: + +```wasm +;; WAT +(memory 1 1) +(export "memory" (memory 0)) + +// C Makefile +LDFLAGS = -no-entry --export=user_entrypoint --export=memory +``` + +### Size too large + +**Error**: `Compressed WASM exceeds 24KB` + +**Solutions**: + +1. Optimize with wasm-opt: + + ```shell + wasm-opt -Oz input.wasm -o output.wasm + ``` + +2. Strip symbols: + + ```shell + wasm-strip output.wasm + ``` + +3. Remove unused code: + + ```c + // Use static/inline for internal functions + static inline void helper(void) { } + ``` + +4. Minimize data section: + + ```c + // ✅ Good: Minimal data + const uint8_t PREFIX[4] = {0xEF, 0xF0, 0x00, 0x00}; + + // ❌ Bad: Large data + const char *STRINGS[1000] = { /* ... */ }; + ``` + +### Compilation errors + +**Error**: Clang fails to compile + +**Solutions**: + +1. Target wasm32: + + ```shell + clang -target wasm32 -nostdlib -c main.c + ``` + +2. Disable standard library: + + ```c + // Don't use stdio, stdlib, etc. + // Use SDK-provided functions + ``` + +3. Check imports/exports: + ```shell + wasm-objdump -x contract.wasm + ``` + +### Runtime errors + +**Error**: Contract reverts unexpectedly + +**Solutions**: + +1. Check gas usage: + + ```shell + cargo stylus deploy --estimate-gas --wasm-file=contract.wasm + ``` + +2. Add debug output (testnet only): + + ```c + emit_log(error_msg, sizeof(error_msg)); + ``` + +3. Test with minimal input: + ```shell + # Call with empty calldata + cast call $CONTRACT "0x" + ``` + +## Examples Repository + +Official examples for different languages: + +- [**Stylus C SDK**](https://github.com/OffchainLabs/stylus-sdk-c) - C/C++ examples +- [**Stylus Bf SDK**](https://github.com/OffchainLabs/stylus-sdk-bf) - Brainfuck educational examples +- [**Awesome Stylus**](https://github.com/OffchainLabs/awesome-stylus) - Community examples + +## Language Support Matrix + +| Language | Status | SDK | Best Use Case | +| ------------------ | --------------- | -------------------------------------------------------------- | --------------------------- | +| **Rust** | ✅ Production | [stylus-sdk-rs](https://github.com/OffchainLabs/stylus-sdk-rs) | Full-featured contracts | +| **C/C++** | ✅ Production | [stylus-sdk-c](https://github.com/OffchainLabs/stylus-sdk-c) | Cryptography, algorithms | +| **WAT** | ✅ Supported | Manual | Minimal contracts, learning | +| **AssemblyScript** | 🔶 Community | Custom | TypeScript developers | +| **Go** | 🔶 Experimental | TinyGo | Custom applications | +| **Zig** | 🔶 Experimental | Custom | Systems programming | + +## Advanced: Custom Languages + +To support a new language: + +1. **Compile to wasm32-unknown-unknown** + + ```shell + your-compiler --target=wasm32-unknown-unknown input.src -o output.wasm + ``` + +2. **Export required functions** + + ``` + - user_entrypoint(i32) -> i32 + - memory + ``` + +3. **Import only vm_hooks** + + ``` + - vm_hooks:msg_sender + - vm_hooks:storage_* + - etc. + ``` + +4. **Test and deploy** + ```shell + cargo stylus check --wasm-file=output.wasm + cargo stylus deploy --wasm-file=output.wasm --private-key-path=./key.txt + ``` + +## Resources + +- [Stylus C SDK](https://github.com/OffchainLabs/stylus-sdk-c) +- [WebAssembly specification](https://webassembly.github.io/spec/) +- [WAT format reference](https://webassembly.github.io/spec/core/text/) +- [Cargo Stylus CLI](https://github.com/OffchainLabs/cargo-stylus) +- [Awesome Stylus](https://github.com/OffchainLabs/awesome-stylus) +- [WASM binary toolkit (wabt)](https://github.com/WebAssembly/wabt) +- [Binaryen optimization tools](https://github.com/WebAssembly/binaryen) + +## Sources + +- [GitHub - OffchainLabs/stylus-sdk-c: C/C++ Smart Contracts on Arbitrum](https://github.com/OffchainLabs/stylus-sdk-c) +- [GitHub - OffchainLabs/awesome-stylus](https://github.com/OffchainLabs/awesome-stylus) +- [GitHub - OffchainLabs/stylus-sdk-rs: Rust Smart Contracts on Arbitrum](https://github.com/OffchainLabs/stylus-sdk-rs) diff --git a/docs/stylus/how-tos/exporting-abi.mdx b/docs/stylus/how-tos/exporting-abi.mdx new file mode 100644 index 0000000000..f75a1e2945 --- /dev/null +++ b/docs/stylus/how-tos/exporting-abi.mdx @@ -0,0 +1,996 @@ +--- +title: 'Exporting ABIs' +description: 'Exporting Solidity ABIs from Stylus contracts' +author: chrisco +sme: chrisco +sidebar_position: 2 +target_audience: Developers who need to understand how to export ABIs from Stylus contracts for integration with front-end applications and other tools. +displayed_sidebar: buildStylusSidebar +--- + +Stylus contracts written in Rust can automatically generate Solidity Application Binary Interfaces (ABIs) that enable seamless interoperability with existing Ethereum tools, front-end libraries, and other smart contracts. + +## What is an ABI? + +An Application Binary Interface (ABI) defines how to interact with a smart contract: + +- **Function signatures**: Names, parameters, and return types +- **Events**: Event definitions and indexed parameters +- **Errors**: Custom error types and parameters +- **Constructor**: Initialization parameters + +ABIs enable: + +- Front-end libraries (ethers.js, web3.js, viem) to interact with contracts +- Solidity contracts to call Rust contracts +- Block explorers to decode transactions +- Development tools to provide type-safe interfaces + +## Overview: ABI Generation + +Stylus contracts generate ABIs through: + +1. **`#[public]` macro**: Annotates public functions +2. **`export-abi` feature**: Enables ABI generation code +3. **`cargo stylus export-abi`**: CLI command to generate output +4. **Solidity interface**: Generated Solidity interface file +5. **JSON format**: Optional JSON ABI for tool integration + +The process is automatic—just annotate your functions with `#[public]` and run the export command. + +## Basic Usage + +### Export Solidity interface + +Generate a Solidity interface for your contract: + +```shell +cargo stylus export-abi +``` + +Output: + +```solidity +/** + * This file was automatically generated by Stylus and represents a Rust program. + * For more information, please see [The Stylus SDK](https://github.com/OffchainLabs/stylus-sdk-rs). + */ + +// SPDX-License-Identifier: MIT-OR-APACHE-2.0 +pragma solidity ^0.8.23; + +interface IMyContract { + function getValue() external view returns (uint256); + + function setValue(uint256 new_value) external; + + error Unauthorized(address caller); +} +``` + +### Export to file + +Save the interface to a file: + +```shell +cargo stylus export-abi > IMyContract.sol +``` + +Or specify output path: + +```shell +cargo stylus export-abi --output=./interfaces/IMyContract.sol +``` + +### Export JSON ABI + +Generate JSON format ABI (requires `solc` installed): + +```shell +cargo stylus export-abi --json > abi.json +``` + +Output: + +```json +[ + { + "type": "function", + "name": "getValue", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setValue", + "inputs": [ + { + "name": "new_value", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + } +] +``` + +## Writing ABI-Compatible Contracts + +### Basic contract structure + +Contracts must use the `#[public]` macro to generate ABIs: + +```rust +use stylus_sdk::{alloy_primitives::U256, prelude::*}; + +sol_storage! { + #[entrypoint] + pub struct Counter { + uint256 count; + } +} + +#[public] +impl Counter { + // This function will be included in the ABI + pub fn get_count(&self) -> U256 { + self.count.get() + } + + // This function will also be included + pub fn increment(&mut self) { + let count = self.count.get() + U256::from(1); + self.count.set(count); + } +} +``` + +Generated interface: + +```solidity +interface ICounter { + function getCount() external view returns (uint256); + + function increment() external; +} +``` + +### Function visibility mapping + +Rust function signatures map to Solidity visibility: + +```rust +#[public] +impl MyContract { + // Immutable reference → view function + pub fn read_value(&self) -> U256 { + self.value.get() + } + + // Mutable reference → non-view function + pub fn write_value(&mut self, new_value: U256) { + self.value.set(new_value); + } + + // Pure computation (no self) → pure function + pub fn compute(a: U256, b: U256) -> U256 { + a + b + } +} +``` + +Generated Solidity: + +```solidity +interface IMyContract { + function readValue() external view returns (uint256); + + function writeValue(uint256 new_value) external; + + function compute(uint256 a, uint256 b) external pure returns (uint256); +} +``` + +### Type mapping + +Rust types map to Solidity types automatically: + +| Rust Type | Solidity Type | Example | +| ------------------------- | ------------------------------------- | ---------------------- | +| `U256` | `uint256` | Token amounts | +| `U128`, `u128` | `uint128` | Medium integers | +| `u64`, `u32`, `u16`, `u8` | `uint64`, `uint32`, `uint16`, `uint8` | Small integers | +| `I256` | `int256` | Signed integers | +| `Address` | `address` | Account addresses | +| `bool` | `bool` | Boolean values | +| `FixedBytes` | `bytesN` | Fixed-size byte arrays | +| `Bytes` | `bytes` | Dynamic byte arrays | +| `String` | `string` | UTF-8 strings | +| `Vec` | `T[]` | Dynamic arrays | +| `[T; N]` | `T[N]` | Fixed-size arrays | + +Example: + +```rust +#[public] +impl MyContract { + pub fn process( + owner: Address, + amount: U256, + data: Bytes, + flags: Vec, + ) -> Result { + // Implementation + } +} +``` + +Generates: + +```solidity +interface IMyContract { + function process( + address owner, + uint256 amount, + bytes calldata data, + bool[] calldata flags + ) external returns (string memory); +} +``` + +### Custom errors + +Define custom errors with parameters: + +```rust +use stylus_sdk::prelude::*; + +sol! { + error InsufficientBalance(address account, uint256 requested, uint256 available); + error Unauthorized(address caller); + error InvalidAmount(); +} + +#[public] +impl Token { + pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), InsufficientBalance> { + let balance = self.balances.get(msg::sender()); + if balance < amount { + return Err(InsufficientBalance { + account: msg::sender(), + requested: amount, + available: balance, + }); + } + // Transfer logic + Ok(()) + } +} +``` + +Generated interface includes errors: + +```solidity +interface IToken { + function transfer(address to, uint256 amount) external; + + error InsufficientBalance(address account, uint256 requested, uint256 available); + error Unauthorized(address caller); + error InvalidAmount(); +} +``` + +### Events + +Events are automatically included in the ABI: + +```rust +use stylus_sdk::prelude::*; + +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 { + // Transfer logic + evm::log(Transfer { + from: msg::sender(), + to, + value, + }); + true + } +} +``` + +Generated interface: + +```solidity +interface IToken { + function transfer(address to, uint256 value) external returns (bool); + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); +} +``` + +## Trait Implementation + +Export ABIs for trait implementations: + +### Define a trait + +```rust +// ierc20.rs +use stylus_sdk::prelude::*; + +#[public] +pub trait IErc20 { + fn name(&self) -> String; + fn symbol(&self) -> String; + fn decimals(&self) -> u8; + fn total_supply(&self) -> U256; + fn balance_of(&self, owner: Address) -> U256; + fn transfer(&mut self, to: Address, value: U256) -> Result; +} +``` + +### Implement the trait + +```rust +// lib.rs +use stylus_sdk::prelude::*; + +sol_storage! { + #[entrypoint] + struct MyToken { + // Storage fields + } +} + +#[public] +#[implements(IErc20)] +impl MyToken { + // Additional functions beyond the trait + pub fn mint(&mut self, to: Address, value: U256) { + // Mint logic + } +} + +#[public] +impl IErc20 for MyToken { + fn name(&self) -> String { + "My Token".to_string() + } + + fn symbol(&self) -> String { + "MTK".to_string() + } + + fn decimals(&self) -> u8 { + 18 + } + + fn total_supply(&self) -> U256 { + self.total_supply.get() + } + + fn balance_of(&self, owner: Address) -> U256 { + self.balances.get(owner) + } + + fn transfer(&mut self, to: Address, value: U256) -> Result { + // Transfer logic + Ok(true) + } +} +``` + +Generated interface with inheritance: + +```solidity +interface IMyToken is IIErc20 { + function mint(address to, uint256 value) external; +} + +interface IIErc20 { + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + function totalSupply() external view returns (uint256); + function balanceOf(address owner) external view returns (uint256); + function transfer(address to, uint256 value) external returns (bool); +} +``` + +## Constructor Signatures + +Export constructor signatures for deployment: + +```rust +sol_storage! { + #[entrypoint] + struct MyContract { + address owner; + uint256 initial_value; + } +} + +#[public] +impl MyContract { + #[constructor] + pub fn new(owner: Address, initial_value: U256) { + self.owner.set(owner); + self.initial_value.set(initial_value); + } + + // Other methods... +} +``` + +Export constructor signature: + +```shell +cargo stylus export-abi constructor +``` + +Output: + +``` +constructor(address owner, uint256 initial_value) +``` + +For payable constructors: + +```rust +#[public] +impl MyContract { + #[constructor] + #[payable] + pub fn new(owner: Address) { + self.owner.set(owner); + // msg::value() is available + } +} +``` + +Output: + +``` +constructor(address owner) payable +``` + +## Export Configuration + +### Custom license + +Specify a custom SPDX license identifier: + +```shell +cargo stylus export-abi --license=GPL-3.0 +``` + +Output includes: + +```solidity +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; +``` + +### Custom pragma + +Specify a custom Solidity version pragma: + +```shell +cargo stylus export-abi --pragma="pragma solidity ^0.8.20;" +``` + +### Rust features + +Export ABI with specific Rust features enabled: + +```shell +cargo stylus export-abi --rust-features=feature1,feature2 +``` + +This is useful when your contract has conditional compilation: + +```rust +#[cfg(feature = "advanced")] +#[public] +impl MyContract { + pub fn advanced_function(&self) -> U256 { + // Advanced logic + } +} +``` + +## Integration with Front-End + +### Using ethers.js + +```typescript +import { ethers } from 'ethers'; +import MyContractABI from './abi.json'; + +const provider = new ethers.JsonRpcProvider('https://arb1.arbitrum.io/rpc'); +const contract = new ethers.Contract( + '0x1234567890123456789012345678901234567890', + MyContractABI, + provider, +); + +// Call view function +const value = await contract.getValue(); +console.log('Value:', value.toString()); + +// Call state-changing function (requires signer) +const signer = provider.getSigner(); +const contractWithSigner = contract.connect(signer); +const tx = await contractWithSigner.setValue(42); +await tx.wait(); +``` + +### Using viem + +```typescript +import { createPublicClient, http } from 'viem'; +import { arbitrum } from 'viem/chains'; +import MyContractABI from './abi.json'; + +const client = createPublicClient({ + chain: arbitrum, + transport: http(), +}); + +// Read contract +const value = await client.readContract({ + address: '0x1234567890123456789012345678901234567890', + abi: MyContractABI, + functionName: 'getValue', +}); + +// Write contract +const hash = await client.writeContract({ + address: '0x1234567890123456789012345678901234567890', + abi: MyContractABI, + functionName: 'setValue', + args: [42n], +}); +``` + +### Using wagmi/RainbowKit + +```typescript +import { useContractRead, useContractWrite } from 'wagmi'; +import MyContractABI from './abi.json'; + +function MyComponent() { + // Read contract + const { data: value } = useContractRead({ + address: '0x1234567890123456789012345678901234567890', + abi: MyContractABI, + functionName: 'getValue', + }); + + // Write contract + const { write } = useContractWrite({ + address: '0x1234567890123456789012345678901234567890', + abi: MyContractABI, + functionName: 'setValue', + }); + + return ( +
+

Current value: {value?.toString()}

+ +
+ ); +} +``` + +## Solidity Integration + +Use exported interfaces in Solidity contracts: + +### Import the interface + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./IMyContract.sol"; + +contract SolidityContract { + IMyContract public stylusContract; + + constructor(address _stylusContract) { + stylusContract = IMyContract(_stylusContract); + } + + function interactWithStylus() external { + // Read from Stylus contract + uint256 value = stylusContract.getValue(); + + // Write to Stylus contract + stylusContract.setValue(value + 1); + } +} +``` + +### Cross-language composition + +Combine Solidity and Rust contracts: + +```solidity +contract Router { + IToken public token; + IStaking public staking; + + constructor(address _token, address _staking) { + token = IToken(_token); // Rust contract + staking = IStaking(_staking); // Rust contract + } + + function stakeTokens(uint256 amount) external { + // Transfer tokens (Rust contract) + require( + token.transferFrom(msg.sender, address(this), amount), + "Transfer failed" + ); + + // Stake tokens (Rust contract) + token.approve(address(staking), amount); + staking.stake(msg.sender, amount); + } +} +``` + +## How It Works + +### The export-abi feature + +The `export-abi` feature enables ABI generation: + +```toml +# Cargo.toml +[features] +export-abi = ["stylus-sdk/export-abi"] + +[lib] +crate-type = ["lib", "cdylib"] +``` + +When enabled, the SDK generates: + +1. A `GenerateAbi` trait implementation +2. A CLI entry point for running ABI export +3. Formatting logic for Solidity interface generation + +### Main function + +Your contract needs a main function for ABI export: + +```rust +// main.rs +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] + +#[cfg(not(any(test, feature = "export-abi")))] +#[no_mangle] +pub extern "C" fn main() {} + +#[cfg(feature = "export-abi")] +fn main() { + my_contract::print_from_args(); +} +``` + +This main function: + +- Runs only when `export-abi` feature is enabled +- Executes the ABI generation logic +- Outputs the Solidity interface to stdout + +### The #[public] macro + +The `#[public]` macro generates ABI code: + +```rust +// From stylus-proc/src/macros/public/export_abi.rs +impl GenerateAbi for MyContract { + const NAME: &'static str = "MyContract"; + + fn fmt_abi(f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "interface I{} {{", Self::NAME)?; + // Generate function signatures + write!(f, "\n function getValue() external view returns (uint256);")?; + writeln!(f, "}}")?; + Ok(()) + } +} +``` + +Key transformations: + +- `snake_case` → `camelCase` function names +- Rust types → Solidity types +- `&self` → `view`, `&mut self` → non-view +- `Result` → return type `T`, error `E` + +## Best Practices + +### 1. Always export ABIs for integration + +```shell +# ✅ Good: Generate and version control ABIs +cargo stylus export-abi > interfaces/IMyContract.sol +git add interfaces/IMyContract.sol +git commit -m "Update contract ABI" + +# ❌ Bad: Rely on manual interface definitions +``` + +### 2. Use semantic function names + +```rust +// ✅ Good: Clear, descriptive names +#[public] +impl Token { + pub fn get_balance(&self, account: Address) -> U256 { } + pub fn transfer_from(&mut self, from: Address, to: Address, amount: U256) { } +} + +// ❌ Bad: Unclear abbreviations +#[public] +impl Token { + pub fn bal(&self, acc: Address) -> U256 { } + pub fn xfer(&mut self, f: Address, t: Address, amt: U256) { } +} +``` + +### 3. Document complex functions + +```rust +#[public] +impl Staking { + /// Stakes tokens for a specified duration + /// + /// # Arguments + /// * `amount` - Amount of tokens to stake + /// * `duration` - Lock duration in seconds + /// + /// # Returns + /// The unique stake ID + pub fn stake(&mut self, amount: U256, duration: u64) -> U256 { + // Implementation + } +} +``` + +### 4. Export JSON for tooling + +```shell +# ✅ Good: Generate both formats +cargo stylus export-abi > IMyContract.sol +cargo stylus export-abi --json > abi.json + +# Share with front-end team +cp abi.json ../frontend/src/abis/ +``` + +### 5. Version control constructor changes + +When adding or modifying constructors, regenerate and commit: + +```shell +cargo stylus export-abi constructor > CONSTRUCTOR.txt +git add CONSTRUCTOR.txt +git commit -m "Update constructor signature" +``` + +### 6. Test ABI compatibility + +```typescript +// test/abi.test.ts +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import MyContractABI from '../abi.json'; + +describe('ABI Compatibility', () => { + it('should match deployed contract', async () => { + const contract = await ethers.getContractAt(MyContractABI, deployedAddress); + + // Verify functions exist + expect(contract.getValue).to.exist; + expect(contract.setValue).to.exist; + + // Call and verify + const value = await contract.getValue(); + expect(value).to.be.a('bigint'); + }); +}); +``` + +### 7. Keep interfaces synchronized + +Use CI/CD to verify ABI is up to date: + +```yaml +# .github/workflows/check-abi.yml +name: Check ABI + +on: [pull_request] + +jobs: + check-abi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install Rust + uses: actions-rs/toolchain@v1 + - name: Generate ABI + run: cargo stylus export-abi > /tmp/abi.sol + - name: Check for changes + run: diff /tmp/abi.sol interfaces/IMyContract.sol +``` + +## Troubleshooting + +### solc not found + +**Error**: `failed to run solc: No such file or directory` + +**Solution**: Install Solidity compiler: + +```shell +# macOS +brew install solidity + +# Ubuntu/Debian +sudo add-apt-repository ppa:ethereum/ethereum +sudo apt-get update +sudo apt-get install solc + +# Or use solc-select +pip install solc-select +solc-select install 0.8.23 +solc-select use 0.8.23 +``` + +### Feature not enabled + +**Error**: `no main function` + +**Solution**: Ensure `export-abi` feature is defined and main.rs exists: + +```toml +# Cargo.toml +[features] +export-abi = ["stylus-sdk/export-abi"] +``` + +```rust +// main.rs +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] + +#[cfg(feature = "export-abi")] +fn main() { + my_contract::print_from_args(); +} +``` + +### Type not supported + +**Error**: `the trait AbiType is not implemented for MyType` + +**Solution**: Use supported types or implement `AbiType`: + +```rust +// ✅ Use supported types +pub fn process(&self, amount: U256) -> U256 { } + +// ❌ Custom types need AbiType implementation +pub fn process(&self, amount: MyCustomType) -> MyCustomType { } +``` + +For custom types, implement `AbiType`: + +```rust +use stylus_sdk::abi::AbiType; + +#[derive(Clone)] +struct MyType(U256); + +impl AbiType for MyType { + type SolType = alloy_sol_types::sol_data::Uint<256>; + + fn encode(&self) -> Vec { + self.0.encode() + } + + fn decode(data: &[u8]) -> Result { + U256::decode(data).map(MyType) + } +} +``` + +### Missing function in ABI + +**Error**: Function doesn't appear in exported ABI + +**Solutions**: + +1. Ensure function is in `#[public]` impl block: + + ```rust + #[public] + impl MyContract { + pub fn my_function(&self) -> U256 { } // ✅ Exported + } + + impl MyContract { + pub fn helper(&self) -> U256 { } // ❌ Not exported + } + ``` + +2. Check function visibility is `pub`: + ```rust + #[public] + impl MyContract { + pub fn exported(&self) -> U256 { } // ✅ Exported + fn not_exported(&self) -> U256 { } // ❌ Not exported + } + ``` + +## Advanced: Multiple Contracts + +Export ABIs for all contracts in a workspace: + +```shell +# Export specific contract +cargo stylus export-abi --contract=my-token + +# Export all contracts +for contract in token staking governance; do + cargo stylus export-abi --contract=$contract > interfaces/I${contract^}.sol +done +``` + +Or create a script: + +```shell +#!/bin/bash +# export-all-abis.sh + +contracts=("token" "staking" "governance") + +for contract in "${contracts[@]}"; do + echo "Exporting ABI for $contract..." + cargo stylus export-abi --contract=$contract > "interfaces/I${contract^}.sol" + cargo stylus export-abi --contract=$contract --json > "abis/${contract}.json" +done + +echo "✅ All ABIs exported" +``` + +## Resources + +- [Stylus SDK repository](https://github.com/OffchainLabs/stylus-sdk-rs) +- [Cargo Stylus CLI](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/cargo-stylus) +- [ERC-20 example](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/examples/erc20) +- [ABI specification](https://docs.soliditylang.org/en/latest/abi-spec.html) +- [ethers.js documentation](https://docs.ethers.org/) +- [viem documentation](https://viem.sh/) +- [wagmi documentation](https://wagmi.sh/) diff --git a/docs/stylus/how-tos/optimizing-binaries.mdx b/docs/stylus/how-tos/optimizing-binaries.mdx index d88274f8ad..c8de5e1323 100644 --- a/docs/stylus/how-tos/optimizing-binaries.mdx +++ b/docs/stylus/how-tos/optimizing-binaries.mdx @@ -6,7 +6,7 @@ sme: rauljordan target_audience: 'Developers deploying smart contracts using Stylus' content_type: how-to sidebar_position: 1 -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildStylusSidebar --- To be deployed onchain, the size of your **uncompressed WebAssembly (WASM) file** must not exceed 128Kb, while the **compressed binary** must not exceed 24KB. Stylus conforms with the same contract size limit as the EVM to remain fully interoperable with all smart contracts on Arbitrum chains. @@ -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/how-tos/testing-contracts.mdx b/docs/stylus/how-tos/testing-contracts.mdx index dd61568bb5..c960961ba2 100644 --- a/docs/stylus/how-tos/testing-contracts.mdx +++ b/docs/stylus/how-tos/testing-contracts.mdx @@ -5,7 +5,7 @@ description: 'A comprehensive guide to writing and running tests for Stylus smar sme: anegg0 target_audience: 'Developers writing smart contracts using Stylus.' sidebar_position: 3 -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildStylusSidebar --- import CustomDetails from '@site/src/components/CustomDetails'; diff --git a/docs/stylus/how-tos/using-constructors.mdx b/docs/stylus/how-tos/using-constructors.mdx index c50a713bd4..8f58324d2c 100644 --- a/docs/stylus/how-tos/using-constructors.mdx +++ b/docs/stylus/how-tos/using-constructors.mdx @@ -7,7 +7,7 @@ sme: 'anegg0' user_story: 'As a Rust developer, I want to understand how to implement and use constructors in Stylus smart contracts' content_type: 'how-to' sidebar_position: 3 -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildStylusSidebar --- import CustomDetails from '@site/src/components/CustomDetails'; diff --git a/docs/stylus/how-tos/using-inheritance.mdx b/docs/stylus/how-tos/using-inheritance.mdx index 155ff8813f..a413e427d1 100644 --- a/docs/stylus/how-tos/using-inheritance.mdx +++ b/docs/stylus/how-tos/using-inheritance.mdx @@ -5,7 +5,7 @@ author: anegg0 sme: mahsamoosavi content_type: how-to sidebar_position: 1 -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildStylusSidebar --- import CustomDetails from '@site/src/components/CustomDetails'; diff --git a/docs/stylus/how-tos/verifying-contracts-arbiscan.mdx b/docs/stylus/how-tos/verifying-contracts-arbiscan.mdx index 358f20ba2c..54a6420c9b 100644 --- a/docs/stylus/how-tos/verifying-contracts-arbiscan.mdx +++ b/docs/stylus/how-tos/verifying-contracts-arbiscan.mdx @@ -6,7 +6,7 @@ sme: mahsamoosavi target_audience: 'Developers deploying smart contracts using Stylus' content_type: how-to sidebar_position: 1 -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildStylusSidebar --- import ImageZoom from '@site/src/components/ImageZoom'; diff --git a/docs/stylus/how-tos/verifying-contracts.mdx b/docs/stylus/how-tos/verifying-contracts.mdx index 819ec19f4b..e19d19e036 100644 --- a/docs/stylus/how-tos/verifying-contracts.mdx +++ b/docs/stylus/how-tos/verifying-contracts.mdx @@ -8,7 +8,7 @@ target_audience: 'Developers learning how to use Stylus' sidebar_position: 2 user_story: 'As a current or prospective Stylus user, I want to learn how to make sure my Stylus contracts deployment are reproducible by anyone running the same environment.' content_type: how-to -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildStylusSidebar --- :::caution 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! - -
- - - - - - - - - - - - -
diff --git a/docs/stylus/quickstart.mdx b/docs/stylus/quickstart.mdx index a3b28c3042..7651067793 100644 --- a/docs/stylus/quickstart.mdx +++ b/docs/stylus/quickstart.mdx @@ -6,7 +6,7 @@ author: chrisco512, anegg0 sme: chrisco512, anegg0 sidebar_position: 2 target_audience: Developers writing Stylus contracts in Rust using Stylus -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildStylusSidebar --- import ImageZoom from '@site/src/components/ImageZoom'; diff --git a/docs/stylus/reference/contracts.mdx b/docs/stylus/reference/contracts.mdx new file mode 100644 index 0000000000..0b23a35abb --- /dev/null +++ b/docs/stylus/reference/contracts.mdx @@ -0,0 +1,1114 @@ +--- +title: 'Stylus contracts' +description: 'Stylus Rust SDK contracts, methods, and lifecycle' +author: chrisco +sme: chrisco +sidebar_position: 2 +target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts. +displayed_sidebar: buildStylusSidebar +--- + +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 + +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 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..00b7e89b2b --- /dev/null +++ b/docs/stylus/reference/data-types/compound-types.mdx @@ -0,0 +1,845 @@ +--- +title: 'Stylus compound types' +description: 'Stylus Rust SDK compound types' +author: chrisco +sme: chrisco +sidebar_position: 2 +target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts. +displayed_sidebar: buildStylusSidebar +--- + +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 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..e2f77358e8 --- /dev/null +++ b/docs/stylus/reference/data-types/conversions-between-types.mdx @@ -0,0 +1,377 @@ +--- +title: 'Conversions between types' +description: 'Learn how to convert between Rust types, Alloy primitives, and Solidity types in Stylus smart contracts' +author: chrisco +sme: chrisco +sidebar_position: 1 +target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts. +displayed_sidebar: buildStylusSidebar +--- + +Stylus smart contracts often need to convert between different type representations: Rust native types, Alloy primitives, and Solidity types. The Stylus SDK provides comprehensive conversion mechanisms through the `AbiType` trait and various standard Rust conversion traits. + +## Understanding type relationships + +The Stylus SDK establishes a bidirectional relationship between Rust and Solidity types: + +- **Alloy provides**: Solidity types → Rust types mapping via `SolType` +- **Stylus SDK provides**: Rust types → Solidity types mapping via `AbiType` + +Together, these create a complete two-way type system for interoperability. + +### Key type mappings + +| Rust/Alloy type | Solidity type | Notes | +| --------------------------------- | ------------------------- | ---------------------------------------------- | +| `bool` | `bool` | Native boolean | +| `u8`, `u16`, `u32`, `u64`, `u128` | `uint8` through `uint128` | Native unsigned integers | +| `i8`, `i16`, `i32`, `i64`, `i128` | `int8` through `int128` | Native signed integers | +| `Uint` | `uintBITS` | Arbitrary-sized unsigned integers (8-256 bits) | +| `Signed` | `intBITS` | Arbitrary-sized signed integers (8-256 bits) | +| `U256` | `uint256` | 256-bit unsigned integer (most common) | +| `Address` | `address` | 20-byte Ethereum address | +| `FixedBytes` | `bytesN` | Fixed-size byte array | +| `Bytes` | `bytes` | Dynamic byte array | +| `String` | `string` | Dynamic UTF-8 string | +| `Vec` | `T[]` | Dynamic array | +| `[T; N]` | `T[N]` | Fixed-size array | +| `(T1, T2, ...)` | `(T1, T2, ...)` | Tuple types | + +**Important**: The SDK treats `Vec` as Solidity `uint8[]`. For Solidity `bytes`, use `alloy_primitives::Bytes`. + +## Converting numeric types + +### Creating integers from literals + +```rust +use stylus_sdk::alloy_primitives::{U256, I256, U8, I8}; + +// From integer literals +let small: U8 = U8::from(1); +let large: U256 = U256::from(255); + +// For signed integers +let positive: I8 = I8::unchecked_from(127); +let negative: I8 = I8::unchecked_from(-1); +let signed_large: I256 = I256::unchecked_from(0xff_u64); +``` + +### Parsing from strings + +```rust +use stylus_sdk::alloy_primitives::I256; + +// Parse decimal strings +let a = I256::try_from(20003000).unwrap(); +let b = "100".parse::().unwrap(); + +// Parse hexadecimal strings +let c = "-0x138f".parse::().unwrap(); + +// Underscores are ignored for readability +let d = "1_000_000".parse::().unwrap(); + +// Arithmetic works as expected +let result = a * b + c - d; +``` + +### Integer constants + +```rust +use stylus_sdk::alloy_primitives::I256; + +let max = I256::MAX; // Maximum value +let min = I256::MIN; // Minimum value +let zero = I256::ZERO; // Zero +let minus_one = I256::MINUS_ONE; // -1 +``` + +### Converting between integer sizes + +```rust +use stylus_sdk::alloy_primitives::{Uint, Signed, U256}; + +// Between Alloy integer types (same bit-width) +let uint_value = Uint::<128, 2>::from(999); +let u128_value: u128 = uint_value.try_into() + .map_err(|_| "conversion error") + .unwrap(); + +// Between different bit-widths +let small = Uint::<8, 1>::from(100); +let large = U256::from(small); +``` + +The SDK uses the `ConvertInt` trait internally to enable conversions between Alloy's `Uint` types and Rust native integer types like `u8`, `u16`, `u32`, `u64`, and `u128`. + +## Converting addresses + +### Creating addresses + +```rust +use stylus_sdk::alloy_primitives::{Address, address}; + +// From a 20-byte array +let addr1 = Address::from([0x11; 20]); + +// Using the address! macro with checksummed string +let addr2 = address!("d8da6bf26964af9d7eed9e03e53415d37aa96045"); + +// From a byte slice +let bytes: [u8; 20] = [0xd8, 0xda, 0x6b, 0xf2, /* ... */]; +let addr3 = Address::from(bytes); +``` + +### Converting addresses to bytes + +```rust +use stylus_sdk::alloy_primitives::Address; + +let addr = address!("d8da6bf26964af9d7eed9e03e53415d37aa96045"); + +// Get reference to underlying bytes +let bytes_ref: &[u8] = addr.as_ref(); + +// Use in byte concatenation +let data = [addr.as_ref(), other_data].concat(); +``` + +## Converting byte types + +### Fixed-size bytes + +```rust +use stylus_sdk::alloy_primitives::FixedBytes; + +// Create from array +let fixed = FixedBytes::<32>::new([0u8; 32]); + +// Create from slice +let slice: &[u8] = &[1, 2, 3, 4]; +let fixed = FixedBytes::<4>::from_slice(slice); + +// Convert to slice +let bytes_ref: &[u8] = fixed.as_ref(); +``` + +### Dynamic bytes + +```rust +use stylus_sdk::abi::Bytes; + +// Create from Vec +let bytes = Bytes::from(vec![1, 2, 3, 4]); + +// Create empty +let empty = Bytes::new(); + +// Get reference to underlying data +let data: &[u8] = bytes.as_ref(); + +// Convert to Vec +let vec: Vec = bytes.to_vec(); +``` + +### Byte array conversions + +```rust +use stylus_sdk::alloy_primitives::U256; + +// Convert U256 to big-endian bytes +let value = U256::from(12345); +let bytes_vec: Vec = value.to_be_bytes_vec(); +let bytes_array: [u8; 32] = value.to_be_bytes(); + +// Convert from big-endian bytes +let from_slice = U256::try_from_be_slice(&bytes_vec).unwrap(); +``` + +## Converting strings + +```rust +use alloc::string::{String, ToString}; + +// String conversions +let rust_string = "hello".to_string(); +let bytes = rust_string.as_bytes(); + +// For Solidity string parameters in functions +// the String type is automatically handled by AbiType +pub fn process_string(&self, text: String) -> String { + text +} +``` + +## Converting collections + +### Dynamic arrays (Vec) + +```rust +use stylus_sdk::alloy_primitives::U256; +use alloc::vec::Vec; + +// Vec is used directly as Solidity dynamic arrays +let numbers: Vec = vec![ + U256::from(1), + U256::from(2), + U256::from(3), +]; + +// For Vec, note this maps to uint8[], not bytes +let uint8_array: Vec = vec![1, 2, 3]; +``` + +### Fixed-size arrays + +```rust +use stylus_sdk::alloy_primitives::U256; + +// Fixed arrays map directly to Solidity fixed arrays +let fixed: [U256; 3] = [ + U256::from(1), + U256::from(2), + U256::from(3), +]; + +// Nested arrays +let nested: [[u32; 2]; 4] = [[1, 2], [3, 4], [5, 6], [7, 8]]; +``` + +## ABI encoding and decoding + +### Encoding types + +```rust +use stylus_sdk::abi::{encode, encode_params}; +use stylus_sdk::alloy_primitives::{Address, U256}; +use alloy_sol_types::{sol_data::*, SolType}; + +// Encode a single value +let value = U256::from(100); +let encoded = encode(&value); + +// Encode tuple of parameters +type TransferParams = (Address, Uint<256>); +let params = (address, amount); +let encoded = TransferParams::abi_encode_params(¶ms); +``` + +### Decoding types + +```rust +use stylus_sdk::abi::decode_params; +use stylus_sdk::alloy_primitives::{Address, U256}; +use alloy_sol_types::{sol_data::*, SolType}; + +// Define the expected type structure +type TransferParams = (Address, Uint<256>); + +// Decode from bytes +let decoded: (Address, U256) = TransferParams::abi_decode_params(&encoded_data) + .map_err(|_| "decode error")?; +``` + +### Packed encoding + +Packed encoding is useful for hashing and signature verification: + +```rust +use stylus_sdk::alloy_primitives::{Address, U256}; +use alloy_sol_types::{sol_data::*, SolType}; + +// Method 1: Using SolType::abi_encode_packed +type DataTypes = (Address, Uint<256>, String, Bytes, Uint<256>); +let data = (target, value, func, bytes, timestamp); +let packed = DataTypes::abi_encode_packed(&data); + +// Method 2: Manual concatenation +let packed_manual = [ + target.as_ref(), + &value.to_be_bytes_vec(), + func.as_bytes(), + bytes.as_ref(), + ×tamp.to_be_bytes_vec(), +].concat(); +``` + +## Error type conversions + +Stylus error types can be converted using the `Into` trait: + +```rust +use stylus_sdk::prelude::*; + +sol! { + error InvalidParam(); + error NotFound(); +} + +#[derive(SolidityError)] +pub enum MyError { + InvalidParam(InvalidParam), + NotFound(NotFound), +} + +pub fn check_value(&self, value: U256) -> Result<(), MyError> { + if value == U256::ZERO { + return Err(InvalidParam {}.into()); + } + Ok(()) +} +``` + +## Storage type conversions + +Storage types require special handling for persistence: + +```rust +use stylus_sdk::prelude::*; +use stylus_sdk::alloy_primitives::U256; + +#[storage] +pub struct Counter { + count: StorageU256, +} + +#[public] +impl Counter { + // Get value from storage + pub fn get_count(&self) -> U256 { + self.count.get() + } + + // Set value in storage + pub fn set_count(&mut self, value: U256) { + self.count.set(value); + } + + // Increment using arithmetic + pub fn increment(&mut self) { + let current = self.count.get(); + self.count.set(current + U256::from(1)); + } +} +``` + +## Best practices + +1. **Use try_from for fallible conversions**: When converting between types where overflow is possible, use `try_from` instead of panicking conversions. + +2. **Prefer native types when appropriate**: Use Rust's native `bool`, `u8`-`u128`, and `i8`-`i128` types when they match your needs exactly. They're more efficient and ergonomic. + +3. **Be explicit about byte types**: Remember that `Vec` maps to `uint8[]`, not `bytes`. Use `Bytes` from `stylus_sdk::abi` for Solidity `bytes` type. + +4. **Use the address! macro**: For hardcoded addresses, use the `address!` macro which performs compile-time validation and checksumming. + +5. **Handle conversion errors**: Always handle potential errors from `try_from`, `try_into`, and `parse` operations rather than using unwrap in production code. + +6. **Consider packed encoding for hashing**: When preparing data for hashing or signature verification, packed encoding produces more compact representations. + +## Reference + +For complete implementation details, see: + +- `/stylus-sdk/src/abi/mod.rs` - AbiType trait and encoding functions +- `/stylus-sdk/src/abi/ints.rs` - Integer type conversions +- `/stylus-sdk/src/abi/impls.rs` - Implementations for standard types +- `/stylus-sdk/src/storage/traits.rs` - Storage type conversion traits diff --git a/docs/stylus/reference/data-types/primitives.mdx b/docs/stylus/reference/data-types/primitives.mdx new file mode 100644 index 0000000000..d46d8c6f1b --- /dev/null +++ b/docs/stylus/reference/data-types/primitives.mdx @@ -0,0 +1,564 @@ +--- +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: buildStylusSidebar +--- + +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 diff --git a/docs/stylus/reference/data-types/storage.mdx b/docs/stylus/reference/data-types/storage.mdx new file mode 100644 index 0000000000..8f353542fe --- /dev/null +++ b/docs/stylus/reference/data-types/storage.mdx @@ -0,0 +1,1097 @@ +--- +title: 'Stylus Rust SDK storage' +description: 'Stylus Rust SDK storage types' +author: chrisco +sme: chrisco +sidebar_position: 3 +target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts. +displayed_sidebar: buildStylusSidebar +--- + +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 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..8d0bafbb19 --- /dev/null +++ b/docs/stylus/reference/global-variables-and-functions.mdx @@ -0,0 +1,894 @@ +--- +title: 'Global variables and functions' +description: 'Stylus Rust SDK global variables and functions for blockchain context access' +author: chrisco +sme: chrisco +sidebar_position: 3 +target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts. +displayed_sidebar: buildStylusSidebar +--- + +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 diff --git a/docs/stylus/reference/project-structure.mdx b/docs/stylus/reference/project-structure.mdx index c37969b3d7..e6f6cd19c8 100644 --- a/docs/stylus/reference/project-structure.mdx +++ b/docs/stylus/reference/project-structure.mdx @@ -5,7 +5,7 @@ author: chrisco sme: chrisco sidebar_position: 1 target_audience: Developers using the Stylus Rust SDK to write and deploy smart contracts. -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildStylusSidebar --- Contracts in Rust are similar to contracts in Solidity. Each contract can contain declarations of State Variables, Functions, Function Modifiers, Events, Errors, Struct Types, and Enum Types. In addition, Rust contracts can import third-party packages from [crates.io](https://crates.io) as dependencies and use them for advanced functionality. @@ -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 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! - -
- - - - - - - - - - - - -
diff --git a/docs/stylus/using-cli.mdx b/docs/stylus/using-cli.mdx index 9fa36931e9..da6b73b4fa 100644 --- a/docs/stylus/using-cli.mdx +++ b/docs/stylus/using-cli.mdx @@ -6,7 +6,7 @@ author: 'anegg0' sme: 'anegg0' sidebar_position: 2 target_audience: Developers writing Stylus contracts in Rust using Stylus -displayed_sidebar: buildAppsSidebar +displayed_sidebar: buildStylusSidebar --- This guide will get you started using [cargo stylus](https://github.com/OffchainLabs/cargo-stylus), a CLI toolkit to help developers manage, compile, deploy, and optimize their Stylus contracts efficiently. diff --git a/sidebars.js b/sidebars.js index 0261cb84e5..bff9cf567a 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 = { @@ -1197,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', @@ -1217,142 +1210,606 @@ const sidebars = { id: 'stylus/reference/project-structure', label: 'Structure of a Contract', }, - ...stylusByExampleBasicExamples, + { + 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/how-tos/using-inheritance', - label: 'Composition and trait-based routing model', + id: 'stylus/reference/global-variables-and-functions', + label: 'Global variables and functions', }, { type: 'doc', - id: 'stylus/reference/rust-sdk-guide', - label: 'Advanced features', + id: 'stylus/reference/contracts', + label: 'Contracts', + }, + { + type: 'doc', + id: 'stylus/how-tos/testing-contracts', + label: 'Writing Tests', + }, + { + 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: 'doc', - id: 'stylus/recommended-libraries', - label: 'Recommended Rust Crates', + id: 'stylus/troubleshooting-building-stylus', + label: 'Troubleshooting', + }, + { + type: 'category', + label: 'Using the CLI', + collapsed: true, + items: [ + { + 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-binaries', + label: 'Optimizing WASM binary size', + }, + { + type: 'doc', + id: 'stylus/how-tos/deploying-non-rust-wasm-contracts', + label: 'Deploying non-Rust WASM contracts', + }, + ], + }, + { + type: 'category', + label: 'WM Concepts', + collapsed: true, + items: [ + { + type: 'doc', + id: 'stylus/concepts/webassembly', + label: 'Webassembly', + }, + { + 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: '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/stylus-sdk-rs/tree/main/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-sdk-rs', + }, + ], }, ], }, + ], + }, + { + type: 'html', + value: + 'Chain Info', + }, + { + type: 'html', + value: + 'Glossary', + }, + { + type: 'html', + value: + 'Contribute', + }, + ], + + // Solidity-focused sidebar (only shows Solidity content) + buildSoliditySidebar: [ + { + type: 'category', + label: 'Build apps with Solidity', + collapsed: false, + items: [ + { + type: 'doc', + id: 'build-decentralized-apps/quickstart-solidity-remix', + label: 'Quickstart', + }, + { + type: 'doc', + label: 'Estimate gas', + id: 'build-decentralized-apps/how-to-estimate-gas', + }, + { + type: 'doc', + label: 'Chains and testnets', + id: 'build-decentralized-apps/public-chains', + }, + { + type: 'doc', + label: 'Cross-chain messaging', + id: 'build-decentralized-apps/cross-chain-messaging', + }, + { + type: 'doc', + id: 'build-decentralized-apps/custom-gas-token-sdk', + label: 'Custom gas token SDK', + }, { type: 'category', - label: 'Rust CLI', - collapsed: true, + label: 'Arbitrum vs Ethereum', items: [ { type: 'doc', - id: 'stylus/using-cli', - label: 'Overview', - }, - { - type: 'doc', - id: 'stylus/how-tos/debugging-tx', - label: 'Debug transactions', + label: 'Comparison overview', + id: 'build-decentralized-apps/arbitrum-vs-ethereum/comparison-overview', }, { type: 'doc', - id: 'stylus/how-tos/testing-contracts', - label: 'Testing contracts', + label: 'Block gas limit, numbers and time', + id: 'build-decentralized-apps/arbitrum-vs-ethereum/block-numbers-and-time', }, { type: 'doc', - id: 'stylus/how-tos/verifying-contracts', - label: 'Verify contracts', + label: 'RPC methods', + id: 'build-decentralized-apps/arbitrum-vs-ethereum/rpc-methods', }, { type: 'doc', - id: 'stylus/how-tos/caching-contracts', - label: 'Cache contracts', + label: 'Solidity support', + id: 'build-decentralized-apps/arbitrum-vs-ethereum/solidity-support', }, + ], + }, + { + type: 'doc', + label: 'Oracles', + id: 'build-decentralized-apps/oracles/overview-oracles', + }, + { + type: 'category', + label: 'Precompiles', + collapsed: true, + items: [ { type: 'doc', - id: 'stylus/how-tos/verifying-contracts-arbiscan', - label: 'Verify on Arbiscan', + label: 'Overview', + id: 'build-decentralized-apps/precompiles/overview', }, { type: 'doc', - id: 'stylus/how-tos/optimizing-binaries', - label: 'Optimize WASM binaries', + label: 'Reference', + id: 'build-decentralized-apps/precompiles/reference', }, ], }, - { - type: 'html', - value: - 'Run a local dev node', - }, { type: 'category', - label: 'Concepts', + label: 'NodeInterface', collapsed: true, items: [ { type: 'doc', - id: 'stylus/concepts/how-it-works', - label: 'Architecture overview', + label: 'Overview', + id: 'build-decentralized-apps/nodeinterface/overview', }, { type: 'doc', - id: 'stylus/concepts/gas-metering', - label: 'Gas metering', + label: 'Reference', + id: 'build-decentralized-apps/nodeinterface/reference', }, ], }, { type: 'category', - label: 'Examples', + label: 'Token bridging', collapsed: true, items: [ - ...stylusByExampleApplications, { - type: 'link', - label: 'Awesome Stylus', - href: 'https://github.com/OffchainLabs/awesome-stylus', + type: 'doc', + label: 'Overview', + id: 'build-decentralized-apps/token-bridging/overview', + }, + { + type: 'doc', + label: 'ETH bridging', + id: 'build-decentralized-apps/token-bridging/token-bridge-ether', + }, + { + type: 'doc', + label: 'ERC-20 token bridging', + id: 'build-decentralized-apps/token-bridging/token-bridge-erc20', + }, + { + type: 'category', + label: 'Bridge tokens programmatically', + items: [ + { + type: 'doc', + label: 'Get started', + id: 'build-decentralized-apps/token-bridging/bridge-tokens-programmatically/get-started', + }, + { + type: 'doc', + label: 'Use the standard gateway', + id: 'build-decentralized-apps/token-bridging/bridge-tokens-programmatically/how-to-bridge-tokens-standard', + }, + { + type: 'doc', + label: 'Use the generic-custom gateway', + id: 'build-decentralized-apps/token-bridging/bridge-tokens-programmatically/how-to-bridge-tokens-generic-custom', + }, + { + type: 'doc', + label: 'Use the custom gateway', + id: 'build-decentralized-apps/token-bridging/bridge-tokens-programmatically/how-to-bridge-tokens-custom-gateway', + }, + ], }, ], }, { type: 'category', label: 'Reference', - collapsed: true, items: [ { type: 'doc', - id: 'stylus/reference/opcode-hostio-pricing', - label: 'Gas & Ink Pricing', + id: 'build-decentralized-apps/reference/node-providers', + label: 'RPC endpoints and providers', }, { - type: 'link', - label: 'Stylus by Example', - href: 'https://stylus-by-example.org/', + type: 'doc', + label: 'Smart contract addresses', + id: 'build-decentralized-apps/reference/contract-addresses', }, { - type: 'link', - label: 'Cargo Stylus CLI GitHub', - href: 'https://github.com/OffchainLabs/cargo-stylus', + type: 'doc', + label: 'Chain parameters', + id: 'build-decentralized-apps/reference/chain-params', }, { - type: 'link', - label: 'Rust SDK Crate', - href: 'https://docs.rs/stylus-sdk/latest/stylus_sdk/index.html', + type: 'doc', + label: 'Development frameworks', + id: 'build-decentralized-apps/reference/development-frameworks', }, { - type: 'link', - label: 'Source Code Repository', - href: 'https://github.com/OffchainLabs/stylus', + type: 'doc', + label: 'Web3 libraries and tools', + id: 'build-decentralized-apps/reference/web3-libraries-tools', + }, + { + type: 'doc', + label: 'Monitoring tools and block explorers', + id: 'build-decentralized-apps/reference/monitoring-tools-block-explorers', + }, + { + type: 'doc', + label: 'Debugging tools', + id: 'build-decentralized-apps/reference/debugging-tools', + }, + + { + type: 'doc', + id: 'build-decentralized-apps/reference/mainnet-risks', + label: 'Mainnet risks', }, ], }, { type: 'doc', - id: 'stylus/how-tos/adding-support-for-new-languages', - label: 'Using other languages', + label: 'Troubleshooting', + id: 'for-devs/troubleshooting-building', }, + { + type: 'category', + label: 'Arbitrum SDK', + items: sdkSidebar.sdkSidebar, + }, + { + type: 'link', + label: 'Tutorials', + href: 'https://github.com/OffchainLabs/arbitrum-tutorials', + }, + ], + }, + { + type: 'html', + value: + 'Chain Info', + }, + { + type: 'html', + value: + 'Glossary', + }, + { + type: 'html', + value: + 'Contribute', + }, + ], + + // Stylus-focused sidebar (only shows Stylus content) + buildStylusSidebar: [ + { + type: 'category', + label: 'Build apps with Stylus', + collapsed: false, + items: [ { type: 'doc', - id: 'stylus/troubleshooting-building-stylus', - label: 'Troubleshooting', + id: 'stylus/gentle-introduction', + label: 'A gentle introduction', + }, + { + type: 'category', + label: 'Rust SDK', + collapsed: true, + items: [ + { + type: 'doc', + id: 'stylus/reference/overview', + label: 'Overview', + }, + { + type: 'doc', + 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/testing-contracts', + label: 'Writing Tests', + }, + { + 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: 'doc', + id: 'stylus/troubleshooting-building-stylus', + label: 'Troubleshooting', + }, + { + 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-binaries', + label: 'Optimizing WASM binary size', + }, + { + type: 'doc', + id: 'stylus/how-tos/deploying-non-rust-wasm-contracts', + label: 'Deploying non-Rust WASM contracts', + }, + ], + }, + { + type: 'category', + label: 'WM Concepts', + collapsed: true, + items: [ + { + type: 'doc', + id: 'stylus/concepts/webassembly', + label: 'Webassembly', + }, + { + 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: '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/stylus-sdk-rs/tree/main/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-sdk-rs', + }, + ], + }, + ], }, ], }, diff --git a/vercel.json b/vercel.json index ad693a20fc..4e007d0734 100644 --- a/vercel.json +++ b/vercel.json @@ -431,6 +431,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",