From e4d273269a96b31c4ff63726b65f66ba2361299a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Wed, 24 Jun 2026 16:18:25 -0700 Subject: [PATCH 1/5] docs(stylus): update how-tos for SDK 0.10.7 --- docs/stylus/how-tos/exporting-abi.mdx | 83 +++++-------------- .../how-tos/trait-based-composition.mdx | 52 ++++++------ docs/stylus/how-tos/using-constructors.mdx | 71 ++++++++++++++-- 3 files changed, 109 insertions(+), 97 deletions(-) diff --git a/docs/stylus/how-tos/exporting-abi.mdx b/docs/stylus/how-tos/exporting-abi.mdx index c72c4db3dd..8e21421706 100644 --- a/docs/stylus/how-tos/exporting-abi.mdx +++ b/docs/stylus/how-tos/exporting-abi.mdx @@ -65,7 +65,7 @@ interface IMyContract { function setValue(uint256 new_value) external; - error Unauthorized(address caller); + error Unauthorized(address); } ``` @@ -91,39 +91,16 @@ Generate JSON format ABI (requires `solc` installed): cargo stylus export-abi --json > abi.json ``` -Output: +The JSON output is produced by `solc`, so it includes `solc`'s header lines before the ABI array: -```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" - } -] +``` +======= :IMyContract ======= +Contract JSON ABI +[{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"Unauthorized","type":"error"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"new_value","type":"uint256"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}] ``` +Strip the two header lines before feeding the array to front-end tooling. + ## Writing ABI-compatible contracts ### Basic contract structure @@ -281,14 +258,14 @@ impl Token { } ``` -Generated interface includes errors: +Generated interface includes errors. Note that the exported Solidity drops the error parameter names, keeping only their types: ```solidity interface IToken { function transfer(address to, uint256 amount) external; - error InsufficientBalance(address account, uint256 requested, uint256 available); - error Unauthorized(address caller); + error InsufficientBalance(address, uint256, uint256); + error Unauthorized(address); error InvalidAmount(); } ``` @@ -444,10 +421,10 @@ impl MyContract { } ``` -Export constructor signature: +Export constructor signature with the top-level `constructor` command (the constructor is not part of `export-abi` output): ```shell -cargo stylus export-abi constructor +cargo stylus constructor ``` Output: @@ -478,29 +455,6 @@ 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: @@ -795,7 +749,7 @@ cp abi.json ../frontend/src/abis/ When adding or modifying constructors, regenerate and commit: ```shell -cargo stylus export-abi constructor > CONSTRUCTOR.txt +cargo stylus constructor > CONSTRUCTOR.txt git add CONSTRUCTOR.txt git commit -m "Update constructor signature" ``` @@ -958,11 +912,16 @@ impl AbiType for MyType { Export ABIs for all contracts in a workspace: +The `--contract` value is a cargo package name. Repeat the flag to select several contracts; with no `--contract`, the workspace's default contracts are exported. + ```shell -# Export specific contract +# Export specific contract by package name cargo stylus export-abi --contract=my-token -# Export all contracts +# Select several contracts in one invocation +cargo stylus export-abi --contract=token --contract=staking + +# Export each to its own file for contract in token staking governance; do cargo stylus export-abi --contract=$contract > interfaces/I${contract^}.sol done diff --git a/docs/stylus/how-tos/trait-based-composition.mdx b/docs/stylus/how-tos/trait-based-composition.mdx index a20467ffb5..e66abed830 100644 --- a/docs/stylus/how-tos/trait-based-composition.mdx +++ b/docs/stylus/how-tos/trait-based-composition.mdx @@ -63,6 +63,7 @@ use stylus_sdk::{ }; // Define traits for different functionality +#[public] trait IErc20 { fn name(&self) -> String; fn symbol(&self) -> String; @@ -72,6 +73,7 @@ trait IErc20 { fn transfer(&mut self, to: Address, value: U256) -> bool; } +#[public] trait IOwnable { fn owner(&self) -> Address; fn transfer_ownership(&mut self, new_owner: Address) -> bool; @@ -181,44 +183,41 @@ When using trait-based composition, you need to be careful about function select + ```rust -// In Solidity, both these functions would have different selectors: -// function safeTransferFrom(address from, address to, uint256 tokenId) -// function safeTransferFrom(address from, address to, uint256 tokenId, bytes data) - -// In Rust, we need to use different method names, but want the same selectors: #[public] -impl Erc721 { -// Use the #[selector] attribute to specify the correct Solidity-compatible name #[selector(name = "safeTransferFrom")] -pub fn safe_transfer_from_with_data>( -storage: &mut S, -from: Address, -to: Address, -token_id: U256, -data: Bytes, -) -> Result<(), Erc721Error> { -// Implementation -} +// In Solidity, these two functions share the name `safeTransferFrom` but have +// different selectors because their parameter lists differ: +// function safeTransferFrom(address from, address to, uint256 tokenId) +// function safeTransferFrom(address from, address to, uint256 tokenId, bytes data) + +// Rust has no method overloading, so each variant needs a distinct Rust name. +// Use the #[selector(name = "...")] attribute to export a Solidity-compatible +// selector. Because the two variants take different parameters, both can keep +// the `safeTransferFrom` name on the ABI side without colliding. +#[public] +impl Erc721 { + #[selector(name = "safeTransferFrom")] + pub fn safe_transfer_from(&mut self, from: Address, to: Address, token_id: U256) { + // Implementation here + } - // This method also needs the same selector name #[selector(name = "safeTransferFrom")] - pub fn safe_transfer_from>( - storage: &mut S, + pub fn safe_transfer_from_with_data( + &mut self, from: Address, to: Address, token_id: U256, - ) -> Result<(), Erc721Error> { - // Implementation + data: Bytes, + ) { + // Implementation here } - } - ``` + - The Stylus SDK generates ABIs based on the methods that are available at the entrypoint contract. - When using trait-based composition, make sure that all methods you want exposed in the ABI are - properly included through the #[implements] attribute. + The Stylus SDK generates ABIs based on the methods that are available at the entrypoint contract. When using trait-based composition, make sure that all methods you want exposed in the ABI are properly included through the #[implements] attribute. ## Methods search order @@ -235,4 +234,3 @@ In a typical composition chain: - If not found there, it looks in the first trait specified in the inheritance list - If still not found, it searches in the next trait in the list - This continues until the method is found or all possibilities are exhausted -``` diff --git a/docs/stylus/how-tos/using-constructors.mdx b/docs/stylus/how-tos/using-constructors.mdx index b9adc0f165..515a8364b4 100644 --- a/docs/stylus/how-tos/using-constructors.mdx +++ b/docs/stylus/how-tos/using-constructors.mdx @@ -206,7 +206,7 @@ pub fn constructor( self.balances.setter(deployer).set(initial_supply); // Emit initialization event - log(self.vm(), TokenCreated { + self.vm().log(TokenCreated { creator: deployer, name: name.clone(), symbol: symbol.clone(), @@ -222,12 +222,30 @@ pub fn constructor( Stylus uses trait-based composition instead of traditional inheritance. When implementing constructors with traits, each component typically has its own initialization logic: ```rust -// Define traits for different functionality +use alloy_primitives::{Address, U256}; +use alloy_sol_types::sol; +use stylus_sdk::prelude::*; +use stylus_sdk::storage::{StorageAddress, StorageMap, StorageString, StorageU256}; + +sol! { + #[derive(Debug)] + error InvalidSupply(); +} + +#[derive(SolidityError, Debug)] +pub enum TokenError { + InvalidSupply(InvalidSupply), +} + +// Define traits for different functionality. Traits exposed through +// #[implements(...)] must themselves be annotated with #[public]. +#[public] trait IErc20 { fn balance_of(&self, account: Address) -> U256; fn transfer(&mut self, to: Address, value: U256) -> bool; } +#[public] trait IOwnable { fn owner(&self) -> Address; fn transfer_ownership(&mut self, new_owner: Address) -> bool; @@ -284,7 +302,7 @@ impl MyToken { fn initialize_erc20(&mut self, initial_supply: U256) -> Result<(), TokenError> { if initial_supply == U256::ZERO { - return Err(TokenError::InvalidSupply); + return Err(TokenError::InvalidSupply(InvalidSupply {})); } let deployer = self.vm().tx_origin(); @@ -293,6 +311,41 @@ impl MyToken { Ok(()) } } + +// Each trait listed in #[implements(...)] must have a matching implementation. +#[public] +impl IErc20 for MyToken { + 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 + } +} + +#[public] +impl IOwnable for MyToken { + fn owner(&self) -> Address { + self.ownable.owner.get() + } + + fn transfer_ownership(&mut self, new_owner: Address) -> bool { + if self.vm().msg_sender() != self.ownable.owner.get() { + return false; + } + self.ownable.owner.set(new_owner); + true + } +} ``` @@ -363,12 +416,14 @@ mod tests { Deploy your contract with constructor arguments using `cargo stylus deploy`: -```bash -# Deploy with constructor parameters +```shell +# Deploy with constructor parameters. +# The constructor is annotated #[payable], so use --constructor-value to send ETH to it. cargo stylus deploy \ --private-key-path ~/.arbitrum/key \ --endpoint https://sepolia-rollup.arbitrum.io/rpc \ - --constructor-args "MyToken" "MTK" 1000000 + --constructor-args "MyToken" "MTK" 1000000 \ + --constructor-value 0 ``` ### Constructor argument encoding @@ -382,7 +437,7 @@ For complex types: - **Addresses**: Provide as hex strings with 0x prefix - **Arrays**: Use JSON array syntax -```bash +```shell # Example with multiple argument types cargo stylus deploy \ --constructor-args "TokenName" "TKN" 1000000 "0x742d35Cc6635C0532925a3b8D95B5C1b0ea3C28F" @@ -463,7 +518,7 @@ pub fn constructor(&mut self, params: ConstructorParams) -> Result<(), Error> { self.balances.setter(deployer).set(params.initial_supply); // 6. Emit events last - log(self.vm(), ContractInitialized { /* ... */ }); + self.vm().log(ContractInitialized { /* ... */ }); Ok(()) } From 9ac5f6abbf791cf86afa8bbeb5b385d01236276e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Mon, 29 Jun 2026 10:34:19 -0700 Subject: [PATCH 2/5] Update docs/stylus/how-tos/exporting-abi.mdx Co-authored-by: Emre Dincoglu <85258206+EmreDincoglu@users.noreply.github.com> --- docs/stylus/how-tos/exporting-abi.mdx | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/stylus/how-tos/exporting-abi.mdx b/docs/stylus/how-tos/exporting-abi.mdx index 8e21421706..dabf552611 100644 --- a/docs/stylus/how-tos/exporting-abi.mdx +++ b/docs/stylus/how-tos/exporting-abi.mdx @@ -255,6 +255,33 @@ impl Token { // Transfer logic Ok(()) } +sol! { +error InsufficientBalance(address account, uint256 requested, uint256 available); +error Unauthorized(address caller); +error InvalidAmount(); +} +#[derive(SolidityError)] +pub enum TokenError { + InsufficientBalance(InsufficientBalance), + Unauthorized(Unauthorized), + InvalidAmount(InvalidAmount) +} + +#[public] +impl Token { +pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), TokenError> { +let sender = self.vm().msg_sender(); +let balance = self.balances.get(sender); +if balance < amount { +return Err(TokenError::InsufficientBalance(InsufficientBalance { +account: sender, +requested: amount, +available: balance, +})); +} +// Transfer logic +Ok(()) +} } ``` From 289a356dfe9373e491d30060373fb8ad7e901221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Mon, 29 Jun 2026 10:39:59 -0700 Subject: [PATCH 3/5] Update docs/stylus/how-tos/exporting-abi.mdx Co-authored-by: Emre Dincoglu <85258206+EmreDincoglu@users.noreply.github.com> --- docs/stylus/how-tos/exporting-abi.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stylus/how-tos/exporting-abi.mdx b/docs/stylus/how-tos/exporting-abi.mdx index dabf552611..fa028656ac 100644 --- a/docs/stylus/how-tos/exporting-abi.mdx +++ b/docs/stylus/how-tos/exporting-abi.mdx @@ -439,7 +439,7 @@ sol_storage! { #[public] impl MyContract { #[constructor] - pub fn new(owner: Address, initial_value: U256) { + pub fn new(&mut self, owner: Address, initial_value: U256) { self.owner.set(owner); self.initial_value.set(initial_value); } From c64dda3675dd1ab2adf0dd20f778fb511774113d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Mon, 29 Jun 2026 13:43:27 -0700 Subject: [PATCH 4/5] fix incorrect code blocks --- docs/stylus/how-tos/exporting-abi.mdx | 71 ++++++++++----------------- 1 file changed, 27 insertions(+), 44 deletions(-) diff --git a/docs/stylus/how-tos/exporting-abi.mdx b/docs/stylus/how-tos/exporting-abi.mdx index fa028656ac..1bc5a06e3b 100644 --- a/docs/stylus/how-tos/exporting-abi.mdx +++ b/docs/stylus/how-tos/exporting-abi.mdx @@ -232,6 +232,7 @@ interface IMyContract { Define custom errors with parameters: ```rust +use stylus_sdk::alloy_sol_types::sol; use stylus_sdk::prelude::*; sol! { @@ -240,48 +241,28 @@ sol! { error InvalidAmount(); } +#[derive(SolidityError)] +pub enum TokenError { + InsufficientBalance(InsufficientBalance), + Unauthorized(Unauthorized), + InvalidAmount(InvalidAmount), +} + #[public] impl Token { - pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), InsufficientBalance> { + pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), TokenError> { let sender = self.vm().msg_sender(); let balance = self.balances.get(sender); if balance < amount { - return Err(InsufficientBalance { + return Err(TokenError::InsufficientBalance(InsufficientBalance { account: sender, requested: amount, available: balance, - }); + })); } // Transfer logic Ok(()) } -sol! { -error InsufficientBalance(address account, uint256 requested, uint256 available); -error Unauthorized(address caller); -error InvalidAmount(); -} -#[derive(SolidityError)] -pub enum TokenError { - InsufficientBalance(InsufficientBalance), - Unauthorized(Unauthorized), - InvalidAmount(InvalidAmount) -} - -#[public] -impl Token { -pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), TokenError> { -let sender = self.vm().msg_sender(); -let balance = self.balances.get(sender); -if balance < amount { -return Err(TokenError::InsufficientBalance(InsufficientBalance { -account: sender, -requested: amount, -available: balance, -})); -} -// Transfer logic -Ok(()) -} } ``` @@ -302,6 +283,7 @@ interface IToken { Events are automatically included in the ABI: ```rust +use stylus_sdk::alloy_sol_types::sol; use stylus_sdk::prelude::*; sol! { @@ -876,33 +858,34 @@ fn main() { **Error**: `the trait AbiType is not implemented for MyType` -**Solution**: Use supported types or implement `AbiType`: +**Solution**: Use supported types, or derive `AbiType` for a custom struct: ```rust // ✅ Use supported types pub fn process(&self, amount: U256) -> U256 { } -// ❌ Custom types need AbiType implementation +// ❌ Arbitrary Rust types have no ABI representation pub fn process(&self, amount: MyCustomType) -> MyCustomType { } ``` -For custom types, implement `AbiType`: +For custom struct types, derive `AbiType` inside a `sol!` block. The struct can then be used in public method parameters and return values: ```rust -use stylus_sdk::abi::AbiType; - -#[derive(Clone)] -struct MyType(U256); - -impl AbiType for MyType { - type SolType = alloy_sol_types::sol_data::Uint<256>; +use stylus_sdk::alloy_sol_types::sol; +use stylus_sdk::prelude::*; - fn encode(&self) -> Vec { - self.0.encode() +sol! { + #[derive(AbiType)] + struct Point { + uint256 x; + uint256 y; } +} - fn decode(data: &[u8]) -> Result { - U256::decode(data).map(MyType) +#[public] +impl MyContract { + pub fn echo_point(&self, p: Point) -> Point { + p } } ``` From e1a119189217b73846b2fcfad86b0f7b8ec4d90f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Mon, 29 Jun 2026 14:05:16 -0700 Subject: [PATCH 5/5] minor fix --- docs/stylus/how-tos/exporting-abi.mdx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/stylus/how-tos/exporting-abi.mdx b/docs/stylus/how-tos/exporting-abi.mdx index 1bc5a06e3b..bf63c45ca3 100644 --- a/docs/stylus/how-tos/exporting-abi.mdx +++ b/docs/stylus/how-tos/exporting-abi.mdx @@ -671,17 +671,22 @@ This is the main function: The `#[public]` macro generates ABI code: ```rust -// From stylus-proc/src/macros/public/export_abi.rs +// The macro generates an implementation roughly like this: 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 + // Generated function signatures write!(f, "\n function getValue() external view returns (uint256);")?; - writeln!(f, "}}")?; + writeln!(f, "\n}}")?; Ok(()) } + + fn fmt_constructor_signature(f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + // Emits the `constructor(...)` line printed by `cargo stylus constructor` + write!(f, "constructor()") + } } ```