diff --git a/docs/stylus/best-practices/gas-optimization.mdx b/docs/stylus/best-practices/gas-optimization.mdx
index 238402a206..eb29400990 100644
--- a/docs/stylus/best-practices/gas-optimization.mdx
+++ b/docs/stylus/best-practices/gas-optimization.mdx
@@ -9,7 +9,7 @@ sme: bragaigor
sidebar_position: 2
---
-Stylus contracts offer significant gas savings compared to Solidity (10-100x for compute-heavy operations), but following optimization best practices can reduce costs even further.
+Stylus contracts can offer significant gas savings compared to Solidity for compute-heavy operations, and following the optimization best practices below can reduce costs even further. Exact savings depend on the workload, so benchmark your own contract.
## Why Stylus is cheaper
@@ -19,15 +19,17 @@ _Figure: Stylus WASM executes natively, avoiding EVM interpretation overhead._
### Performance comparison
-| Operation | Solidity (EVM) | Stylus (WASM) | Savings |
-| ---------------------- | -------------- | ------------- | ----------- |
-| Keccak256 hashing | ~30 gas/byte | ~3 gas/byte | **10x** |
-| Signature verification | ~3,000 gas | ~300 gas | **10x** |
-| Memory operations | ~3 gas/word | ~0.3 gas/word | **10x** |
-| Compute-heavy loops | High | Very low | **50-100x** |
-| Storage operations | Same | Same | **1x** |
+| Operation | Solidity (EVM) | Stylus (WASM) | Relative savings |
+| ------------------------------------- | ----------------------- | ---------------------- | ----------------------- |
+| Compute-heavy loops | High | Very low | ~50–100x |
+| Signature verification (`ecrecover`) | ~3,000 gas (precompile) | ~300 gas | ~10x |
+| Memory operations (`MLOAD`/`MSTORE`) | ~3 gas/word | ~0.3 gas/word | ~10x |
+| Keccak256 hashing | 30 gas + 6 gas/word | native `keccak` hostio | Varies (small per byte) |
+| Storage operations (`SLOAD`/`SSTORE`) | EVM cost | Same EVM cost | None (1x) |
-**Key insight**: [Storage operations](/stylus/advanced/hostio-exports#storage-operations) cost the same in Stylus and Solidity. Optimize by reducing storage access and maximizing compute efficiency.
+The EVM-side costs are fixed protocol prices: `ecrecover` = 3,000 gas, `MLOAD`/`MSTORE` = 3 gas/word, and `KECCAK256` = 30 gas + 6 gas per 32-byte word. The Stylus-side figures and the multipliers are directional — drawn from Offchain Labs' Stylus benchmarks — and vary with workload, input size, and ArbOS version. Benchmark your own contract to get numbers you can rely on. Note that Keccak256 is already cheap per byte on the EVM, so hashing is not a headline saving; Stylus' large wins come from compute-heavy logic, memory, and native cryptography.
+
+**Key insight**: [Storage operations](/stylus/advanced/hostio-exports#storage-operations) map to the same underlying EVM `SLOAD`/`SSTORE` costs in Stylus as in Solidity, so they are not where Stylus saves gas. Optimize by reducing storage access and maximizing compute efficiency.
## Storage optimization
@@ -57,7 +59,7 @@ pub fn calculate_good(&self, iterations: u32) -> U256 {
}
```
-**Gas impact**: Each storage read costs ~100 gas. The optimized version can save thousands of gas for large loops.
+**Gas impact**: Storage reads map to EVM `SLOAD` costs, where a cold slot (first access in a transaction, per EIP-2929) is far more expensive than a warm one. The SDK also caches storage, so repeated reads of the same slot within a single call are cheap. Caching the value in a local variable, as shown above, avoids repeated `SLOAD` work and can save significant gas in large loops.
### 2. Batch storage writes
@@ -72,22 +74,26 @@ pub fn update_user_bad(&mut self, addr: Address, amount: U256, active: bool) {
// ✅ Good: Combine into struct
sol_storage! {
pub struct UserData {
- U256 balance;
- U256 last_update;
+ uint256 balance;
+ uint256 last_update;
bool is_active;
}
pub struct OptimizedContract {
- StorageMap
users;
+ mapping(address => UserData) users;
}
}
pub fn update_user_good(&mut self, addr: Address, amount: U256, active: bool) {
+ // Read host state before taking the storage setter to avoid borrowing
+ // `self` both mutably (the setter) and immutably (`self.vm()`).
+ let timestamp = U256::from(self.vm().block_timestamp());
+
let mut user = self.users.setter(addr);
user.balance.set(amount);
- user.last_update.set(U256::from(self.vm().block_timestamp()));
+ user.last_update.set(timestamp);
user.is_active.set(active);
- // Single storage slot update instead of three
+ // Grouped fields share contiguous slots instead of three unrelated slots
}
```
@@ -121,7 +127,9 @@ sol_storage! {
pub fn cleanup(&mut self, addr: Address) -> Result<(), Vec> {
let balance = self.balances.get(addr);
- ensure!(balance == U256::ZERO, "Balance not zero");
+ if balance != U256::ZERO {
+ return Err(b"Balance not zero".to_vec());
+ }
// ✅ Deleting storage refunds gas
self.balances.delete(addr);
@@ -131,7 +139,7 @@ pub fn cleanup(&mut self, addr: Address) -> Result<(), Vec> {
}
```
-**Gas refund**: Deleting storage refunds up to 15,000 gas per slot cleared.
+**Gas refund**: Clearing a storage slot (setting it back to zero) triggers an `SSTORE` refund. Since EIP-3529 this refund is capped at 4,800 gas per cleared slot, and the total refund for a transaction cannot exceed one fifth (20%) of the gas the transaction used.
## Memory optimization
@@ -212,12 +220,13 @@ pub fn verify_merkle_proof(
) -> bool {
let mut computed_hash = leaf;
- // This loop is 10-50x cheaper in Stylus than Solidity
+ // This loop is typically much cheaper in Stylus than Solidity
for proof_element in proof {
+ // keccak256 returns a B256; `.0` extracts the [u8; 32] array
computed_hash = if computed_hash <= proof_element {
- keccak256(&[computed_hash, proof_element].concat())
+ keccak256([computed_hash, proof_element].concat()).0
} else {
- keccak256(&[proof_element, computed_hash].concat())
+ keccak256([proof_element, computed_hash].concat()).0
};
}
@@ -225,12 +234,13 @@ pub fn verify_merkle_proof(
}
```
-**Why it's faster**: Native WASM execution vs. EVM interpretation makes loops dramatically cheaper.
+**Why it's faster**: Native WASM execution avoids EVM interpretation overhead, which makes compute-heavy loops cheaper. Benchmark to quantify the savings for your specific workload.
### 2. Optimize hot paths
```rust
-// ✅ Optimize frequently-called functions
+// ✅ Hint the compiler to inline small, frequently-called helpers.
+// `#[inline(always)]` is a hint, not a guarantee; measure before relying on it.
#[inline(always)]
pub fn is_valid_amount(&self, amount: U256) -> bool {
amount > U256::ZERO && amount <= self.max_amount.get()
@@ -238,7 +248,9 @@ pub fn is_valid_amount(&self, amount: U256) -> bool {
// Use in hot path
pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), Vec> {
- ensure!(self.is_valid_amount(amount), "Invalid amount");
+ if !self.is_valid_amount(amount) {
+ return Err(b"Invalid amount".to_vec());
+ }
// Transfer logic...
Ok(())
}
@@ -249,12 +261,17 @@ pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), Vec> {
```rust
// ❌ Bad: Redundant zero check
pub fn add_to_balance_bad(&mut self, addr: Address, amount: U256) -> Result<(), Vec> {
- ensure!(amount > U256::ZERO, "Amount must be positive");
+ if amount == U256::ZERO {
+ return Err(b"Amount must be positive".to_vec());
+ }
let current = self.balances.get(addr);
- ensure!(current + amount > current, "Overflow"); // Redundant if amount > 0
+ if current + amount <= current {
+ // Redundant if amount > 0
+ return Err(b"Overflow".to_vec());
+ }
- self.balances.setter(addr).add_assign(amount);
+ self.balances.setter(addr).set(current + amount);
Ok(())
}
@@ -264,7 +281,7 @@ pub fn add_to_balance_good(&mut self, addr: Address, amount: U256) -> Result<(),
let new_balance = current
.checked_add(amount)
- .ok_or("Overflow or invalid amount")?;
+ .ok_or(b"Overflow or invalid amount".to_vec())?;
self.balances.setter(addr).set(new_balance);
Ok(())
@@ -276,23 +293,34 @@ pub fn add_to_balance_good(&mut self, addr: Address, amount: U256) -> Result<(),
### 1. Minimize cross-contract calls
```rust
+// The interface is declared with sol_interface!:
+// sol_interface! {
+// interface IOracle {
+// function getPrice(address token) external view returns (uint256);
+// function getDecimals(address token) external view returns (uint256);
+// function getTimestamp(address token) external view returns (uint256);
+// function getPriceData(address token)
+// external view returns (uint256, uint256, uint256);
+// }
+// }
+
// ❌ Bad: Multiple external calls
pub fn get_price_bad(&self, token: Address) -> Result> {
let oracle = IOracle::new(self.oracle_address.get());
- let price = oracle.get_price(self, token)?;
- let decimals = oracle.get_decimals(self, token)?; // Second call
- let timestamp = oracle.get_timestamp(self, token)?; // Third call
+ let price = oracle.get_price(self.vm(), Call::new(), token)?;
+ let _decimals = oracle.get_decimals(self.vm(), Call::new(), token)?; // Second call
+ let _timestamp = oracle.get_timestamp(self.vm(), Call::new(), token)?; // Third call
Ok(price)
}
// ✅ Good: Batch external calls
-pub fn get_price_good(&self, token: Address) -> Result> {
+pub fn get_price_good(&self, token: Address) -> Result<(U256, U256, U256), Vec> {
let oracle = IOracle::new(self.oracle_address.get());
// Single call returns all data
- oracle.get_price_data(self, token)
+ Ok(oracle.get_price_data(self.vm(), Call::new(), token)?)
}
```
@@ -355,7 +383,7 @@ sol! {
}
```
-**Gas impact**: Each indexed parameter costs ~375 additional gas. Only index fields you'll search by.
+**Gas impact**: Each additional log topic (indexed parameter) adds to the cost of emitting the event, so only index fields you will actually filter by. The exact per-topic cost is set by EVM `LOG` opcode pricing; verify against current gas-schedule values if you need a precise figure.
### 2. Batch events when possible
@@ -413,70 +441,61 @@ pub fn calculate(&self, value: U256) -> U256 {
complex_sqrt(value) // Using 1% of library
}
-// ✅ Good: Implement simple operations yourself
+// ✅ Good: Implement simple operations yourself (sketch)
pub fn simple_sqrt(&self, value: U256) -> U256 {
- // Custom implementation adds minimal binary size
- // Newton's method or similar
+ // Custom implementation adds minimal binary size.
+ // Provide a real algorithm (Newton's method or similar) here.
+ unimplemented!("integer square root")
}
```
-### 3. Use cargo stylus optimization
+### 3. Check binary size and optimize the build
```shell
-# Check binary size
+# Compile and report the activated contract size
cargo stylus check
-
-# Optimize with wasm-opt
-cargo stylus deploy --optimize
-
-# Maximum optimization (slower build, smaller binary)
-cargo stylus deploy --optimize-level 3
```
+`cargo stylus` does not expose `--optimize` flags. Control binary size through your
+Cargo release profile (see "Optimize compilation flags" above) and, if you need
+further shrinking, by running `wasm-opt` from [Binaryen](https://github.com/WebAssembly/binaryen)
+on the compiled `.wasm`. See [optimizing binaries](/stylus/how-tos/optimizing-binaries) for details.
+
## Gas measurement
-### 1. Profile your contracts
+### 1. Test behavior with the unit-test VM
+
+The `TestVM` from `stylus_sdk::testing` runs your contract logic off-chain so you
+can assert behavior quickly. It does not expose a gas meter (there is no
+`gas_left()` getter on `TestVM`), so use it to verify correctness, not to measure gas.
```rust
#[cfg(test)]
mod gas_tests {
use super::*;
+ use stylus_sdk::testing::*;
#[test]
- fn benchmark_transfer() {
+ fn update_user_persists() {
let vm = TestVM::default();
- let mut contract = Token::from(&vm);
+ let mut contract = OptimizedContract::from(&vm);
- // Measure gas for operation
- let gas_before = vm.gas_left();
- contract.transfer(recipient, amount).unwrap();
- let gas_used = gas_before - vm.gas_left();
+ let user = Address::from([0x11; 20]);
+ contract.update_user_good(user, U256::from(100), true);
- println!("Transfer gas used: {}", gas_used);
- assert!(gas_used < 50000, "Transfer too expensive");
+ let stored = contract.users.get(user);
+ assert_eq!(stored.balance.get(), U256::from(100));
+ assert!(stored.is_active.get());
}
}
```
-### 2. Compare implementations
+### 2. Measure gas on a live endpoint
-```rust
-#[cfg(test)]
-mod optimization_tests {
- #[test]
- fn compare_storage_patterns() {
- // Test pattern A
- let gas_a = measure_pattern_a();
-
- // Test pattern B
- let gas_b = measure_pattern_b();
-
- println!("Pattern A: {} gas", gas_a);
- println!("Pattern B: {} gas", gas_b);
- println!("Savings: {}%", (gas_a - gas_b) * 100 / gas_a);
- }
-}
-```
+To compare the gas cost of two implementations, deploy each to a Stylus dev node
+and measure the gas used by real transactions (for example with `cast estimate`
+or by reading the gas used from the transaction receipt). On-chain measurement is
+the reliable way to compare optimization patterns; the unit-test VM cannot report gas.
## Optimization checklist
@@ -496,40 +515,44 @@ Before deploying, verify you've:
## Common optimizations summary
-| Pattern | Gas Savings | Complexity |
-| ------------------------- | ------------------------------ | ---------- |
-| Cache storage reads | High (100+ gas per read saved) | Low |
-| Delete unused storage | Medium (15,000 gas refund) | Low |
-| Batch storage writes | Medium (varies) | Medium |
-| Use iterators vs. collect | Low-Medium | Low |
-| Minimize external calls | High | Medium |
-| Optimize binary size | High (deployment only) | Medium |
-| Right-size data types | Low-Medium | Low |
+| Pattern | Gas savings | Complexity |
+| ------------------------- | ----------------------------------- | ---------- |
+| Cache storage reads | High (avoids repeated `SLOAD`) | Low |
+| Delete unused storage | Medium (≤4,800 gas refund per slot) | Low |
+| Batch storage writes | Medium (varies) | Medium |
+| Use iterators vs. collect | Low-Medium | Low |
+| Minimize external calls | High | Medium |
+| Optimize binary size | High (deployment only) | Medium |
+| Right-size data types | Low-Medium | Low |
## Advanced optimization
### Custom memory allocators
-For advanced users, custom allocators can reduce memory overhead:
-
-```rust
-#[global_allocator]
-static ALLOCATOR: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
-```
+The Stylus SDK ships with `mini-alloc` enabled by default (the `mini-alloc`
+feature in the generated `Cargo.toml`), a small WASM-oriented allocator that is
+already a good fit for most contracts. Reach for a custom `#[global_allocator]`
+only if profiling shows allocation is a bottleneck.
-**Warning**: Only use if you understand the trade-offs.
+Note that `wee_alloc`, once a common choice for size-constrained WASM, is
+unmaintained (archived upstream) and is not recommended for new contracts. Prefer
+the SDK default unless you have a specific, measured reason to change it.
### Assembly optimization
-For critical paths, you can use WASM intrinsics:
+For critical paths, advanced developers can reach for WASM intrinsics from
+`core::arch::wasm32`. The following is pseudocode showing where such an
+optimization would live; the body is intentionally omitted because a real
+implementation depends on the specific operation you are optimizing:
```rust
use core::arch::wasm32::*;
-// ✅ Advanced: Use WASM intrinsics for critical operations
+// ✅ Advanced: use WASM intrinsics for critical operations.
+// Pseudocode — fill in a complete, measured implementation before using.
pub fn optimized_hash(&self, data: &[u8]) -> [u8; 32] {
- // WASM-optimized hashing
- // Only use if you're an advanced developer
+ // WASM-optimized hashing goes here.
+ unimplemented!("provide a real implementation")
}
```
diff --git a/docs/stylus/best-practices/security.mdx b/docs/stylus/best-practices/security.mdx
index 7df1dce701..948910c511 100644
--- a/docs/stylus/best-practices/security.mdx
+++ b/docs/stylus/best-practices/security.mdx
@@ -24,34 +24,52 @@ Compiling Rust to WebAssembly is not guaranteed to be deterministic on its own
Always validate external inputs before using them in your contract logic.
```rust
-use stylus_sdk::{prelude::*, msg};
-use alloy_primitives::{Address, U256};
+use stylus_sdk::{
+ alloy_primitives::{Address, U256},
+ prelude::*,
+ storage::{StorageMap, StorageU256},
+};
+
+#[storage]
+#[entrypoint]
+pub struct MyContract {
+ balances: StorageMap,
+}
-#[external]
+#[public]
impl MyContract {
// ❌ Bad: No validation
pub fn transfer_bad(&mut self, recipient: Address, amount: U256) -> Result<(), Vec> {
- let sender = msg::sender();
- self.balances.setter(sender).sub_assign(amount);
- self.balances.setter(recipient).add_assign(amount);
+ let sender = self.vm().msg_sender();
+ let sender_balance = self.balances.get(sender);
+ let recipient_balance = self.balances.get(recipient);
+ self.balances.setter(sender).set(sender_balance - amount);
+ self.balances.setter(recipient).set(recipient_balance + amount);
Ok(())
}
// ✅ Good: Proper validation
pub fn transfer_good(&mut self, recipient: Address, amount: U256) -> Result<(), Vec> {
// Validate inputs
- ensure!(!recipient.is_zero(), "Invalid recipient");
- ensure!(amount > U256::ZERO, "Amount must be positive");
+ if recipient.is_zero() {
+ return Err("Invalid recipient".into());
+ }
+ if amount == U256::ZERO {
+ return Err("Amount must be positive".into());
+ }
- let sender = msg::sender();
+ let sender = self.vm().msg_sender();
let sender_balance = self.balances.get(sender);
// Check sufficient balance
- ensure!(sender_balance >= amount, "Insufficient balance");
+ if sender_balance < amount {
+ return Err("Insufficient balance".into());
+ }
// Safe arithmetic
- self.balances.setter(sender).sub_assign(amount);
- self.balances.setter(recipient).add_assign(amount);
+ let recipient_balance = self.balances.get(recipient);
+ self.balances.setter(sender).set(sender_balance - amount);
+ self.balances.setter(recipient).set(recipient_balance + amount);
Ok(())
}
@@ -63,27 +81,35 @@ impl MyContract {
Implement proper authorization checks for privileged operations.
```rust
-use stylus_sdk::{prelude::*, msg, storage::StorageAddress};
+use stylus_sdk::{
+ alloy_primitives::{Address, U256},
+ prelude::*,
+};
sol_storage! {
+ #[entrypoint]
pub struct Ownable {
- StorageAddress owner;
+ address owner;
}
}
-#[external]
+#[public]
impl Ownable {
// Initialize owner in constructor-like pattern
pub fn init(&mut self) -> Result<(), Vec> {
let owner = self.owner.get();
- ensure!(owner.is_zero(), "Already initialized");
- self.owner.set(msg::sender());
+ if !owner.is_zero() {
+ return Err("Already initialized".into());
+ }
+ self.owner.set(self.vm().msg_sender());
Ok(())
}
// Modifier pattern for owner-only functions
fn only_owner(&self) -> Result<(), Vec> {
- ensure!(msg::sender() == self.owner.get(), "Not authorized");
+ if self.vm().msg_sender() != self.owner.get() {
+ return Err("Not authorized".into());
+ }
Ok(())
}
@@ -95,7 +121,9 @@ impl Ownable {
pub fn transfer_ownership(&mut self, new_owner: Address) -> Result<(), Vec> {
self.only_owner()?;
- ensure!(!new_owner.is_zero(), "Invalid new owner");
+ if new_owner.is_zero() {
+ return Err("Invalid new owner".into());
+ }
self.owner.set(new_owner);
Ok(())
}
@@ -106,50 +134,67 @@ impl Ownable {
Protect against reentrancy attacks using the checks-effects-interactions pattern.
+
+
+Don't rely on the old opt-in reentrancy guard. The `reentrant` feature flag and `deny_reentrant` entrypoint guard were deprecated in SDK 0.10.5: the high-level call functions automatically flush the storage cache before every external call, so any state you write before a call is observed by a reentrant call. That makes the guard redundant. Checks-effects-interactions — updating state before the external call — remains the correct way to write reentrancy-safe contracts.
+
+
+
```rust
-use stylus_sdk::{prelude::*, call::transfer_eth, msg};
+use stylus_sdk::{
+ alloy_primitives::{Address, U256},
+ call::transfer::transfer_eth,
+ prelude::*,
+};
sol_storage! {
+ #[entrypoint]
pub struct Vault {
- StorageMap balances;
- StorageBool locked; // Reentrancy guard
+ mapping(address => uint256) balances;
+ bool locked; // Optional application-level guard
}
}
-#[external]
+#[public]
impl Vault {
// ❌ Bad: Vulnerable to reentrancy
pub fn withdraw_bad(&mut self, amount: U256) -> Result<(), Vec> {
- let sender = msg::sender();
+ let sender = self.vm().msg_sender();
let balance = self.balances.get(sender);
- ensure!(balance >= amount, "Insufficient balance");
+ if balance < amount {
+ return Err("Insufficient balance".into());
+ }
// DANGER: External call before state update
- transfer_eth(sender, amount)?;
+ transfer_eth(self.vm(), sender, amount)?;
// State updated after external call - vulnerable!
- self.balances.setter(sender).sub_assign(amount);
+ self.balances.setter(sender).set(balance - amount);
Ok(())
}
// ✅ Good: Checks-Effects-Interactions pattern
pub fn withdraw_good(&mut self, amount: U256) -> Result<(), Vec> {
- // Check: Reentrancy guard
- ensure!(!self.locked.get(), "Reentrancy detected");
+ // Optional application-level guard
+ if self.locked.get() {
+ return Err("Reentrancy detected".into());
+ }
self.locked.set(true);
- let sender = msg::sender();
+ let sender = self.vm().msg_sender();
let balance = self.balances.get(sender);
// Check: Validate conditions
- ensure!(balance >= amount, "Insufficient balance");
+ if balance < amount {
+ return Err("Insufficient balance".into());
+ }
// Effect: Update state BEFORE external call
- self.balances.setter(sender).sub_assign(amount);
+ self.balances.setter(sender).set(balance - amount);
// Interaction: External call last
- let result = transfer_eth(sender, amount);
+ let result = transfer_eth(self.vm(), sender, amount);
// Release lock
self.locked.set(false);
@@ -164,9 +209,13 @@ impl Vault {
While Rust prevents overflows in debug mode, use explicit checks for production.
```rust
-use alloy_primitives::U256;
+use stylus_sdk::{alloy_primitives::U256, prelude::*};
+
+#[storage]
+#[entrypoint]
+pub struct SafeMath {}
-#[external]
+#[public]
impl SafeMath {
// ✅ Use checked arithmetic
pub fn safe_add(&self, a: U256, b: U256) -> Result> {
@@ -179,7 +228,9 @@ impl SafeMath {
// ✅ Validate before operations
pub fn calculate_fee(&self, amount: U256, basis_points: U256) -> Result> {
- ensure!(basis_points <= U256::from(10000), "Invalid fee");
+ if basis_points > U256::from(10000) {
+ return Err("Invalid fee".into());
+ }
amount
.checked_mul(basis_points)
@@ -227,36 +278,48 @@ external_contract.call(data).map_err(|e| "External call failed")?;
```rust
// ✅ Use commit-reveal pattern for sensitive operations
+use stylus_sdk::{
+ alloy_primitives::{FixedBytes, U256},
+ crypto::keccak,
+ prelude::*,
+};
+
sol_storage! {
+ #[entrypoint]
pub struct CommitReveal {
- StorageMap commits;
- StorageMap reveal_times;
+ mapping(address => bytes32) commits;
+ mapping(address => uint256) reveal_times;
}
}
+#[public]
impl CommitReveal {
- pub fn commit(&mut self, commitment: [u8; 32]) -> Result<(), Vec> {
- let sender = msg::sender();
+ pub fn commit(&mut self, commitment: FixedBytes<32>) -> Result<(), Vec> {
+ let sender = self.vm().msg_sender();
+ // block_timestamp() returns u64; convert before storing in a U256 map
+ let reveal_at = U256::from(self.vm().block_timestamp()) + U256::from(100);
self.commits.setter(sender).set(commitment);
- self.reveal_times.setter(sender).set(block::timestamp() + 100);
+ self.reveal_times.setter(sender).set(reveal_at);
Ok(())
}
- pub fn reveal(&mut self, value: U256, salt: [u8; 32]) -> Result<(), Vec> {
- let sender = msg::sender();
+ pub fn reveal(&mut self, value: U256, salt: FixedBytes<32>) -> Result<(), Vec> {
+ let sender = self.vm().msg_sender();
// Verify commit period passed
- ensure!(
- block::timestamp() >= self.reveal_times.get(sender),
- "Too early to reveal"
- );
+ let now = U256::from(self.vm().block_timestamp());
+ if now < self.reveal_times.get(sender) {
+ return Err("Too early to reveal".into());
+ }
// Verify commitment
- let expected = keccak256(&[&value.to_be_bytes(), &salt].concat());
- ensure!(
- expected == self.commits.get(sender),
- "Invalid reveal"
- );
+ let mut preimage = Vec::new();
+ preimage.extend_from_slice(&value.to_be_bytes::<32>());
+ preimage.extend_from_slice(salt.as_slice());
+ let expected = keccak(&preimage);
+ if expected != self.commits.get(sender) {
+ return Err("Invalid reveal".into());
+ }
// Process reveal...
Ok(())
@@ -286,27 +349,34 @@ pub fn distribute_rewards_good(
start_index: U256,
count: U256
) -> Result<(), Vec> {
- ensure!(count <= U256::from(50), "Batch too large");
+ if count > U256::from(50) {
+ return Err("Batch too large".into());
+ }
let end = start_index + count;
- for i in start_index..end {
- let recipient = self.recipients.get(i);
+ let mut i = start_index;
+ while i < end {
+ let idx = usize::try_from(i).map_err(|_| b"index overflow".to_vec())?;
+ let recipient = self.recipients.get(idx).unwrap_or(Address::ZERO);
if !recipient.is_zero() {
self.send_reward(recipient)?;
}
+ i += U256::from(1);
}
Ok(())
}
// ✅ Better: Pull-based (users claim their own rewards)
pub fn claim_reward(&mut self) -> Result<(), Vec> {
- let sender = msg::sender();
+ let sender = self.vm().msg_sender();
let reward = self.pending_rewards.get(sender);
- ensure!(reward > U256::ZERO, "No rewards");
+ if reward == U256::ZERO {
+ return Err("No rewards".into());
+ }
self.pending_rewards.setter(sender).set(U256::ZERO);
- transfer_eth(sender, reward)?;
+ transfer_eth(self.vm(), sender, reward)?;
Ok(())
}
@@ -318,19 +388,20 @@ pub fn claim_reward(&mut self) -> Result<(), Vec> {
```rust
sol_storage! {
+ #[entrypoint]
pub struct SecureVault {
// Public read, controlled write
- StorageU256 public_total;
+ uint256 public_total;
// Private storage - not visible off-chain without knowing slot
- StorageMap private_balances;
+ mapping(address => uint256) private_balances;
// Owner-controlled
- StorageAddress owner;
+ address owner;
}
}
-#[external]
+#[public]
impl SecureVault {
// ✅ Expose only what's necessary
pub fn get_total(&self) -> U256 {
@@ -339,7 +410,10 @@ impl SecureVault {
// ✅ Don't expose internal mappings directly
pub fn get_balance(&self, account: Address) -> Result> {
- ensure!(msg::sender() == account || msg::sender() == self.owner.get(), "Unauthorized");
+ let caller = self.vm().msg_sender();
+ if caller != account && caller != self.owner.get() {
+ return Err("Unauthorized".into());
+ }
Ok(self.private_balances.get(account))
}
}
@@ -348,20 +422,26 @@ impl SecureVault {
### Prevent storage collisions
```rust
-// ✅ Use unique storage namespace for upgradeable contracts
-sol_storage! {
- pub struct MyContract {
- // Prefix with contract name to avoid collisions
- #[borrow]
- MyContractStorage storage;
- }
+use stylus_sdk::{
+ alloy_primitives::{Address, U256},
+ prelude::*,
+ storage::{StorageMap, StorageU256},
+};
+
+// ✅ Group related state in its own storage struct...
+#[storage]
+pub struct MyContractStorage {
+ value: StorageU256,
+ balances: StorageMap,
}
-sol_storage! {
- pub struct MyContractStorage {
- StorageU256 value;
- StorageMap balances;
- }
+// ...then compose it into the entrypoint as a named field.
+// Each nested storage struct gets its own slot range, which avoids
+// collisions between logically separate pieces of state.
+#[storage]
+#[entrypoint]
+pub struct MyContract {
+ inner: MyContractStorage,
}
```
@@ -370,26 +450,42 @@ sol_storage! {
### Informative error messages
```rust
+use stylus_sdk::{
+ alloy_primitives::{Address, U256},
+ alloy_sol_types::sol,
+ prelude::*,
+};
+
+// Each enum variant wraps a Solidity error type declared in a sol! block.
+sol! {
+ error InsufficientBalance(uint256 available);
+ error Unauthorized(address caller);
+ error InvalidAmount(uint256 amount);
+ error TransferFailed(address to, uint256 amount);
+}
+
#[derive(SolidityError)]
pub enum MyError {
- InsufficientBalance(U256),
- Unauthorized(Address),
- InvalidAmount(U256),
- TransferFailed(Address, U256),
+ InsufficientBalance(InsufficientBalance),
+ Unauthorized(Unauthorized),
+ InvalidAmount(InvalidAmount),
+ TransferFailed(TransferFailed),
}
-#[external]
+#[public]
impl MyContract {
pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), MyError> {
- let sender = msg::sender();
+ let sender = self.vm().msg_sender();
let balance = self.balances.get(sender);
if balance < amount {
- return Err(MyError::InsufficientBalance(balance));
+ return Err(MyError::InsufficientBalance(InsufficientBalance {
+ available: balance,
+ }));
}
if to.is_zero() {
- return Err(MyError::InvalidAmount(amount));
+ return Err(MyError::InvalidAmount(InvalidAmount { amount }));
}
// Transfer logic...
@@ -404,10 +500,12 @@ impl MyContract {
// ✅ Fail closed, not open
pub fn privileged_function(&mut self) -> Result<(), Vec> {
// Default to denying access
- let is_authorized = self.check_authorization(msg::sender());
+ let is_authorized = self.check_authorization(self.vm().msg_sender());
// Explicit check required to proceed
- ensure!(is_authorized, "Access denied");
+ if !is_authorized {
+ return Err("Access denied".into());
+ }
// Privileged operation
Ok(())
@@ -418,43 +516,52 @@ pub fn privileged_function(&mut self) -> Result<(), Vec> {
### Write comprehensive tests
+The `testing` module is gated behind the `stylus-test` feature, so add it as a dev-dependency in `Cargo.toml`:
+
+```toml
+[dev-dependencies]
+stylus-sdk = { version = "0.10.7", features = ["stylus-test"] }
+```
+
```rust
#[cfg(test)]
mod tests {
use super::*;
+ use alloy_primitives::address;
use stylus_sdk::testing::*;
#[test]
- fn test_reentrancy_protection() {
+ fn test_withdraw_balance_checks() {
let vm = TestVM::default();
let mut contract = Vault::from(&vm);
- // Setup
- contract.deposit(U256::from(100)).unwrap();
+ // Deposit funds
+ vm.set_value(U256::from(100));
+ contract.deposit();
- // Attempt reentrancy
- let result = contract.withdraw(U256::from(50));
- assert!(result.is_ok());
+ // A withdrawal within balance succeeds
+ assert!(contract.withdraw_good(U256::from(50)).is_ok());
- // Second withdrawal should fail if still locked
- let result2 = contract.withdraw(U256::from(50));
- assert!(result2.is_err());
+ // A withdrawal exceeding the remaining balance fails
+ assert!(contract.withdraw_good(U256::from(100)).is_err());
}
#[test]
fn test_access_control() {
let vm = TestVM::default();
+ // Capture the default sender, which becomes the owner on init
+ let owner = vm.msg_sender();
let mut contract = Ownable::from(&vm);
// Initialize owner
contract.init().unwrap();
// Non-owner should be rejected
- vm.set_caller(address!("0x0000000000000000000000000000000000000001"));
+ vm.set_sender(address!("0x0000000000000000000000000000000000000001"));
assert!(contract.sensitive_operation().is_err());
// Owner should succeed
- vm.set_caller(/* original owner */);
+ vm.set_sender(owner);
assert!(contract.sensitive_operation().is_ok());
}