From 2767073d0a1c341510c697bb431fdd818c6e964c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Fri, 14 Feb 2025 16:04:04 -0800 Subject: [PATCH 01/21] feat: add first draft --- .../stylus/how-tos/testing-contracts.mdx | 134 ++++++++++++++++++ website/sidebars.js | 5 + 2 files changed, 139 insertions(+) create mode 100644 arbitrum-docs/stylus/how-tos/testing-contracts.mdx diff --git a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx new file mode 100644 index 000000000..0562f92db --- /dev/null +++ b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx @@ -0,0 +1,134 @@ +--- +id: 'testing-contracts' +title: 'Testing contracts with Stylus' +description: 'A hands-on guide to testing with Stylus.' +sme: anegg0 +target_audience: 'Developers writing smart contracts using Stylus.' +sidebar_position: 3 +--- + +import { VanillaAdmonition } from '@site/src/components/VanillaAdmonition/'; + +This guide explains how to write tests for Stylus contracts using the built-in testing framework. + +## Overview + +The Stylus SDK provides a testing framework through the `stylus_test` crate, which is automatically re-exported via `stylus_sdk::testing` when targeting native architectures. + +## Basic Testing Setup + +Let's walk through testing a simple counter contract: + +```rust +use stylus_sdk::{alloy_primitives::U256, prelude::*}; + +#[entrypoint] +#[storage] +pub struct Counter { + number: StorageU256, +} + +#[public] +impl Counter { + pub fn number(&self) -> U256 { + self.number.get() + } + + pub fn increment(&mut self) { + let number = self.number.get(); + self.number.set(number + U256::from(1)); + } +} + +#[cfg(test)] +mod test { + use super::*; + use stylus_sdk::testing::*; + + #[test] + fn test_counter() { + // Create a test VM environment + let vm = TestVM::default(); + + // Initialize the contract with the test VM + let mut contract = Counter::from(&vm); + + // Test initial state + assert_eq!(U256::ZERO, contract.number()); + + // Test state changes + contract.increment(); + assert_eq!(U256::from(1), contract.number()); + } +} +``` + +## Key Testing Components + +### TestVM + +The `TestVM` struct provides a simulated Ethereum environment for tests. + +`TestVM` handles: + +- Contract storage +- Transaction context +- Block information +- Gas metering + +You can customize the test environment using `TestVMBuilder`: + +```rust +#[test] +fn test_with_custom_setup() { + let vm = TestVMBuilder::new() + .with_sender(Address::from([0x1; 20])) + .with_value(U256::from(100)) + .build(); + + let contract = Counter::from(&vm); + // ... test logic +} +``` + +### Storage Testing + +The testing framework automatically handles persistent storage simulation. Your contract's storage operations work exactly as they would on-chain, but in a controlled test environment. + +### Best Practices + +1. **Test Organization** + + - Keep tests in a separate module marked with `#[cfg(test)]` + +2. **Isolation** + - Create a new `TestVM` instance for each test + - Don't rely on state from previous tests + +## Running Tests + +**Note:**: I'm really not sure how to run tests in the context of the Stylus SDK, so I winged it here. +Is it done with `cargo stylus`? + +Run your tests using the standard Rust test command: + +```shell +cargo test +``` + +For more verbose output: + +```shell +cargo test -- --nocapture +``` + +## Advanced Testing Features + +The testing framework also supports: + +- Transaction context simulation +- Gas metering +- Block information +- Contract-to-contract calls + +Check the [Stylus SDK documentation](https://docs.arbitrum.io/stylus/stylus-overview) for more advanced testing scenarios. diff --git a/website/sidebars.js b/website/sidebars.js index 2e0b86654..a2cdbbf93 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -551,6 +551,11 @@ const sidebars = { id: 'stylus/how-tos/debugging-tx', label: 'Debug transactions', }, + { + type: 'doc', + id: 'stylus/how-tos/testing-contracts', + label: 'Testing contracts', + }, { type: 'doc', id: 'stylus/how-tos/verifying-contracts', From 0c2071ac2792aeae9cd3d701bf19b54878e63422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Fri, 28 Feb 2025 09:44:14 -0800 Subject: [PATCH 02/21] feat: update erc721.rs and unneeded pointers --- .../stylus/how-tos/testing-contracts.mdx | 577 ++++++++++++++++-- 1 file changed, 521 insertions(+), 56 deletions(-) diff --git a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx index 0562f92db..931d7536a 100644 --- a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx +++ b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx @@ -9,74 +9,491 @@ sidebar_position: 3 import { VanillaAdmonition } from '@site/src/components/VanillaAdmonition/'; -This guide explains how to write tests for Stylus contracts using the built-in testing framework. +## Introduction -## Overview +The Stylus SDK provides a testing framework that allows developers to write and run tests directly in Rust without deploying to a blockchain. This guide will walk you through the process of writing and running tests for Stylus contracts using the built-in testing framework. -The Stylus SDK provides a testing framework through the `stylus_test` crate, which is automatically re-exported via `stylus_sdk::testing` when targeting native architectures. +The Stylus testing framework allows you to: -## Basic Testing Setup +- Simulate an Ethereum environment for your tests +- Test storage operations +- Mock transaction context and block information +- Test contract-to-contract interactions +- Verify contract logic without deployment costs -Let's walk through testing a simple counter contract: +### Prerequisites +Before you begin, make sure you have: + +- Rust installed (version 1.81.0 or later) +- The Stylus SDK installed +- Basic familiarity with Rust and smart contract development +- Cargo configured for your project + +## Example Smart Contract + +Let's look at an ERC-721 (NFT) implementation using the Stylus SDK. This example demonstrates the core functionality we'll test: + +
```rust -use stylus_sdk::{alloy_primitives::U256, prelude::*}; +//! Implementation of the ERC-721 standard +//! +//! The eponymous [`Erc721`] type provides all the standard methods, +//! and is intended to be inherited by other contract types. +//! +//! You can configure the behavior of [`Erc721`] via the [`Erc721Params`] trait, +//! which allows specifying the name, symbol, and token uri. +//! +//! Note that this code is unaudited and not fit for production use. + +use alloc::{string::String, vec, vec::Vec}; +use alloy_primitives::{Address, U256, FixedBytes}; +use alloy_sol_types::sol; +use core::{borrow::BorrowMut, marker::PhantomData}; +use stylus_sdk::{ + abi::Bytes, + evm, + msg, + prelude::* +}; + +pub trait Erc721Params { + /// Immutable NFT name. + const NAME: &'static str; + + /// Immutable NFT symbol. + const SYMBOL: &'static str; + + /// The NFT's Uniform Resource Identifier. + fn token_uri(token_id: U256) -> String; +} + +sol_storage! { + /// Erc721 implements all ERC-721 methods + pub struct Erc721 { + /// Token id to owner map + mapping(uint256 => address) owners; + /// User to balance map + mapping(address => uint256) balances; + /// Token id to approved user map + mapping(uint256 => address) token_approvals; + /// User to operator map (the operator can manage all NFTs of the owner) + mapping(address => mapping(address => bool)) operator_approvals; + /// Total supply + uint256 total_supply; + /// Used to allow [`Erc721Params`] + PhantomData phantom; + } +} + +// Declare events and Solidity error types +sol! { + event Transfer(address indexed from, address indexed to, uint256 indexed token_id); + event Approval(address indexed owner, address indexed approved, uint256 indexed token_id); + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + // Token id has not been minted, or it has been burned + error InvalidTokenId(uint256 token_id); + // The specified address is not the owner of the specified token id + error NotOwner(address from, uint256 token_id, address real_owner); + // The specified address does not have allowance to spend the specified token id + error NotApproved(address owner, address spender, uint256 token_id); + // Attempt to transfer token id to the Zero address + error TransferToZero(uint256 token_id); + // The receiver address refused to receive the specified token id + error ReceiverRefused(address receiver, uint256 token_id, bytes4 returned); +} + +/// Represents the ways methods may fail. +#[derive(SolidityError)] +pub enum Erc721Error { + InvalidTokenId(InvalidTokenId), + NotOwner(NotOwner), + NotApproved(NotApproved), + TransferToZero(TransferToZero), + ReceiverRefused(ReceiverRefused), +} + +// External interfaces +sol_interface! { + /// Allows calls to the `onERC721Received` method of other contracts implementing `IERC721TokenReceiver`. + interface IERC721TokenReceiver { + function onERC721Received(address operator, address from, uint256 token_id, bytes data) external returns(bytes4); + } +} + +/// Selector for `onERC721Received`, which is returned by contracts implementing `IERC721TokenReceiver`. +const ERC721_TOKEN_RECEIVER_ID: u32 = 0x150b7a02; + +// These methods aren't external, but are helpers used by external methods. +// Methods marked as "pub" here are usable outside of the erc721 module (i.e. they're callable from lib.rs). +impl Erc721 { + /// Requires that msg::sender() is authorized to spend a given token + fn require_authorized_to_spend(&self, from: Address, token_id: U256) -> Result<(), Erc721Error> { + // `from` must be the owner of the token_id + let owner = self.owner_of(token_id)?; + if from != owner { + return Err(Erc721Error::NotOwner(NotOwner { + from, + token_id, + real_owner: owner, + })); + } + + // caller is the owner + if msg::sender() == owner { + return Ok(()); + } + + // caller is an operator for the owner (can manage their tokens) + if self.operator_approvals.getter(owner).get(msg::sender()) { + return Ok(()); + } + + // caller is approved to manage this token_id + if msg::sender() == self.token_approvals.get(token_id) { + return Ok(()); + } + + // otherwise, caller is not allowed to manage this token_id + Err(Erc721Error::NotApproved(NotApproved { + owner, + spender: msg::sender(), + token_id, + })) + } + + /// Transfers `token_id` from `from` to `to`. + /// This function does check that `from` is the owner of the token, but it does not check + /// that `to` is not the zero address, as this function is usable for burning. + pub fn transfer(&mut self, token_id: U256, from: Address, to: Address) -> Result<(), Erc721Error> { + let mut owner = self.owners.setter(token_id); + let previous_owner = owner.get(); + if previous_owner != from { + return Err(Erc721Error::NotOwner(NotOwner { + from, + token_id, + real_owner: previous_owner, + })); + } + owner.set(to); + + // right now working with storage can be verbose, but this will change upcoming version of the Stylus SDK + let mut from_balance = self.balances.setter(from); + let balance = from_balance.get() - U256::from(1); + from_balance.set(balance); + + let mut to_balance = self.balances.setter(to); + let balance = to_balance.get() + U256::from(1); + to_balance.set(balance); + + // cleaning app the approved mapping for this token + self.token_approvals.delete(token_id); + + evm::log(Transfer { from, to, token_id }); + Ok(()) + } + + /// Calls `onERC721Received` on the `to` address if it is a contract. + /// Otherwise it does nothing + fn call_receiver( + storage: &mut S, + token_id: U256, + from: Address, + to: Address, + data: Vec, + ) -> Result<(), Erc721Error> { + if to.has_code() { + let receiver = IERC721TokenReceiver::new(to); + let received = receiver + .on_erc_721_received(&mut *storage, msg::sender(), from, token_id, data.into()) + .map_err(|_e| Erc721Error::ReceiverRefused(ReceiverRefused { + receiver: receiver.address, + token_id, + returned: alloy_primitives::FixedBytes(0_u32.to_be_bytes()), + }))? + .0; + + if u32::from_be_bytes(received) != ERC721_TOKEN_RECEIVER_ID { + return Err(Erc721Error::ReceiverRefused(ReceiverRefused { + receiver: receiver.address, + token_id, + returned: alloy_primitives::FixedBytes(received), + })); + } + } + Ok(()) + } + + /// Transfers and calls `onERC721Received` + pub fn safe_transfer>( + storage: &mut S, + token_id: U256, + from: Address, + to: Address, + data: Vec, + ) -> Result<(), Erc721Error> { + storage.borrow_mut().transfer(token_id, from, to)?; + Self::call_receiver(storage, token_id, from, to, data) + } -#[entrypoint] -#[storage] -pub struct Counter { - number: StorageU256, + /// Mints a new token and transfers it to `to` + pub fn mint(&mut self, to: Address) -> Result<(), Erc721Error> { + let new_token_id = self.total_supply.get(); + self.total_supply.set(new_token_id + U256::from(1u8)); + self.transfer(new_token_id, Address::default(), to)?; + Ok(()) + } + + /// Burns the token `token_id` from `from` + /// Note that total_supply is not reduced since it's used to calculate the next token_id to mint + pub fn burn(&mut self, from: Address, token_id: U256) -> Result<(), Erc721Error> { + self.transfer(token_id, from, Address::default())?; + Ok(()) + } } +// these methods are external to other contracts #[public] -impl Counter { - pub fn number(&self) -> U256 { - self.number.get() +impl Erc721 { + /// Immutable NFT name. + pub fn name() -> Result { + Ok(T::NAME.into()) + } + + /// Immutable NFT symbol. + pub fn symbol() -> Result { + Ok(T::SYMBOL.into()) + } + + /// The NFT's Uniform Resource Identifier. + #[selector(name = "tokenURI")] + pub fn token_uri(&self, token_id: U256) -> Result { + self.owner_of(token_id)?; // require NFT exist + Ok(T::token_uri(token_id)) + } + + /// Gets the number of NFTs owned by an account. + pub fn balance_of(&self, owner: Address) -> Result { + Ok(self.balances.get(owner)) + } + + /// Gets the owner of the NFT, if it exists. + pub fn owner_of(&self, token_id: U256) -> Result { + let owner = self.owners.get(token_id); + if owner.is_zero() { + return Err(Erc721Error::InvalidTokenId(InvalidTokenId { token_id })); + } + Ok(owner) + } + + /// Transfers an NFT, but only after checking the `to` address can receive the NFT. + /// It includes additional data for the receiver. + #[selector(name = "safeTransferFrom")] + pub fn safe_transfer_from_with_data>( + storage: &mut S, + from: Address, + to: Address, + token_id: U256, + data: Bytes, + ) -> Result<(), Erc721Error> { + if to.is_zero() { + return Err(Erc721Error::TransferToZero(TransferToZero { token_id })); + } + storage + .borrow_mut() + .require_authorized_to_spend(from, token_id)?; + + Self::safe_transfer(storage, token_id, from, to, data.0) + } + + /// Equivalent to [`safe_transfer_from_with_data`], but without the additional data. + /// + /// Note: because Rust doesn't allow multiple methods with the same name, + /// we use the `#[selector]` macro attribute to simulate solidity overloading. + #[selector(name = "safeTransferFrom")] + pub fn safe_transfer_from>( + storage: &mut S, + from: Address, + to: Address, + token_id: U256, + ) -> Result<(), Erc721Error> { + Self::safe_transfer_from_with_data(storage, from, to, token_id, Bytes(vec![])) + } + + /// Transfers the NFT. + pub fn transfer_from(&mut self, from: Address, to: Address, token_id: U256) -> Result<(), Erc721Error> { + if to.is_zero() { + return Err(Erc721Error::TransferToZero(TransferToZero { token_id })); + } + self.require_authorized_to_spend(from, token_id)?; + self.transfer(token_id, from, to)?; + Ok(()) + } + + /// Grants an account the ability to manage the sender's NFT. + pub fn approve(&mut self, approved: Address, token_id: U256) -> Result<(), Erc721Error> { + let owner = self.owner_of(token_id)?; + + // require authorization + if msg::sender() != owner && !self.operator_approvals.getter(owner).get(msg::sender()) { + return Err(Erc721Error::NotApproved(NotApproved { + owner, + spender: msg::sender(), + token_id, + })); + } + self.token_approvals.insert(token_id, approved); + + evm::log(Approval { + approved, + owner, + token_id, + }); + Ok(()) + } + + /// Grants an account the ability to manage all of the sender's NFTs. + pub fn set_approval_for_all(&mut self, operator: Address, approved: bool) -> Result<(), Erc721Error> { + let owner = msg::sender(); + self.operator_approvals + .setter(owner) + .insert(operator, approved); + + evm::log(ApprovalForAll { + owner, + operator, + approved, + }); + Ok(()) + } + + /// Gets the account managing an NFT, or zero if unmanaged. + pub fn get_approved(&mut self, token_id: U256) -> Result { + Ok(self.token_approvals.get(token_id)) } - pub fn increment(&mut self) { - let number = self.number.get(); - self.number.set(number + U256::from(1)); + /// Determines if an account has been authorized to managing all of a user's NFTs. + pub fn is_approved_for_all(&mut self, owner: Address, operator: Address) -> Result { + Ok(self.operator_approvals.getter(owner).get(operator)) + } + + /// Whether the NFT supports a given standard. + pub fn supports_interface(interface: FixedBytes<4>) -> Result { + let interface_slice_array: [u8; 4] = interface.as_slice().try_into().unwrap(); + + if u32::from_be_bytes(interface_slice_array) == 0xffffffff { + // special cased in the ERC165 standard + return Ok(false); + } + + const IERC165: u32 = 0x01ffc9a7; + const IERC721: u32 = 0x80ac58cd; + const IERC721_METADATA: u32 = 0x5b5e139f; + + Ok(matches!(u32::from_be_bytes(interface_slice_array), IERC165 | IERC721 | IERC721_METADATA)) } } +``` +
+ +## Writing Tests + +The Stylus SDK testing framework is available through the `stylus_sdk::testing` module, which is re-exported when targeting native architectures. This allows you to write and run tests using Rust's standard testing infrastructure. + +### Setting Up Your Test Environment + +To write tests for your contract, follow these steps: + +1. Create a test module in your contract file or in a separate file +2. Import the testing framework +3. Create a test VM environment +4. Initialize your contract with the test VM +5. Write your test assertions + +Here's a complete example of how to test our NFT contract: +```rust #[cfg(test)] mod test { use super::*; use stylus_sdk::testing::*; + use alloy_primitives::{Address, U256}; #[test] - fn test_counter() { + fn test_mint() { // Create a test VM environment let vm = TestVM::default(); - + + // Set a specific sender address for the test + let sender = Address::from([0x1; 20]); + vm.set_sender(sender); + // Initialize the contract with the test VM - let mut contract = Counter::from(&vm); - + let mut contract = StylusTestNFT::from(&vm); + // Test initial state - assert_eq!(U256::ZERO, contract.number()); - - // Test state changes - contract.increment(); - assert_eq!(U256::from(1), contract.number()); + assert_eq!(contract.total_supply().unwrap(), U256::ZERO); + + // Test minting + contract.mint().unwrap(); + + // Verify the result + assert_eq!(contract.total_supply().unwrap(), U256::from(1)); + assert_eq!(contract.erc721.balance_of(sender).unwrap(), U256::from(1)); + } + + #[test] + fn test_mint_to() { + // Create a test VM environment + let vm = TestVM::default(); + + // Initialize the contract with the test VM + let mut contract = StylusTestNFT::from(&vm); + + // Set up recipient address + let recipient = Address::from([0x2; 20]); + + // Test minting to a specific address + contract.mint_to(recipient).unwrap(); + + // Verify the result + assert_eq!(contract.total_supply().unwrap(), U256::from(1)); + assert_eq!(contract.erc721.balance_of(recipient).unwrap(), U256::from(1)); + } + + #[test] + fn test_burn() { + // Create a test VM environment + let vm = TestVM::default(); + + // Set a specific sender address for the test + let sender = Address::from([0x1; 20]); + vm.set_sender(sender); + + // Initialize the contract with the test VM + let mut contract = StylusTestNFT::from(&vm); + + // Mint a token first + contract.mint().unwrap(); + assert_eq!(contract.erc721.balance_of(sender).unwrap(), U256::from(1)); + + // Burn the token + contract.burn(U256::ZERO).unwrap(); + + // Verify the token was burned + // Note: total_supply doesn't decrease after burning + assert_eq!(contract.total_supply().unwrap(), U256::from(1)); + assert_eq!(contract.erc721.balance_of(sender).unwrap(), U256::ZERO); } } ``` -## Key Testing Components - -### TestVM - -The `TestVM` struct provides a simulated Ethereum environment for tests. - -`TestVM` handles: +### Advanced Testing Features -- Contract storage -- Transaction context -- Block information -- Gas metering +#### Customizing the Test Environment -You can customize the test environment using `TestVMBuilder`: +You can customize your test environment using `TestVMBuilder` for more complex scenarios: ```rust #[test] @@ -84,51 +501,99 @@ fn test_with_custom_setup() { let vm = TestVMBuilder::new() .with_sender(Address::from([0x1; 20])) .with_value(U256::from(100)) + .with_contract_address(Address::from([0x3; 20])) .build(); - let contract = Counter::from(&vm); - // ... test logic + let contract = StylusTestNFT::from(&vm); + // Test logic here +} +``` + +#### Testing Contract Interactions + +To test contract interactions, you can mock calls to other contracts: + +```rust +#[test] +fn test_external_contract_interaction() { + let vm = TestVM::default(); + + // Address of an external contract + let external_contract = Address::from([0x5; 20]); + + // Mock data and response + let call_data = vec![/* function selector and parameters */]; + let expected_response = vec![/* expected return data */]; + + // Mock the call + vm.mock_call(external_contract, call_data.clone(), Ok(expected_response)); + + // Initialize your contract + let contract = StylusTestNFT::from(&vm); + + // Test logic that involves calling the external contract + // ... } ``` -### Storage Testing +#### Testing Storage -The testing framework automatically handles persistent storage simulation. Your contract's storage operations work exactly as they would on-chain, but in a controlled test environment. +The testing framework automatically handles persistent storage simulation. Storage operations in your tests will work exactly as they would on-chain, but in a controlled test environment. + +```rust +#[test] +fn test_storage_persistence() { + let vm = TestVM::default(); + + // You can also set storage values directly + let key = U256::from(1); + let value = B256::from([0xff; 32]); + vm.set_storage(key, value); + + // And retrieve them + assert_eq!(vm.get_storage(key), value); +} +``` ### Best Practices 1. **Test Organization** - - Keep tests in a separate module marked with `#[cfg(test)]` + - Group related tests together -2. **Isolation** +2. **Test Isolation** - Create a new `TestVM` instance for each test - Don't rely on state from previous tests +3. **Comprehensive Testing** + - Test happy paths and error cases + - Test edge cases and boundary conditions + - Test access control and authorization + +4. **Meaningful Assertions** + - Make assertions that verify the actual behavior you care about + - Use descriptive error messages in assertions + ## Running Tests -**Note:**: I'm really not sure how to run tests in the context of the Stylus SDK, so I winged it here. -Is it done with `cargo stylus`? +### Testing with cargo-stylus -Run your tests using the standard Rust test command: +When using the `cargo-stylus` CLI tool, you can run tests with: ```shell -cargo test +cargo stylus test ``` -For more verbose output: + +You can also run specific tests by name: ```shell -cargo test -- --nocapture +cargo test test_mint ``` -## Advanced Testing Features -The testing framework also supports: +## Conclusion -- Transaction context simulation -- Gas metering -- Block information -- Contract-to-contract calls +Testing is an essential part of smart contract development to ensure security, correctness, and reliability. The Stylus SDK provides powerful testing tools that allow you to thoroughly test your contracts before deployment. -Check the [Stylus SDK documentation](https://docs.arbitrum.io/stylus/stylus-overview) for more advanced testing scenarios. +The ability to test Rust contracts directly, without requiring a blockchain environment, makes the development cycle faster and more efficient. From 4d5a29d8a180bd00f211b7d9ac5a8f671b20fbfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Mon, 3 Mar 2025 10:42:35 -0800 Subject: [PATCH 03/21] feat: add first CustomDetails component --- arbitrum-docs/stylus/how-tos/testing-contracts.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx index 931d7536a..64d665131 100644 --- a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx +++ b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx @@ -6,7 +6,7 @@ sme: anegg0 target_audience: 'Developers writing smart contracts using Stylus.' sidebar_position: 3 --- - +import CustomDetails from '@site/src/components/CustomDetails'; import { VanillaAdmonition } from '@site/src/components/VanillaAdmonition/'; ## Introduction @@ -34,7 +34,7 @@ Before you begin, make sure you have: Let's look at an ERC-721 (NFT) implementation using the Stylus SDK. This example demonstrates the core functionality we'll test: -
+ ```rust //! Implementation of the ERC-721 standard //! @@ -395,7 +395,7 @@ impl Erc721 { } } ``` -
+ ## Writing Tests From 377f66ad7e736df9432ee1fc4ec17ee975b071e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Wed, 5 Mar 2025 19:54:35 -0800 Subject: [PATCH 04/21] feat: add CustomDetails > code still breaks --- arbitrum-docs/stylus/how-tos/testing-contracts.mdx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx index 64d665131..6dc68402d 100644 --- a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx +++ b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx @@ -413,6 +413,7 @@ To write tests for your contract, follow these steps: Here's a complete example of how to test our NFT contract: + ```rust #[cfg(test)] mod test { @@ -488,6 +489,8 @@ mod test { } } ``` + + ### Advanced Testing Features @@ -495,6 +498,7 @@ mod test { You can customize your test environment using `TestVMBuilder` for more complex scenarios: + ```rust #[test] fn test_with_custom_setup() { @@ -535,6 +539,7 @@ fn test_external_contract_interaction() { // ... } ``` + #### Testing Storage From e4ad6cf4e0d53b5eb534bf2de68f879e07a76d29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Wed, 19 Mar 2025 12:52:20 +0100 Subject: [PATCH 05/21] feat: replace ERC721 with vending machine contract + better pre-requisites --- .../stylus/how-tos/testing-contracts.mdx | 430 ++++-------------- 1 file changed, 90 insertions(+), 340 deletions(-) diff --git a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx index 6dc68402d..4fbedfafb 100644 --- a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx +++ b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx @@ -25,373 +25,123 @@ The Stylus testing framework allows you to: Before you begin, make sure you have: -- Rust installed (version 1.81.0 or later) -- The Stylus SDK installed -- Basic familiarity with Rust and smart contract development -- Cargo configured for your project - -## Example Smart Contract - -Let's look at an ERC-721 (NFT) implementation using the Stylus SDK. This example demonstrates the core functionality we'll test: - - -```rust -//! Implementation of the ERC-721 standard -//! -//! The eponymous [`Erc721`] type provides all the standard methods, -//! and is intended to be inherited by other contract types. -//! -//! You can configure the behavior of [`Erc721`] via the [`Erc721Params`] trait, -//! which allows specifying the name, symbol, and token uri. -//! -//! Note that this code is unaudited and not fit for production use. - -use alloc::{string::String, vec, vec::Vec}; -use alloy_primitives::{Address, U256, FixedBytes}; -use alloy_sol_types::sol; -use core::{borrow::BorrowMut, marker::PhantomData}; -use stylus_sdk::{ - abi::Bytes, - evm, - msg, - prelude::* -}; - -pub trait Erc721Params { - /// Immutable NFT name. - const NAME: &'static str; - - /// Immutable NFT symbol. - const SYMBOL: &'static str; - - /// The NFT's Uniform Resource Identifier. - fn token_uri(token_id: U256) -> String; -} - -sol_storage! { - /// Erc721 implements all ERC-721 methods - pub struct Erc721 { - /// Token id to owner map - mapping(uint256 => address) owners; - /// User to balance map - mapping(address => uint256) balances; - /// Token id to approved user map - mapping(uint256 => address) token_approvals; - /// User to operator map (the operator can manage all NFTs of the owner) - mapping(address => mapping(address => bool)) operator_approvals; - /// Total supply - uint256 total_supply; - /// Used to allow [`Erc721Params`] - PhantomData phantom; - } -} - -// Declare events and Solidity error types -sol! { - event Transfer(address indexed from, address indexed to, uint256 indexed token_id); - event Approval(address indexed owner, address indexed approved, uint256 indexed token_id); - event ApprovalForAll(address indexed owner, address indexed operator, bool approved); - - // Token id has not been minted, or it has been burned - error InvalidTokenId(uint256 token_id); - // The specified address is not the owner of the specified token id - error NotOwner(address from, uint256 token_id, address real_owner); - // The specified address does not have allowance to spend the specified token id - error NotApproved(address owner, address spender, uint256 token_id); - // Attempt to transfer token id to the Zero address - error TransferToZero(uint256 token_id); - // The receiver address refused to receive the specified token id - error ReceiverRefused(address receiver, uint256 token_id, bytes4 returned); -} +
+Rust toolchain -/// Represents the ways methods may fail. -#[derive(SolidityError)] -pub enum Erc721Error { - InvalidTokenId(InvalidTokenId), - NotOwner(NotOwner), - NotApproved(NotApproved), - TransferToZero(TransferToZero), - ReceiverRefused(ReceiverRefused), -} +Follow the instructions on [Rust Lang's installation page](https://www.rust-lang.org/tools/install) to install a complete Rust toolchain (v1.81 or newer) on your system. After installation, ensure you can access the programs `rustup`, `rustc`, and `cargo` from your preferred terminal application. -// External interfaces -sol_interface! { - /// Allows calls to the `onERC721Received` method of other contracts implementing `IERC721TokenReceiver`. - interface IERC721TokenReceiver { - function onERC721Received(address operator, address from, uint256 token_id, bytes data) external returns(bytes4); - } -} +
-/// Selector for `onERC721Received`, which is returned by contracts implementing `IERC721TokenReceiver`. -const ERC721_TOKEN_RECEIVER_ID: u32 = 0x150b7a02; - -// These methods aren't external, but are helpers used by external methods. -// Methods marked as "pub" here are usable outside of the erc721 module (i.e. they're callable from lib.rs). -impl Erc721 { - /// Requires that msg::sender() is authorized to spend a given token - fn require_authorized_to_spend(&self, from: Address, token_id: U256) -> Result<(), Erc721Error> { - // `from` must be the owner of the token_id - let owner = self.owner_of(token_id)?; - if from != owner { - return Err(Erc721Error::NotOwner(NotOwner { - from, - token_id, - real_owner: owner, - })); - } + - // caller is the owner - if msg::sender() == owner { - return Ok(()); - } +
+Docker - // caller is an operator for the owner (can manage their tokens) - if self.operator_approvals.getter(owner).get(msg::sender()) { - return Ok(()); - } - - // caller is approved to manage this token_id - if msg::sender() == self.token_approvals.get(token_id) { - return Ok(()); - } +The testnode we will use as well as some `cargo stylus` commands require Docker to operate. - // otherwise, caller is not allowed to manage this token_id - Err(Erc721Error::NotApproved(NotApproved { - owner, - spender: msg::sender(), - token_id, - })) - } +You can download Docker from [Docker's website](https://www.docker.com/products/docker-desktop). - /// Transfers `token_id` from `from` to `to`. - /// This function does check that `from` is the owner of the token, but it does not check - /// that `to` is not the zero address, as this function is usable for burning. - pub fn transfer(&mut self, token_id: U256, from: Address, to: Address) -> Result<(), Erc721Error> { - let mut owner = self.owners.setter(token_id); - let previous_owner = owner.get(); - if previous_owner != from { - return Err(Erc721Error::NotOwner(NotOwner { - from, - token_id, - real_owner: previous_owner, - })); - } - owner.set(to); +
- // right now working with storage can be verbose, but this will change upcoming version of the Stylus SDK - let mut from_balance = self.balances.setter(from); - let balance = from_balance.get() - U256::from(1); - from_balance.set(balance); +
+Nitro devnode - let mut to_balance = self.balances.setter(to); - let balance = to_balance.get() + U256::from(1); - to_balance.set(balance); +Stylus is available on Arbitrum Sepolia, but we'll use nitro devnode which has a pre-funded wallet saving us the effort of wallet provisioning or running out of tokens to send transactions. - // cleaning app the approved mapping for this token - self.token_approvals.delete(token_id); - - evm::log(Transfer { from, to, token_id }); - Ok(()) - } +```shell title="Install your devnode" +git clone https://github.com/OffchainLabs/nitro-devnode.git +cd nitro-devnode +``` - /// Calls `onERC721Received` on the `to` address if it is a contract. - /// Otherwise it does nothing - fn call_receiver( - storage: &mut S, - token_id: U256, - from: Address, - to: Address, - data: Vec, - ) -> Result<(), Erc721Error> { - if to.has_code() { - let receiver = IERC721TokenReceiver::new(to); - let received = receiver - .on_erc_721_received(&mut *storage, msg::sender(), from, token_id, data.into()) - .map_err(|_e| Erc721Error::ReceiverRefused(ReceiverRefused { - receiver: receiver.address, - token_id, - returned: alloy_primitives::FixedBytes(0_u32.to_be_bytes()), - }))? - .0; - - if u32::from_be_bytes(received) != ERC721_TOKEN_RECEIVER_ID { - return Err(Erc721Error::ReceiverRefused(ReceiverRefused { - receiver: receiver.address, - token_id, - returned: alloy_primitives::FixedBytes(received), - })); - } - } - Ok(()) - } +```shell title="Launch your devnode" +./run-dev-node.sh +``` - /// Transfers and calls `onERC721Received` - pub fn safe_transfer>( - storage: &mut S, - token_id: U256, - from: Address, - to: Address, - data: Vec, - ) -> Result<(), Erc721Error> { - storage.borrow_mut().transfer(token_id, from, to)?; - Self::call_receiver(storage, token_id, from, to, data) - } +
- /// Mints a new token and transfers it to `to` - pub fn mint(&mut self, to: Address) -> Result<(), Erc721Error> { - let new_token_id = self.total_supply.get(); - self.total_supply.set(new_token_id + U256::from(1u8)); - self.transfer(new_token_id, Address::default(), to)?; - Ok(()) - } - /// Burns the token `token_id` from `from` - /// Note that total_supply is not reduced since it's used to calculate the next token_id to mint - pub fn burn(&mut self, from: Address, token_id: U256) -> Result<(), Erc721Error> { - self.transfer(token_id, from, Address::default())?; - Ok(()) - } -} - -// these methods are external to other contracts -#[public] -impl Erc721 { - /// Immutable NFT name. - pub fn name() -> Result { - Ok(T::NAME.into()) - } +- Basic familiarity with Rust and smart contract development +- Cargo configured for your project - /// Immutable NFT symbol. - pub fn symbol() -> Result { - Ok(T::SYMBOL.into()) - } +## Example Smart Contract - /// The NFT's Uniform Resource Identifier. - #[selector(name = "tokenURI")] - pub fn token_uri(&self, token_id: U256) -> Result { - self.owner_of(token_id)?; // require NFT exist - Ok(T::token_uri(token_id)) - } +Let's look at the implementation of a decentralized cupcake vending machine using the Stylus SDK. This example demonstrates the core functionality we'll test. - /// Gets the number of NFTs owned by an account. - pub fn balance_of(&self, owner: Address) -> Result { - Ok(self.balances.get(owner)) - } - /// Gets the owner of the NFT, if it exists. - pub fn owner_of(&self, token_id: U256) -> Result { - let owner = self.owners.get(token_id); - if owner.is_zero() { - return Err(Erc721Error::InvalidTokenId(InvalidTokenId { token_id })); - } - Ok(owner) - } - /// Transfers an NFT, but only after checking the `to` address can receive the NFT. - /// It includes additional data for the receiver. - #[selector(name = "safeTransferFrom")] - pub fn safe_transfer_from_with_data>( - storage: &mut S, - from: Address, - to: Address, - token_id: U256, - data: Bytes, - ) -> Result<(), Erc721Error> { - if to.is_zero() { - return Err(Erc721Error::TransferToZero(TransferToZero { token_id })); - } - storage - .borrow_mut() - .require_authorized_to_spend(from, token_id)?; + +```rust +//! +//! Stylus Cupcake Example +//! +//! The contract is ABI-equivalent with Solidity, which means you can call it from both Solidity and Rust. +//! To do this, run `cargo stylus export-abi`. +//! +//! Note: this code is a template-only and has not been audited. +//! - Self::safe_transfer(storage, token_id, from, to, data.0) - } +// Allow `cargo stylus export-abi` to generate a main function if the "export-abi" feature is enabled. +#![cfg_attr(not(feature = "export-abi"), no_main)] +extern crate alloc; - /// Equivalent to [`safe_transfer_from_with_data`], but without the additional data. - /// - /// Note: because Rust doesn't allow multiple methods with the same name, - /// we use the `#[selector]` macro attribute to simulate solidity overloading. - #[selector(name = "safeTransferFrom")] - pub fn safe_transfer_from>( - storage: &mut S, - from: Address, - to: Address, - token_id: U256, - ) -> Result<(), Erc721Error> { - Self::safe_transfer_from_with_data(storage, from, to, token_id, Bytes(vec![])) - } +use alloy_primitives::{Address, Uint}; +// Import items from the SDK. The prelude contains common traits and macros. +use stylus_sdk::alloy_primitives::U256; +use stylus_sdk::prelude::*; +use stylus_sdk::{block, console}; - /// Transfers the NFT. - pub fn transfer_from(&mut self, from: Address, to: Address, token_id: U256) -> Result<(), Erc721Error> { - if to.is_zero() { - return Err(Erc721Error::TransferToZero(TransferToZero { token_id })); - } - self.require_authorized_to_spend(from, token_id)?; - self.transfer(token_id, from, to)?; - Ok(()) +// Define persistent storage using the Solidity ABI. +// `VendingMachine` will be the entrypoint for the contract. +sol_storage! { + #[entrypoint] + pub struct VendingMachine { + // Mapping from user addresses to their cupcake balances. + mapping(address => uint256) cupcake_balances; + // Mapping from user addresses to the last time they received a cupcake. + mapping(address => uint256) cupcake_distribution_times; } +} - /// Grants an account the ability to manage the sender's NFT. - pub fn approve(&mut self, approved: Address, token_id: U256) -> Result<(), Erc721Error> { - let owner = self.owner_of(token_id)?; - - // require authorization - if msg::sender() != owner && !self.operator_approvals.getter(owner).get(msg::sender()) { - return Err(Erc721Error::NotApproved(NotApproved { - owner, - spender: msg::sender(), - token_id, - })); +// Declare that `VendingMachine` is a contract with the following external methods. +#[public] +impl VendingMachine { + // Give a cupcake to the specified user if they are eligible (i.e., if at least 5 seconds have passed since their last cupcake). + pub fn give_cupcake_to(&mut self, user_address: Address) -> bool { + // Get the last distribution time for the user. + let last_distribution = self.cupcake_distribution_times.get(user_address); + // Calculate the earliest next time the user can receive a cupcake. + let five_seconds_from_last_distribution = last_distribution + U256::from(5); + + // Get the current block timestamp. + let current_time = block::timestamp(); + // Check if the user can receive a cupcake. + let user_can_receive_cupcake = + five_seconds_from_last_distribution <= Uint::<256, 4>::from(current_time); + + if user_can_receive_cupcake { + // Increment the user's cupcake balance. + let mut balance_accessor = self.cupcake_balances.setter(user_address); + let balance = balance_accessor.get() + U256::from(1); + balance_accessor.set(balance); + + // Update the distribution time to the current time. + let mut time_accessor = self.cupcake_distribution_times.setter(user_address); + let new_distribution_time = block::timestamp(); + time_accessor.set(Uint::<256, 4>::from(new_distribution_time)); + return true; + } else { + // User must wait before receiving another cupcake. + console!( + "HTTP 429: Too Many Cupcakes (you must wait at least 5 seconds between cupcakes)" + ); + return false; } - self.token_approvals.insert(token_id, approved); - - evm::log(Approval { - approved, - owner, - token_id, - }); - Ok(()) - } - - /// Grants an account the ability to manage all of the sender's NFTs. - pub fn set_approval_for_all(&mut self, operator: Address, approved: bool) -> Result<(), Erc721Error> { - let owner = msg::sender(); - self.operator_approvals - .setter(owner) - .insert(operator, approved); - - evm::log(ApprovalForAll { - owner, - operator, - approved, - }); - Ok(()) } - /// Gets the account managing an NFT, or zero if unmanaged. - pub fn get_approved(&mut self, token_id: U256) -> Result { - Ok(self.token_approvals.get(token_id)) - } - - /// Determines if an account has been authorized to managing all of a user's NFTs. - pub fn is_approved_for_all(&mut self, owner: Address, operator: Address) -> Result { - Ok(self.operator_approvals.getter(owner).get(operator)) - } - - /// Whether the NFT supports a given standard. - pub fn supports_interface(interface: FixedBytes<4>) -> Result { - let interface_slice_array: [u8; 4] = interface.as_slice().try_into().unwrap(); - - if u32::from_be_bytes(interface_slice_array) == 0xffffffff { - // special cased in the ERC165 standard - return Ok(false); - } - - const IERC165: u32 = 0x01ffc9a7; - const IERC721: u32 = 0x80ac58cd; - const IERC721_METADATA: u32 = 0x5b5e139f; - - Ok(matches!(u32::from_be_bytes(interface_slice_array), IERC165 | IERC721 | IERC721_METADATA)) + // Get the cupcake balance for the specified user. + pub fn get_cupcake_balance_for(&self, user_address: Address) -> Uint<256, 4> { + // Return the user's cupcake balance from storage. + return self.cupcake_balances.get(user_address); } } ``` From 169a411bc91c17b5f4913db1d104b588c827d60f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Wed, 19 Mar 2025 13:20:41 +0100 Subject: [PATCH 06/21] fix: reformat --- .../stylus/how-tos/testing-contracts.mdx | 110 +++++++++--------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx index 4fbedfafb..554668091 100644 --- a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx +++ b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx @@ -6,6 +6,7 @@ sme: anegg0 target_audience: 'Developers writing smart contracts using Stylus.' sidebar_position: 3 --- + import CustomDetails from '@site/src/components/CustomDetails'; import { VanillaAdmonition } from '@site/src/components/VanillaAdmonition/'; @@ -25,6 +26,8 @@ The Stylus testing framework allows you to: Before you begin, make sure you have: +- Basic familiarity with Rust and smart contract development +
Rust toolchain @@ -59,16 +62,10 @@ cd nitro-devnode
- -- Basic familiarity with Rust and smart contract development -- Cargo configured for your project - ## Example Smart Contract Let's look at the implementation of a decentralized cupcake vending machine using the Stylus SDK. This example demonstrates the core functionality we'll test. - - ```rust //! @@ -80,37 +77,34 @@ Let's look at the implementation of a decentralized cupcake vending machine usin //! Note: this code is a template-only and has not been audited. //! -// Allow `cargo stylus export-abi` to generate a main function if the "export-abi" feature is enabled. -#![cfg_attr(not(feature = "export-abi"), no_main)] +// Allow `cargo stylus export-abi` to generate a main function if the "export-abi" feature is enabled. #![cfg_attr(not(feature = "export-abi"), no_main)] extern crate alloc; use alloy_primitives::{Address, Uint}; // Import items from the SDK. The prelude contains common traits and macros. use stylus_sdk::alloy_primitives::U256; -use stylus_sdk::prelude::*; +use stylus_sdk::prelude::\*; use stylus_sdk::{block, console}; // Define persistent storage using the Solidity ABI. // `VendingMachine` will be the entrypoint for the contract. -sol_storage! { - #[entrypoint] - pub struct VendingMachine { - // Mapping from user addresses to their cupcake balances. - mapping(address => uint256) cupcake_balances; - // Mapping from user addresses to the last time they received a cupcake. - mapping(address => uint256) cupcake_distribution_times; - } +sol_storage! { #[entrypoint] +pub struct VendingMachine { +// Mapping from user addresses to their cupcake balances. +mapping(address => uint256) cupcake_balances; +// Mapping from user addresses to the last time they received a cupcake. +mapping(address => uint256) cupcake_distribution_times; +} } -// Declare that `VendingMachine` is a contract with the following external methods. -#[public] +// Declare that `VendingMachine` is a contract with the following external methods. #[public] impl VendingMachine { - // Give a cupcake to the specified user if they are eligible (i.e., if at least 5 seconds have passed since their last cupcake). - pub fn give_cupcake_to(&mut self, user_address: Address) -> bool { - // Get the last distribution time for the user. - let last_distribution = self.cupcake_distribution_times.get(user_address); - // Calculate the earliest next time the user can receive a cupcake. - let five_seconds_from_last_distribution = last_distribution + U256::from(5); +// Give a cupcake to the specified user if they are eligible (i.e., if at least 5 seconds have passed since their last cupcake). +pub fn give_cupcake_to(&mut self, user_address: Address) -> bool { +// Get the last distribution time for the user. +let last_distribution = self.cupcake_distribution_times.get(user_address); +// Calculate the earliest next time the user can receive a cupcake. +let five_seconds_from_last_distribution = last_distribution + U256::from(5); // Get the current block timestamp. let current_time = block::timestamp(); @@ -143,8 +137,10 @@ impl VendingMachine { // Return the user's cupcake balance from storage. return self.cupcake_balances.get(user_address); } + } -``` + +```` ## Writing Tests @@ -175,72 +171,72 @@ mod test { fn test_mint() { // Create a test VM environment let vm = TestVM::default(); - + // Set a specific sender address for the test let sender = Address::from([0x1; 20]); vm.set_sender(sender); - + // Initialize the contract with the test VM let mut contract = StylusTestNFT::from(&vm); - + // Test initial state assert_eq!(contract.total_supply().unwrap(), U256::ZERO); - + // Test minting contract.mint().unwrap(); - + // Verify the result assert_eq!(contract.total_supply().unwrap(), U256::from(1)); assert_eq!(contract.erc721.balance_of(sender).unwrap(), U256::from(1)); } - + #[test] fn test_mint_to() { // Create a test VM environment let vm = TestVM::default(); - + // Initialize the contract with the test VM let mut contract = StylusTestNFT::from(&vm); - + // Set up recipient address let recipient = Address::from([0x2; 20]); - + // Test minting to a specific address contract.mint_to(recipient).unwrap(); - + // Verify the result assert_eq!(contract.total_supply().unwrap(), U256::from(1)); assert_eq!(contract.erc721.balance_of(recipient).unwrap(), U256::from(1)); } - + #[test] fn test_burn() { // Create a test VM environment let vm = TestVM::default(); - + // Set a specific sender address for the test let sender = Address::from([0x1; 20]); vm.set_sender(sender); - + // Initialize the contract with the test VM let mut contract = StylusTestNFT::from(&vm); - + // Mint a token first contract.mint().unwrap(); assert_eq!(contract.erc721.balance_of(sender).unwrap(), U256::from(1)); - + // Burn the token contract.burn(U256::ZERO).unwrap(); - + // Verify the token was burned // Note: total_supply doesn't decrease after burning assert_eq!(contract.total_supply().unwrap(), U256::from(1)); assert_eq!(contract.erc721.balance_of(sender).unwrap(), U256::ZERO); } } -``` -
+```` +
### Advanced Testing Features @@ -260,8 +256,10 @@ fn test_with_custom_setup() { let contract = StylusTestNFT::from(&vm); // Test logic here + } -``` + +```` #### Testing Contract Interactions @@ -271,24 +269,25 @@ To test contract interactions, you can mock calls to other contracts: #[test] fn test_external_contract_interaction() { let vm = TestVM::default(); - + // Address of an external contract let external_contract = Address::from([0x5; 20]); - + // Mock data and response let call_data = vec![/* function selector and parameters */]; let expected_response = vec![/* expected return data */]; - + // Mock the call vm.mock_call(external_contract, call_data.clone(), Ok(expected_response)); - + // Initialize your contract let contract = StylusTestNFT::from(&vm); - + // Test logic that involves calling the external contract // ... } -``` +```` + #### Testing Storage @@ -299,12 +298,12 @@ The testing framework automatically handles persistent storage simulation. Stora #[test] fn test_storage_persistence() { let vm = TestVM::default(); - + // You can also set storage values directly let key = U256::from(1); let value = B256::from([0xff; 32]); vm.set_storage(key, value); - + // And retrieve them assert_eq!(vm.get_storage(key), value); } @@ -313,14 +312,17 @@ fn test_storage_persistence() { ### Best Practices 1. **Test Organization** + - Keep tests in a separate module marked with `#[cfg(test)]` - Group related tests together 2. **Test Isolation** + - Create a new `TestVM` instance for each test - Don't rely on state from previous tests 3. **Comprehensive Testing** + - Test happy paths and error cases - Test edge cases and boundary conditions - Test access control and authorization @@ -339,14 +341,12 @@ When using the `cargo-stylus` CLI tool, you can run tests with: cargo stylus test ``` - You can also run specific tests by name: ```shell cargo test test_mint ``` - ## Conclusion Testing is an essential part of smart contract development to ensure security, correctness, and reliability. The Stylus SDK provides powerful testing tools that allow you to thoroughly test your contracts before deployment. From 7fb37d1d1c248317d270235bffa97c428cd5911b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 20 Mar 2025 14:36:35 +0100 Subject: [PATCH 07/21] feat: add vending machine contract description --- arbitrum-docs/stylus/how-tos/testing-contracts.mdx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx index 554668091..d4e26f3c5 100644 --- a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx +++ b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx @@ -64,7 +64,13 @@ cd nitro-devnode ## Example Smart Contract -Let's look at the implementation of a decentralized cupcake vending machine using the Stylus SDK. This example demonstrates the core functionality we'll test. +Let's look at the implementation of a decentralized cupcake vending machine using the Stylus SDK. + +This example demonstrates the core functionality we'll test. +We're going to test a Rust Smart Contract defining a cupcake vending machine. +This vending machine will follow two rules: +1. The vending machine will distribute a cupcake to anyone who hasn't recently received one. +2. The vending machine's rules can't be changed by anyone. ```rust From 44613c1f816ca7acdc15870915e10a3b901fa348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 20 Mar 2025 15:55:15 +0100 Subject: [PATCH 08/21] feat: Add Arbitrum Stylus vending machine contract how-to example --- .../how-tos/vending_machine_contract.rs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 arbitrum-docs/stylus/how-tos/vending_machine_contract.rs diff --git a/arbitrum-docs/stylus/how-tos/vending_machine_contract.rs b/arbitrum-docs/stylus/how-tos/vending_machine_contract.rs new file mode 100644 index 000000000..0704a9cbc --- /dev/null +++ b/arbitrum-docs/stylus/how-tos/vending_machine_contract.rs @@ -0,0 +1,71 @@ +//! +//! Stylus Cupcake Example +//! +//! The contract is ABI-equivalent with Solidity, which means you can call it from both Solidity and Rust. +//! To do this, run `cargo stylus export-abi`. +//! +//! Note: this code is a template-only and has not been audited. +//! + +// Allow `cargo stylus export-abi` to generate a main function if the "export-abi" feature is enabled. #![cfg_attr(not(feature = "export-abi"), no_main)] +extern crate alloc; + +use alloy_primitives::{Address, Uint}; +// Import items from the SDK. The prelude contains common traits and macros. +use stylus_sdk::alloy_primitives::U256; +use stylus_sdk::prelude::\*; +use stylus_sdk::{block, console}; + +// Define persistent storage using the Solidity ABI. +// `VendingMachine` will be the entrypoint for the contract. +sol_storage! { #[entrypoint] +pub struct VendingMachine { +// Mapping from user addresses to their cupcake balances. +mapping(address => uint256) cupcake_balances; +// Mapping from user addresses to the last time they received a cupcake. +mapping(address => uint256) cupcake_distribution_times; +} +} + +// Declare that `VendingMachine` is a contract with the following external methods. #[public] +impl VendingMachine { +// Give a cupcake to the specified user if they are eligible (i.e., if at least 5 seconds have passed since their last cupcake). +pub fn give_cupcake_to(&mut self, user_address: Address) -> bool { +// Get the last distribution time for the user. +let last_distribution = self.cupcake_distribution_times.get(user_address); +// Calculate the earliest next time the user can receive a cupcake. +let five_seconds_from_last_distribution = last_distribution + U256::from(5); + + // Get the current block timestamp. + let current_time = block::timestamp(); + // Check if the user can receive a cupcake. + let user_can_receive_cupcake = + five_seconds_from_last_distribution <= Uint::<256, 4>::from(current_time); + + if user_can_receive_cupcake { + // Increment the user's cupcake balance. + let mut balance_accessor = self.cupcake_balances.setter(user_address); + let balance = balance_accessor.get() + U256::from(1); + balance_accessor.set(balance); + + // Update the distribution time to the current time. + let mut time_accessor = self.cupcake_distribution_times.setter(user_address); + let new_distribution_time = block::timestamp(); + time_accessor.set(Uint::<256, 4>::from(new_distribution_time)); + return true; + } else { + // User must wait before receiving another cupcake. + console!( + "HTTP 429: Too Many Cupcakes (you must wait at least 5 seconds between cupcakes)" + ); + return false; + } + } + + // Get the cupcake balance for the specified user. + pub fn get_cupcake_balance_for(&self, user_address: Address) -> Uint<256, 4> { + // Return the user's cupcake balance from storage. + return self.cupcake_balances.get(user_address); + } + +} From 611b28708cf3c99ac2fff8aaec24a988d26dc555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain=20=28aider=29?= Date: Thu, 20 Mar 2025 15:55:17 +0100 Subject: [PATCH 09/21] test: Add comprehensive test suite for vending machine contract --- .../how-tos/vending_machine_contract.rs | 6 ++ .../stylus/how-tos/vending_machine_test.rs | 69 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 arbitrum-docs/stylus/how-tos/vending_machine_test.rs diff --git a/arbitrum-docs/stylus/how-tos/vending_machine_contract.rs b/arbitrum-docs/stylus/how-tos/vending_machine_contract.rs index 0704a9cbc..c04f690e9 100644 --- a/arbitrum-docs/stylus/how-tos/vending_machine_contract.rs +++ b/arbitrum-docs/stylus/how-tos/vending_machine_contract.rs @@ -25,6 +25,12 @@ mapping(address => uint256) cupcake_balances; // Mapping from user addresses to the last time they received a cupcake. mapping(address => uint256) cupcake_distribution_times; } + +#[cfg(test)] +mod tests { + use super::*; + mod vending_machine_test; +} } // Declare that `VendingMachine` is a contract with the following external methods. #[public] diff --git a/arbitrum-docs/stylus/how-tos/vending_machine_test.rs b/arbitrum-docs/stylus/how-tos/vending_machine_test.rs new file mode 100644 index 000000000..df32e7505 --- /dev/null +++ b/arbitrum-docs/stylus/how-tos/vending_machine_test.rs @@ -0,0 +1,69 @@ +use super::*; +use stylus_sdk::stylus_test::*; +use alloy_primitives::{address, Address, U256}; + +#[test] +fn test_initial_balance() { + // Set up test VM and contract + let vm = TestVM::default(); + let contract = VendingMachine::from(&vm); + + // Check initial balance is zero for a random address + let test_address = address!("dCE82b5f92C98F27F116F70491a487EFFDb6a2a9"); + assert_eq!(contract.get_cupcake_balance_for(test_address), U256::ZERO); +} + +#[test] +fn test_cupcake_distribution() { + // Set up test VM with custom configuration + let vm = TestVMBuilder::new() + .block_timestamp(100) + .build(); + let mut contract = VendingMachine::from(&vm); + + let test_address = address!("dCE82b5f92C98F27F116F70491a487EFFDb6a2a9"); + + // First distribution should succeed + assert!(contract.give_cupcake_to(test_address)); + assert_eq!(contract.get_cupcake_balance_for(test_address), U256::from(1)); + + // Immediate second distribution should fail (less than 5 seconds) + assert!(!contract.give_cupcake_to(test_address)); + assert_eq!(contract.get_cupcake_balance_for(test_address), U256::from(1)); + + // Advance time by 6 seconds + vm.set_block_timestamp(106); + + // Distribution should now succeed + assert!(contract.give_cupcake_to(test_address)); + assert_eq!(contract.get_cupcake_balance_for(test_address), U256::from(2)); +} + +#[test] +fn test_multiple_users() { + let vm = TestVMBuilder::new() + .block_timestamp(100) + .build(); + let mut contract = VendingMachine::from(&vm); + + let user1 = address!("dCE82b5f92C98F27F116F70491a487EFFDb6a2a9"); + let user2 = address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"); + + // Give cupcakes to both users + assert!(contract.give_cupcake_to(user1)); + assert!(contract.give_cupcake_to(user2)); + + // Verify balances + assert_eq!(contract.get_cupcake_balance_for(user1), U256::from(1)); + assert_eq!(contract.get_cupcake_balance_for(user2), U256::from(1)); + + // Advance time + vm.set_block_timestamp(106); + + // Give another cupcake to user1 only + assert!(contract.give_cupcake_to(user1)); + + // Verify updated balances + assert_eq!(contract.get_cupcake_balance_for(user1), U256::from(2)); + assert_eq!(contract.get_cupcake_balance_for(user2), U256::from(1)); +} From 20f6022568d127edc0f7e52f86a8f4db12589847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain=20=28aider=29?= Date: Thu, 20 Mar 2025 15:55:33 +0100 Subject: [PATCH 10/21] fix: Remove escaped backslash in Rust use statement --- arbitrum-docs/stylus/how-tos/vending_machine_contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arbitrum-docs/stylus/how-tos/vending_machine_contract.rs b/arbitrum-docs/stylus/how-tos/vending_machine_contract.rs index c04f690e9..c1647f9ba 100644 --- a/arbitrum-docs/stylus/how-tos/vending_machine_contract.rs +++ b/arbitrum-docs/stylus/how-tos/vending_machine_contract.rs @@ -13,7 +13,7 @@ extern crate alloc; use alloy_primitives::{Address, Uint}; // Import items from the SDK. The prelude contains common traits and macros. use stylus_sdk::alloy_primitives::U256; -use stylus_sdk::prelude::\*; +use stylus_sdk::prelude::*; use stylus_sdk::{block, console}; // Define persistent storage using the Solidity ABI. From 83e7c517410ab25ee55aad984bb2075b8a499278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain=20=28aider=29?= Date: Tue, 25 Mar 2025 10:53:19 +0100 Subject: [PATCH 11/21] test: Add comprehensive tests for VendingMachine contract functionality --- .../vending_machine_test.rs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 arbitrum-docs/stylus/how-tos/vending_machine_contract/vending_machine_test.rs diff --git a/arbitrum-docs/stylus/how-tos/vending_machine_contract/vending_machine_test.rs b/arbitrum-docs/stylus/how-tos/vending_machine_contract/vending_machine_test.rs new file mode 100644 index 000000000..b338989f4 --- /dev/null +++ b/arbitrum-docs/stylus/how-tos/vending_machine_contract/vending_machine_test.rs @@ -0,0 +1,79 @@ +use super::*; +use stylus_test::*; +use stylus_sdk::alloy_primitives::Address; + +#[test] +fn test_give_cupcake() { + // Setup test environment + let vm = TestVM::default(); + let mut contract = VendingMachine::from(&vm); + + // Create a test user address + let user = Address::repeat_byte(0x42); + + // Initial balance should be zero + let initial_balance = contract.get_cupcake_balance_for(user); + assert_eq!(initial_balance, U256::ZERO); + + // First cupcake should be given successfully + let result = contract.give_cupcake_to(user); + assert!(result); + + // Balance should be incremented to 1 + let balance_after_first = contract.get_cupcake_balance_for(user); + assert_eq!(balance_after_first, U256::from(1)); + + // Trying to get another cupcake immediately should fail (5 second cooldown) + let result = contract.give_cupcake_to(user); + assert!(!result); + + // Balance should still be 1 + let balance_after_second_attempt = contract.get_cupcake_balance_for(user); + assert_eq!(balance_after_second_attempt, U256::from(1)); + + // Advance time by 6 seconds + vm.advance_time(6); + + // Now we should be able to get another cupcake + let result = contract.give_cupcake_to(user); + assert!(result); + + // Balance should be incremented to 2 + let final_balance = contract.get_cupcake_balance_for(user); + assert_eq!(final_balance, U256::from(2)); +} + +#[test] +fn test_multiple_users() { + // Setup test environment + let vm = TestVM::default(); + let mut contract = VendingMachine::from(&vm); + + // Create two test user addresses + let user1 = Address::repeat_byte(0x11); + let user2 = Address::repeat_byte(0x22); + + // Give cupcake to first user + let result = contract.give_cupcake_to(user1); + assert!(result); + + // First user should have 1 cupcake + let user1_balance = contract.get_cupcake_balance_for(user1); + assert_eq!(user1_balance, U256::from(1)); + + // Second user should have 0 cupcakes + let user2_balance = contract.get_cupcake_balance_for(user2); + assert_eq!(user2_balance, U256::ZERO); + + // Give cupcake to second user + let result = contract.give_cupcake_to(user2); + assert!(result); + + // Second user should now have 1 cupcake + let user2_balance = contract.get_cupcake_balance_for(user2); + assert_eq!(user2_balance, U256::from(1)); + + // First user should still have 1 cupcake + let user1_balance = contract.get_cupcake_balance_for(user1); + assert_eq!(user1_balance, U256::from(1)); +} From aacef3fcb1cfcbb899bb66fd9f46b848341460e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 25 Mar 2025 11:06:06 +0100 Subject: [PATCH 12/21] refactor: remove example files --- .../how-tos/vending_machine_contract.rs | 77 ------------------- .../stylus/how-tos/vending_machine_test.rs | 69 ----------------- 2 files changed, 146 deletions(-) delete mode 100644 arbitrum-docs/stylus/how-tos/vending_machine_contract.rs delete mode 100644 arbitrum-docs/stylus/how-tos/vending_machine_test.rs diff --git a/arbitrum-docs/stylus/how-tos/vending_machine_contract.rs b/arbitrum-docs/stylus/how-tos/vending_machine_contract.rs deleted file mode 100644 index c1647f9ba..000000000 --- a/arbitrum-docs/stylus/how-tos/vending_machine_contract.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! -//! Stylus Cupcake Example -//! -//! The contract is ABI-equivalent with Solidity, which means you can call it from both Solidity and Rust. -//! To do this, run `cargo stylus export-abi`. -//! -//! Note: this code is a template-only and has not been audited. -//! - -// Allow `cargo stylus export-abi` to generate a main function if the "export-abi" feature is enabled. #![cfg_attr(not(feature = "export-abi"), no_main)] -extern crate alloc; - -use alloy_primitives::{Address, Uint}; -// Import items from the SDK. The prelude contains common traits and macros. -use stylus_sdk::alloy_primitives::U256; -use stylus_sdk::prelude::*; -use stylus_sdk::{block, console}; - -// Define persistent storage using the Solidity ABI. -// `VendingMachine` will be the entrypoint for the contract. -sol_storage! { #[entrypoint] -pub struct VendingMachine { -// Mapping from user addresses to their cupcake balances. -mapping(address => uint256) cupcake_balances; -// Mapping from user addresses to the last time they received a cupcake. -mapping(address => uint256) cupcake_distribution_times; -} - -#[cfg(test)] -mod tests { - use super::*; - mod vending_machine_test; -} -} - -// Declare that `VendingMachine` is a contract with the following external methods. #[public] -impl VendingMachine { -// Give a cupcake to the specified user if they are eligible (i.e., if at least 5 seconds have passed since their last cupcake). -pub fn give_cupcake_to(&mut self, user_address: Address) -> bool { -// Get the last distribution time for the user. -let last_distribution = self.cupcake_distribution_times.get(user_address); -// Calculate the earliest next time the user can receive a cupcake. -let five_seconds_from_last_distribution = last_distribution + U256::from(5); - - // Get the current block timestamp. - let current_time = block::timestamp(); - // Check if the user can receive a cupcake. - let user_can_receive_cupcake = - five_seconds_from_last_distribution <= Uint::<256, 4>::from(current_time); - - if user_can_receive_cupcake { - // Increment the user's cupcake balance. - let mut balance_accessor = self.cupcake_balances.setter(user_address); - let balance = balance_accessor.get() + U256::from(1); - balance_accessor.set(balance); - - // Update the distribution time to the current time. - let mut time_accessor = self.cupcake_distribution_times.setter(user_address); - let new_distribution_time = block::timestamp(); - time_accessor.set(Uint::<256, 4>::from(new_distribution_time)); - return true; - } else { - // User must wait before receiving another cupcake. - console!( - "HTTP 429: Too Many Cupcakes (you must wait at least 5 seconds between cupcakes)" - ); - return false; - } - } - - // Get the cupcake balance for the specified user. - pub fn get_cupcake_balance_for(&self, user_address: Address) -> Uint<256, 4> { - // Return the user's cupcake balance from storage. - return self.cupcake_balances.get(user_address); - } - -} diff --git a/arbitrum-docs/stylus/how-tos/vending_machine_test.rs b/arbitrum-docs/stylus/how-tos/vending_machine_test.rs deleted file mode 100644 index df32e7505..000000000 --- a/arbitrum-docs/stylus/how-tos/vending_machine_test.rs +++ /dev/null @@ -1,69 +0,0 @@ -use super::*; -use stylus_sdk::stylus_test::*; -use alloy_primitives::{address, Address, U256}; - -#[test] -fn test_initial_balance() { - // Set up test VM and contract - let vm = TestVM::default(); - let contract = VendingMachine::from(&vm); - - // Check initial balance is zero for a random address - let test_address = address!("dCE82b5f92C98F27F116F70491a487EFFDb6a2a9"); - assert_eq!(contract.get_cupcake_balance_for(test_address), U256::ZERO); -} - -#[test] -fn test_cupcake_distribution() { - // Set up test VM with custom configuration - let vm = TestVMBuilder::new() - .block_timestamp(100) - .build(); - let mut contract = VendingMachine::from(&vm); - - let test_address = address!("dCE82b5f92C98F27F116F70491a487EFFDb6a2a9"); - - // First distribution should succeed - assert!(contract.give_cupcake_to(test_address)); - assert_eq!(contract.get_cupcake_balance_for(test_address), U256::from(1)); - - // Immediate second distribution should fail (less than 5 seconds) - assert!(!contract.give_cupcake_to(test_address)); - assert_eq!(contract.get_cupcake_balance_for(test_address), U256::from(1)); - - // Advance time by 6 seconds - vm.set_block_timestamp(106); - - // Distribution should now succeed - assert!(contract.give_cupcake_to(test_address)); - assert_eq!(contract.get_cupcake_balance_for(test_address), U256::from(2)); -} - -#[test] -fn test_multiple_users() { - let vm = TestVMBuilder::new() - .block_timestamp(100) - .build(); - let mut contract = VendingMachine::from(&vm); - - let user1 = address!("dCE82b5f92C98F27F116F70491a487EFFDb6a2a9"); - let user2 = address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"); - - // Give cupcakes to both users - assert!(contract.give_cupcake_to(user1)); - assert!(contract.give_cupcake_to(user2)); - - // Verify balances - assert_eq!(contract.get_cupcake_balance_for(user1), U256::from(1)); - assert_eq!(contract.get_cupcake_balance_for(user2), U256::from(1)); - - // Advance time - vm.set_block_timestamp(106); - - // Give another cupcake to user1 only - assert!(contract.give_cupcake_to(user1)); - - // Verify updated balances - assert_eq!(contract.get_cupcake_balance_for(user1), U256::from(2)); - assert_eq!(contract.get_cupcake_balance_for(user2), U256::from(1)); -} From 8d8ff226a4b36ec4c401dd87e0edb9a809005719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 25 Mar 2025 11:39:51 +0100 Subject: [PATCH 13/21] refactor: remove bad test file --- .../vending_machine_test.rs | 79 ------------------- 1 file changed, 79 deletions(-) delete mode 100644 arbitrum-docs/stylus/how-tos/vending_machine_contract/vending_machine_test.rs diff --git a/arbitrum-docs/stylus/how-tos/vending_machine_contract/vending_machine_test.rs b/arbitrum-docs/stylus/how-tos/vending_machine_contract/vending_machine_test.rs deleted file mode 100644 index b338989f4..000000000 --- a/arbitrum-docs/stylus/how-tos/vending_machine_contract/vending_machine_test.rs +++ /dev/null @@ -1,79 +0,0 @@ -use super::*; -use stylus_test::*; -use stylus_sdk::alloy_primitives::Address; - -#[test] -fn test_give_cupcake() { - // Setup test environment - let vm = TestVM::default(); - let mut contract = VendingMachine::from(&vm); - - // Create a test user address - let user = Address::repeat_byte(0x42); - - // Initial balance should be zero - let initial_balance = contract.get_cupcake_balance_for(user); - assert_eq!(initial_balance, U256::ZERO); - - // First cupcake should be given successfully - let result = contract.give_cupcake_to(user); - assert!(result); - - // Balance should be incremented to 1 - let balance_after_first = contract.get_cupcake_balance_for(user); - assert_eq!(balance_after_first, U256::from(1)); - - // Trying to get another cupcake immediately should fail (5 second cooldown) - let result = contract.give_cupcake_to(user); - assert!(!result); - - // Balance should still be 1 - let balance_after_second_attempt = contract.get_cupcake_balance_for(user); - assert_eq!(balance_after_second_attempt, U256::from(1)); - - // Advance time by 6 seconds - vm.advance_time(6); - - // Now we should be able to get another cupcake - let result = contract.give_cupcake_to(user); - assert!(result); - - // Balance should be incremented to 2 - let final_balance = contract.get_cupcake_balance_for(user); - assert_eq!(final_balance, U256::from(2)); -} - -#[test] -fn test_multiple_users() { - // Setup test environment - let vm = TestVM::default(); - let mut contract = VendingMachine::from(&vm); - - // Create two test user addresses - let user1 = Address::repeat_byte(0x11); - let user2 = Address::repeat_byte(0x22); - - // Give cupcake to first user - let result = contract.give_cupcake_to(user1); - assert!(result); - - // First user should have 1 cupcake - let user1_balance = contract.get_cupcake_balance_for(user1); - assert_eq!(user1_balance, U256::from(1)); - - // Second user should have 0 cupcakes - let user2_balance = contract.get_cupcake_balance_for(user2); - assert_eq!(user2_balance, U256::ZERO); - - // Give cupcake to second user - let result = contract.give_cupcake_to(user2); - assert!(result); - - // Second user should now have 1 cupcake - let user2_balance = contract.get_cupcake_balance_for(user2); - assert_eq!(user2_balance, U256::from(1)); - - // First user should still have 1 cupcake - let user1_balance = contract.get_cupcake_balance_for(user1); - assert_eq!(user1_balance, U256::from(1)); -} From 53b64446298b14744cefab7693909d50ce10cd2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 25 Mar 2025 11:40:12 +0100 Subject: [PATCH 14/21] feat: add bespoke vending machine test file --- .../stylus/how-tos/testing-contracts.mdx | 137 +++++++----------- 1 file changed, 51 insertions(+), 86 deletions(-) diff --git a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx index d4e26f3c5..da3c4184e 100644 --- a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx +++ b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx @@ -166,82 +166,60 @@ To write tests for your contract, follow these steps: Here's a complete example of how to test our NFT contract: -```rust -#[cfg(test)] -mod test { - use super::*; - use stylus_sdk::testing::*; - use alloy_primitives::{Address, U256}; - - #[test] - fn test_mint() { - // Create a test VM environment - let vm = TestVM::default(); - - // Set a specific sender address for the test - let sender = Address::from([0x1; 20]); - vm.set_sender(sender); - - // Initialize the contract with the test VM - let mut contract = StylusTestNFT::from(&vm); - - // Test initial state - assert_eq!(contract.total_supply().unwrap(), U256::ZERO); - - // Test minting - contract.mint().unwrap(); - - // Verify the result - assert_eq!(contract.total_supply().unwrap(), U256::from(1)); - assert_eq!(contract.erc721.balance_of(sender).unwrap(), U256::from(1)); - } - - #[test] - fn test_mint_to() { - // Create a test VM environment - let vm = TestVM::default(); - - // Initialize the contract with the test VM - let mut contract = StylusTestNFT::from(&vm); - - // Set up recipient address - let recipient = Address::from([0x2; 20]); - - // Test minting to a specific address - contract.mint_to(recipient).unwrap(); - - // Verify the result - assert_eq!(contract.total_supply().unwrap(), U256::from(1)); - assert_eq!(contract.erc721.balance_of(recipient).unwrap(), U256::from(1)); - } - - #[test] - fn test_burn() { - // Create a test VM environment - let vm = TestVM::default(); - - // Set a specific sender address for the test - let sender = Address::from([0x1; 20]); - vm.set_sender(sender); +`use stylus_sdk::{alloy_primitives::Address, prelude::*}; +use stylus_test::*; - // Initialize the contract with the test VM - let mut contract = StylusTestNFT::from(&vm); +use stylus_cupcake_example::*; - // Mint a token first - contract.mint().unwrap(); - assert_eq!(contract.erc721.balance_of(sender).unwrap(), U256::from(1)); - - // Burn the token - contract.burn(U256::ZERO).unwrap(); - - // Verify the token was burned - // Note: total_supply doesn't decrease after burning - assert_eq!(contract.total_supply().unwrap(), U256::from(1)); - assert_eq!(contract.erc721.balance_of(sender).unwrap(), U256::ZERO); - } +#[test] +fn test_give_cupcake() { + // Setup test environment + let vm = TestVM::default(); + let mut contract = VendingMachine::from(&vm); + + // Create a test user address + let user = Address::from([0x1; 20]); + + // First cupcake should succeed + assert!(contract.give_cupcake_to(user)); + + // Balance should be 1 + assert_eq!(contract.get_cupcake_balance_for(user), 1.into()); + + // Immediate second attempt should fail (needs 5 second wait) + assert!(!contract.give_cupcake_to(user)); + + // Balance should still be 1 + assert_eq!(contract.get_cupcake_balance_for(user), 1.into()); + + // Advance block timestamp by 6 seconds + vm.set_timestamp(6); + + // Should be able to get another cupcake now + assert!(contract.give_cupcake_to(user)); + + // Balance should be 2 + assert_eq!(contract.get_cupcake_balance_for(user), 2.into()); } -```` +#[test] +fn test_get_cupcake_balance() { + let vm = TestVM::default(); + let mut contract = VendingMachine::from(&vm); + + // Create a test user address + let user = Address::from([0x2; 20]); + + // Initial balance should be 0 + assert_eq!(contract.get_cupcake_balance_for(user), 0.into()); + + // Give a cupcake + assert!(contract.give_cupcake_to(user)); + + // Balance should be 1 + assert_eq!(contract.get_cupcake_balance_for(user), 1.into()); +} +```rust ### Advanced Testing Features @@ -250,21 +228,8 @@ mod test { You can customize your test environment using `TestVMBuilder` for more complex scenarios: - + ```rust -#[test] -fn test_with_custom_setup() { - let vm = TestVMBuilder::new() - .with_sender(Address::from([0x1; 20])) - .with_value(U256::from(100)) - .with_contract_address(Address::from([0x3; 20])) - .build(); - - let contract = StylusTestNFT::from(&vm); - // Test logic here - -} - ```` #### Testing Contract Interactions From 3b1b0271e6034b4dcf1ccbd2da8050b837b6812c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 25 Mar 2025 11:47:56 +0100 Subject: [PATCH 15/21] fix: reformat --- .../stylus/how-tos/testing-contracts.mdx | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx index da3c4184e..7d22556df 100644 --- a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx +++ b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx @@ -64,11 +64,12 @@ cd nitro-devnode ## Example Smart Contract -Let's look at the implementation of a decentralized cupcake vending machine using the Stylus SDK. +Let's look at the implementation of a decentralized cupcake vending machine using the Stylus SDK. This example demonstrates the core functionality we'll test. We're going to test a Rust Smart Contract defining a cupcake vending machine. This vending machine will follow two rules: + 1. The vending machine will distribute a cupcake to anyone who hasn't recently received one. 2. The vending machine's rules can't be changed by anyone. @@ -166,7 +167,8 @@ To write tests for your contract, follow these steps: Here's a complete example of how to test our NFT contract: -`use stylus_sdk::{alloy_primitives::Address, prelude::*}; +``` rust +use stylus_sdk::{alloy_primitives::Address, prelude::*}; use stylus_test::*; use stylus_cupcake_example::*; @@ -176,28 +178,28 @@ fn test_give_cupcake() { // Setup test environment let vm = TestVM::default(); let mut contract = VendingMachine::from(&vm); - + // Create a test user address let user = Address::from([0x1; 20]); - + // First cupcake should succeed assert!(contract.give_cupcake_to(user)); - + // Balance should be 1 assert_eq!(contract.get_cupcake_balance_for(user), 1.into()); - + // Immediate second attempt should fail (needs 5 second wait) assert!(!contract.give_cupcake_to(user)); - + // Balance should still be 1 assert_eq!(contract.get_cupcake_balance_for(user), 1.into()); - + // Advance block timestamp by 6 seconds vm.set_timestamp(6); - + // Should be able to get another cupcake now assert!(contract.give_cupcake_to(user)); - + // Balance should be 2 assert_eq!(contract.get_cupcake_balance_for(user), 2.into()); } @@ -206,31 +208,28 @@ fn test_give_cupcake() { fn test_get_cupcake_balance() { let vm = TestVM::default(); let mut contract = VendingMachine::from(&vm); - + // Create a test user address let user = Address::from([0x2; 20]); - + // Initial balance should be 0 assert_eq!(contract.get_cupcake_balance_for(user), 0.into()); - + // Give a cupcake assert!(contract.give_cupcake_to(user)); - + // Balance should be 1 assert_eq!(contract.get_cupcake_balance_for(user), 1.into()); } ```rust -### Advanced Testing Features + -#### Customizing the Test Environment + -You can customize your test environment using `TestVMBuilder` for more complex scenarios: + - -```rust -```` #### Testing Contract Interactions @@ -257,7 +256,7 @@ fn test_external_contract_interaction() { // Test logic that involves calling the external contract // ... } -```` +``` @@ -323,3 +322,4 @@ cargo test test_mint Testing is an essential part of smart contract development to ensure security, correctness, and reliability. The Stylus SDK provides powerful testing tools that allow you to thoroughly test your contracts before deployment. The ability to test Rust contracts directly, without requiring a blockchain environment, makes the development cycle faster and more efficient. +```` From f06ddecdaf36ab39686c5989a5c8f842e901ddaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 1 Apr 2025 06:49:00 +0200 Subject: [PATCH 16/21] fix: tmp rename old article --- .../how-tos/{testing-contracts.mdx => testing-contracts-OG.mdx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename arbitrum-docs/stylus/how-tos/{testing-contracts.mdx => testing-contracts-OG.mdx} (100%) diff --git a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx b/arbitrum-docs/stylus/how-tos/testing-contracts-OG.mdx similarity index 100% rename from arbitrum-docs/stylus/how-tos/testing-contracts.mdx rename to arbitrum-docs/stylus/how-tos/testing-contracts-OG.mdx From 7f01c1fbf6f7466cc2e7a9c8a4df3fa69231a691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 1 Apr 2025 06:49:28 +0200 Subject: [PATCH 17/21] fix: refactor article --- .../stylus/how-tos/testing-contracts.mdx | 491 ++++++++++++++++++ 1 file changed, 491 insertions(+) create mode 100644 arbitrum-docs/stylus/how-tos/testing-contracts.mdx diff --git a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx new file mode 100644 index 000000000..2e8723af9 --- /dev/null +++ b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx @@ -0,0 +1,491 @@ +--- +id: 'testing-contracts' +title: 'Testing Smart Contracts with Stylus' +description: 'A comprehensive guide to writing and running tests for Stylus smart contracts.' +sme: anegg0 +target_audience: 'Developers writing smart contracts using Stylus.' +sidebar_position: 3 +--- + +import CustomDetails from '@site/src/components/CustomDetails'; +import { VanillaAdmonition } from '@site/src/components/VanillaAdmonition/'; + +## Introduction + +The Stylus SDK provides a robust testing framework that allows developers to write and run tests for their contracts directly in Rust without deploying to a blockchain. This guide will walk you through the process of writing and running tests for Stylus contracts using the built-in testing framework. + +The Stylus testing framework allows you to: + +- Simulate a complete Ethereum environment for your tests +- Test contract storage operations and state transitions +- Mock transaction context and block information +- Test contract-to-contract interactions with mocked calls +- Verify contract logic without deployment costs or delays +- Simulate various user scenarios and edge cases + +### Prerequisites + +Before you begin, make sure you have: + +- Basic familiarity with Rust and smart contract development +- Understanding of unit testing concepts + +
+Rust toolchain + +Follow the instructions on [Rust Lang's installation page](https://www.rust-lang.org/tools/install) to install a complete Rust toolchain (v1.81 or newer) on your system. After installation, ensure you can access the programs `rustup`, `rustc`, and `cargo` from your preferred terminal application. + +
+ +## The Stylus Testing Framework + +The Stylus SDK includes `stylus_test`, a module that provides all the tools you need to test your contracts. This module includes: + +- **TestVM**: A mock implementation of the Stylus VM that can simulate all host functions +- **TestVMBuilder**: A builder pattern to conveniently configure the test VM +- Built-in utilities for mocking calls, storage, and other EVM operations + +### Key Components + +Here are the key components you'll use when testing your Stylus contracts: + +- `TestVM`: The core component that simulates the Stylus execution environment +- Storage accessors: For testing contract state changes +- Call mocking: For simulating interactions with other contracts +- Block context: For testing time-dependent logic + +## Example Smart Contract: Cupcake Vending Machine + +Let's look at a Rust-based cupcake vending machine smart contract. This contract follows two simple rules: + +1. The vending machine will distribute a cupcake to anyone who hasn't received one in the last 5 seconds +2. The vending machine tracks each user's cupcake balance + + +```rust +//! +//! Stylus Cupcake Example +//! + +#![cfg_attr(not(feature = "export-abi"), no_main)] +extern crate alloc; + +use alloy_primitives::{Address, Uint}; +use stylus_sdk::alloy_primitives::U256; +use stylus_sdk::prelude::\*; +use stylus_sdk::{block, console}; + +sol_storage! { #[entrypoint] +pub struct VendingMachine { +// Mapping from user addresses to their cupcake balances. +mapping(address => uint256) cupcake_balances; +// Mapping from user addresses to the last time they received a cupcake. +mapping(address => uint256) cupcake_distribution_times; +} +} + +#[public] +impl VendingMachine { +// Give a cupcake to the specified user if they are eligible +// (i.e., if at least 5 seconds have passed since their last cupcake). +pub fn give_cupcake_to(&mut self, user_address: Address) -> bool { +// Get the last distribution time for the user. +let last_distribution = self.cupcake_distribution_times.get(user_address); +// Calculate the earliest next time the user can receive a cupcake. +let five_seconds_from_last_distribution = last_distribution + U256::from(5); + + // Get the current block timestamp. + let current_time = block::timestamp(); + // Check if the user can receive a cupcake. + let user_can_receive_cupcake = + five_seconds_from_last_distribution <= Uint::<256, 4>::from(current_time); + + if user_can_receive_cupcake { + // Increment the user's cupcake balance. + let mut balance_accessor = self.cupcake_balances.setter(user_address); + let balance = balance_accessor.get() + U256::from(1); + balance_accessor.set(balance); + + // Update the distribution time to the current time. + let mut time_accessor = self.cupcake_distribution_times.setter(user_address); + let new_distribution_time = block::timestamp(); + time_accessor.set(Uint::<256, 4>::from(new_distribution_time)); + return true; + } else { + // User must wait before receiving another cupcake. + console!( + "HTTP 429: Too Many Cupcakes (you must wait at least 5 seconds between cupcakes)" + ); + return false; + } + } + + // Get the cupcake balance for the specified user. + pub fn get_cupcake_balance_for(&self, user_address: Address) -> Uint<256, 4> { + // Return the user's cupcake balance from storage. + return self.cupcake_balances.get(user_address); + } + +} + +```` + + +## Writing Tests for the Vending Machine + +Now, let's write comprehensive tests for our vending machine contract using the Stylus testing framework. We'll create tests that verify: + +1. Users can get an initial cupcake +2. Users must wait 5 seconds between cupcakes +3. Cupcake balances are tracked correctly +4. The contract state updates properly + +### Basic Test Structure + +Create a test file using standard Rust test patterns. Here's the basic structure: + +```rust +// Import necessary dependencies +use stylus_sdk::{alloy_primitives::Address, prelude::*}; +use stylus_test::*; + +// Import your contract +use stylus_cupcake_example::*; + +#[test] +fn test_give_cupcake() { + // Set up test environment + let vm = TestVM::default(); + let mut contract = VendingMachine::from(&vm); + + // Test logic goes here... +} +```` + +### Using the TestVM + +The `TestVM` simulates the execution environment for your Stylus contract. You can control aspects like: + +- Block timestamp and number +- Account balances +- Transaction value and sender +- Storage state + +For the vending machine contract, we need to control the block timestamp to test the 5-second rule: + +```rust +#[test] +fn test_give_cupcake() { + // Setup test environment + let vm = TestVM::default(); + let mut contract = VendingMachine::from(&vm); + + // Create a test user address + let user = Address::from([0x1; 20]); + + // First cupcake should succeed + assert!(contract.give_cupcake_to(user)); + + // Balance should be 1 + assert_eq!(contract.get_cupcake_balance_for(user), 1.into()); + + // Immediate second attempt should fail (needs 5 second wait) + assert!(!contract.give_cupcake_to(user)); + + // Balance should still be 1 + assert_eq!(contract.get_cupcake_balance_for(user), 1.into()); + + // Advance block timestamp by 6 seconds + vm.set_block_timestamp(6); + + // Should be able to get another cupcake now + assert!(contract.give_cupcake_to(user)); + + // Balance should be 2 + assert_eq!(contract.get_cupcake_balance_for(user), 2.into()); +} +``` + +### Comprehensive Test Suite + +Let's create a more comprehensive test suite that covers all aspects of our contract: + + +```rust +use stylus_sdk::{alloy_primitives::Address, prelude::*}; +use stylus_test::*; + +use stylus_cupcake_example::\*; + +#[test] +fn test_initial_balance() { +let vm = TestVM::default(); +let contract = VendingMachine::from(&vm); + + let user = Address::from([0x1; 20]); + + // Initial balance should be zero + assert_eq!(contract.get_cupcake_balance_for(user), 0.into()); + +} + +#[test] +fn test_give_first_cupcake() { +let vm = TestVM::default(); +let mut contract = VendingMachine::from(&vm); + + let user = Address::from([0x1; 20]); + + // First cupcake should succeed + assert!(contract.give_cupcake_to(user)); + + // Balance should be 1 + assert_eq!(contract.get_cupcake_balance_for(user), 1.into()); + +} + +#[test] +fn test_cooldown_period() { +let vm = TestVM::default(); +let mut contract = VendingMachine::from(&vm); + + let user = Address::from([0x1; 20]); + + // First cupcake should succeed + assert!(contract.give_cupcake_to(user)); + + // Immediate second attempt should fail (needs 5 second wait) + assert!(!contract.give_cupcake_to(user)); + + // Try after 4 seconds (still too early) + vm.set_block_timestamp(4); + assert!(!contract.give_cupcake_to(user)); + + // Try after 5 seconds (exactly at threshold, should succeed) + vm.set_block_timestamp(5); + assert!(contract.give_cupcake_to(user)); + + // Balance should be 2 + assert_eq!(contract.get_cupcake_balance_for(user), 2.into()); + +} + +#[test] +fn test_multiple_users() { +let vm = TestVM::default(); +let mut contract = VendingMachine::from(&vm); + + let user1 = Address::from([0x1; 20]); + let user2 = Address::from([0x2; 20]); + + // User1 gets a cupcake + assert!(contract.give_cupcake_to(user1)); + + // User2 also gets a cupcake + assert!(contract.give_cupcake_to(user2)); + + // Both should have a balance of 1 + assert_eq!(contract.get_cupcake_balance_for(user1), 1.into()); + assert_eq!(contract.get_cupcake_balance_for(user2), 1.into()); + + // User1 has to wait, but User2 can get another if time passes + vm.set_block_timestamp(6); + assert!(contract.give_cupcake_to(user1)); + assert!(contract.give_cupcake_to(user2)); + + // Balances should update accordingly + assert_eq!(contract.get_cupcake_balance_for(user1), 2.into()); + assert_eq!(contract.get_cupcake_balance_for(user2), 2.into()); + +} + +```` + + +## Advanced Testing Techniques + +### Using TestVMBuilder + +For more complex test setups, you can use the `TestVMBuilder` to configure the test environment: + +```rust +#[test] +fn test_with_custom_environment() { + // Create a custom VM with specific parameters + let vm = TestVMBuilder::new() + .sender(Address::from([0x3; 20])) // Set the transaction sender + .contract_address(Address::from([0x4; 20])) // Set contract address + .block_timestamp(100) // Set the block timestamp + .build(); + + let mut contract = VendingMachine::from(&vm); + + // Your test logic here... +} +```` + +### Testing Storage State + +You can directly inspect and manipulate the contract's storage: + +```rust +#[test] +fn test_storage_state() { + let vm = TestVM::default(); + let mut contract = VendingMachine::from(&vm); + + let user = Address::from([0x1; 20]); + contract.give_cupcake_to(user); + + // Get the key for the user's balance in storage + let key = U256::from(keccak256( + &[user.as_bytes(), "cupcake_balances".as_bytes()].concat() + )); + + // Check the raw storage value + let raw_value = vm.get_storage(key); + assert_eq!(raw_value, U256::from(1).into()); + + // We can also manipulate storage directly + vm.set_storage(key, U256::from(10).into()); + + // Contract should reflect the modified storage + assert_eq!(contract.get_cupcake_balance_for(user), 10.into()); +} +``` + +### Mocking Block Data + +For testing time-dependent logic, you can control the block data: + +```rust +#[test] +fn test_time_dependency() { + let vm = TestVM::default(); + let mut contract = VendingMachine::from(&vm); + + let user = Address::from([0x1; 20]); + + // Set block timestamp to a known value + vm.set_block_timestamp(1000); + + // Get the first cupcake + assert!(contract.give_cupcake_to(user)); + + // Set block timestamp to exactly 5 seconds later + vm.set_block_timestamp(1005); + + // User can now get another cupcake + assert!(contract.give_cupcake_to(user)); + + // Check the balance is 2 + assert_eq!(contract.get_cupcake_balance_for(user), 2.into()); +} +``` + +## Testing Contract Interactions + +If your contract interacts with other contracts, you can mock these interactions: + +```rust +sol_interface! { + interface ICupcakeInventory { + function checkInventory() external view returns (uint256); + } +} + +#[test] +fn test_external_contract_interaction() { + let vm = TestVM::default(); + + // Address of the inventory contract + let inventory_contract = Address::from([0x5; 20]); + + // Create the function selector for checkInventory() + let call_data = &function_selector!("checkInventory"); + + // Mock a response with 100 cupcakes available + let expected_response = U256::from(100).abi_encode(); + + // Set up the mock call + vm.mock_call(inventory_contract, call_data.to_vec(), Ok(expected_response)); + + // Now if your contract calls the inventory contract, it will get the mocked response + // ... +} +``` + +## Running Tests + +To run your tests, you can use the standard Rust test command: + +```shell +cargo test +``` + +Or with the `cargo-stylus` CLI tool: + +```shell +cargo stylus test +``` + +To run a specific test: + +```shell +cargo test test_give_cupcake +``` + +To see test output: + +```shell +cargo test -- --nocapture +``` + +## Testing Best Practices + +1. **Test Isolation** + + - Create a new `TestVM` instance for each test + - Avoid relying on state from previous tests + +2. **Comprehensive Coverage** + + - Test both success and error conditions + - Test edge cases and boundary conditions + - Verify all public functions and important state transitions + +3. **Clear Assertions** + + - Use descriptive error messages in assertions + - Make assertions that verify the actual behavior you care about + +4. **Realistic Scenarios** + + - Test real-world usage patterns + - Include tests for authorization and access control + +5. **Gas and Resource Efficiency** + - For complex contracts, consider testing gas usage patterns + - Look for storage optimization opportunities + +## Migrating from Global Accessors to VM Accessors + +As of Stylus SDK 0.8.0, there's a shift away from global host function invocations to using the `.vm()` method. This is a safer approach that makes testing easier. For example: + +```rust +// Old style (deprecated) +let timestamp = block::timestamp(); + +// New style (preferred) +let timestamp = self.vm().block_timestamp(); +``` + +To make your contracts more testable, make sure they access host methods through the `HostAccess` trait with the `.vm()` method. + +## Conclusion + +Testing is an essential part of smart contract development to ensure security, correctness, and reliability. The Stylus SDK provides a powerful testing framework that allows you to thoroughly verify your contracts before deployment. + +By using the techniques outlined in this guide, you can create comprehensive test suites that give you confidence in your contract's behavior under various conditions, without the costs or delays associated with on-chain testing. + +The ability to test Rust contracts natively, without requiring a blockchain environment, makes the development cycle faster and more efficient while maintaining high standards of security and correctness. From 81a29efe981c694b7e95792fd7d36732703ed60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 1 Apr 2025 07:07:58 +0200 Subject: [PATCH 18/21] fix: tmp mod --- arbitrum-docs/stylus/how-tos/testing-contracts-OG.mdx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/arbitrum-docs/stylus/how-tos/testing-contracts-OG.mdx b/arbitrum-docs/stylus/how-tos/testing-contracts-OG.mdx index 7d22556df..e8470fe8f 100644 --- a/arbitrum-docs/stylus/how-tos/testing-contracts-OG.mdx +++ b/arbitrum-docs/stylus/how-tos/testing-contracts-OG.mdx @@ -1,12 +1,3 @@ ---- -id: 'testing-contracts' -title: 'Testing contracts with Stylus' -description: 'A hands-on guide to testing with Stylus.' -sme: anegg0 -target_audience: 'Developers writing smart contracts using Stylus.' -sidebar_position: 3 ---- - import CustomDetails from '@site/src/components/CustomDetails'; import { VanillaAdmonition } from '@site/src/components/VanillaAdmonition/'; From 8a1845f0e618e2a035031293e21df799854fdb14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Tue, 1 Apr 2025 07:37:43 +0200 Subject: [PATCH 19/21] fix: import TestVM --- arbitrum-docs/stylus/how-tos/testing-contracts.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx index 2e8723af9..82e9a4e2c 100644 --- a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx +++ b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx @@ -155,8 +155,8 @@ use stylus_cupcake_example::*; #[test] fn test_give_cupcake() { // Set up test environment - let vm = TestVM::default(); - let mut contract = VendingMachine::from(&vm); +// let vm = TestVM::default(); + // let mut contract = VendingMachine::from(&vm); // Test logic goes here... } @@ -177,6 +177,8 @@ For the vending machine contract, we need to control the block timestamp to test #[test] fn test_give_cupcake() { // Setup test environment + use stylus_test::{TestVM, TestVMBuilder}; + let vm = TestVM::default(); let mut contract = VendingMachine::from(&vm); From 80cd931b83a6de8580ac006eed1302cecdfa2334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 17 Apr 2025 09:09:16 -0700 Subject: [PATCH 20/21] refactor: fix deprecated block::timestamp() call --- .../stylus/how-tos/testing-contracts.mdx | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx index 82e9a4e2c..5eaeabb10 100644 --- a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx +++ b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx @@ -63,39 +63,40 @@ Let's look at a Rust-based cupcake vending machine smart contract. This contract ```rust -//! -//! Stylus Cupcake Example -//! - +// Allow `cargo stylus export-abi` to generate a main function if the "export-abi" feature is enabled. #![cfg_attr(not(feature = "export-abi"), no_main)] extern crate alloc; use alloy_primitives::{Address, Uint}; +// Import items from the SDK. The prelude contains common traits and macros. use stylus_sdk::alloy_primitives::U256; -use stylus_sdk::prelude::\*; -use stylus_sdk::{block, console}; - -sol_storage! { #[entrypoint] -pub struct VendingMachine { -// Mapping from user addresses to their cupcake balances. -mapping(address => uint256) cupcake_balances; -// Mapping from user addresses to the last time they received a cupcake. -mapping(address => uint256) cupcake_distribution_times; -} +use stylus_sdk::console; +use stylus_sdk::prelude::*; + +// Define persistent storage using the Solidity ABI. +// `VendingMachine` will be the entrypoint for the contract. +sol_storage! { + #[entrypoint] + pub struct VendingMachine { + // Mapping from user addresses to their cupcake balances. + mapping(address => uint256) cupcake_balances; + // Mapping from user addresses to the last time they received a cupcake. + mapping(address => uint256) cupcake_distribution_times; + } } +// Declare that `VendingMachine` is a contract with the following external methods. #[public] impl VendingMachine { -// Give a cupcake to the specified user if they are eligible -// (i.e., if at least 5 seconds have passed since their last cupcake). -pub fn give_cupcake_to(&mut self, user_address: Address) -> bool { -// Get the last distribution time for the user. -let last_distribution = self.cupcake_distribution_times.get(user_address); -// Calculate the earliest next time the user can receive a cupcake. -let five_seconds_from_last_distribution = last_distribution + U256::from(5); - - // Get the current block timestamp. - let current_time = block::timestamp(); + // Give a cupcake to the specified user if they are eligible (i.e., if at least 5 seconds have passed since their last cupcake). + pub fn give_cupcake_to(&mut self, user_address: Address) -> Result> { + // Get the last distribution time for the user. + let last_distribution = self.cupcake_distribution_times.get(user_address); + // Calculate the earliest next time the user can receive a cupcake. + let five_seconds_from_last_distribution = last_distribution + U256::from(5); + + // Get the current block timestamp using the VM pattern + let current_time = self.vm().block_timestamp(); // Check if the user can receive a cupcake. let user_can_receive_cupcake = five_seconds_from_last_distribution <= Uint::<256, 4>::from(current_time); @@ -106,29 +107,30 @@ let five_seconds_from_last_distribution = last_distribution + U256::from(5); let balance = balance_accessor.get() + U256::from(1); balance_accessor.set(balance); + // Get current timestamp using the VM pattern BEFORE creating the mutable borrow + let new_distribution_time = self.vm().block_timestamp(); + // Update the distribution time to the current time. let mut time_accessor = self.cupcake_distribution_times.setter(user_address); - let new_distribution_time = block::timestamp(); time_accessor.set(Uint::<256, 4>::from(new_distribution_time)); - return true; + return Ok(true); } else { // User must wait before receiving another cupcake. console!( "HTTP 429: Too Many Cupcakes (you must wait at least 5 seconds between cupcakes)" ); - return false; + return Ok(false); } } // Get the cupcake balance for the specified user. - pub fn get_cupcake_balance_for(&self, user_address: Address) -> Uint<256, 4> { + pub fn get_cupcake_balance_for(&self, user_address: Address) -> Result, Vec> { // Return the user's cupcake balance from storage. - return self.cupcake_balances.get(user_address); + Ok(self.cupcake_balances.get(user_address)) } - } -```` +``` ## Writing Tests for the Vending Machine From 8f8be58cab87e57c018f25c7ce642b4982ab6f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Blanchemain?= Date: Thu, 17 Apr 2025 09:44:42 -0700 Subject: [PATCH 21/21] refactor: replaced former test code with updated new one --- .../stylus/how-tos/testing-contracts.mdx | 150 +++++++----------- 1 file changed, 59 insertions(+), 91 deletions(-) diff --git a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx index 5eaeabb10..ed9cc88e7 100644 --- a/arbitrum-docs/stylus/how-tos/testing-contracts.mdx +++ b/arbitrum-docs/stylus/how-tos/testing-contracts.mdx @@ -71,29 +71,27 @@ use alloy_primitives::{Address, Uint}; // Import items from the SDK. The prelude contains common traits and macros. use stylus_sdk::alloy_primitives::U256; use stylus_sdk::console; -use stylus_sdk::prelude::*; +use stylus_sdk::prelude::\*; // Define persistent storage using the Solidity ABI. // `VendingMachine` will be the entrypoint for the contract. -sol_storage! { - #[entrypoint] - pub struct VendingMachine { - // Mapping from user addresses to their cupcake balances. - mapping(address => uint256) cupcake_balances; - // Mapping from user addresses to the last time they received a cupcake. - mapping(address => uint256) cupcake_distribution_times; - } +sol_storage! { #[entrypoint] +pub struct VendingMachine { +// Mapping from user addresses to their cupcake balances. +mapping(address => uint256) cupcake_balances; +// Mapping from user addresses to the last time they received a cupcake. +mapping(address => uint256) cupcake_distribution_times; +} } -// Declare that `VendingMachine` is a contract with the following external methods. -#[public] +// Declare that `VendingMachine` is a contract with the following external methods. #[public] impl VendingMachine { - // Give a cupcake to the specified user if they are eligible (i.e., if at least 5 seconds have passed since their last cupcake). - pub fn give_cupcake_to(&mut self, user_address: Address) -> Result> { - // Get the last distribution time for the user. - let last_distribution = self.cupcake_distribution_times.get(user_address); - // Calculate the earliest next time the user can receive a cupcake. - let five_seconds_from_last_distribution = last_distribution + U256::from(5); +// Give a cupcake to the specified user if they are eligible (i.e., if at least 5 seconds have passed since their last cupcake). +pub fn give_cupcake_to(&mut self, user_address: Address) -> Result> { +// Get the last distribution time for the user. +let last_distribution = self.cupcake_distribution_times.get(user_address); +// Calculate the earliest next time the user can receive a cupcake. +let five_seconds_from_last_distribution = last_distribution + U256::from(5); // Get the current block timestamp using the VM pattern let current_time = self.vm().block_timestamp(); @@ -128,9 +126,10 @@ impl VendingMachine { // Return the user's cupcake balance from storage. Ok(self.cupcake_balances.get(user_address)) } + } -``` +````
## Writing Tests for the Vending Machine @@ -216,90 +215,59 @@ Let's create a more comprehensive test suite that covers all aspects of our cont ```rust -use stylus_sdk::{alloy_primitives::Address, prelude::*}; -use stylus_test::*; - -use stylus_cupcake_example::\*; - -#[test] -fn test_initial_balance() { -let vm = TestVM::default(); -let contract = VendingMachine::from(&vm); - - let user = Address::from([0x1; 20]); - - // Initial balance should be zero - assert_eq!(contract.get_cupcake_balance_for(user), 0.into()); - -} +use alloy_primitives::{address, U256}; +use stylus_sdk::testing::*; +use stylus_testing_example::VendingMachine; #[test] -fn test_give_first_cupcake() { -let vm = TestVM::default(); -let mut contract = VendingMachine::from(&vm); - - let user = Address::from([0x1; 20]); - - // First cupcake should succeed - assert!(contract.give_cupcake_to(user)); - - // Balance should be 1 - assert_eq!(contract.get_cupcake_balance_for(user), 1.into()); - -} - -#[test] -fn test_cooldown_period() { -let vm = TestVM::default(); -let mut contract = VendingMachine::from(&vm); - - let user = Address::from([0x1; 20]); - - // First cupcake should succeed - assert!(contract.give_cupcake_to(user)); - - // Immediate second attempt should fail (needs 5 second wait) - assert!(!contract.give_cupcake_to(user)); - - // Try after 4 seconds (still too early) - vm.set_block_timestamp(4); - assert!(!contract.give_cupcake_to(user)); +fn test_give_cupcake_to() { +// Create a new TestVM +// let vm = TestVM::default(); +let vm: TestVM = TestVMBuilder::new() +.sender(address!("dCE82b5f92C98F27F116F70491a487EFFDb6a2a9")) +.contract_address(address!("0x11b57fe348584f042e436c6bf7c3c3def171de49")) +.value(U256::from(1)) +.rpc_url("http://localhost:8547") +.build(); + + // Initialize the contract with the VM + let mut contract = VendingMachine::from(&vm); - // Try after 5 seconds (exactly at threshold, should succeed) - vm.set_block_timestamp(5); - assert!(contract.give_cupcake_to(user)); + // Test address + let user = address!("0xCDC41bff86a62716f050622325CC17a317f99404"); - // Balance should be 2 - assert_eq!(contract.get_cupcake_balance_for(user), 2.into()); + // Check initial balance is zero + assert_eq!(contract.get_cupcake_balance_for(user).unwrap(), U256::ZERO); -} + // Give a cupcake and verify it succeeds + assert!(contract.give_cupcake_to(user).unwrap()); -#[test] -fn test_multiple_users() { -let vm = TestVM::default(); -let mut contract = VendingMachine::from(&vm); - - let user1 = Address::from([0x1; 20]); - let user2 = Address::from([0x2; 20]); + // Check balance is now 1 + assert_eq!( + contract.get_cupcake_balance_for(user).unwrap(), + U256::from(1) + ); - // User1 gets a cupcake - assert!(contract.give_cupcake_to(user1)); + // Try to give another cupcake immediately - should fail due to time restriction + assert!(!contract.give_cupcake_to(user).unwrap()); - // User2 also gets a cupcake - assert!(contract.give_cupcake_to(user2)); + // Balance should still be 1 + assert_eq!( + contract.get_cupcake_balance_for(user).unwrap(), + U256::from(1) + ); - // Both should have a balance of 1 - assert_eq!(contract.get_cupcake_balance_for(user1), 1.into()); - assert_eq!(contract.get_cupcake_balance_for(user2), 1.into()); + // Advance block timestamp by 6 seconds + vm.set_block_timestamp(vm.block_timestamp() + 6); - // User1 has to wait, but User2 can get another if time passes - vm.set_block_timestamp(6); - assert!(contract.give_cupcake_to(user1)); - assert!(contract.give_cupcake_to(user2)); + // Now giving a cupcake should succeed + assert!(contract.give_cupcake_to(user).unwrap()); - // Balances should update accordingly - assert_eq!(contract.get_cupcake_balance_for(user1), 2.into()); - assert_eq!(contract.get_cupcake_balance_for(user2), 2.into()); + // Balance should now be 2 + assert_eq!( + contract.get_cupcake_balance_for(user).unwrap(), + U256::from(2) + ); }