Starkbiter Documentation
# ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
# ░░ ░░░ ░░░ ░░░ ░░░░ ░░░ ░░ ░░░ ░░ ░░ ░░ ░░░
# ▒ ▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒ ▒▒ ▒▒▒ ▒▒▒ ▒▒ ▒▒▒ ▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒ ▒▒
# ▓▓ ▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓ ▓▓ ▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓▓ ▓▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓ ▓▓▓
# ██████ ████ █████ ██ ███ ███ ██ ███ ███ █████ ██████ █████ ██████ ███ ██
# ██ █████ █████ ███ ██ ████ ██ ███ ██ ███ ████ █████ ██ ████ █
# ███████████████████████████████████████████████████████████████████████████████████████████
Starkbiter is a powerful framework for orchestrating event-based agentic simulations on top of Starknet.
The framework features a starknet-rs middleware built on top of starknet-devnet which allows you to interact with a sandboxed Starknet Sequencer instance as if it were a live Starknet node.
Why Starkbiter?
Starkbiter enables you to:
- 🔬 Test smart contracts against adversarial environments and dynamic parameters
- 🤖 Build autonomous agents that interact with Starknet contracts in realistic scenarios
- 📊 Model economic systems and DeFi protocols with sophisticated simulations
- 🔍 Detect anomalies and vulnerabilities before deployment
- ⚡ Rapid iteration with high-performance local testing
Overview
The Starkbiter workspace consists of five crates:
starkbiter: Binary crate providing a CLI for contract bindings generationstarkbiter-core: Core library withEnvironmentsandbox and middleware for Starknet interactionstarkbiter-engine: High-level abstractions for building simulations, agents, and behaviorsstarkbiter-macros: Proc macros to simplify developmentstarkbiter-bindings: Pre-generated bindings for common utility contracts
All contract bytecode runs directly using Starknet Devnet (powered by Blockifier, Starkware's sequencer implementation), ensuring your contracts are tested in an environment identical to production.
Key Features
🏗️ Stateful Simulation
Test contracts against dynamic, stateful environments that mirror real-world Starknet conditions.
🎯 Event-Based Architecture
Build reactive agents that respond to blockchain events, enabling complex behavioral modeling.
🔌 Full JSON-RPC Support
Complete Starknet node capabilities with additional methods for controlling block production and deployments.
🚀 High Performance
Local execution provides unmatched speed for rapid testing and iteration.
🧪 Forking Support
Fork from any Starknet network state to test against real mainnet or testnet conditions.
Use Cases
Smart Contract Testing
Move beyond static, stateless tests. Simulate contracts in adversarial environments with various parameters and agent behaviors.
DeFi Protocol Development
Model complex economic systems with multiple interacting agents, market conditions, and edge cases.
Simulation-Driven Development
Build tests that validate not just code correctness, but economic incentives and mechanism design.
Strategy Backtesting
Test trading strategies, liquidation bots, and other autonomous agents against thousands of scenarios.
Security Auditing
Perform domain-specific fuzzing and anomaly detection to uncover vulnerabilities before deployment.
Getting Started
Ready to start building with Starkbiter? Here's what you need:
- Installation - Set up Rust and install Starkbiter
- Quick Start - Your first simulation in 5 minutes
- Examples - Learn from working examples
Architecture
Starkbiter's architecture is built around three core components:
Environment
A sandboxed Starknet instance that provides:
- Full sequencer capabilities via Starknet Devnet
- Complete control over block production
- State forking from live networks
- Contract deployment and declaration
Middleware
A familiar interface for contract interaction:
- Implements
starknet-rspatterns - Seamless integration with existing tooling
- Additional control methods for testing
Engine
High-level abstractions for simulations:
- Agent behaviors and event handling
- World and universe management
- Configuration-driven setup
- Inter-agent messaging
Quick Example
Here's a simple example of creating an environment and deploying a contract:
#![allow(unused)] fn main() { use starkbiter_core::environment::Environment; use starknet::core::types::Felt; // Create a new environment let env = Environment::builder() .with_chain_id(Felt::from_hex("0x534e5f5345504f4c4941").unwrap()) .build() .await?; // Create an account let account = env.create_single_owner_account( Felt::from_hex("0xprivate_key").unwrap(), Felt::from_hex("0xaccount_address").unwrap(), ).await?; // Deploy your contracts and start simulating! }
Resources
Documentation
- 📖 This Book - Complete guide and tutorials
- 📚 API Docs - Detailed API documentation
- 🎓 Examples - Working code examples
Crates
All Starkbiter crates are available on crates.io:
starkbiter- CLI toolstarkbiter-core- Core librarystarkbiter-engine- Simulation enginestarkbiter-macros- Proc macrosstarkbiter-bindings- Contract bindings
Community
- 🐙 GitHub - Source code and issues
- 💬 Discussions - Ask questions and share ideas
- 🐛 Issues - Report bugs and request features
Contributing
Starkbiter is open source and welcomes contributions! Check out our Contributing Guide to get started.
License
Starkbiter is licensed under the MIT License. See the LICENSE file for details.
Getting Started
Welcome to Starkbiter! This section will guide you through everything you need to start building simulations on Starknet.
What You'll Learn
In this section, you'll learn how to:
- Install Starkbiter and set up your development environment
- Create your first simulation
- Deploy and interact with Starknet contracts
- Build agents with custom behaviors
- Run and analyze simulations
Prerequisites
Before starting with Starkbiter, you should have:
- Basic knowledge of Rust programming
- Familiarity with Starknet and smart contracts
- Understanding of blockchain fundamentals
Don't worry if you're not an expert - we'll guide you through each step!
Setup Options
Option 1: Using the CLI (Recommended)
The Starkbiter CLI provides the easiest way to get started:
- Install the CLI tool
- Generate contract bindings
- Use pre-built templates
Best for: Most users, especially those new to Starkbiter
Option 2: Direct Crate Usage
Use Starkbiter crates directly in your Rust projects:
[dependencies]
starkbiter-core = "0.1"
starkbiter-bindings = "0.1"
starkbiter-engine = "0.1"
Best for: Advanced users who want full control over their setup
Learning Path
Follow these guides in order for the best learning experience:
- Installation - Set up Rust and install Starkbiter
- Quick Start - Build your first simulation in 5 minutes
- Examples - Explore working examples and learn best practices
Use Cases for Starkbiter
Smart Contract Testing
Test your contracts in realistic environments with multiple interacting agents, simulating mainnet conditions without deployment costs.
DeFi Protocol Development
Model complex economic systems, test liquidation mechanisms, and validate AMM designs before going live.
Strategy Backtesting
Develop and test trading strategies, arbitrage bots, and MEV searchers in controlled environments.
Security Auditing
Perform sophisticated fuzzing, anomaly detection, and vulnerability assessment with agent-based modeling.
Support and Resources
Documentation
- 📖 This book - comprehensive guides and tutorials
- 📚 API docs - detailed API reference
- 🎓 Examples - working code samples
Community
- 💬 Discussions - ask questions
- 🐛 Issues - report bugs
- 🐙 GitHub - source code
Next Steps
Ready to start? Head to the Installation guide!
Installation
This guide will help you install Starkbiter and set up your development environment.
Prerequisites
Before installing Starkbiter, you need to have Rust installed on your system.
Installing Rust
If you don't have Rust installed, you can install it using rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
After installation, make sure your Rust toolchain is up to date:
rustup update
Starkbiter requires Rust 1.75 or later. You can check your Rust version with:
rustc --version
Installing Starkbiter CLI
The Starkbiter CLI tool is useful for generating contract bindings and managing your projects.
Install from crates.io
cargo install starkbiter
Verify Installation
Check that Starkbiter was installed correctly:
starkbiter --help
You should see the Starkbiter help menu with available commands.
Adding Starkbiter to Your Project
To use Starkbiter in your Rust project, add the necessary crates to your Cargo.toml:
[dependencies]
starkbiter-core = "0.1"
starkbiter-engine = "0.1"
starkbiter-bindings = "0.1"
starkbiter-macros = "0.1"
# Required dependencies
starknet = "0.11"
tokio = { version = "1.0", features = ["full"] }
Optional Tools
cargo-generate
For creating new projects from templates, install cargo-generate:
cargo install cargo-generate
Next Steps
Now that you have Starkbiter installed, continue to the Quick Start guide to create your first simulation!
Troubleshooting
Common Issues
Compilation Errors
If you encounter compilation errors, make sure you have the latest stable Rust toolchain:
rustup update stable
rustup default stable
Missing System Dependencies
On Linux, you may need to install additional development packages:
Ubuntu/Debian:
sudo apt-get update
sudo apt-get install build-essential pkg-config libssl-dev
Fedora:
sudo dnf install gcc pkg-config openssl-devel
macOS:
xcode-select --install
Getting Help
If you continue to have issues:
- Check the GitHub Issues
- Start a Discussion
- Read the FAQ
Quick Start
This guide will walk you through creating your first Starkbiter simulation in just a few minutes.
Your First Simulation
Let's create a simple simulation that deploys and interacts with an ERC20 token contract on Starknet.
Step 1: Create a New Project
cargo new my-starkbiter-sim
cd my-starkbiter-sim
Step 2: Add Dependencies
Edit your Cargo.toml:
[package]
name = "my-starkbiter-sim"
version = "0.1.0"
edition = "2021"
[dependencies]
starkbiter-core = "0.1"
starkbiter-bindings = "0.1"
starknet = "0.11"
tokio = { version = "1.0", features = ["full"] }
anyhow = "1.0"
Step 3: Create an Environment
Replace the contents of src/main.rs:
use anyhow::Result; use starkbiter_core::environment::Environment; use starknet::core::types::Felt; #[tokio::main] async fn main() -> Result<()> { // Create a new Starknet environment let env = Environment::builder() .with_chain_id(Felt::from_hex("0x534e5f5345504f4c4941").unwrap()) // Sepolia testnet .build() .await?; println!("✅ Starkbiter environment created!"); // Get the current block number let block_number = env.get_block_number().await?; println!("📦 Current block: {}", block_number); Ok(()) }
Step 4: Run Your Simulation
cargo run
You should see:
✅ Starkbiter environment created!
📦 Current block: 0
Adding Contract Interaction
Let's expand the example to deploy and interact with an ERC20 contract.
Step 1: Generate Contract Bindings
First, you'll need a compiled Starknet contract. For this example, we'll use the pre-generated bindings from starkbiter-bindings.
Update your Cargo.toml:
[dependencies]
starkbiter-core = "0.1"
starkbiter-bindings = "0.1"
starknet = "0.11"
tokio = { version = "1.0", features = ["full"] }
anyhow = "1.0"
Step 2: Deploy and Interact
Update src/main.rs:
use anyhow::Result; use starkbiter_core::environment::Environment; use starkbiter_bindings::erc_20_mintable_oz0::ERC20; use starknet::core::types::Felt; #[tokio::main] async fn main() -> Result<()> { // Create environment let env = Environment::builder() .with_chain_id(Felt::from_hex("0x534e5f5345504f4c4941").unwrap()) .build() .await?; println!("✅ Environment created"); // Create an account let private_key = Felt::from_hex("0x1234").unwrap(); let account_address = Felt::from_hex("0x1").unwrap(); let account = env.create_single_owner_account( private_key, account_address ).await?; println!("✅ Account created: {:#x}", account_address); // In a real scenario, you would: // 1. Declare the contract class // 2. Deploy an instance // 3. Interact with the deployed contract Ok(()) }
Working with the Example
Starkbiter comes with a complete example. Let's run it:
# Clone the repository
git clone https://github.com/astraly-labs/starkbiter
cd starkbiter
# Run the minter example
cargo run --example minter simulate ./examples/minter/config.toml -vvvv
This runs a simulation with:
- A Token Admin agent that creates ERC20 tokens
- A Token Requester agent that requests token minting
- Event-based communication between agents
- Automated token minting loop
Understanding the Example
The minter example demonstrates:
- Environment setup - Creating a sandboxed Starknet instance
- Agent creation - Building autonomous agents with behaviors
- Event handling - Responding to blockchain events
- Inter-agent messaging - Communication between agents
- Contract interaction - Deploying and calling contracts
Next Steps
Now that you've created your first simulation, explore:
- Examples - Learn from more complex examples
- Core Concepts - Understand Starkbiter's architecture
- Usage Guide - Deep dive into each crate
- Advanced Topics - Advanced simulation techniques
Tips
Logging
Enable detailed logging with the -v flags:
cargo run -- -v # Info level
cargo run -- -vv # Debug level
cargo run -- -vvv # Trace level
Development Mode
Use cargo watch for automatic recompilation:
cargo install cargo-watch
cargo watch -x run
Testing
Write tests for your simulations:
#![allow(unused)] fn main() { #[tokio::test] async fn test_token_minting() -> Result<()> { let env = Environment::builder().build().await?; // Your test code here Ok(()) } }
Troubleshooting
Environment Not Starting
Make sure all dependencies are installed correctly:
cargo clean
cargo build
Contract Deployment Failures
Check that:
- Your contract is properly compiled to Sierra 1.0
- The contract JSON is in the correct location
- You have the necessary permissions
Getting Help
- Check the API Documentation
- Browse Examples
- Ask on GitHub Discussions
Examples
This page provides an overview of the examples included with Starkbiter. These examples demonstrate key features and best practices for building simulations.
Running the Examples
All examples are located in the examples directory of the Starkbiter repository.
To run an example:
# Clone the repository if you haven't already
git clone https://github.com/astraly-labs/starkbiter
cd starkbiter
# Run an example
cargo run --example <example_name>
Available Examples
Token Minter Simulation
Location: examples/minter/
A comprehensive example demonstrating event-based agent communication and contract interaction.
Run it:
cargo run --example minter simulate ./examples/minter/config.toml -vvvv
What it demonstrates:
- Creating and managing multiple agents
- Event-based communication between agents
- ERC20 token deployment and interaction
- High-level messaging between agents
- Configuration-driven simulation setup
How it works:
The simulation involves two agents:
-
Token Admin (TA):
- Creates and deploys ERC20 contracts
- Subscribes to a high-level messenger for mint requests
- Mints tokens upon receiving requests
-
Token Requester (TR):
- Subscribes to
TokenMintedevents from ERC20 contracts - Requests more tokens when minting events are detected
- Creates an endless loop until a threshold is reached
- Subscribes to
Key Code Locations:
examples/minter/main.rs- Entry point and setupexamples/minter/behaviors/token_admin.rs- Admin agent logicexamples/minter/behaviors/token_requester.rs- Requester agent logicexamples/minter/config.toml- Configuration file
Learning outcomes:
- Agent behavior patterns
- Event subscription and handling
- Inter-agent messaging
- Contract lifecycle management
Configuration Options
The minter example uses a TOML configuration file. Here's what you can configure:
# Environment settings
[environment]
chain_id = "0x534e5f5345504f4c4941" # Starknet Sepolia
# Agent configurations
[agents.token_admin]
# Admin-specific settings
[agents.token_requester]
# Requester-specific settings
Advanced Example: Forking
Starkbiter supports forking from live Starknet networks to test against real state.
Basic forking example:
use starkbiter_core::environment::Environment; use starknet::core::types::Felt; use url::Url; use std::str::FromStr; #[tokio::main] async fn main() -> anyhow::Result<()> { // Fork from Starknet mainnet at a specific block let env = Environment::builder() .with_chain_id(Felt::from_hex("0x534e5f4d41494e").unwrap()) .with_fork( Url::from_str("https://starknet-mainnet.public.blastapi.io")?, 1000, // Block number to fork from Some(Felt::from_hex("0xblock_hash").unwrap()), ) .build() .await?; // Now you can interact with mainnet state locally! let block = env.get_block_number().await?; println!("Forked at block: {}", block); Ok(()) }
What forking enables:
- Test against real mainnet/testnet state
- Simulate interactions with live protocols
- Debug issues without spending real tokens
- Analyze historical scenarios
Note: Forking requires an active RPC endpoint during simulation.
Building Your Own Examples
Template Structure
Here's a basic template for creating your own simulation:
use anyhow::Result; use starkbiter_core::environment::Environment; use starkbiter_engine::{Agent, Behavior, World}; use starknet::core::types::Felt; // Define your behavior struct MyBehavior; impl Behavior for MyBehavior { async fn execute(&mut self, world: &World) -> Result<()> { // Your agent logic here Ok(()) } } #[tokio::main] async fn main() -> Result<()> { // 1. Create environment let env = Environment::builder() .with_chain_id(Felt::from_hex("0x534e5f5345504f4c4941")?) .build() .await?; // 2. Create world and agents let world = World::new(env); let agent = Agent::new("my-agent", MyBehavior); // 3. Run simulation world.add_agent(agent); world.run().await?; Ok(()) }
Best Practices
- Modular behaviors: Keep behavior logic in separate files
- Configuration-driven: Use TOML files for simulation parameters
- Logging: Add comprehensive logging with
tracingorlogcrates - Error handling: Use
anyhoworthiserrorfor robust error handling - Testing: Write unit tests for individual behaviors
Example Projects Gallery
Looking for more inspiration? Check out these community examples:
- DEX Arbitrage Bot: Simulates arbitrage opportunities across multiple DEXes
- Liquidation Engine: Models liquidation mechanisms for lending protocols
- MEV Searcher: Demonstrates MEV extraction strategies
- Oracle Price Feed: Simulates price feed updates and consumer reactions
Note: Community examples are maintained by their respective authors
Next Steps
After exploring the examples:
- Modify an example - Change parameters and observe the results
- Combine patterns - Mix concepts from different examples
- Build your own - Create a simulation for your use case
- Share it - Contribute your example back to the community!
Troubleshooting
Example Won't Run
# Clean and rebuild
cargo clean
cargo build --example <example_name>
Missing Dependencies
# Update dependencies
cargo update
Configuration Errors
Make sure your config file paths are correct:
cargo run --example minter simulate ./examples/minter/config.toml
Getting Help
- 📖 Read the Usage Guide
- 💬 Ask in Discussions
- 🐛 Report issues on GitHub
Architecture
Starkbiter's architecture is designed to provide a seamless simulation experience while maintaining compatibility with existing Starknet tooling. This chapter explains the high-level architecture and how different components work together.
System Overview
┌─────────────────────────────────────────────────────────────┐
│ User Code │
│ (Agents, Behaviors, Simulations) │
└────────────────┬────────────────────────────────────────────┘
│
┌────────────────▼────────────────────────────────────────────┐
│ Starkbiter Engine │
│ • Agent Management │
│ • Behavior Orchestration │
│ • World & Universe Abstractions │
└────────────────┬────────────────────────────────────────────┘
│
┌────────────────▼────────────────────────────────────────────┐
│ Starkbiter Core │
│ • Environment (Sandbox) │
│ • Middleware (starknet-rs compatible) │
│ • Token Management │
└────────────────┬────────────────────────────────────────────┘
│
┌────────────────▼────────────────────────────────────────────┐
│ Starknet Devnet │
│ • Blockifier (Sequencer Implementation) │
│ • JSON-RPC Interface │
│ • State Management │
└─────────────────────────────────────────────────────────────┘
Core Components
1. Starkbiter Core
The foundation of Starkbiter, providing low-level primitives for Starknet interaction.
Key Responsibilities:
- Environment Management: Creates and manages sandboxed Starknet instances
- Middleware Layer: Provides
starknet-rscompatible interface for contract interaction - State Control: Manages blockchain state, block production, and time manipulation
- Account Management: Handles account creation and deployment
Key Types:
Environment- Sandboxed Starknet instanceCheatingProvider- Middleware with additional testing capabilitiesConnection- Manages RPC communication
2. Starkbiter Engine
High-level abstractions for building complex simulations.
Key Responsibilities:
- Agent Lifecycle: Creates, manages, and coordinates agents
- Behavior Execution: Schedules and runs agent behaviors
- Event System: Handles blockchain events and inter-agent messaging
- World Management: Provides simulation environments with shared state
Key Types:
Agent- Autonomous entity with behaviorsBehavior- Trait for defining agent actionsWorld- Simulation environmentUniverse- Collection of worldsMessager- Inter-agent communication
3. Starkbiter CLI
Command-line tools for project management.
Key Responsibilities:
- Binding Generation: Creates Rust bindings from Cairo contracts
- Project Templates: Scaffolds new simulation projects
- Build Tools: Compiles and manages contract artifacts
4. Starkbiter Bindings
Pre-generated bindings for common contracts.
Includes:
- ERC20 tokens
- Account contracts (Argent, OpenZeppelin)
- DEX contracts (Ekubo)
- Utility contracts
Data Flow
Contract Deployment Flow
User Code
│
└─> Environment.declare_contract()
│
└─> Devnet declares contract class
│
└─> Returns class hash
User Code
│
└─> Environment.deploy_contract()
│
└─> Devnet deploys instance
│
└─> Returns contract address
Transaction Execution Flow
Agent Behavior
│
└─> Contract.call_method()
│
└─> Middleware prepares transaction
│
└─> Devnet executes transaction
│
├─> Updates state
├─> Emits events
└─> Returns receipt
│
└─> Agent processes result
Event Handling Flow
Contract emits event
│
└─> Devnet captures event
│
└─> Environment polls for events
│
└─> Event distributed to subscribers
│
└─> Agent behaviors triggered
Design Principles
1. Compatibility First
Starkbiter maintains compatibility with starknet-rs to ensure:
- Seamless integration with existing code
- Familiar APIs for developers
- Easy transition between testing and production
2. Layered Abstraction
Each layer serves a specific purpose:
- Low-level (Core): Maximum control and flexibility
- Mid-level (Engine): Balanced abstraction for common patterns
- High-level (User Code): Domain-specific logic
3. Performance Oriented
- Local execution eliminates network latency
- Efficient state management
- Optimized for rapid iteration
4. Testing First
Built specifically for testing scenarios:
- Time manipulation
- State snapshots and rollbacks
- Deterministic execution
- Block production control
Integration Points
With Starknet-rs
Starkbiter implements starknet-rs traits:
Provider- For read operationsAccount- For transaction signing and submission
This allows seamless use of:
- Contract bindings generated with
cainome - Existing Starknet libraries
- Standard tooling
With Starknet Devnet
Starkbiter wraps Starknet Devnet to provide:
- Full sequencer capabilities
- JSON-RPC interface
- State forking
- Additional testing methods
With Cairo Contracts
Starkbiter works with standard Cairo contracts:
- Compiled to Sierra 1.0
- Standard JSON format
- ABI compatibility
Execution Model
Synchronous Simulation
#![allow(unused)] fn main() { // Create environment let env = Environment::builder().build().await?; // Operations execute immediately let account = env.create_account(...).await?; let contract = deploy_contract(&account, ...).await?; // State is updated synchronously let result = contract.transfer(...).await?; }
Event-Driven Simulation
#![allow(unused)] fn main() { // Agents react to events let mut agent = Agent::new("trader", TradingBehavior); agent.on_event("SwapExecuted", |event| { // React to DEX swaps handle_swap(event) }); // Engine coordinates execution world.add_agent(agent); world.run().await?; }
Memory and State Management
Environment Lifecycle
- Initialization: Devnet starts with genesis state
- Execution: State updates with each transaction
- Cleanup: Resources released on drop
State Isolation
Each Environment instance:
- Has its own isolated state
- Independent block production
- Separate account namespaces
Forking
When forking from live networks:
- Initial state loaded lazily
- Missing state fetched on-demand
- Local modifications isolated
Concurrency Model
Async/Await
Starkbiter uses Tokio for async execution:
#[tokio::main] async fn main() -> Result<()> { let env = Environment::builder().build().await?; // Concurrent operations let (r1, r2) = tokio::join!( operation1(&env), operation2(&env), ); Ok(()) }
Agent Concurrency
Multiple agents can execute concurrently:
- Coordinated by the engine
- Shared state through the world
- Message passing for communication
Error Handling
Error Types
Starkbiter defines structured errors:
EnvironmentError- Environment setup and operation failuresContractError- Contract deployment and interaction errorsAgentError- Agent execution failures
Error Propagation
Errors use anyhow or thiserror for:
- Rich context
- Easy error chaining
- Flexible handling
Next Steps
- Environment - Deep dive into the Environment API
- Middleware - Understanding the middleware layer
- Forking - State forking from live networks
Environment
The Environment is the core abstraction in Starkbiter, representing a sandboxed Starknet instance. It provides complete control over blockchain state, block production, and contract interaction.
Overview
An Environment wraps a Starknet Devnet instance, giving you:
- Full JSON-RPC capabilities
- Additional testing methods (cheating methods)
- State management and control
- Account creation and management
Think of it as your personal Starknet network that you have complete control over.
Creating an Environment
Basic Setup
use starkbiter_core::environment::Environment; use starknet::core::types::Felt; #[tokio::main] async fn main() -> anyhow::Result<()> { let env = Environment::builder() .with_chain_id(Felt::from_hex("0x534e5f5345504f4c4941")?) .build() .await?; println!("Environment ready!"); Ok(()) }
Builder Pattern
The EnvironmentBuilder provides a fluent API for configuration:
#![allow(unused)] fn main() { let env = Environment::builder() .with_chain_id(chain_id) .with_gas_price(100_000_000_000) // 100 gwei .with_block_time(10) // 10 seconds per block .build() .await?; }
Configuration Options
Chain ID
Specify which network to simulate:
#![allow(unused)] fn main() { // Starknet Mainnet let mainnet_id = Felt::from_hex("0x534e5f4d41494e")?; // Starknet Sepolia Testnet let sepolia_id = Felt::from_hex("0x534e5f5345504f4c4941")?; let env = Environment::builder() .with_chain_id(sepolia_id) .build() .await?; }
Block Time
Control block production:
#![allow(unused)] fn main() { // Automatic block production every 5 seconds let env = Environment::builder() .with_block_time(5) .build() .await?; // Manual block production let env = Environment::builder() .with_block_time(0) // 0 = manual mode .build() .await?; }
Gas Configuration
Set gas prices:
#![allow(unused)] fn main() { let env = Environment::builder() .with_gas_price(50_000_000_000) // 50 gwei .build() .await?; }
State Management
Block Production
Control when blocks are produced:
#![allow(unused)] fn main() { // Manual block production env.mine_block().await?; // Mine multiple blocks env.mine_blocks(10).await?; // Get current block number let block_num = env.get_block_number().await?; println!("Current block: {}", block_num); }
Time Manipulation
Control blockchain time:
#![allow(unused)] fn main() { // Increase time by 1 hour env.increase_time(3600).await?; // Set specific timestamp env.set_timestamp(1234567890).await?; // Get current timestamp let timestamp = env.get_timestamp().await?; }
State Snapshots
Save and restore state:
#![allow(unused)] fn main() { // Take a snapshot let snapshot_id = env.snapshot().await?; // Make some changes contract.do_something().await?; // Restore to snapshot env.restore(snapshot_id).await?; }
Account Management
Creating Accounts
#![allow(unused)] fn main() { use starknet::core::types::Felt; // Create with random keys let account = env.create_account().await?; // Create with specific keys let private_key = Felt::from_hex("0x123...")? ; let account_address = Felt::from_hex("0x456...")?; let account = env.create_single_owner_account( private_key, account_address ).await?; }
Predeployed Accounts
Devnet comes with predeployed accounts for testing:
#![allow(unused)] fn main() { // Get predeployed accounts let accounts = env.get_predeployed_accounts().await?; for account in accounts { println!("Address: {:#x}", account.address); println!("Private Key: {:#x}", account.private_key); } }
Contract Management
Declaring Contracts
Before deploying, contracts must be declared:
#![allow(unused)] fn main() { use std::fs; // Read contract JSON let contract_json = fs::read_to_string("path/to/contract.json")?; // Declare the contract let class_hash = env.declare_contract( &account, contract_json ).await?; println!("Contract declared: {:#x}", class_hash); }
Deploying Contracts
#![allow(unused)] fn main() { use starknet::core::types::Felt; // Deploy with constructor args let constructor_args = vec![ Felt::from(1000u64), // Initial supply Felt::from_hex("0x...")?, // Owner address ]; let contract_address = env.deploy_contract( &account, class_hash, constructor_args ).await?; println!("Contract deployed at: {:#x}", contract_address); }
Using Bindings
With cainome-generated bindings:
#![allow(unused)] fn main() { use starkbiter_bindings::erc_20_mintable_oz0::ERC20; // Deploy using binding let erc20 = ERC20::deploy( &account, name, symbol, decimals, initial_supply, recipient ).await?; // Interact with contract let balance = erc20.balance_of(address).await?; }
Querying State
Block Information
#![allow(unused)] fn main() { // Get latest block let block = env.get_block_latest().await?; println!("Block number: {}", block.block_number); println!("Timestamp: {}", block.timestamp); // Get specific block let block = env.get_block_by_number(100).await?; }
Transaction Information
#![allow(unused)] fn main() { // Get transaction by hash let tx = env.get_transaction(tx_hash).await?; // Get transaction receipt let receipt = env.get_transaction_receipt(tx_hash).await?; // Get transaction status let status = env.get_transaction_status(tx_hash).await?; }
Contract State
#![allow(unused)] fn main() { // Get contract storage let storage_value = env.get_storage_at( contract_address, storage_key ).await?; // Get contract nonce let nonce = env.get_nonce(contract_address).await?; // Get contract class let contract_class = env.get_class_at(contract_address).await?; }
Event Handling
Polling for Events
#![allow(unused)] fn main() { // Get events from latest block let events = env.get_events( from_block, to_block, contract_address, keys ).await?; for event in events { println!("Event: {:?}", event); } }
Event Filtering
#![allow(unused)] fn main() { use starknet::core::types::EventFilter; let filter = EventFilter { from_block: Some(0), to_block: Some(100), address: Some(contract_address), keys: Some(vec![event_key]), }; let events = env.get_events_filtered(filter).await?; }
Forking
Fork from live networks:
#![allow(unused)] fn main() { use url::Url; use std::str::FromStr; let env = Environment::builder() .with_chain_id(mainnet_id) .with_fork( Url::from_str("https://starknet-mainnet.public.blastapi.io")?, 12345, // Block number Some(Felt::from_hex("0xblock_hash")?), ) .build() .await?; }
See Forking for more details.
Cheating Methods
Starkbiter provides additional testing methods:
Impersonation
#![allow(unused)] fn main() { // Impersonate an address env.start_prank(target_address, impersonator_address).await?; // Make calls as the impersonator contract.privileged_function().await?; // Stop impersonating env.stop_prank(target_address).await?; }
Balance Manipulation
#![allow(unused)] fn main() { // Set ETH balance env.set_balance(address, amount).await?; // Get balance let balance = env.get_balance(address).await?; }
Storage Manipulation
#![allow(unused)] fn main() { // Write directly to storage env.store( contract_address, storage_key, value ).await?; // Load from storage let value = env.load(contract_address, storage_key).await?; }
Cleanup and Shutdown
Environments are automatically cleaned up when dropped:
#![allow(unused)] fn main() { { let env = Environment::builder().build().await?; // Use env } // env is dropped, resources cleaned up }
Explicit shutdown:
#![allow(unused)] fn main() { env.shutdown().await?; }
Best Practices
1. Use Builder Pattern
Always use the builder for consistent configuration:
#![allow(unused)] fn main() { let env = Environment::builder() .with_chain_id(chain_id) .build() .await?; }
2. Error Handling
Always handle environment errors:
#![allow(unused)] fn main() { match env.get_block_number().await { Ok(block) => println!("Block: {}", block), Err(e) => eprintln!("Error: {}", e), } }
3. Resource Management
Create environments in appropriate scopes:
#![allow(unused)] fn main() { #[tokio::test] async fn test_contract() -> Result<()> { let env = Environment::builder().build().await?; // Test code Ok(()) } // Environment cleaned up automatically }
4. Snapshots for Testing
Use snapshots to isolate test cases:
#![allow(unused)] fn main() { let snapshot = env.snapshot().await?; // Test case 1 run_test_1(&env).await?; env.restore(snapshot).await?; // Test case 2 run_test_2(&env).await?; env.restore(snapshot).await?; }
Common Patterns
Deploy and Initialize
#![allow(unused)] fn main() { async fn deploy_and_initialize(env: &Environment) -> Result<ContractAddress> { let account = env.create_account().await?; // Declare let class_hash = env.declare_contract(&account, contract_json).await?; // Deploy let address = env.deploy_contract(&account, class_hash, vec![]).await?; // Initialize let contract = MyContract::new(address, &account); contract.initialize().await?; Ok(address) } }
Time-Based Testing
#![allow(unused)] fn main() { async fn test_time_lock(env: &Environment) -> Result<()> { let contract = deploy_timelock(&env).await?; // Try before time lock expires (should fail) assert!(contract.withdraw().await.is_err()); // Fast forward env.increase_time(86400).await?; // +24 hours env.mine_block().await?; // Now should succeed contract.withdraw().await?; Ok(()) } }
Next Steps
- Middleware - Understanding the middleware layer
- Forking - State forking from live networks
- Usage Guide - Detailed API reference
Middleware
The middleware layer in Starkbiter provides a familiar interface for interacting with Starknet contracts, compatible with the starknet-rs ecosystem. This chapter explains how middleware works and how to use it effectively.
Overview
Starkbiter's middleware implements the standard Provider trait from starknet-rs, extended with additional "cheating" methods for testing. This means:
- Familiar API: If you know
starknet-rs, you know Starkbiter - Seamless Integration: Works with
cainomebindings and other tooling - Extended Capabilities: Additional methods for testing scenarios
The CheatingProvider
The CheatingProvider is the main middleware implementation in Starkbiter.
#![allow(unused)] fn main() { use starkbiter_core::middleware::CheatingProvider; // Access through environment let provider = env.provider(); // Use standard Provider methods let block_number = provider.block_number().await?; let block = provider.get_block_with_tx_hashes(block_number).await?; }
Standard Provider Methods
Block Queries
#![allow(unused)] fn main() { use starknet::providers::Provider; // Get latest block number let block_num = provider.block_number().await?; // Get block by number let block = provider.get_block_with_tx_hashes(block_num).await?; // Get block by hash let block = provider.get_block_with_tx_hashes_by_hash(block_hash).await?; }
Transaction Queries
#![allow(unused)] fn main() { // Get transaction by hash let tx = provider.get_transaction_by_hash(tx_hash).await?; // Get transaction receipt let receipt = provider.get_transaction_receipt(tx_hash).await?; // Get transaction status let status = provider.get_transaction_status(tx_hash).await?; }
State Queries
#![allow(unused)] fn main() { // Get storage at address let value = provider.get_storage_at( contract_address, key, block_id ).await?; // Get nonce let nonce = provider.get_nonce( block_id, contract_address ).await?; // Get class hash at contract let class_hash = provider.get_class_hash_at( block_id, contract_address ).await?; }
Contract Class Queries
#![allow(unused)] fn main() { // Get class let class = provider.get_class( block_id, class_hash ).await?; // Get class at contract address let class = provider.get_class_at( block_id, contract_address ).await?; }
Event Queries
#![allow(unused)] fn main() { use starknet::core::types::{EventFilter, EventsPage}; let filter = EventFilter { from_block: Some(BlockId::Number(0)), to_block: Some(BlockId::Number(100)), address: Some(contract_address), keys: None, }; let events: EventsPage = provider.get_events( filter, None, // continuation_token 100, // chunk_size ).await?; }
Cheating Methods
Beyond standard Provider methods, CheatingProvider offers testing-specific capabilities.
Time Manipulation
#![allow(unused)] fn main() { // Get current timestamp let timestamp = provider.get_timestamp().await?; // Set specific timestamp provider.set_timestamp(new_timestamp).await?; // Increase time provider.increase_time(seconds).await?; }
Block Manipulation
#![allow(unused)] fn main() { // Mine a single block provider.mine_block().await?; // Mine multiple blocks provider.mine_blocks(count).await?; // Set block interval provider.set_block_interval(seconds).await?; }
Account Impersonation
#![allow(unused)] fn main() { // Start impersonating an address provider.start_prank(target_contract, impersonator).await?; // Make calls as the impersonator // All calls to target_contract will appear to come from impersonator // Stop impersonating provider.stop_prank(target_contract).await?; }
Balance Manipulation
#![allow(unused)] fn main() { // Set ETH balance provider.set_balance(address, amount).await?; // Deal tokens (if supported) provider.deal(token_address, recipient, amount).await?; }
Storage Manipulation
#![allow(unused)] fn main() { // Write to storage provider.store( contract_address, storage_key, value ).await?; // Read from storage let value = provider.load( contract_address, storage_key ).await?; }
Snapshots
#![allow(unused)] fn main() { // Create snapshot let snapshot_id = provider.snapshot().await?; // Make changes... contract.modify_state().await?; // Restore to snapshot provider.revert(snapshot_id).await?; }
Using with Accounts
The middleware integrates seamlessly with Starknet accounts.
Account Creation
#![allow(unused)] fn main() { use starknet::accounts::{Account, SingleOwnerAccount}; use starknet::signers::LocalWallet; // Create wallet let signer = LocalWallet::from(private_key); // Create account with the provider let account = SingleOwnerAccount::new( provider.clone(), signer, account_address, chain_id, ); }
Signing Transactions
#![allow(unused)] fn main() { use starknet::accounts::Call; // Prepare call let call = Call { to: contract_address, selector: get_selector_from_name("transfer")?, calldata: vec![recipient, amount_low, amount_high], }; // Execute through account let result = account.execute(vec![call]).send().await?; }
Using with Contract Bindings
The middleware works seamlessly with cainome-generated bindings.
Reading from Contracts
#![allow(unused)] fn main() { use starkbiter_bindings::erc_20_mintable_oz0::ERC20; // Create contract instance let token = ERC20::new(token_address, &account); // Read state (calls Provider methods) let balance = token.balance_of(address).await?; let total_supply = token.total_supply().await?; }
Writing to Contracts
#![allow(unused)] fn main() { // Prepare transaction let tx = token.transfer(recipient, amount); // Execute (uses Account methods) let result = tx.send().await?; // Wait for confirmation let receipt = result.wait_for_acceptance().await?; }
Connection Management
The middleware manages the underlying connection to Devnet.
Connection Configuration
#![allow(unused)] fn main() { use starkbiter_core::middleware::Connection; // Default configuration (automatic) let connection = Connection::new()?; // Custom port let connection = Connection::with_port(5050)?; // Custom URL let connection = Connection::with_url("http://localhost:5050")?; }
Health Checks
#![allow(unused)] fn main() { // Check if Devnet is responsive if provider.is_alive().await? { println!("Devnet is running"); } }
Error Handling
The middleware uses standard Starknet error types.
Common Errors
#![allow(unused)] fn main() { use starknet::providers::ProviderError; match provider.get_block_with_tx_hashes(1000000).await { Ok(block) => { // Process block } Err(ProviderError::StarknetError(e)) => { // Starknet-specific error eprintln!("Starknet error: {:?}", e); } Err(e) => { // Other errors eprintln!("Provider error: {:?}", e); } } }
Best Practices
#![allow(unused)] fn main() { // Use Result types async fn get_balance( provider: &CheatingProvider, address: Felt, ) -> Result<Felt> { let balance = provider.get_balance(address).await?; Ok(balance) } // Handle specific error cases async fn safe_get_block( provider: &CheatingProvider, block_num: u64, ) -> Option<Block> { match provider.get_block_with_tx_hashes(block_num).await { Ok(block) => Some(block), Err(e) => { log::warn!("Failed to get block {}: {}", block_num, e); None } } } }
Advanced Patterns
Concurrent Requests
#![allow(unused)] fn main() { use tokio::try_join; // Execute multiple queries in parallel let (balance, nonce, block) = try_join!( provider.get_balance(address), provider.get_nonce(BlockId::Pending, address), provider.block_number(), )?; }
Retry Logic
#![allow(unused)] fn main() { use tokio::time::{sleep, Duration}; async fn get_receipt_with_retry( provider: &CheatingProvider, tx_hash: Felt, max_retries: u32, ) -> Result<TransactionReceipt> { for attempt in 0..max_retries { match provider.get_transaction_receipt(tx_hash).await { Ok(receipt) => return Ok(receipt), Err(e) if attempt < max_retries - 1 => { sleep(Duration::from_millis(100)).await; continue; } Err(e) => return Err(e.into()), } } unreachable!() } }
Polling for Events
#![allow(unused)] fn main() { async fn watch_for_event( provider: &CheatingProvider, contract: Felt, event_key: Felt, ) -> Result<Event> { let mut last_block = provider.block_number().await?; loop { let current_block = provider.block_number().await?; if current_block > last_block { let events = provider.get_events( EventFilter { from_block: Some(BlockId::Number(last_block + 1)), to_block: Some(BlockId::Number(current_block)), address: Some(contract), keys: Some(vec![vec![event_key]]), }, None, 100, ).await?; if let Some(event) = events.events.first() { return Ok(event.clone()); } last_block = current_block; } sleep(Duration::from_millis(100)).await; } } }
Testing with Middleware
Unit Tests
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use starkbiter_core::environment::Environment; #[tokio::test] async fn test_provider_queries() { let env = Environment::builder().build().await.unwrap(); let provider = env.provider(); let block_num = provider.block_number().await.unwrap(); assert_eq!(block_num, 0); } } }
Integration Tests
#![allow(unused)] fn main() { #[tokio::test] async fn test_contract_interaction() { let env = Environment::builder().build().await?; let provider = env.provider(); let account = env.create_account().await?; // Deploy contract let contract = deploy_test_contract(&account).await?; // Interact through provider let call = Call { to: contract.address, selector: get_selector_from_name("set_value")?, calldata: vec![Felt::from(42u64)], }; account.execute(vec![call]).send().await?; // Verify let value = contract.get_value().await?; assert_eq!(value, Felt::from(42u64)); } }
Next Steps
- Environment - Deep dive into Environment
- Forking - State forking from live networks
- Usage Guide - Detailed middleware API
Forking
Forking allows you to create a local simulation based on the state of a live Starknet network. This is incredibly powerful for testing against real mainnet/testnet conditions without spending real tokens.
Overview
When you fork a network, Starkbiter:
- Connects to a live Starknet RPC endpoint
- Fetches state lazily (only when needed)
- Stores state locally for fast access
- Allows you to make local modifications without affecting the real network
Basic Forking
Fork from Mainnet
use starkbiter_core::environment::Environment; use starknet::core::types::Felt; use url::Url; use std::str::FromStr; #[tokio::main] async fn main() -> anyhow::Result<()> { let env = Environment::builder() .with_chain_id(Felt::from_hex("0x534e5f4d41494e")?) // Mainnet .with_fork( Url::from_str("https://starknet-mainnet.public.blastapi.io")?, 100000, // Block number to fork from None, // Optional: block hash for verification ) .build() .await?; // Now you have mainnet state locally! let block = env.get_block_number().await?; println!("Forked at block: {}", block); Ok(()) }
Fork from Sepolia
#![allow(unused)] fn main() { let env = Environment::builder() .with_chain_id(Felt::from_hex("0x534e5f5345504f4c4941")?) .with_fork( Url::from_str("https://starknet-sepolia.public.blastapi.io")?, 50000, None, ) .build() .await?; }
Configuration
Block Selection
You can fork from any block:
#![allow(unused)] fn main() { // Fork from latest block (use a very high number) .with_fork(rpc_url, u64::MAX, None) // Fork from specific block .with_fork(rpc_url, 123456, None) // Fork with block hash verification .with_fork( rpc_url, 123456, Some(Felt::from_hex("0xblock_hash")?), ) }
RPC Endpoints
Popular Starknet RPC providers:
#![allow(unused)] fn main() { // Public endpoints const MAINNET: &str = "https://starknet-mainnet.public.blastapi.io"; const SEPOLIA: &str = "https://starknet-sepolia.public.blastapi.io"; // Alchemy const ALCHEMY: &str = "https://starknet-mainnet.g.alchemy.com/v2/YOUR_API_KEY"; // Infura const INFURA: &str = "https://starknet-mainnet.infura.io/v3/YOUR_PROJECT_ID"; }
Use Cases
Testing Against Real Protocols
#![allow(unused)] fn main() { use starkbiter_bindings::erc_20_mintable_oz0::ERC20; // Fork mainnet let env = Environment::builder() .with_fork(mainnet_url, block_num, None) .build() .await?; // Interact with real USDC contract let usdc_address = Felt::from_hex("0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8")?; let account = env.create_account().await?; let usdc = ERC20::new(usdc_address, &account); // Read real state let total_supply = usdc.total_supply().await?; println!("USDC total supply: {}", total_supply); }
Debugging Transactions
#![allow(unused)] fn main() { // Fork at the block before an issue occurred let env = Environment::builder() .with_fork(mainnet_url, problem_block - 1, None) .build() .await?; // Reproduce the issue locally let tx_result = reproduce_issue(&env).await?; // Debug with full control env.increase_time(1).await?; let next_result = try_fix(&env).await?; }
Strategy Backtesting
#![allow(unused)] fn main() { // Fork from historical block let env = Environment::builder() .with_fork(mainnet_url, historical_block, None) .build() .await?; // Run your strategy against real historical state let profit = run_strategy(&env).await?; println!("Strategy would have made: {}", profit); }
Protocol Integration Testing
#![allow(unused)] fn main() { // Test your protocol against real DEX let env = Environment::builder() .with_fork(mainnet_url, latest_block, None) .build() .await?; // Deploy your protocol let my_contract = deploy_my_protocol(&env).await?; // Test integration with real Jediswap/10k swap/Ekubo let result = test_swap_integration(&env, my_contract).await?; }
Lazy State Loading
Starkbiter loads state lazily for efficiency:
#![allow(unused)] fn main() { // Fork is created let env = Environment::builder() .with_fork(url, block, None) .build() .await?; // State is fetched only when accessed let balance = env.get_balance(address).await?; // Fetches balance let storage = env.get_storage_at(contract, key).await?; // Fetches storage // Subsequent accesses use cached state let balance2 = env.get_balance(address).await?; // Uses cache }
Local Modifications
Changes you make are local and don't affect the real network:
#![allow(unused)] fn main() { // Fork mainnet let env = Environment::builder() .with_fork(mainnet_url, block, None) .build() .await?; let account = env.create_account().await?; // Modify state locally env.set_balance(account.address(), Felt::from(1_000_000u64)).await?; // Deploy contracts let my_contract = deploy_contract(&account).await?; // Make transactions my_contract.do_something().await?; // All changes are local - mainnet is unaffected! }
Impersonation in Forks
Interact with contracts as if you were any address:
#![allow(unused)] fn main() { // Fork mainnet let env = Environment::builder() .with_fork(mainnet_url, block, None) .build() .await?; // Impersonate a whale address let whale = Felt::from_hex("0xwhale_address")?; env.start_prank(contract_address, whale).await?; // Make calls as the whale let contract = Token::new(token_address, &env); contract.transfer(my_address, large_amount).await?; env.stop_prank(contract_address).await?; }
Snapshot and Restore with Forks
Combine forking with snapshots for powerful testing:
#![allow(unused)] fn main() { // Fork mainnet let env = Environment::builder() .with_fork(mainnet_url, block, None) .build() .await?; // Take snapshot of forked state let snapshot = env.snapshot().await?; // Test scenario 1 test_scenario_1(&env).await?; // Restore forked state env.restore(snapshot).await?; // Test scenario 2 with same starting state test_scenario_2(&env).await?; }
Performance Considerations
Network Latency
State fetching requires network calls:
#![allow(unused)] fn main() { // First access: slow (network fetch) let balance = env.get_balance(address).await?; // Subsequent accesses: fast (cached) let balance2 = env.get_balance(address).await?; }
Batch Queries
Optimize by batching related operations:
#![allow(unused)] fn main() { // Instead of sequential queries let balance1 = env.get_balance(addr1).await?; let balance2 = env.get_balance(addr2).await?; // Use concurrent queries use tokio::try_join; let (balance1, balance2) = try_join!( env.get_balance(addr1), env.get_balance(addr2), )?; }
Persistent Caching
Consider caching fork state for repeated runs:
#![allow(unused)] fn main() { // Future enhancement (not yet implemented) let env = Environment::builder() .with_fork(url, block, None) .with_cache_dir("./fork_cache") .build() .await?; }
Limitations
Active Connection Required
Forking requires the RPC endpoint to remain available:
#![allow(unused)] fn main() { // ❌ This will fail if RPC goes down let env = Environment::builder() .with_fork(unreliable_url, block, None) .build() .await?; // First access works let state1 = env.get_storage_at(addr, key).await?; // If RPC goes down, subsequent fetches fail let state2 = env.get_storage_at(other_addr, key).await?; // Error! }
State Consistency
Forked state is point-in-time:
#![allow(unused)] fn main() { // Fork at block 100000 let env = Environment::builder() .with_fork(url, 100000, None) .build() .await?; // State is frozen at block 100000 // Real network has moved on // Your fork doesn't see newer transactions }
Block Hash Verification
If provided, block hash must match:
#![allow(unused)] fn main() { // This will fail if block hash doesn't match block number let env = Environment::builder() .with_fork( url, 123456, Some(wrong_block_hash), // ❌ Error! ) .build() .await?; }
Best Practices
1. Use Recent Blocks
#![allow(unused)] fn main() { // Good: Recent block, less likely to be pruned .with_fork(url, recent_block, None) // Risky: Very old block might be pruned by RPC .with_fork(url, very_old_block, None) }
2. Verify Block Hash for Critical Tests
#![allow(unused)] fn main() { // For production testing, verify block hash .with_fork( url, critical_block, Some(verified_block_hash), ) }
3. Handle Network Errors
#![allow(unused)] fn main() { let env = match Environment::builder() .with_fork(url, block, None) .build() .await { Ok(env) => env, Err(e) => { eprintln!("Failed to fork: {}", e); // Fallback to non-forked environment Environment::builder().build().await? } }; }
4. Use Local Snapshots
#![allow(unused)] fn main() { // After forking, take snapshot for fast resets let snapshot = env.snapshot().await?; for test in tests { env.restore(snapshot).await?; test.run(&env).await?; } }
Troubleshooting
Fork Fails to Connect
Error: Failed to connect to RPC endpoint
Solutions:
- Check RPC URL is correct
- Verify network connectivity
- Try a different RPC provider
- Check if the endpoint requires API key
Block Not Found
Error: Block not found
Solutions:
- Verify block number exists
- Check if block is too old (pruned)
- Try a more recent block
- Use an archive node for historical blocks
State Loading Timeout
Error: Timeout fetching state
Solutions:
- Increase timeout duration
- Use a faster RPC provider
- Pre-warm cache by accessing state upfront
Next Steps
- Environment - Deep dive into Environment API
- Middleware - Understanding the middleware layer
- Advanced Topics - Advanced simulation techniques
Usage Guide Overview
This section provides detailed documentation for each component of the Starkbiter framework. Whether you're building simple contract tests or complex multi-agent simulations, you'll find the information you need here.
Crates Overview
Starkbiter is organized into several crates, each serving a specific purpose:
Starkbiter Core
The foundation layer providing direct interaction with Starknet.
Key Components:
Environment- Sandboxed Starknet instanceCheatingProvider- Extended middleware with testing capabilities- State management and control
- Account and contract deployment
Best for: Low-level control, custom testing scenarios, framework integration
Starkbiter Engine
High-level abstractions for building agent-based simulations.
Key Components:
Agent- Autonomous entities with behaviorsBehavior- Trait for defining agent actionsWorld- Simulation environmentUniverse- Multi-world orchestration- Inter-agent messaging
Best for: Complex simulations, agent-based modeling, DeFi protocol testing
Starkbiter CLI
Command-line tools for project management.
Key Features:
- Contract binding generation
- Project initialization
- Build management
Best for: Project setup, contract integration
Starkbiter Macros
Procedural macros to reduce boilerplate.
Provides:
- Behavior derivation
- Agent configuration
- Simplified async patterns
Best for: Cleaner code, rapid development
Starkbiter Bindings
Pre-generated contract bindings for common contracts.
Includes:
- ERC20 tokens
- Account contracts
- DEX protocols (Ekubo)
- Test utilities
Best for: Quick testing, common contract interactions
Choosing the Right Level
Use Core When:
- You need maximum control over the simulation
- Building custom testing frameworks
- Integrating with other tools
- Implementing novel testing patterns
#![allow(unused)] fn main() { use starkbiter_core::environment::Environment; let env = Environment::builder().build().await?; let account = env.create_account().await?; // Direct, low-level control }
Use Engine When:
- Building multi-agent simulations
- Modeling economic systems
- Testing protocol interactions
- Simulating user behaviors
#![allow(unused)] fn main() { use starkbiter_engine::{Agent, World}; let world = World::new(env); world.add_agent(Agent::new("trader", TradingBehavior)); world.run().await?; }
Use CLI When:
- Starting new projects
- Generating contract bindings
- Managing build artifacts
starkbiter bind
starkbiter init my-project
Common Patterns
Pattern 1: Simple Contract Testing
#![allow(unused)] fn main() { use starkbiter_core::environment::Environment; #[tokio::test] async fn test_my_contract() { let env = Environment::builder().build().await?; let account = env.create_account().await?; // Deploy and test let contract = deploy_contract(&account).await?; assert_eq!(contract.get_value().await?, expected_value); } }
Pattern 2: Multi-Agent Simulation
use starkbiter_engine::{Agent, World, Behavior}; #[tokio::main] async fn main() -> Result<()> { let env = Environment::builder().build().await?; let world = World::new(env); // Add multiple agents world.add_agent(Agent::new("liquidator", LiquidatorBehavior)); world.add_agent(Agent::new("borrower", BorrowerBehavior)); world.add_agent(Agent::new("lender", LenderBehavior)); // Run simulation world.run().await?; Ok(()) }
Pattern 3: Fork Testing
#![allow(unused)] fn main() { let env = Environment::builder() .with_fork(mainnet_url, block_num, None) .build() .await?; // Test against real mainnet state let usdc = ERC20::new(usdc_address, &account); let balance = usdc.balance_of(whale_address).await?; }
Navigation
Each crate section includes:
- Overview - What it does and when to use it
- API Reference - Detailed documentation of types and methods
- Examples - Working code samples
- Best Practices - Tips and patterns
Start with the crate that matches your use case:
- Starkbiter Core - Low-level control
- Starkbiter Engine - Agent simulations
- Starkbiter CLI - Project tooling
Additional Resources
- Core Concepts - Architecture and design
- Advanced Topics - Advanced techniques
- Examples - Working examples
- API Docs - Full API reference
Starkbiter Core
starkbiter-core is the foundation of the Starkbiter framework. It provides low-level primitives for interacting with Starknet in a sandboxed environment with complete control over blockchain state.
Overview
The core crate gives you:
- Environment - A sandboxed Starknet Devnet instance
- Middleware -
starknet-rscompatible provider with testing extensions - State Control - Block production, time manipulation, snapshots
- Account Management - Account creation and deployment
- Contract Management - Declaration and deployment
Installation
Add to your Cargo.toml:
[dependencies]
starkbiter-core = "0.1"
starknet = "0.11"
tokio = { version = "1.0", features = ["full"] }
Quick Start
use starkbiter_core::environment::Environment; use starknet::core::types::Felt; #[tokio::main] async fn main() -> anyhow::Result<()> { // Create environment let env = Environment::builder() .with_chain_id(Felt::from_hex("0x534e5f5345504f4c4941")?) .build() .await?; // Create account let account = env.create_account().await?; // Deploy and interact with contracts // ... Ok(()) }
Core Components
Environment
The Environment struct is the main entry point. It encapsulates:
- Starknet Devnet instance
- Block production control
- State management
- Account and contract operations
Learn more about Environment →
Middleware
The CheatingProvider implements the Provider trait from starknet-rs with additional testing methods:
- Standard RPC calls
- Time manipulation
- Balance manipulation
- Storage access
- Impersonation
Key Features
🔧 Full Control
Complete control over the blockchain state:
#![allow(unused)] fn main() { // Control block production env.mine_block().await?; env.mine_blocks(10).await?; // Manipulate time env.increase_time(3600).await?; // +1 hour env.set_timestamp(timestamp).await?; // Take snapshots let snapshot = env.snapshot().await?; // ... make changes ... env.restore(snapshot).await?; }
🚀 High Performance
Local execution with no network latency:
- Instant transaction confirmation
- Rapid state queries
- Fast iteration cycles
🔌 Compatible
Works seamlessly with the Starknet ecosystem:
starknet-rstypes and traitscainomecontract bindings- Standard tooling and libraries
🧪 Testing First
Built specifically for testing scenarios:
#![allow(unused)] fn main() { // Impersonate addresses env.start_prank(contract, impersonator).await?; // Set balances env.set_balance(address, amount).await?; // Manipulate storage env.store(contract, key, value).await?; }
Architecture
Your Code
↓
Environment
↓
CheatingProvider (Middleware)
↓
Starknet Devnet
↓
Blockifier (Sequencer)
Use Cases
Unit Testing
#![allow(unused)] fn main() { #[tokio::test] async fn test_token_transfer() { let env = Environment::builder().build().await?; let account = env.create_account().await?; let token = deploy_token(&account).await?; token.transfer(recipient, amount).await?; let balance = token.balance_of(recipient).await?; assert_eq!(balance, amount); } }
Integration Testing
#![allow(unused)] fn main() { #[tokio::test] async fn test_defi_protocol() { let env = Environment::builder().build().await?; // Deploy multiple contracts let token = deploy_token(&env).await?; let pool = deploy_pool(&env).await?; let router = deploy_router(&env).await?; // Test interactions test_swap(&env, &router, &pool).await?; } }
Fork Testing
#![allow(unused)] fn main() { let env = Environment::builder() .with_fork(mainnet_url, block_num, None) .build() .await?; // Test against real mainnet state let usdc = ERC20::new(usdc_mainnet_address, &account); let balance = usdc.balance_of(whale).await?; }
Time-Based Testing
#![allow(unused)] fn main() { // Test time-locked features let contract = deploy_timelock(&env).await?; // Try before lock expires assert!(contract.withdraw().await.is_err()); // Fast forward env.increase_time(lock_duration).await?; env.mine_block().await?; // Now succeeds contract.withdraw().await?; }
API Documentation
Environment API
The Environment provides these main capabilities:
Setup & Configuration:
Environment::builder()- Create environment builder.with_chain_id()- Set chain ID.with_block_time()- Configure block production.with_fork()- Fork from live network.build()- Build environment
Block Control:
.mine_block()- Produce one block.mine_blocks(n)- Produce n blocks.get_block_number()- Get current block.get_block()- Get block details
Time Control:
.get_timestamp()- Get current time.set_timestamp()- Set specific time.increase_time()- Advance time
State Management:
.snapshot()- Save state.restore()- Restore state
Account Management:
.create_account()- Create new account.create_single_owner_account()- Create with specific keys.get_predeployed_accounts()- Get test accounts
Contract Management:
.declare_contract()- Declare contract class.deploy_contract()- Deploy contract instance
Middleware API
The CheatingProvider extends standard Provider:
Standard Methods:
.block_number()- Get latest block.get_block_with_tx_hashes()- Get block data.get_transaction()- Get transaction.get_storage_at()- Read storage.get_events()- Query events
Testing Methods:
.start_prank()- Start impersonation.stop_prank()- Stop impersonation.set_balance()- Set ETH balance.store()- Write to storage.load()- Read from storage.snapshot()- Save state.revert()- Restore state
Error Handling
The core crate uses standard Rust error handling:
#![allow(unused)] fn main() { use anyhow::Result; async fn deploy_contract(env: &Environment) -> Result<ContractAddress> { let account = env.create_account().await?; let class_hash = env.declare_contract(&account, contract_json).await?; let address = env.deploy_contract(&account, class_hash, vec![]).await?; Ok(address) } }
Examples
Complete Testing Example
#![allow(unused)] fn main() { use starkbiter_core::environment::Environment; use starkbiter_bindings::erc_20_mintable_oz0::ERC20; use starknet::core::types::Felt; use anyhow::Result; #[tokio::test] async fn test_erc20_operations() -> Result<()> { // Setup let env = Environment::builder() .with_chain_id(Felt::from_hex("0x534e5f5345504f4c4941")?) .build() .await?; let owner = env.create_account().await?; let recipient = env.create_account().await?; // Deploy token let initial_supply = Felt::from(1_000_000u64); let token = ERC20::deploy( &owner, "Test Token", "TEST", 18, initial_supply, owner.address(), ).await?; // Test balance let balance = token.balance_of(owner.address()).await?; assert_eq!(balance, initial_supply); // Test transfer let transfer_amount = Felt::from(1000u64); token.transfer(recipient.address(), transfer_amount).await?; let recipient_balance = token.balance_of(recipient.address()).await?; assert_eq!(recipient_balance, transfer_amount); Ok(()) } }
Best Practices
1. Use Builder Pattern
Always configure environments with the builder:
#![allow(unused)] fn main() { let env = Environment::builder() .with_chain_id(chain_id) .with_block_time(10) .build() .await?; }
2. Handle Errors Properly
Use Result and ? operator:
#![allow(unused)] fn main() { async fn setup_test() -> Result<(Environment, Account)> { let env = Environment::builder().build().await?; let account = env.create_account().await?; Ok((env, account)) } }
3. Clean Up Resources
Leverage Rust's ownership for automatic cleanup:
#![allow(unused)] fn main() { #[tokio::test] async fn my_test() -> Result<()> { let env = Environment::builder().build().await?; // Test code Ok(()) } // Environment automatically cleaned up }
4. Use Snapshots for Isolation
#![allow(unused)] fn main() { let snapshot = env.snapshot().await?; for test_case in test_cases { env.restore(snapshot).await?; run_test_case(&env, test_case).await?; } }
Performance Tips
Parallel Testing
Run independent tests in parallel:
#![allow(unused)] fn main() { #[tokio::test(flavor = "multi_thread")] async fn parallel_test() { // Each test gets its own environment // Tests run concurrently } }
Batch Operations
Group related operations:
#![allow(unused)] fn main() { use tokio::try_join; let (balance, nonce, storage) = try_join!( env.get_balance(address), env.get_nonce(address), env.get_storage_at(contract, key), )?; }
Next Steps
- Environment API - Detailed environment documentation
- Middleware API - Detailed middleware documentation
- Examples - Working code examples
- Advanced Topics - Advanced techniques
Environment
The Environment is the core abstraction in Starkbiter, representing a sandboxed Starknet instance. It provides complete control over blockchain state, block production, and contract interaction.
Overview
An Environment wraps a Starknet Devnet instance, giving you:
- Full JSON-RPC capabilities
- Additional testing methods (cheating methods)
- State management and control
- Account creation and management
Think of it as your personal Starknet network that you have complete control over.
Creating an Environment
Basic Setup
use starkbiter_core::environment::Environment; use starknet::core::types::Felt; #[tokio::main] async fn main() -> anyhow::Result<()> { let env = Environment::builder() .with_chain_id(Felt::from_hex("0x534e5f5345504f4c4941")?) .build() .await?; println!("Environment ready!"); Ok(()) }
Builder Pattern
The EnvironmentBuilder provides a fluent API for configuration:
#![allow(unused)] fn main() { let env = Environment::builder() .with_chain_id(chain_id) .with_gas_price(100_000_000_000) // 100 gwei .with_block_time(10) // 10 seconds per block .build() .await?; }
Configuration Options
Chain ID
Specify which network to simulate:
#![allow(unused)] fn main() { // Starknet Mainnet let mainnet_id = Felt::from_hex("0x534e5f4d41494e")?; // Starknet Sepolia Testnet let sepolia_id = Felt::from_hex("0x534e5f5345504f4c4941")?; let env = Environment::builder() .with_chain_id(sepolia_id) .build() .await?; }
Block Time
Control block production:
#![allow(unused)] fn main() { // Automatic block production every 5 seconds let env = Environment::builder() .with_block_time(5) .build() .await?; // Manual block production let env = Environment::builder() .with_block_time(0) // 0 = manual mode .build() .await?; }
Gas Configuration
Set gas prices:
#![allow(unused)] fn main() { let env = Environment::builder() .with_gas_price(50_000_000_000) // 50 gwei .build() .await?; }
State Management
Block Production
Control when blocks are produced:
#![allow(unused)] fn main() { // Manual block production env.mine_block().await?; // Mine multiple blocks env.mine_blocks(10).await?; // Get current block number let block_num = env.get_block_number().await?; println!("Current block: {}", block_num); }
Time Manipulation
Control blockchain time:
#![allow(unused)] fn main() { // Increase time by 1 hour env.increase_time(3600).await?; // Set specific timestamp env.set_timestamp(1234567890).await?; // Get current timestamp let timestamp = env.get_timestamp().await?; }
State Snapshots
Save and restore state:
#![allow(unused)] fn main() { // Take a snapshot let snapshot_id = env.snapshot().await?; // Make some changes contract.do_something().await?; // Restore to snapshot env.restore(snapshot_id).await?; }
Account Management
Creating Accounts
#![allow(unused)] fn main() { use starknet::core::types::Felt; // Create with random keys let account = env.create_account().await?; // Create with specific keys let private_key = Felt::from_hex("0x123...")? ; let account_address = Felt::from_hex("0x456...")?; let account = env.create_single_owner_account( private_key, account_address ).await?; }
Predeployed Accounts
Devnet comes with predeployed accounts for testing:
#![allow(unused)] fn main() { // Get predeployed accounts let accounts = env.get_predeployed_accounts().await?; for account in accounts { println!("Address: {:#x}", account.address); println!("Private Key: {:#x}", account.private_key); } }
Contract Management
Declaring Contracts
Before deploying, contracts must be declared:
#![allow(unused)] fn main() { use std::fs; // Read contract JSON let contract_json = fs::read_to_string("path/to/contract.json")?; // Declare the contract let class_hash = env.declare_contract( &account, contract_json ).await?; println!("Contract declared: {:#x}", class_hash); }
Deploying Contracts
#![allow(unused)] fn main() { use starknet::core::types::Felt; // Deploy with constructor args let constructor_args = vec![ Felt::from(1000u64), // Initial supply Felt::from_hex("0x...")?, // Owner address ]; let contract_address = env.deploy_contract( &account, class_hash, constructor_args ).await?; println!("Contract deployed at: {:#x}", contract_address); }
Using Bindings
With cainome-generated bindings:
#![allow(unused)] fn main() { use starkbiter_bindings::erc_20_mintable_oz0::ERC20; // Deploy using binding let erc20 = ERC20::deploy( &account, name, symbol, decimals, initial_supply, recipient ).await?; // Interact with contract let balance = erc20.balance_of(address).await?; }
Querying State
Block Information
#![allow(unused)] fn main() { // Get latest block let block = env.get_block_latest().await?; println!("Block number: {}", block.block_number); println!("Timestamp: {}", block.timestamp); // Get specific block let block = env.get_block_by_number(100).await?; }
Transaction Information
#![allow(unused)] fn main() { // Get transaction by hash let tx = env.get_transaction(tx_hash).await?; // Get transaction receipt let receipt = env.get_transaction_receipt(tx_hash).await?; // Get transaction status let status = env.get_transaction_status(tx_hash).await?; }
Contract State
#![allow(unused)] fn main() { // Get contract storage let storage_value = env.get_storage_at( contract_address, storage_key ).await?; // Get contract nonce let nonce = env.get_nonce(contract_address).await?; // Get contract class let contract_class = env.get_class_at(contract_address).await?; }
Event Handling
Polling for Events
#![allow(unused)] fn main() { // Get events from latest block let events = env.get_events( from_block, to_block, contract_address, keys ).await?; for event in events { println!("Event: {:?}", event); } }
Event Filtering
#![allow(unused)] fn main() { use starknet::core::types::EventFilter; let filter = EventFilter { from_block: Some(0), to_block: Some(100), address: Some(contract_address), keys: Some(vec![event_key]), }; let events = env.get_events_filtered(filter).await?; }
Forking
Fork from live networks:
#![allow(unused)] fn main() { use url::Url; use std::str::FromStr; let env = Environment::builder() .with_chain_id(mainnet_id) .with_fork( Url::from_str("https://starknet-mainnet.public.blastapi.io")?, 12345, // Block number Some(Felt::from_hex("0xblock_hash")?), ) .build() .await?; }
See Forking for more details.
Cheating Methods
Starkbiter provides additional testing methods:
Impersonation
#![allow(unused)] fn main() { // Impersonate an address env.start_prank(target_address, impersonator_address).await?; // Make calls as the impersonator contract.privileged_function().await?; // Stop impersonating env.stop_prank(target_address).await?; }
Balance Manipulation
#![allow(unused)] fn main() { // Set ETH balance env.set_balance(address, amount).await?; // Get balance let balance = env.get_balance(address).await?; }
Storage Manipulation
#![allow(unused)] fn main() { // Write directly to storage env.store( contract_address, storage_key, value ).await?; // Load from storage let value = env.load(contract_address, storage_key).await?; }
Cleanup and Shutdown
Environments are automatically cleaned up when dropped:
#![allow(unused)] fn main() { { let env = Environment::builder().build().await?; // Use env } // env is dropped, resources cleaned up }
Explicit shutdown:
#![allow(unused)] fn main() { env.shutdown().await?; }
Best Practices
1. Use Builder Pattern
Always use the builder for consistent configuration:
#![allow(unused)] fn main() { let env = Environment::builder() .with_chain_id(chain_id) .build() .await?; }
2. Error Handling
Always handle environment errors:
#![allow(unused)] fn main() { match env.get_block_number().await { Ok(block) => println!("Block: {}", block), Err(e) => eprintln!("Error: {}", e), } }
3. Resource Management
Create environments in appropriate scopes:
#![allow(unused)] fn main() { #[tokio::test] async fn test_contract() -> Result<()> { let env = Environment::builder().build().await?; // Test code Ok(()) } // Environment cleaned up automatically }
4. Snapshots for Testing
Use snapshots to isolate test cases:
#![allow(unused)] fn main() { let snapshot = env.snapshot().await?; // Test case 1 run_test_1(&env).await?; env.restore(snapshot).await?; // Test case 2 run_test_2(&env).await?; env.restore(snapshot).await?; }
Common Patterns
Deploy and Initialize
#![allow(unused)] fn main() { async fn deploy_and_initialize(env: &Environment) -> Result<ContractAddress> { let account = env.create_account().await?; // Declare let class_hash = env.declare_contract(&account, contract_json).await?; // Deploy let address = env.deploy_contract(&account, class_hash, vec![]).await?; // Initialize let contract = MyContract::new(address, &account); contract.initialize().await?; Ok(address) } }
Time-Based Testing
#![allow(unused)] fn main() { async fn test_time_lock(env: &Environment) -> Result<()> { let contract = deploy_timelock(&env).await?; // Try before time lock expires (should fail) assert!(contract.withdraw().await.is_err()); // Fast forward env.increase_time(86400).await?; // +24 hours env.mine_block().await?; // Now should succeed contract.withdraw().await?; Ok(()) } }
Next Steps
- Middleware - Understanding the middleware layer
- Forking - State forking from live networks
- Usage Guide - Detailed API reference
Middleware
The middleware layer in Starkbiter provides a familiar interface for interacting with Starknet contracts, compatible with the starknet-rs ecosystem. This chapter explains how middleware works and how to use it effectively.
Overview
Starkbiter's middleware implements the standard Provider trait from starknet-rs, extended with additional "cheating" methods for testing. This means:
- Familiar API: If you know
starknet-rs, you know Starkbiter - Seamless Integration: Works with
cainomebindings and other tooling - Extended Capabilities: Additional methods for testing scenarios
The CheatingProvider
The CheatingProvider is the main middleware implementation in Starkbiter.
#![allow(unused)] fn main() { use starkbiter_core::middleware::CheatingProvider; // Access through environment let provider = env.provider(); // Use standard Provider methods let block_number = provider.block_number().await?; let block = provider.get_block_with_tx_hashes(block_number).await?; }
Standard Provider Methods
Block Queries
#![allow(unused)] fn main() { use starknet::providers::Provider; // Get latest block number let block_num = provider.block_number().await?; // Get block by number let block = provider.get_block_with_tx_hashes(block_num).await?; // Get block by hash let block = provider.get_block_with_tx_hashes_by_hash(block_hash).await?; }
Transaction Queries
#![allow(unused)] fn main() { // Get transaction by hash let tx = provider.get_transaction_by_hash(tx_hash).await?; // Get transaction receipt let receipt = provider.get_transaction_receipt(tx_hash).await?; // Get transaction status let status = provider.get_transaction_status(tx_hash).await?; }
State Queries
#![allow(unused)] fn main() { // Get storage at address let value = provider.get_storage_at( contract_address, key, block_id ).await?; // Get nonce let nonce = provider.get_nonce( block_id, contract_address ).await?; // Get class hash at contract let class_hash = provider.get_class_hash_at( block_id, contract_address ).await?; }
Contract Class Queries
#![allow(unused)] fn main() { // Get class let class = provider.get_class( block_id, class_hash ).await?; // Get class at contract address let class = provider.get_class_at( block_id, contract_address ).await?; }
Event Queries
#![allow(unused)] fn main() { use starknet::core::types::{EventFilter, EventsPage}; let filter = EventFilter { from_block: Some(BlockId::Number(0)), to_block: Some(BlockId::Number(100)), address: Some(contract_address), keys: None, }; let events: EventsPage = provider.get_events( filter, None, // continuation_token 100, // chunk_size ).await?; }
Cheating Methods
Beyond standard Provider methods, CheatingProvider offers testing-specific capabilities.
Time Manipulation
#![allow(unused)] fn main() { // Get current timestamp let timestamp = provider.get_timestamp().await?; // Set specific timestamp provider.set_timestamp(new_timestamp).await?; // Increase time provider.increase_time(seconds).await?; }
Block Manipulation
#![allow(unused)] fn main() { // Mine a single block provider.mine_block().await?; // Mine multiple blocks provider.mine_blocks(count).await?; // Set block interval provider.set_block_interval(seconds).await?; }
Account Impersonation
#![allow(unused)] fn main() { // Start impersonating an address provider.start_prank(target_contract, impersonator).await?; // Make calls as the impersonator // All calls to target_contract will appear to come from impersonator // Stop impersonating provider.stop_prank(target_contract).await?; }
Balance Manipulation
#![allow(unused)] fn main() { // Set ETH balance provider.set_balance(address, amount).await?; // Deal tokens (if supported) provider.deal(token_address, recipient, amount).await?; }
Storage Manipulation
#![allow(unused)] fn main() { // Write to storage provider.store( contract_address, storage_key, value ).await?; // Read from storage let value = provider.load( contract_address, storage_key ).await?; }
Snapshots
#![allow(unused)] fn main() { // Create snapshot let snapshot_id = provider.snapshot().await?; // Make changes... contract.modify_state().await?; // Restore to snapshot provider.revert(snapshot_id).await?; }
Using with Accounts
The middleware integrates seamlessly with Starknet accounts.
Account Creation
#![allow(unused)] fn main() { use starknet::accounts::{Account, SingleOwnerAccount}; use starknet::signers::LocalWallet; // Create wallet let signer = LocalWallet::from(private_key); // Create account with the provider let account = SingleOwnerAccount::new( provider.clone(), signer, account_address, chain_id, ); }
Signing Transactions
#![allow(unused)] fn main() { use starknet::accounts::Call; // Prepare call let call = Call { to: contract_address, selector: get_selector_from_name("transfer")?, calldata: vec![recipient, amount_low, amount_high], }; // Execute through account let result = account.execute(vec![call]).send().await?; }
Using with Contract Bindings
The middleware works seamlessly with cainome-generated bindings.
Reading from Contracts
#![allow(unused)] fn main() { use starkbiter_bindings::erc_20_mintable_oz0::ERC20; // Create contract instance let token = ERC20::new(token_address, &account); // Read state (calls Provider methods) let balance = token.balance_of(address).await?; let total_supply = token.total_supply().await?; }
Writing to Contracts
#![allow(unused)] fn main() { // Prepare transaction let tx = token.transfer(recipient, amount); // Execute (uses Account methods) let result = tx.send().await?; // Wait for confirmation let receipt = result.wait_for_acceptance().await?; }
Connection Management
The middleware manages the underlying connection to Devnet.
Connection Configuration
#![allow(unused)] fn main() { use starkbiter_core::middleware::Connection; // Default configuration (automatic) let connection = Connection::new()?; // Custom port let connection = Connection::with_port(5050)?; // Custom URL let connection = Connection::with_url("http://localhost:5050")?; }
Health Checks
#![allow(unused)] fn main() { // Check if Devnet is responsive if provider.is_alive().await? { println!("Devnet is running"); } }
Error Handling
The middleware uses standard Starknet error types.
Common Errors
#![allow(unused)] fn main() { use starknet::providers::ProviderError; match provider.get_block_with_tx_hashes(1000000).await { Ok(block) => { // Process block } Err(ProviderError::StarknetError(e)) => { // Starknet-specific error eprintln!("Starknet error: {:?}", e); } Err(e) => { // Other errors eprintln!("Provider error: {:?}", e); } } }
Best Practices
#![allow(unused)] fn main() { // Use Result types async fn get_balance( provider: &CheatingProvider, address: Felt, ) -> Result<Felt> { let balance = provider.get_balance(address).await?; Ok(balance) } // Handle specific error cases async fn safe_get_block( provider: &CheatingProvider, block_num: u64, ) -> Option<Block> { match provider.get_block_with_tx_hashes(block_num).await { Ok(block) => Some(block), Err(e) => { log::warn!("Failed to get block {}: {}", block_num, e); None } } } }
Advanced Patterns
Concurrent Requests
#![allow(unused)] fn main() { use tokio::try_join; // Execute multiple queries in parallel let (balance, nonce, block) = try_join!( provider.get_balance(address), provider.get_nonce(BlockId::Pending, address), provider.block_number(), )?; }
Retry Logic
#![allow(unused)] fn main() { use tokio::time::{sleep, Duration}; async fn get_receipt_with_retry( provider: &CheatingProvider, tx_hash: Felt, max_retries: u32, ) -> Result<TransactionReceipt> { for attempt in 0..max_retries { match provider.get_transaction_receipt(tx_hash).await { Ok(receipt) => return Ok(receipt), Err(e) if attempt < max_retries - 1 => { sleep(Duration::from_millis(100)).await; continue; } Err(e) => return Err(e.into()), } } unreachable!() } }
Polling for Events
#![allow(unused)] fn main() { async fn watch_for_event( provider: &CheatingProvider, contract: Felt, event_key: Felt, ) -> Result<Event> { let mut last_block = provider.block_number().await?; loop { let current_block = provider.block_number().await?; if current_block > last_block { let events = provider.get_events( EventFilter { from_block: Some(BlockId::Number(last_block + 1)), to_block: Some(BlockId::Number(current_block)), address: Some(contract), keys: Some(vec![vec![event_key]]), }, None, 100, ).await?; if let Some(event) = events.events.first() { return Ok(event.clone()); } last_block = current_block; } sleep(Duration::from_millis(100)).await; } } }
Testing with Middleware
Unit Tests
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use starkbiter_core::environment::Environment; #[tokio::test] async fn test_provider_queries() { let env = Environment::builder().build().await.unwrap(); let provider = env.provider(); let block_num = provider.block_number().await.unwrap(); assert_eq!(block_num, 0); } } }
Integration Tests
#![allow(unused)] fn main() { #[tokio::test] async fn test_contract_interaction() { let env = Environment::builder().build().await?; let provider = env.provider(); let account = env.create_account().await?; // Deploy contract let contract = deploy_test_contract(&account).await?; // Interact through provider let call = Call { to: contract.address, selector: get_selector_from_name("set_value")?, calldata: vec![Felt::from(42u64)], }; account.execute(vec![call]).send().await?; // Verify let value = contract.get_value().await?; assert_eq!(value, Felt::from(42u64)); } }
Next Steps
- Environment - Deep dive into Environment
- Forking - State forking from live networks
- Usage Guide - Detailed middleware API
Starkbiter Engine
starkbiter-engine provides high-level abstractions for building complex, multi-agent simulations on Starknet. It sits on top of starkbiter-core and offers ergonomic interfaces for agent-based modeling.
Overview
The engine crate enables you to:
- Create Agents - Autonomous entities with custom behaviors
- Define Behaviors - Reusable action patterns for agents
- Build Worlds - Shared simulation environments
- Orchestrate Universes - Multiple parallel simulations
- Enable Messaging - Inter-agent communication
Installation
Add to your Cargo.toml:
[dependencies]
starkbiter-engine = "0.1"
starkbiter-core = "0.1"
tokio = { version = "1.0", features = ["full"] }
Quick Start
use starkbiter_core::environment::Environment; use starkbiter_engine::{Agent, Behavior, World}; use anyhow::Result; // Define a behavior struct TradingBehavior; impl Behavior for TradingBehavior { async fn execute(&mut self, world: &World) -> Result<()> { // Agent logic here println!("Executing trading strategy"); Ok(()) } } #[tokio::main] async fn main() -> Result<()> { // Create environment let env = Environment::builder().build().await?; // Create world let world = World::new(env); // Create and add agents let trader = Agent::new("trader", TradingBehavior); world.add_agent(trader); // Run simulation world.run().await?; Ok(()) }
Core Concepts
Agents
Agents are autonomous entities that execute behaviors. They can:
- React to blockchain events
- Maintain internal state
- Communicate with other agents
- Execute transactions
Behaviors
Behaviors define what agents do. They are:
- Reusable action patterns
- Composable and modular
- Event-driven or scheduled
- Stateful or stateless
Worlds
Worlds provide the simulation environment:
- Shared blockchain state
- Agent coordination
- Event distribution
- Execution scheduling
Universes
Universes manage multiple worlds:
- Parallel simulations
- Cross-world analytics
- Resource management
- Coordinated execution
Architecture
Universe
├─ World 1
│ ├─ Agent A (Behavior 1)
│ ├─ Agent B (Behavior 2)
│ └─ Environment
└─ World 2
├─ Agent C (Behavior 3)
└─ Environment
Key Features
🤖 Agent-Based Modeling
Build sophisticated simulations with multiple autonomous agents:
#![allow(unused)] fn main() { // Create different agent types let liquidator = Agent::new("liquidator", LiquidatorBehavior); let borrower = Agent::new("borrower", BorrowerBehavior); let lender = Agent::new("lender", LenderBehavior); // Add to world world.add_agent(liquidator); world.add_agent(borrower); world.add_agent(lender); // Agents interact autonomously world.run().await?; }
📡 Event-Driven Architecture
Agents react to blockchain events:
#![allow(unused)] fn main() { impl Behavior for ArbitrageBehavior { async fn on_event(&mut self, event: Event) -> Result<()> { if event.name == "Swap" { // Check for arbitrage opportunity if let Some(profit) = self.check_arbitrage(&event).await? { self.execute_arbitrage(profit).await?; } } Ok(()) } } }
💬 Inter-Agent Communication
Agents can message each other:
#![allow(unused)] fn main() { // Agent A sends message world.send_message("agent-b", Message::RequestPrice).await?; // Agent B receives and responds impl Behavior for PriceOracleBehavior { async fn on_message(&mut self, msg: Message) -> Result<()> { match msg { Message::RequestPrice => { let price = self.get_current_price().await?; self.respond(Message::PriceUpdate(price)).await?; } _ => {} } Ok(()) } } }
⚙️ Configuration-Driven
Define simulations in TOML:
# config.toml
[environment]
chain_id = "0x534e5f5345504f4c4941"
block_time = 10
[agents.liquidator]
behavior = "LiquidatorBehavior"
threshold = 0.8
[agents.borrower]
behavior = "BorrowerBehavior"
risk_profile = "aggressive"
Learn more about Configuration →
Use Cases
DeFi Protocol Testing
Simulate complex DeFi scenarios:
#![allow(unused)] fn main() { // Setup lending protocol let world = create_lending_world().await?; // Add diverse agents world.add_agent(Agent::new("whale-lender", WhaleLender)); world.add_agent(Agent::new("retail-borrower", RetailBorrower)); world.add_agent(Agent::new("liquidator-bot", LiquidatorBot)); world.add_agent(Agent::new("oracle", PriceOracle)); // Simulate market conditions world.run_for_blocks(1000).await?; // Analyze results let metrics = world.get_metrics(); assert!(metrics.protocol_health > 0.95); }
Economic Modeling
Model economic systems:
#![allow(unused)] fn main() { // AMM simulation struct LiquidityProvider; struct Arbitrageur; struct RetailTrader; let world = World::new(env); world.add_agent(Agent::new("lp-1", LiquidityProvider)); world.add_agent(Agent::new("arb-1", Arbitrageur)); world.add_agent(Agent::new("trader-1", RetailTrader)); // Simulate trading activity world.run().await?; // Analyze pool dynamics let pool_metrics = analyze_pool_behavior(&world); }
Stress Testing
Test protocol under extreme conditions:
#![allow(unused)] fn main() { // Create stress test scenario let world = setup_stress_test().await?; // Add malicious agents world.add_agent(Agent::new("attacker", FlashLoanAttacker)); world.add_agent(Agent::new("price-manipulator", PriceManipulator)); // Run attack scenarios world.run_until(conditions_met).await?; // Verify protocol safety assert!(protocol_remains_solvent(&world)); }
Strategy Development
Develop and test trading strategies:
#![allow(unused)] fn main() { struct BacktestBehavior { strategy: TradingStrategy, performance: PerformanceTracker, } impl Behavior for BacktestBehavior { async fn execute(&mut self, world: &World) -> Result<()> { let signal = self.strategy.generate_signal(world).await?; if let Some(trade) = signal { let result = execute_trade(world, trade).await?; self.performance.record(result); } Ok(()) } } }
Agent Lifecycle
Create → Initialize → Register Events → Execute → Cleanup
Creation
#![allow(unused)] fn main() { let agent = Agent::new("my-agent", MyBehavior::new()); }
Initialization
#![allow(unused)] fn main() { impl Behavior for MyBehavior { async fn init(&mut self, world: &World) -> Result<()> { // Setup state, deploy contracts, etc. self.contract = deploy_contract(world).await?; Ok(()) } } }
Event Registration
#![allow(unused)] fn main() { impl Behavior for MyBehavior { fn events(&self) -> Vec<EventFilter> { vec![ EventFilter::contract_event(self.contract, "Transfer"), EventFilter::contract_event(self.contract, "Approval"), ] } } }
Execution
#![allow(unused)] fn main() { impl Behavior for MyBehavior { async fn execute(&mut self, world: &World) -> Result<()> { // Main agent logic self.process_events(world).await?; self.update_state(world).await?; Ok(()) } } }
Behavior Patterns
Reactive Behavior
Respond to events:
#![allow(unused)] fn main() { impl Behavior for ReactiveBehavior { async fn on_event(&mut self, event: Event) -> Result<()> { match event.name.as_str() { "Swap" => self.handle_swap(event).await?, "Mint" => self.handle_mint(event).await?, _ => {} } Ok(()) } } }
Scheduled Behavior
Execute on schedule:
#![allow(unused)] fn main() { impl Behavior for ScheduledBehavior { fn schedule(&self) -> Schedule { Schedule::Every(Duration::from_secs(60)) // Every minute } async fn execute(&mut self, world: &World) -> Result<()> { // Periodic action self.rebalance_portfolio(world).await?; Ok(()) } } }
Stateful Behavior
Maintain state across executions:
#![allow(unused)] fn main() { struct StatefulBehavior { state: AgentState, history: Vec<Action>, } impl Behavior for StatefulBehavior { async fn execute(&mut self, world: &World) -> Result<()> { // Use and update state let action = self.state.decide_action(world).await?; self.history.push(action.clone()); self.execute_action(action, world).await?; Ok(()) } } }
Messaging System
High-Level Messaging
#![allow(unused)] fn main() { use starkbiter_engine::messager::{Messager, Message}; // Create messager let messager = Messager::new(); // Agent A subscribes messager.subscribe("agent-a", callback).await?; // Agent B publishes messager.publish("agent-a", Message::Data(value)).await?; }
Custom Messages
#![allow(unused)] fn main() { #[derive(Debug, Clone)] enum CustomMessage { PriceUpdate(u64), TradeSignal { asset: String, action: Action }, Alert(String), } // Send custom message messager.publish("trader", CustomMessage::PriceUpdate(1000)).await?; }
Error Handling
Behavior Errors
#![allow(unused)] fn main() { impl Behavior for MyBehavior { async fn execute(&mut self, world: &World) -> Result<()> { self.risky_operation(world) .await .map_err(|e| anyhow!("Agent failed: {}", e))?; Ok(()) } } }
World-Level Error Handling
#![allow(unused)] fn main() { match world.run().await { Ok(_) => println!("Simulation completed successfully"), Err(e) => { eprintln!("Simulation failed: {}", e); world.dump_state().await?; } } }
Testing Agents
Unit Tests
#![allow(unused)] fn main() { #[tokio::test] async fn test_agent_behavior() { let mut behavior = MyBehavior::new(); let world = create_test_world().await; behavior.init(&world).await.unwrap(); behavior.execute(&world).await.unwrap(); assert_eq!(behavior.get_state(), expected_state); } }
Integration Tests
#![allow(unused)] fn main() { #[tokio::test] async fn test_multi_agent_interaction() { let world = World::new(env); world.add_agent(Agent::new("agent-1", Behavior1)); world.add_agent(Agent::new("agent-2", Behavior2)); world.run_for_blocks(100).await.unwrap(); let results = world.get_results(); assert!(results.agents_interacted_correctly()); } }
Performance Optimization
Parallel Agent Execution
#![allow(unused)] fn main() { // Agents execute in parallel when possible world.set_execution_mode(ExecutionMode::Parallel); world.run().await?; }
Batch Operations
#![allow(unused)] fn main() { impl Behavior for BatchBehavior { async fn execute(&mut self, world: &World) -> Result<()> { // Batch multiple operations let operations = self.prepare_batch(); world.execute_batch(operations).await?; Ok(()) } } }
Best Practices
1. Keep Behaviors Focused
#![allow(unused)] fn main() { // Good: Single responsibility struct LiquidatorBehavior; // Avoid: Too many responsibilities struct GodBehavior; // Don't do this! }
2. Use Composition
#![allow(unused)] fn main() { struct ComposedBehavior { price_oracle: PriceOracle, risk_manager: RiskManager, executor: TradeExecutor, } }
3. Handle Errors Gracefully
#![allow(unused)] fn main() { impl Behavior for RobustBehavior { async fn execute(&mut self, world: &World) -> Result<()> { match self.try_execute(world).await { Ok(_) => Ok(()), Err(e) => { log::error!("Execution failed: {}", e); self.recover().await?; Ok(()) } } } } }
4. Log Extensively
#![allow(unused)] fn main() { impl Behavior for LoggingBehavior { async fn execute(&mut self, world: &World) -> Result<()> { log::info!("Starting execution"); let result = self.do_work(world).await?; log::info!("Completed with result: {:?}", result); Ok(()) } } }
Next Steps
- Agents - Deep dive into agents
- Behaviors - Behavior patterns and examples
- Worlds and Universes - Simulation environments
- Configuration - Configuration-driven simulations
- Examples - Working examples
Agents
Agents are the core building blocks of simulations in Starkbiter Engine. They represent autonomous entities that can interact with the blockchain, respond to events, and communicate with other agents.
Overview
An agent in Starkbiter is an autonomous entity that:
- Executes one or more behaviors
- Maintains its own state
- Reacts to blockchain events
- Communicates with other agents through messaging
- Has access to the blockchain through middleware
Creating Agents
Basic Agent
#![allow(unused)] fn main() { use starkbiter_engine::Agent; // Create agent with a behavior let agent = Agent::new("my-agent", MyBehavior::new()); }
Agent with Multiple Behaviors
#![allow(unused)] fn main() { let mut agent = Agent::new("multi-behavior-agent", PrimaryBehavior); agent.add_behavior(SecondaryBehavior); agent.add_behavior(MonitoringBehavior); }
Agent Structure
#![allow(unused)] fn main() { pub struct Agent { pub id: String, client: Arc<Middleware>, messager: Messager, behaviors: Vec<Box<dyn StateMachine>>, } }
Key Components
- ID: Unique identifier for the agent
- Client: Connection to the blockchain (middleware)
- Messager: For inter-agent communication
- Behaviors: List of behaviors the agent executes
Agent Types
Reactive Agents
Respond to events on the blockchain:
#![allow(unused)] fn main() { struct EventReactiveAgent { target_contract: ContractAddress, } impl Behavior for EventReactiveAgent { async fn on_event(&mut self, event: Event) -> Result<()> { if event.from_address == self.target_contract { // React to events from specific contract self.handle_event(event).await?; } Ok(()) } } }
Proactive Agents
Take initiative based on strategy:
#![allow(unused)] fn main() { struct ProactiveTrader { strategy: TradingStrategy, } impl Behavior for ProactiveTrader { async fn execute(&mut self, world: &World) -> Result<()> { // Generate trading signal if let Some(trade) = self.strategy.generate_signal().await? { self.execute_trade(trade).await?; } Ok(()) } } }
Hybrid Agents
Combine reactive and proactive behaviors:
#![allow(unused)] fn main() { struct HybridAgent { reactive: EventHandler, proactive: StrategyExecutor, } }
Agent Lifecycle
1. Creation
#![allow(unused)] fn main() { let agent = Agent::new("agent-id", behavior); }
2. Initialization
#![allow(unused)] fn main() { impl Behavior for MyBehavior { async fn init(&mut self, world: &World) -> Result<()> { // Deploy contracts, load state, etc. self.contract = deploy_my_contract(world).await?; Ok(()) } } }
3. Execution
#![allow(unused)] fn main() { // Agent added to world world.add_agent(agent); // World runs agents world.run().await?; }
4. Cleanup
Agents are automatically cleaned up when dropped.
Agent Communication
Sending Messages
#![allow(unused)] fn main() { impl Behavior for SenderAgent { async fn execute(&mut self, world: &World) -> Result<()> { // Send message to another agent world.send_message( "receiver-agent", Message::custom("price-update", 1000) ).await?; Ok(()) } } }
Receiving Messages
#![allow(unused)] fn main() { impl Behavior for ReceiverAgent { async fn on_message(&mut self, msg: Message) -> Result<()> { match msg { Message::Custom { topic, data } if topic == "price-update" => { self.handle_price_update(data).await?; } _ => {} } Ok(()) } } }
Agent Patterns
The Observer
Monitors contract state and reports:
#![allow(unused)] fn main() { struct ObserverAgent { watched_contracts: Vec<ContractAddress>, alert_threshold: u64, } impl Behavior for ObserverAgent { async fn execute(&mut self, world: &World) -> Result<()> { for contract in &self.watched_contracts { let value = self.check_value(contract).await?; if value > self.alert_threshold { world.send_alert(format!("Threshold exceeded: {}", value)).await?; } } Ok(()) } } }
The Executor
Executes transactions based on conditions:
#![allow(unused)] fn main() { struct ExecutorAgent { pending_txs: Vec<Transaction>, } impl Behavior for ExecutorAgent { async fn execute(&mut self, world: &World) -> Result<()> { for tx in &self.pending_txs { if self.should_execute(tx).await? { self.submit_transaction(tx, world).await?; } } Ok(()) } } }
The Coordinator
Coordinates actions between multiple agents:
#![allow(unused)] fn main() { struct CoordinatorAgent { managed_agents: Vec<String>, } impl Behavior for CoordinatorAgent { async fn execute(&mut self, world: &World) -> Result<()> { // Send commands to managed agents for agent_id in &self.managed_agents { world.send_message( agent_id, Message::Command(Action::Execute) ).await?; } Ok(()) } } }
Best Practices
1. Single Responsibility
Each agent should have a clear, focused purpose:
#![allow(unused)] fn main() { // Good: Focused agent struct LiquidatorAgent; // Avoid: Too many responsibilities struct DoEverythingAgent; // Don't do this }
2. State Management
Keep agent state minimal and well-organized:
#![allow(unused)] fn main() { struct WellOrganizedAgent { config: AgentConfig, state: AgentState, metrics: PerformanceMetrics, } }
3. Error Handling
Handle errors gracefully:
#![allow(unused)] fn main() { impl Behavior for RobustAgent { async fn execute(&mut self, world: &World) -> Result<()> { match self.try_action(world).await { Ok(_) => Ok(()), Err(e) => { log::error!("Action failed: {}", e); self.recover().await?; Ok(()) } } } } }
4. Logging
Add comprehensive logging:
#![allow(unused)] fn main() { impl Behavior for LoggingAgent { async fn execute(&mut self, world: &World) -> Result<()> { log::info!("Agent {} starting execution", self.id); // ... execution logic log::debug!("Agent {} completed execution", self.id); Ok(()) } } }
Testing Agents
Unit Tests
#![allow(unused)] fn main() { #[tokio::test] async fn test_agent_behavior() { let mut agent = TestAgent::new(); let world = create_test_world().await; agent.init(&world).await.unwrap(); agent.execute(&world).await.unwrap(); assert_eq!(agent.action_count, 1); } }
Integration Tests
#![allow(unused)] fn main() { #[tokio::test] async fn test_agent_interaction() { let world = World::new(env); let agent1 = Agent::new("agent-1", Behavior1); let agent2 = Agent::new("agent-2", Behavior2); world.add_agent(agent1); world.add_agent(agent2); world.run_for_blocks(10).await.unwrap(); assert!(agents_interacted_correctly(&world)); } }
Examples
See the minter example for a complete agent implementation.
Next Steps
- Behaviors - Define agent actions
- Worlds and Universes - Simulation environments
- Configuration - Configure agents via files
Behaviors
Behaviors define what agents do in a simulation. They are the core logic that determines how agents interact with the blockchain, respond to events, and communicate with other agents.
Overview
A Behavior is a trait that defines:
- How an agent initializes
- How it responds to events
- How it executes periodic actions
- How it processes messages
The Behavior Trait
#![allow(unused)] fn main() { pub trait Behavior: Send + Sync { /// Initialize the behavior async fn init(&mut self, world: &World) -> Result<()> { Ok(()) } /// Main execution logic async fn execute(&mut self, world: &World) -> Result<()>; /// Handle blockchain events async fn on_event(&mut self, event: Event) -> Result<()> { Ok(()) } /// Handle messages from other agents async fn on_message(&mut self, message: Message) -> Result<()> { Ok(()) } /// Define which events to subscribe to fn events(&self) -> Vec<EventFilter> { vec![] } } }
Creating Behaviors
Simple Behavior
#![allow(unused)] fn main() { struct SimpleBehavior { counter: u64, } impl Behavior for SimpleBehavior { async fn execute(&mut self, world: &World) -> Result<()> { self.counter += 1; println!("Executed {} times", self.counter); Ok(()) } } }
Event-Driven Behavior
#![allow(unused)] fn main() { struct EventDrivenBehavior { contract_address: Felt, } impl Behavior for EventDrivenBehavior { fn events(&self) -> Vec<EventFilter> { vec![ EventFilter::contract_event(self.contract_address, "Transfer"), ] } async fn on_event(&mut self, event: Event) -> Result<()> { match event.name.as_str() { "Transfer" => self.handle_transfer(event).await?, _ => {} } Ok(()) } } }
Stateful Behavior
#![allow(unused)] fn main() { struct StatefulBehavior { state: AgentState, history: Vec<Action>, } impl Behavior for StatefulBehavior { async fn init(&mut self, world: &World) -> Result<()> { self.state = self.load_initial_state(world).await?; Ok(()) } async fn execute(&mut self, world: &World) -> Result<()> { let action = self.state.decide_next_action(world).await?; self.history.push(action.clone()); self.execute_action(action, world).await?; Ok(()) } } }
Behavior Patterns
The Trading Bot
#![allow(unused)] fn main() { struct TradingBotBehavior { strategy: Box<dyn TradingStrategy>, portfolio: Portfolio, } impl Behavior for TradingBotBehavior { async fn execute(&mut self, world: &World) -> Result<()> { // Get market data let prices = self.fetch_prices(world).await?; // Generate signal let signal = self.strategy.analyze(&prices, &self.portfolio).await?; // Execute if signal is strong enough if signal.strength > 0.8 { self.execute_trade(world, signal.trade).await?; } Ok(()) } } }
The Liquidator
#![allow(unused)] fn main() { struct LiquidatorBehavior { lending_protocol: ContractAddress, min_profit: u64, } impl Behavior for LiquidatorBehavior { fn events(&self) -> Vec<EventFilter> { vec![ EventFilter::contract_event(self.lending_protocol, "Borrow"), EventFilter::contract_event(self.lending_protocol, "PriceUpdate"), ] } async fn on_event(&mut self, event: Event) -> Result<()> { // Check for liquidation opportunities let positions = self.get_unhealthy_positions(event).await?; for position in positions { if let Some(profit) = self.calculate_profit(&position).await? { if profit > self.min_profit { self.liquidate(position).await?; } } } Ok(()) } } }
The Market Maker
#![allow(unused)] fn main() { struct MarketMakerBehavior { pool: ContractAddress, spread: f64, inventory: Inventory, } impl Behavior for MarketMakerBehavior { async fn execute(&mut self, world: &World) -> Result<()> { // Update quotes let mid_price = self.get_mid_price(world).await?; let bid = mid_price * (1.0 - self.spread); let ask = mid_price * (1.0 + self.spread); // Place orders self.place_limit_order(world, Side::Buy, bid).await?; self.place_limit_order(world, Side::Sell, ask).await?; // Rebalance inventory self.rebalance_if_needed(world).await?; Ok(()) } } }
The Oracle
#![allow(unused)] fn main() { struct OracleBehavior { price_feeds: Vec<PriceFeed>, } impl Behavior for OracleBehavior { async fn on_message(&mut self, msg: Message) -> Result<()> { match msg { Message::PriceRequest { asset } => { let price = self.fetch_price(&asset).await?; self.respond(Message::PriceResponse { asset, price }).await?; } _ => {} } Ok(()) } async fn execute(&mut self, world: &World) -> Result<()> { // Periodically update prices for feed in &self.price_feeds { let price = feed.fetch_latest().await?; world.broadcast(Message::PriceUpdate { asset: feed.asset.clone(), price, }).await?; } Ok(()) } } }
Composing Behaviors
Behavior Composition
Combine multiple behaviors:
#![allow(unused)] fn main() { struct ComposedBehavior { monitor: MonitoringBehavior, executor: ExecutionBehavior, reporter: ReportingBehavior, } impl Behavior for ComposedBehavior { async fn execute(&mut self, world: &World) -> Result<()> { // Execute in sequence self.monitor.execute(world).await?; self.executor.execute(world).await?; self.reporter.execute(world).await?; Ok(()) } } }
Conditional Behavior
Execute behaviors conditionally:
#![allow(unused)] fn main() { impl Behavior for ConditionalBehavior { async fn execute(&mut self, world: &World) -> Result<()> { if self.should_trade(world).await? { self.trading_behavior.execute(world).await?; } else { self.monitoring_behavior.execute(world).await?; } Ok(()) } } }
Testing Behaviors
Unit Tests
#![allow(unused)] fn main() { #[tokio::test] async fn test_behavior_logic() { let mut behavior = MyBehavior::new(); let world = create_mock_world(); behavior.init(&world).await.unwrap(); behavior.execute(&world).await.unwrap(); assert_eq!(behavior.state, ExpectedState); } }
Mock World
#![allow(unused)] fn main() { struct MockWorld { // Minimal world for testing } impl MockWorld { fn new() -> Self { // Create test environment Self {} } } }
Best Practices
1. Keep Behaviors Focused
#![allow(unused)] fn main() { // Good: Single purpose struct SwapExecutor; // Avoid: Too many responsibilities struct DoEverything; // Don't do this }
2. Make Behaviors Reusable
#![allow(unused)] fn main() { struct ReusableBehavior<S: Strategy> { strategy: S, } // Can be used with different strategies let behavior1 = ReusableBehavior { strategy: ConservativeStrategy }; let behavior2 = ReusableBehavior { strategy: AggressiveStrategy }; }
3. Handle Errors Gracefully
#![allow(unused)] fn main() { impl Behavior for RobustBehavior { async fn execute(&mut self, world: &World) -> Result<()> { self.try_execute(world).await.or_else(|e| { log::error!("Execution failed: {}", e); self.fallback(world) }) } } }
4. Add Logging
#![allow(unused)] fn main() { impl Behavior for LoggedBehavior { async fn execute(&mut self, world: &World) -> Result<()> { log::info!("Starting execution"); let result = self.do_work(world).await; log::info!("Execution completed: {:?}", result); result } } }
Examples
See the examples for complete behavior implementations.
Next Steps
- Agents - Creating agents with behaviors
- Worlds and Universes - Running simulations
- Configuration - Configuring behaviors
Worlds and Universes
Worlds and Universes are the containers and orchestrators for simulations in Starkbiter Engine.
Worlds
A World represents a single simulation environment where agents interact with a shared blockchain state.
Creating a World
#![allow(unused)] fn main() { use starkbiter_core::environment::Environment; use starkbiter_engine::World; let env = Environment::builder().build().await?; let world = World::new(env); }
Adding Agents
#![allow(unused)] fn main() { world.add_agent(Agent::new("trader", TradingBehavior)); world.add_agent(Agent::new("liquidator", LiquidatorBehavior)); world.add_agent(Agent::new("oracle", OracleBehavior)); }
Running Simulations
#![allow(unused)] fn main() { // Run until completion world.run().await?; // Run for specific number of blocks world.run_for_blocks(1000).await?; // Run until condition met world.run_until(|w| w.condition_met()).await?; }
Universes
A Universe manages multiple parallel worlds, enabling complex multi-world simulations and comparisons.
Creating a Universe
#![allow(unused)] fn main() { use starkbiter_engine::Universe; let universe = Universe::new(); }
Adding Worlds
#![allow(unused)] fn main() { // Create worlds with different configurations let world1 = create_conservative_world().await?; let world2 = create_aggressive_world().await?; universe.add_world("conservative", world1); universe.add_world("aggressive", world2); }
Running Multiple Worlds
#![allow(unused)] fn main() { // Run all worlds in parallel universe.run_all().await?; // Compare results let results = universe.compare_results(); }
World API
State Queries
#![allow(unused)] fn main() { // Get current block let block = world.get_block_number().await?; // Get world state let state = world.get_state(); // Get metrics let metrics = world.get_metrics(); }
Agent Management
#![allow(unused)] fn main() { // Get agent by ID let agent = world.get_agent("trader")?; // List all agents let agents = world.list_agents(); // Remove agent world.remove_agent("trader")?; }
Messaging
#![allow(unused)] fn main() { // Send message to agent world.send_message("receiver", Message::Data(value)).await?; // Broadcast to all agents world.broadcast(Message::Alert("Important".to_string())).await?; }
Use Cases
Scenario Testing
#![allow(unused)] fn main() { // Test different scenarios in separate worlds let bear_market = create_world_with_params(MarketCondition::Bear).await?; let bull_market = create_world_with_params(MarketCondition::Bull).await?; universe.add_world("bear", bear_market); universe.add_world("bull", bull_market); universe.run_all().await?; }
Parameter Sweeps
#![allow(unused)] fn main() { // Test multiple parameter combinations for gas_price in [10, 50, 100, 500] { let world = create_world_with_gas(gas_price).await?; universe.add_world(&format!("gas-{}", gas_price), world); } universe.run_all().await?; let optimal = universe.find_optimal_parameters(); }
A/B Testing
#![allow(unused)] fn main() { // Compare strategy variations let strategy_a = World::new(env1); strategy_a.add_agent(Agent::new("trader", StrategyA)); let strategy_b = World::new(env2); strategy_b.add_agent(Agent::new("trader", StrategyB)); universe.add_world("A", strategy_a); universe.add_world("B", strategy_b); universe.run_all().await?; universe.compare_performance(); }
Next Steps
- Agents - Creating agents
- Behaviors - Defining behaviors
- Configuration - Configuration files
Configuration
Starkbiter supports configuration-driven simulations using TOML files. This allows you to define simulations declaratively and separate configuration from code.
Configuration File Structure
# config.toml
[environment]
chain_id = "0x534e5f5345504f4c4941" # Sepolia
block_time = 10 # seconds
gas_price = 100000000000 # 100 gwei
[agents.liquidator]
behavior = "LiquidatorBehavior"
min_profit = 1000
check_interval = 60
[agents.trader]
behavior = "TradingBehavior"
initial_capital = 10000
risk_tolerance = 0.7
[agents.oracle]
behavior = "OracleBehavior"
update_frequency = 30
price_feeds = ["ETH/USD", "BTC/USD"]
Loading Configuration
#![allow(unused)] fn main() { use serde::Deserialize; use std::fs; #[derive(Deserialize)] struct SimulationConfig { environment: EnvironmentConfig, agents: HashMap<String, AgentConfig>, } let config_str = fs::read_to_string("config.toml")?; let config: SimulationConfig = toml::from_str(&config_str)?; }
Building from Configuration
#![allow(unused)] fn main() { async fn build_from_config(config: SimulationConfig) -> Result<World> { // Build environment let env = Environment::builder() .with_chain_id(Felt::from_hex(&config.environment.chain_id)?) .with_block_time(config.environment.block_time) .build() .await?; let world = World::new(env); // Add agents for (id, agent_config) in config.agents { let behavior = create_behavior(&agent_config)?; world.add_agent(Agent::new(&id, behavior)); } Ok(world) } }
Configuration Examples
DeFi Protocol Testing
[environment]
chain_id = "0x534e5f5345504f4c4941"
block_time = 10
[protocol]
pool_address = "0x..."
router_address = "0x..."
[agents.lender]
behavior = "LenderBehavior"
deposit_amount = 100000
target_apy = 0.05
[agents.borrower]
behavior = "BorrowerBehavior"
collateral_ratio = 1.5
max_leverage = 3
[agents.liquidator]
behavior = "LiquidatorBehavior"
min_profit = 500
Trading Simulation
[environment]
chain_id = "0x534e5f5345504f4c4941"
[agents.market_maker]
behavior = "MarketMakerBehavior"
spread = 0.003
inventory_target = 10000
[agents.arbitrageur]
behavior = "ArbitrageurBehavior"
min_profit_bps = 10
pools = ["pool1", "pool2", "pool3"]
[agents.retail_trader]
behavior = "RetailTraderBehavior"
trade_frequency = 120
average_size = 100
Next Steps
Starkbiter CLI
The Starkbiter command-line interface provides tools for managing your projects and generating contract bindings.
Installation
cargo install starkbiter
Commands
bind - Generate Contract Bindings
Generate Rust bindings from Cairo contract JSON files using cainome.
starkbiter bind [OPTIONS]
Options:
--contracts-dir <DIR>- Directory containing contract JSON files (default:./contracts)--output-dir <DIR>- Output directory for generated bindings (default:./bindings/src)
Example:
# Generate bindings for all contracts in ./contracts/
starkbiter bind
# Custom directories
starkbiter bind --contracts-dir ./my-contracts --output-dir ./src/bindings
Contract Format:
Your contract JSON files should be Sierra 1.0 compiled contracts with ABI:
{
"sierra_program": [...],
"contract_class_version": "0.1.0",
"entry_points_by_type": {...},
"abi": [...]
}
init - Initialize a New Project
(Under development)
Create a new Starkbiter simulation project from a template.
starkbiter init <project-name>
--help - Show Help
Display help information:
starkbiter --help
starkbiter bind --help
Workflow
1. Compile Your Contracts
First, compile your Cairo contracts to Sierra 1.0:
scarb build
This generates JSON files in target/dev/.
2. Copy Contract Files
Copy the contract JSON files to your project:
mkdir contracts
cp target/dev/my_contract.contract_class.json contracts/
3. Generate Bindings
Run the CLI to generate Rust bindings:
starkbiter bind
This creates Rust files in bindings/src/ with typed interfaces for your contracts.
4. Use the Bindings
Import and use the generated bindings in your code:
#![allow(unused)] fn main() { use crate::bindings::my_contract::MyContract; // Deploy let contract = MyContract::deploy(&account, constructor_args).await?; // Call functions let result = contract.my_function(args).await?; }
Project Structure
A typical Starkbiter project structure:
my-project/
├── Cargo.toml
├── contracts/
│ ├── MyContract.json
│ └── MyToken.json
├── bindings/
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs
│ ├── my_contract.rs
│ └── my_token.rs
├── src/
│ ├── main.rs
│ └── behaviors/
│ ├── mod.rs
│ └── trading.rs
└── config.toml
Cargo.toml Setup
Add bindings crate as a dependency:
[package]
name = "my-project"
version = "0.1.0"
edition = "2021"
[dependencies]
starkbiter-core = "0.1"
starkbiter-engine = "0.1"
my-bindings = { path = "./bindings" }
tokio = { version = "1.0", features = ["full"] }
anyhow = "1.0"
Tips
Automatic Regeneration
Use a build script or file watcher to regenerate bindings when contracts change:
# Using cargo-watch
cargo watch -x "run --bin starkbiter -- bind"
CI/CD Integration
Add to your CI pipeline:
# .github/workflows/ci.yml
- name: Generate bindings
run: starkbiter bind
- name: Check bindings are up to date
run: git diff --exit-code bindings/
Multiple Contract Directories
Generate bindings from multiple directories:
starkbiter bind --contracts-dir ./core-contracts
starkbiter bind --contracts-dir ./periphery-contracts
Troubleshooting
Binding Generation Fails
Error: "Failed to parse contract JSON"
Solution: Ensure contracts are compiled with compatible Sierra version:
scarb build --release
Missing Dependencies
Error: "cainome not found"
Solution: The CLI includes cainome, but ensure you have it in your dependencies:
[dependencies]
cainome = "0.3"
Import Errors
Error: "Cannot find module"
Solution: Check your lib.rs in bindings crate includes the generated modules:
#![allow(unused)] fn main() { pub mod my_contract; pub mod my_token; }
Next Steps
- Quick Start - Build your first simulation
- Examples - See bindings in action
- Starkbiter Bindings - Pre-generated bindings
Starkbiter Macros
starkbiter-macros provides procedural macros to reduce boilerplate and improve ergonomics when building simulations.
Overview
The macros crate simplifies common patterns:
- Behavior derivation
- Agent configuration
- Event handling
- Message routing
Installation
[dependencies]
starkbiter-macros = "0.1"
Available Macros
#[behavior]
Automatically implement the Behavior trait:
#![allow(unused)] fn main() { use starkbiter_macros::behavior; #[behavior] struct MyBehavior { counter: u64, } // The macro generates the Behavior implementation impl MyBehavior { async fn execute(&mut self, world: &World) -> Result<()> { self.counter += 1; Ok(()) } } }
#[agent]
Configure agent with derive macro:
#![allow(unused)] fn main() { use starkbiter_macros::agent; #[agent( id = "trader", events = ["Transfer", "Swap"] )] struct TradingAgent { strategy: Strategy, } }
#[event_handler]
Simplify event handling:
#![allow(unused)] fn main() { use starkbiter_macros::event_handler; #[event_handler] impl MyBehavior { #[on_event("Transfer")] async fn handle_transfer(&mut self, event: Event) -> Result<()> { // Handle transfer event Ok(()) } #[on_event("Swap")] async fn handle_swap(&mut self, event: Event) -> Result<()> { // Handle swap event Ok(()) } } }
Usage Examples
Complete Agent with Macros
#![allow(unused)] fn main() { use starkbiter_macros::{behavior, event_handler}; use starkbiter_engine::Behavior; #[behavior] #[event_handler] struct TradingBot { position: Position, profit: u64, } impl TradingBot { #[on_event("PriceUpdate")] async fn on_price_update(&mut self, event: Event) -> Result<()> { let price = parse_price(&event)?; self.update_position(price).await?; Ok(()) } async fn execute(&mut self, world: &World) -> Result<()> { // Main execution logic self.check_exit_conditions(world).await?; Ok(()) } } }
Configuration Macro
#![allow(unused)] fn main() { use starkbiter_macros::config; #[config] struct SimConfig { #[env] chain_id: String, #[agent] trader: TraderConfig, #[agent] liquidator: LiquidatorConfig, } // Automatically load from TOML let config = SimConfig::from_file("config.toml")?; }
Benefits
Reduced Boilerplate
Without macros:
#![allow(unused)] fn main() { struct MyBehavior; impl Behavior for MyBehavior { async fn execute(&mut self, world: &World) -> Result<()> { // Logic Ok(()) } async fn on_event(&mut self, event: Event) -> Result<()> { match event.name.as_str() { "Transfer" => self.handle_transfer(event).await?, "Swap" => self.handle_swap(event).await?, _ => {} } Ok(()) } fn events(&self) -> Vec<EventFilter> { vec![ EventFilter::name("Transfer"), EventFilter::name("Swap"), ] } } }
With macros:
#![allow(unused)] fn main() { #[behavior] #[event_handler] struct MyBehavior; impl MyBehavior { #[on_event("Transfer")] async fn handle_transfer(&mut self, event: Event) -> Result<()> { // Logic Ok(()) } #[on_event("Swap")] async fn handle_swap(&mut self, event: Event) -> Result<()> { // Logic Ok(()) } async fn execute(&mut self, world: &World) -> Result<()> { Ok(()) } } }
Type Safety
Macros provide compile-time checks:
#![allow(unused)] fn main() { #[behavior] struct TypedBehavior { #[validate(min = 0, max = 100)] percentage: u8, } // Compile error if validation fails }
Better IDE Support
Macros generate code that IDEs understand, providing better autocomplete and error messages.
Advanced Usage
Custom Attributes
#![allow(unused)] fn main() { #[behavior( name = "MyBehavior", description = "A sophisticated trading bot" )] struct MyBehavior { #[state] position: Position, #[metric] profit: u64, #[config] risk_tolerance: f64, } }
Conditional Compilation
#![allow(unused)] fn main() { #[behavior] struct DebugBehavior { #[cfg(debug_assertions)] debug_info: DebugInfo, } }
Next Steps
- Starkbiter Core - Core functionality
- Starkbiter Engine - Agent-based simulations
- Examples - See macros in action
Starkbiter Bindings
starkbiter-bindings provides pre-generated Rust bindings for common Starknet contracts, making it easy to interact with standard protocols in your simulations.
Overview
The bindings crate includes:
- ERC20 token contracts
- Account contracts (Argent, OpenZeppelin)
- DEX protocols (Ekubo)
- Test utilities
Installation
[dependencies]
starkbiter-bindings = "0.1"
Available Bindings
ERC20 Tokens
Standard ERC20
#![allow(unused)] fn main() { use starkbiter_bindings::erc_20_mintable_oz0::ERC20; use starknet::core::types::Felt; // Deploy new token let token = ERC20::deploy( &account, "My Token", // name "MTK", // symbol 18, // decimals Felt::from(1_000_000u64), // initial supply owner_address, // recipient ).await?; // Transfer tokens token.transfer(recipient, amount).await?; // Check balance let balance = token.balance_of(address).await?; // Approve spending token.approve(spender, amount).await?; // Check allowance let allowance = token.allowance(owner, spender).await?; }
Account Contracts
Argent Account
#![allow(unused)] fn main() { use starkbiter_bindings::argent_account::ArgentAccount; let account = ArgentAccount::new(account_address, &provider); // Execute transaction account.execute(calls).await?; // Get account info let owner = account.get_owner().await?; }
OpenZeppelin Account
#![allow(unused)] fn main() { use starkbiter_bindings::open_zeppelin_account::OZAccount; let account = OZAccount::new(account_address, &provider); }
DEX Protocols
Ekubo Core
#![allow(unused)] fn main() { use starkbiter_bindings::ekubo_core::EkuboCore; let ekubo = EkuboCore::new(ekubo_address, &account); // Get pool info let pool = ekubo.get_pool(token0, token1, fee).await?; // Swap tokens ekubo.swap( token_in, token_out, amount, min_amount_out, recipient ).await?; }
Test Contracts
Counter
#![allow(unused)] fn main() { use starkbiter_bindings::contracts_counter::Counter; let counter = Counter::deploy(&account).await?; counter.increment().await?; let value = counter.get_value().await?; assert_eq!(value, Felt::from(1u64)); }
UserValues
#![allow(unused)] fn main() { use starkbiter_bindings::contracts_user_values::UserValues; let contract = UserValues::deploy(&account).await?; contract.set_value(key, value).await?; let retrieved = contract.get_value(key).await?; }
Common Patterns
Deploy and Initialize Token
#![allow(unused)] fn main() { async fn setup_token(account: &Account) -> Result<ERC20> { let token = ERC20::deploy( account, "Test Token", "TEST", 18, Felt::from(1_000_000_000u64), account.address(), ).await?; Ok(token) } }
Transfer Between Accounts
#![allow(unused)] fn main() { async fn transfer_tokens( token: &ERC20, from: &Account, to: Felt, amount: Felt ) -> Result<()> { token.transfer(to, amount).await?; Ok(()) } }
Approve and Transfer From
#![allow(unused)] fn main() { // Owner approves spender token.approve(spender_address, amount).await?; // Spender transfers from owner let spender_token = ERC20::new(token.address(), &spender_account); spender_token.transfer_from( owner_address, recipient, amount ).await?; }
Integration with Simulations
Token Distribution Agent
#![allow(unused)] fn main() { struct TokenDistributor { token: ERC20, recipients: Vec<Felt>, } impl Behavior for TokenDistributor { async fn execute(&mut self, world: &World) -> Result<()> { let amount = Felt::from(100u64); for recipient in &self.recipients { self.token.transfer(*recipient, amount).await?; } Ok(()) } } }
DEX Trader Agent
#![allow(unused)] fn main() { struct DexTrader { ekubo: EkuboCore, token_in: Felt, token_out: Felt, } impl Behavior for DexTrader { async fn execute(&mut self, world: &World) -> Result<()> { let amount = Felt::from(1000u64); self.ekubo.swap( self.token_in, self.token_out, amount, Felt::ZERO, world.account().address() ).await?; Ok(()) } } }
Creating Custom Bindings
To generate bindings for your own contracts:
- Compile your Cairo contract to Sierra 1.0
- Place the JSON file in
contracts/ - Run
starkbiter bind
# Copy contract
cp target/dev/my_contract.contract_class.json contracts/
# Generate bindings
starkbiter bind
# Use in code
use crate::bindings::my_contract::MyContract;
See Starkbiter CLI for more details.
Contract Addresses
The bindings work with any deployment. You specify addresses when creating instances:
#![allow(unused)] fn main() { // Use with mainnet deployment let usdc = ERC20::new( Felt::from_hex("0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8")?, &account ); // Or with your own deployment let token = ERC20::deploy(&account, ...).await?; let token_address = token.address(); }
Type Safety
All bindings provide type-safe interfaces:
#![allow(unused)] fn main() { // Compile-time type checking let balance: Felt = token.balance_of(address).await?; // Method signatures match contract ABI token.transfer(recipient: Felt, amount: Felt).await?; }
Error Handling
#![allow(unused)] fn main() { use anyhow::Result; async fn safe_transfer(token: &ERC20, to: Felt, amount: Felt) -> Result<()> { match token.transfer(to, amount).await { Ok(_) => { println!("Transfer successful"); Ok(()) } Err(e) => { eprintln!("Transfer failed: {}", e); Err(e.into()) } } } }
Testing with Bindings
#![allow(unused)] fn main() { #[tokio::test] async fn test_erc20_transfer() { let env = Environment::builder().build().await?; let account = env.create_account().await?; // Deploy token let token = ERC20::deploy( &account, "Test", "TST", 18, Felt::from(1_000_000u64), account.address(), ).await?; // Test transfer let recipient = Felt::from(999u64); let amount = Felt::from(1000u64); token.transfer(recipient, amount).await?; let balance = token.balance_of(recipient).await?; assert_eq!(balance, amount); } }
Next Steps
- Starkbiter CLI - Generate custom bindings
- Quick Start - Use bindings in simulations
- Examples - See bindings in action
Testing Strategies
This chapter covers advanced testing strategies for smart contracts and DeFi protocols using Starkbiter.
Unit Testing
Test individual contract functions in isolation.
#![allow(unused)] fn main() { #[tokio::test] async fn test_token_transfer() { let env = Environment::builder().build().await?; let account = env.create_account().await?; let token = deploy_token(&account).await?; let recipient = Felt::from(999u64); let amount = Felt::from(1000u64); token.transfer(recipient, amount).await?; let balance = token.balance_of(recipient).await?; assert_eq!(balance, amount); } }
Integration Testing
Test interactions between multiple contracts.
#![allow(unused)] fn main() { #[tokio::test] async fn test_swap_integration() { let env = Environment::builder().build().await?; let account = env.create_account().await?; // Deploy all components let token_a = deploy_token(&account, "TokenA").await?; let token_b = deploy_token(&account, "TokenB").await?; let pool = deploy_pool(&account, token_a.address(), token_b.address()).await?; let router = deploy_router(&account, pool.address()).await?; // Test the full flow token_a.approve(router.address(), amount).await?; router.swap(token_a.address(), token_b.address(), amount).await?; // Verify results let balance_b = token_b.balance_of(account.address()).await?; assert!(balance_b > Felt::ZERO); } }
Fuzzing
Test with random inputs to find edge cases.
#![allow(unused)] fn main() { use proptest::prelude::*; #[tokio::test] async fn fuzz_token_operations() { proptest!(|(amount in 1u64..1_000_000u64)| { tokio_test::block_on(async { let env = Environment::builder().build().await.unwrap(); let account = env.create_account().await.unwrap(); let token = deploy_token(&account).await.unwrap(); // Should never panic let _ = token.transfer(recipient, Felt::from(amount)).await; }); }); } }
Invariant Testing
Test that certain properties always hold.
#![allow(unused)] fn main() { #[tokio::test] async fn test_total_supply_invariant() { let env = Environment::builder().build().await?; let account1 = env.create_account().await?; let account2 = env.create_account().await?; let token = deploy_token(&account1).await?; let initial_supply = token.total_supply().await?; // Perform many operations for _ in 0..100 { token.transfer(account2.address(), Felt::from(100u64)).await?; } // Invariant: total supply never changes let final_supply = token.total_supply().await?; assert_eq!(initial_supply, final_supply); } }
Time-Based Testing
Test time-dependent behavior.
#![allow(unused)] fn main() { #[tokio::test] async fn test_timelock() { let env = Environment::builder().build().await?; let account = env.create_account().await?; let timelock = deploy_timelock(&account, 86400).await?; // 24 hours // Should fail before timelock assert!(timelock.withdraw().await.is_err()); // Fast forward env.increase_time(86400).await?; env.mine_block().await?; // Should succeed after timelock assert!(timelock.withdraw().await.is_ok()); } }
Snapshot Testing
Use snapshots to test multiple scenarios from the same starting state.
#![allow(unused)] fn main() { #[tokio::test] async fn test_multiple_scenarios() { let env = Environment::builder().build().await?; let (token, pool) = setup_defi_protocol(&env).await?; // Take snapshot let snapshot = env.snapshot().await?; // Test scenario 1: Normal usage test_normal_usage(&env, &pool).await?; env.restore(snapshot).await?; // Test scenario 2: Extreme volatility test_high_volatility(&env, &pool).await?; env.restore(snapshot).await?; // Test scenario 3: Attack test_flash_loan_attack(&env, &pool).await?; } }
Stress Testing
Test protocol under extreme conditions.
#![allow(unused)] fn main() { #[tokio::test] async fn stress_test_lending_protocol() { let env = Environment::builder().build().await?; let protocol = deploy_lending_protocol(&env).await?; // Simulate high load let mut handles = vec![]; for i in 0..100 { let env = env.clone(); let protocol = protocol.clone(); handles.push(tokio::spawn(async move { let account = env.create_account().await.unwrap(); protocol.borrow(&account, amount).await })); } // Wait for all operations for handle in handles { handle.await.unwrap()?; } // Protocol should still be solvent assert!(protocol.is_solvent().await?); } }
Next Steps
- Simulation Techniques - Advanced simulation patterns
- Anomaly Detection - Detecting unusual behavior
- Performance Optimization - Optimizing your tests
Simulation Techniques
Advanced techniques for building realistic and effective simulations with Starkbiter.
Agent-Based Modeling
Heterogeneous Agents
Create agents with different behaviors and strategies:
#![allow(unused)] fn main() { // Conservative trader let conservative = Agent::new("conservative", ConservativeBehavior { risk_tolerance: 0.3, trade_frequency: Duration::from_secs(3600), }); // Aggressive trader let aggressive = Agent::new("aggressive", AggressiveBehavior { risk_tolerance: 0.9, trade_frequency: Duration::from_secs(60), }); world.add_agent(conservative); world.add_agent(aggressive); }
Adaptive Agents
Agents that learn and adapt:
#![allow(unused)] fn main() { struct AdaptiveBehavior { strategy: Box<dyn Strategy>, performance: PerformanceTracker, } impl Behavior for AdaptiveBehavior { async fn execute(&mut self, world: &World) -> Result<()> { // Execute strategy let result = self.strategy.execute(world).await?; // Track performance self.performance.record(result); // Adapt if performance is poor if self.performance.is_underperforming() { self.strategy = self.select_better_strategy(); } Ok(()) } } }
Economic Modeling
Supply and Demand
Model market dynamics:
#![allow(unused)] fn main() { struct MarketSimulation { suppliers: Vec<Agent>, consumers: Vec<Agent>, price_discovery: PriceDiscovery, } impl MarketSimulation { async fn simulate(&mut self, blocks: u64) -> Result<PriceHistory> { for _ in 0..blocks { // Suppliers offer let supply = self.aggregate_supply().await?; // Consumers bid let demand = self.aggregate_demand().await?; // Clear market let price = self.price_discovery.clear_market(supply, demand); // Execute trades self.execute_at_price(price).await?; } Ok(self.price_discovery.history()) } } }
Liquidity Modeling
Simulate realistic liquidity conditions:
#![allow(unused)] fn main() { struct LiquidityProvider { target_tvl: u64, rebalance_threshold: f64, } impl Behavior for LiquidityProvider { async fn execute(&mut self, world: &World) -> Result<()> { let current_tvl = self.get_tvl(world).await?; let imbalance = (current_tvl as f64 - self.target_tvl as f64).abs() / self.target_tvl as f64; if imbalance > self.rebalance_threshold { self.rebalance(world, current_tvl).await?; } Ok(()) } } }
Monte Carlo Simulation
Run many simulations with random parameters:
#![allow(unused)] fn main() { async fn monte_carlo_analysis( scenarios: usize, ) -> Result<Statistics> { let mut results = vec![]; for i in 0..scenarios { // Create environment with random seed let env = Environment::builder() .with_seed(i as u64) .build() .await?; // Run simulation let outcome = run_simulation(env).await?; results.push(outcome); } // Analyze results Ok(Statistics::from_results(results)) } }
Scenario Analysis
Best Case / Worst Case
#![allow(unused)] fn main() { async fn scenario_analysis() -> Result<ScenarioResults> { let universe = Universe::new(); // Best case let best = create_optimistic_world().await?; universe.add_world("best", best); // Expected case let expected = create_realistic_world().await?; universe.add_world("expected", expected); // Worst case let worst = create_pessimistic_world().await?; universe.add_world("worst", worst); universe.run_all().await?; Ok(universe.compare_outcomes()) } }
Parameter Sweeps
Test across parameter ranges:
#![allow(unused)] fn main() { async fn parameter_sweep() -> Result<HeatMap> { let mut results = HeatMap::new(); for fee in [0.001, 0.003, 0.005, 0.01] { for slippage in [0.001, 0.005, 0.01, 0.05] { let env = Environment::builder().build().await?; let protocol = deploy_with_params(&env, fee, slippage).await?; let profit = simulate_trading(&protocol).await?; results.insert((fee, slippage), profit); } } Ok(results) } }
Time Series Analysis
Price Processes
Simulate realistic price movements:
#![allow(unused)] fn main() { struct GeometricBrownianMotion { mu: f64, // drift sigma: f64, // volatility dt: f64, // time step } impl GeometricBrownianMotion { fn simulate(&self, steps: usize, initial_price: f64) -> Vec<f64> { let mut prices = vec![initial_price]; let mut rng = thread_rng(); for _ in 0..steps { let last_price = prices.last().unwrap(); let dw = Normal::new(0.0, (self.dt).sqrt()).unwrap().sample(&mut rng); let drift = self.mu * self.dt; let diffusion = self.sigma * dw; let next_price = last_price * ((drift + diffusion).exp()); prices.push(next_price); } prices } } }
Event-Driven Updates
#![allow(unused)] fn main() { struct PriceOracle { gbm: GeometricBrownianMotion, current_price: f64, } impl Behavior for PriceOracle { async fn execute(&mut self, world: &World) -> Result<()> { // Update price let new_price = self.gbm.step(self.current_price); self.current_price = new_price; // Broadcast update world.broadcast(Message::PriceUpdate { asset: "ETH".to_string(), price: new_price, }).await?; Ok(()) } } }
Network Effects
Model interactions between agents:
#![allow(unused)] fn main() { struct NetworkSimulation { agents: Vec<Agent>, network: Graph<AgentId, Relationship>, } impl NetworkSimulation { async fn propagate_influence(&mut self, source: AgentId) -> Result<()> { // Find neighbors let neighbors = self.network.neighbors(source); // Influence spreads through network for neighbor in neighbors { if let Some(agent) = self.agents.get_mut(&neighbor) { agent.receive_influence(source).await?; } } Ok(()) } } }
Next Steps
- Anomaly Detection - Detecting unusual behavior
- Testing Strategies - Testing approaches
- Performance Optimization - Optimizing simulations
Anomaly Detection
Use Starkbiter to detect anomalies and vulnerabilities in smart contract systems through simulation-based analysis.
Overview
Anomaly detection in Starkbiter involves:
- Defining normal system behavior
- Running simulations with various conditions
- Identifying deviations from expected behavior
- Analyzing root causes of anomalies
Statistical Anomaly Detection
Baseline Behavior
Establish normal behavior patterns:
#![allow(unused)] fn main() { struct BehaviorBaseline { mean_gas_usage: f64, std_gas_usage: f64, mean_execution_time: f64, typical_event_counts: HashMap<String, f64>, } impl BehaviorBaseline { async fn establish(env: &Environment, runs: usize) -> Result<Self> { let mut gas_samples = vec![]; let mut time_samples = vec![]; for _ in 0..runs { let start = Instant::now(); let result = run_normal_simulation(env).await?; gas_samples.push(result.gas_used as f64); time_samples.push(start.elapsed().as_secs_f64()); } Ok(Self { mean_gas_usage: mean(&gas_samples), std_gas_usage: std_dev(&gas_samples), mean_execution_time: mean(&time_samples), typical_event_counts: result.event_counts, }) } fn is_anomalous(&self, observation: &Observation) -> bool { let z_score = (observation.gas_used as f64 - self.mean_gas_usage) / self.std_gas_usage; z_score.abs() > 3.0 // 3-sigma rule } } }
Detecting Outliers
#![allow(unused)] fn main() { async fn detect_gas_anomalies( protocol: &Protocol, operations: Vec<Operation>, ) -> Result<Vec<Anomaly>> { let baseline = BehaviorBaseline::establish(&protocol.env, 100).await?; let mut anomalies = vec![]; for op in operations { let result = protocol.execute(op).await?; if baseline.is_anomalous(&result) { anomalies.push(Anomaly { operation: op, gas_used: result.gas_used, expected_gas: baseline.mean_gas_usage, deviation: (result.gas_used as f64 - baseline.mean_gas_usage) / baseline.mean_gas_usage, }); } } Ok(anomalies) } }
Invariant Violation Detection
Define Invariants
#![allow(unused)] fn main() { struct ProtocolInvariants { rules: Vec<Box<dyn Invariant>>, } trait Invariant { async fn check(&self, world: &World) -> Result<bool>; fn description(&self) -> &str; } // Example: Total supply invariant struct TotalSupplyInvariant { token_address: Felt, expected_supply: Felt, } impl Invariant for TotalSupplyInvariant { async fn check(&self, world: &World) -> Result<bool> { let token = ERC20::new(self.token_address, &world.account()); let actual = token.total_supply().await?; Ok(actual == self.expected_supply) } fn description(&self) -> &str { "Total token supply must remain constant" } } }
Monitor Invariants
#![allow(unused)] fn main() { async fn monitor_invariants( world: &World, invariants: &ProtocolInvariants, ) -> Result<Vec<Violation>> { let mut violations = vec![]; for invariant in &invariants.rules { if !invariant.check(world).await? { violations.push(Violation { rule: invariant.description().to_string(), timestamp: world.get_timestamp().await?, block: world.get_block_number().await?, }); } } Ok(violations) } }
Behavioral Analysis
Track Agent Behavior
#![allow(unused)] fn main() { struct BehaviorMonitor { agent_patterns: HashMap<String, BehaviorPattern>, } impl BehaviorMonitor { fn record_action(&mut self, agent_id: &str, action: Action) { let pattern = self.agent_patterns .entry(agent_id.to_string()) .or_insert(BehaviorPattern::new()); pattern.add_action(action); } fn detect_suspicious_behavior(&self) -> Vec<Alert> { let mut alerts = vec![]; for (agent_id, pattern) in &self.agent_patterns { // Detect unusual frequency if pattern.action_rate() > pattern.typical_rate() * 10.0 { alerts.push(Alert::UnusualFrequency { agent: agent_id.clone(), rate: pattern.action_rate(), }); } // Detect unusual amounts if pattern.max_amount() > pattern.typical_amount() * 100.0 { alerts.push(Alert::UnusualAmount { agent: agent_id.clone(), amount: pattern.max_amount(), }); } } alerts } } }
Flash Loan Attack Detection
#![allow(unused)] fn main() { struct FlashLoanDetector { threshold_borrow: u64, threshold_profit: u64, } impl FlashLoanDetector { async fn monitor_block(&self, world: &World) -> Result<Vec<AttackAlert>> { let mut alerts = vec![]; let events = world.get_recent_events().await?; // Group events by transaction let tx_groups = self.group_by_transaction(events); for (tx_hash, tx_events) in tx_groups { // Look for borrow-repay in same transaction let borrowed = self.find_borrows(&tx_events); let repaid = self.find_repayments(&tx_events); if !borrowed.is_empty() && !repaid.is_empty() { let total_borrowed: u64 = borrowed.iter().map(|b| b.amount).sum(); let profit = self.calculate_profit(&tx_events); if total_borrowed > self.threshold_borrow && profit > self.threshold_profit { alerts.push(AttackAlert::FlashLoan { tx: tx_hash, borrowed: total_borrowed, profit, }); } } } Ok(alerts) } } }
Price Manipulation Detection
#![allow(unused)] fn main() { struct PriceManipulationDetector { price_impact_threshold: f64, } impl PriceManipulationDetector { async fn detect(&self, pool: &Pool) -> Result<Option<ManipulationAlert>> { let snapshot = pool.snapshot().await?; // Simulate large trade let large_trade = pool.max_trade_size() * 0.5; let price_before = pool.get_price().await?; pool.simulate_swap(large_trade).await?; let price_after = pool.get_price().await?; let price_impact = (price_after - price_before) / price_before; // Restore state pool.restore(snapshot).await?; if price_impact.abs() > self.price_impact_threshold { Ok(Some(ManipulationAlert { pool: pool.address(), price_impact, vulnerable_to_manipulation: true, })) } else { Ok(None) } } } }
Machine Learning-Based Detection
Feature Extraction
#![allow(unused)] fn main() { struct TransactionFeatures { gas_used: f64, value_transferred: f64, num_calls: f64, unique_contracts: f64, loops_detected: bool, reentrancy_risk: f64, } impl TransactionFeatures { async fn extract(tx: &Transaction, world: &World) -> Result<Self> { // Analyze transaction let trace = world.trace_transaction(tx).await?; Ok(Self { gas_used: trace.gas_used as f64, value_transferred: trace.value_transferred as f64, num_calls: trace.calls.len() as f64, unique_contracts: trace.unique_contracts().len() as f64, loops_detected: trace.has_loops(), reentrancy_risk: trace.calculate_reentrancy_risk(), }) } } }
Anomaly Scoring
#![allow(unused)] fn main() { struct AnomalyDetector { model: Box<dyn AnomalyModel>, } impl AnomalyDetector { fn score(&self, features: &TransactionFeatures) -> f64 { self.model.predict_anomaly_score(features) } fn classify(&self, features: &TransactionFeatures) -> Classification { let score = self.score(features); if score > 0.9 { Classification::HighRisk } else if score > 0.7 { Classification::MediumRisk } else { Classification::Normal } } } }
Reporting
#![allow(unused)] fn main() { struct AnomalyReport { anomalies: Vec<Anomaly>, severity_distribution: HashMap<Severity, usize>, recommendations: Vec<String>, } impl AnomalyReport { fn generate(detections: Vec<Detection>) -> Self { let mut anomalies = vec![]; let mut severity_dist = HashMap::new(); for detection in detections { let anomaly = Anomaly::from(detection); *severity_dist.entry(anomaly.severity).or_insert(0) += 1; anomalies.push(anomaly); } let recommendations = Self::generate_recommendations(&anomalies); Self { anomalies, severity_distribution: severity_dist, recommendations, } } fn to_markdown(&self) -> String { // Generate markdown report format!( "# Anomaly Detection Report\n\n\ # Summary\n\ - Total anomalies: {}\n\ - Critical: {}\n\ - High: {}\n\ - Medium: {}\n\ - Low: {}\n\n\ # Recommendations\n{}\n", self.anomalies.len(), self.severity_distribution.get(&Severity::Critical).unwrap_or(&0), self.severity_distribution.get(&Severity::High).unwrap_or(&0), self.severity_distribution.get(&Severity::Medium).unwrap_or(&0), self.severity_distribution.get(&Severity::Low).unwrap_or(&0), self.recommendations.join("\n"), ) } } }
Next Steps
- Testing Strategies - Testing approaches
- Simulation Techniques - Simulation patterns
- Performance Optimization - Optimizing detection
Performance Optimization
Techniques for optimizing the performance of your Starkbiter simulations and tests.
General Optimization
Parallel Execution
Run independent tests in parallel:
#![allow(unused)] fn main() { #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn parallel_tests() { // Tests run concurrently } }
Batch Operations
Group operations to reduce overhead:
#![allow(unused)] fn main() { use tokio::try_join; // Instead of sequential let balance1 = env.get_balance(addr1).await?; let balance2 = env.get_balance(addr2).await?; let balance3 = env.get_balance(addr3).await?; // Use concurrent let (balance1, balance2, balance3) = try_join!( env.get_balance(addr1), env.get_balance(addr2), env.get_balance(addr3), )?; }
Reuse Environments
Reuse environments when possible:
#![allow(unused)] fn main() { // Expensive: create new environment for each test #[tokio::test] async fn test1() { let env = Environment::builder().build().await?; // ... } // Better: use fixtures with snapshots async fn test_fixture() -> (Environment, SnapshotId) { let env = Environment::builder().build().await?; // Setup common state let snapshot = env.snapshot().await?; (env, snapshot) } #[tokio::test] async fn test1() { let (env, snapshot) = test_fixture().await; // ... test ... env.restore(snapshot).await?; } }
Environment Optimization
Disable Unnecessary Features
#![allow(unused)] fn main() { let env = Environment::builder() .with_minimal_setup() // Skip unnecessary initialization .build() .await?; }
Control Block Production
#![allow(unused)] fn main() { // Manual block production for testing let env = Environment::builder() .with_block_time(0) // Manual mode .build() .await?; // Only mine when needed env.mine_block().await?; }
Agent Optimization
Efficient State Management
#![allow(unused)] fn main() { // Bad: Deep cloning on every execution struct InefficientBehavior { large_state: Vec<LargeData>, // Cloned frequently } // Good: Use references and Arc struct EfficientBehavior { large_state: Arc<Vec<LargeData>>, // Shared reference } }
Lazy Evaluation
#![allow(unused)] fn main() { struct LazyBehavior { cached_data: Option<ExpensiveData>, } impl Behavior for LazyBehavior { async fn execute(&mut self, world: &World) -> Result<()> { // Only compute when needed let data = match &self.cached_data { Some(d) => d, None => { let d = self.compute_expensive_data(world).await?; self.cached_data = Some(d); self.cached_data.as_ref().unwrap() } }; self.use_data(data).await?; Ok(()) } } }
Memory Management
Resource Cleanup
#![allow(unused)] fn main() { impl Drop for MyAgent { fn drop(&mut self) { // Clean up resources self.close_connections(); self.flush_caches(); } } }
Limit Cache Sizes
#![allow(unused)] fn main() { use lru::LruCache; struct CachedBehavior { cache: LruCache<Key, Value>, } impl CachedBehavior { fn new() -> Self { Self { cache: LruCache::new(1000.try_into().unwrap()), // Limit cache size } } } }
Profiling
Measure Performance
#![allow(unused)] fn main() { use std::time::Instant; async fn measure_performance<F, T>(name: &str, f: F) -> T where F: Future<Output = T>, { let start = Instant::now(); let result = f.await; let elapsed = start.elapsed(); println!("{} took {:?}", name, elapsed); result } // Usage let result = measure_performance("deploy_contract", async { deploy_contract(&account).await }).await?; }
Profile with tokio-console
// Add to Cargo.toml // [dependencies] // console-subscriber = "0.1" // In main #[tokio::main] async fn main() { console_subscriber::init(); // ... your code }
Benchmarking
Use Criterion for benchmarking:
#![allow(unused)] fn main() { use criterion::{black_box, criterion_group, criterion_main, Criterion}; fn bench_contract_call(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); c.bench_function("contract_call", |b| { b.to_async(&rt).iter(|| async { let env = Environment::builder().build().await.unwrap(); let account = env.create_account().await.unwrap(); let contract = deploy_contract(&account).await.unwrap(); black_box(contract.call_method().await.unwrap()) }); }); } criterion_group!(benches, bench_contract_call); criterion_main!(benches); }
Next Steps
- Testing Strategies - Effective testing
- Simulation Techniques - Advanced simulations
- Anomaly Detection - Detecting issues
Contributing to Starkbiter
Thank you for your interest in contributing to Starkbiter! This guide will help you get started.
Ways to Contribute
There are many ways to contribute to Starkbiter:
- 🐛 Report bugs - Found an issue? Let us know!
- 💡 Suggest features - Have an idea? We'd love to hear it!
- 📖 Improve documentation - Help make our docs better
- 🧪 Add examples - Share your simulations with the community
- 🔧 Fix issues - Submit pull requests for open issues
- ⭐ Spread the word - Tell others about Starkbiter
Getting Started
1. Fork the Repository
Fork starkbiter on GitHub.
2. Clone Your Fork
git clone https://github.com/YOUR_USERNAME/starkbiter
cd starkbiter
3. Set Up Development Environment
# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Build the project
cargo build
# Run tests
cargo test --all --all-features
4. Create a Branch
git checkout -b feature/your-feature-name
Development Workflow
Making Changes
- Write code - Implement your feature or fix
- Add tests - Ensure your code is tested
- Update docs - Document new features
- Run tests - Make sure everything works
- Commit changes - Use clear commit messages
Commit Messages
Follow conventional commits:
feat: add support for custom gas prices
fix: resolve race condition in agent execution
docs: improve quickstart guide
test: add integration tests for forking
refactor: simplify environment builder
Testing
Run the full test suite:
# All tests
cargo test --all --all-features
# Specific package
cargo test -p starkbiter-core
# With output
cargo test -- --nocapture
Code Style
We use rustfmt and clippy:
# Format code
cargo fmt --all
# Check for issues
cargo clippy --all --all-features
Pull Request Process
Before Submitting
- Tests pass locally
-
Code is formatted (
cargo fmt) -
No clippy warnings (
cargo clippy) - Documentation is updated
- Examples work
- Changelog updated (if applicable)
Submitting
-
Push to your fork
git push origin feature/your-feature-name -
Create Pull Request on GitHub
-
Fill out the template
- Describe your changes
- Link related issues
- Add screenshots if applicable
-
Wait for review
- Address feedback
- Make requested changes
- Keep discussion constructive
Review Process
- Maintainers will review your PR
- CI must pass (tests, formatting, clippy)
- At least one approval required
- Maintainer will merge when ready
Code Guidelines
Rust Style
- Follow Rust API Guidelines
- Use
rustfmtdefaults - Prefer explicit types in public APIs
- Document public items
Error Handling
#![allow(unused)] fn main() { // Good: Use Result and descriptive errors async fn deploy_contract(&self) -> Result<ContractAddress> { self.declare().await?; self.deploy().await .context("Failed to deploy contract") } // Avoid: Unwrap or panic in library code async fn bad_deploy(&self) -> ContractAddress { self.deploy().await.unwrap() // Don't do this! } }
Documentation
#![allow(unused)] fn main() { /// Deploys a new contract instance. /// /// # Arguments /// /// * `account` - The account to deploy from /// * `class_hash` - The declared contract class hash /// * `constructor_calldata` - Arguments for the constructor /// /// # Returns /// /// The address of the deployed contract /// /// # Errors /// /// Returns an error if deployment fails or if the class is not declared /// /// # Example /// /// ```rust /// let address = env.deploy_contract( /// &account, /// class_hash, /// vec![], /// ).await?; /// ``` pub async fn deploy_contract( &self, account: &Account, class_hash: Felt, constructor_calldata: Vec<Felt>, ) -> Result<Felt> { // Implementation } }
Testing
#![allow(unused)] fn main() { // Unit tests in same file #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_feature() { // Test code } } // Integration tests in tests/ // tests/integration_test.rs }
Project Structure
starkbiter/
├── bin/ # CLI binary
├── bindings/ # Contract bindings
├── core/ # Core library
│ ├── src/
│ │ ├── environment/
│ │ ├── middleware/
│ │ └── tokens/
│ └── tests/
├── engine/ # Engine library
│ ├── src/
│ │ ├── agent.rs
│ │ ├── behavior.rs
│ │ └── world.rs
│ └── tests/
├── macros/ # Procedural macros
├── examples/ # Example simulations
└── docs/ # Documentation (mdBook)
Documentation
Building the Book
# Install mdbook
cargo install mdbook mdbook-katex
# Build and serve
cd docs
mdbook serve
# Open http://localhost:3000
Adding Documentation
- Update SUMMARY.md for new pages
- Use markdown for content
- Add code examples
- Keep it beginner-friendly
Examples
When adding examples:
- Create directory under
examples/ - Add
main.rsand supporting files - Document in
examples/README.md - Update book's examples page
Community
Communication Channels
- GitHub Discussions - Ask questions, share ideas
- GitHub Issues - Report bugs, request features
- Pull Requests - Submit code changes
Code of Conduct
Be respectful and constructive. We're all here to learn and build together.
Getting Help
- Read the documentation
- Search existing issues
- Ask in discussions
- Check the examples
Recognition
Contributors are recognized in:
CONTRIBUTORS.md- Release notes
- Documentation credits
Thank you for contributing to Starkbiter! 🚀
Development Setup
Detailed guide for setting up your Starkbiter development environment.
Prerequisites
Rust
Install Rust using rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup update
Additional Tools
# Code formatting
rustup component add rustfmt
# Linting
rustup component add clippy
# Documentation
cargo install mdbook mdbook-katex
# Development tools
cargo install cargo-watch # Auto-rebuild on changes
cargo install cargo-expand # Expand macros
Building the Project
Full Build
# Clone repository
git clone https://github.com/astraly-labs/starkbiter
cd starkbiter
# Build all crates
cargo build --all --all-features
# Build in release mode
cargo build --release --all --all-features
Individual Crates
# Build specific crate
cargo build -p starkbiter-core
cargo build -p starkbiter-engine
cargo build -p starkbiter-bindings
# With features
cargo build -p starkbiter-core --features "cheating"
Running Tests
All Tests
# Run all tests
cargo test --all --all-features
# With output
cargo test --all --all-features -- --nocapture
# Specific test
cargo test test_name --all-features
Package-Specific Tests
# Core tests
cargo test -p starkbiter-core
# Engine tests
cargo test -p starkbiter-engine
# Integration tests only
cargo test --test '*'
Test Coverage
# Install tarpaulin
cargo install cargo-tarpaulin
# Generate coverage report
cargo tarpaulin --all --all-features --out Html
Code Quality
Formatting
# Check formatting
cargo fmt --all -- --check
# Format code
cargo fmt --all
Linting
# Check for issues
cargo clippy --all --all-features
# Fix automatically where possible
cargo clippy --all --all-features --fix
Documentation
# Build docs
cargo doc --all --all-features --no-deps
# Open in browser
cargo doc --all --all-features --no-deps --open
# Check for broken links
cargo doc --all --all-features --no-deps 2>&1 | grep warning
Development Workflow
Watch Mode
Auto-rebuild on file changes:
# Watch and rebuild
cargo watch -x build
# Watch and test
cargo watch -x test
# Watch and run example
cargo watch -x "run --example minter"
Debugging
#![allow(unused)] fn main() { // Add debug prints println!("Debug: {:?}", value); dbg!(value); // Or use the log crate use log::{debug, info, error}; debug!("Detailed information"); info!("General information"); error!("Error occurred: {}", e); }
Run with logging:
RUST_LOG=debug cargo test
RUST_LOG=starkbiter_core=trace cargo run --example minter
Benchmarking
# Run benchmarks
cargo bench -p starkbiter-core
# Compare benchmarks
cargo bench --bench bench_name -- --save-baseline before
# ... make changes ...
cargo bench --bench bench_name -- --baseline before
Working with Examples
Running Examples
# List examples
cargo run --example --list
# Run example
cargo run --example minter
# With arguments
cargo run --example minter simulate ./examples/minter/config.toml -vvvv
Creating Examples
# Create new example
mkdir examples/my_example
touch examples/my_example/main.rs
# Add to workspace if needed
# (usually automatic)
Documentation Development
Building the Book
cd docs
# Install dependencies
cargo install mdbook mdbook-katex
# Build
mdbook build
# Serve with auto-reload
mdbook serve
# Open http://localhost:3000
Adding Pages
- Create markdown file in
docs/src/ - Update
docs/src/SUMMARY.md - Test locally with
mdbook serve
IDE Setup
VS Code
Recommended extensions:
- rust-analyzer
- CodeLLDB (debugging)
- Even Better TOML
- Error Lens
settings.json:
{
"rust-analyzer.checkOnSave.command": "clippy",
"rust-analyzer.cargo.features": "all",
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer",
"editor.formatOnSave": true
}
}
IntelliJ IDEA / CLion
- Install Rust plugin
- Enable rustfmt on save
- Configure clippy as external tool
Troubleshooting
Build Issues
# Clean build artifacts
cargo clean
# Update dependencies
cargo update
# Check dependency tree
cargo tree
Test Failures
# Run specific test with output
cargo test test_name -- --nocapture
# Run ignored tests
cargo test -- --ignored
# Run in single-threaded mode
cargo test -- --test-threads=1
Performance Issues
# Profile with flamegraph
cargo install flamegraph
cargo flamegraph --example minter
# Use release mode
cargo build --release
cargo test --release
Continuous Integration
Our CI runs:
- Tests on Linux, macOS, Windows
- Formatting checks
- Clippy lints
- Documentation builds
- Example builds
Make sure your PR passes all checks:
# Run CI checks locally
cargo fmt --all -- --check
cargo clippy --all --all-features -- -D warnings
cargo test --all --all-features
cargo build --examples
Next Steps
- Contributing Guidelines - How to contribute
- Code Style Guide - Coding standards
- Testing Guide - Writing tests
Vulnerability Corpus
A collection of known vulnerabilities and security issues that Starkbiter can help detect and prevent.
Overview
This corpus catalogs common vulnerabilities in smart contracts, particularly in the Starknet/Cairo ecosystem, and demonstrates how Starkbiter can be used to detect them through simulation and testing.
Vulnerability Categories
1. Reentrancy
Description: Attackers exploit function calls that allow external contract calls before state updates.
Detection with Starkbiter:
#![allow(unused)] fn main() { #[tokio::test] async fn test_reentrancy_vulnerability() { let env = Environment::builder().build().await?; let (victim, attacker) = setup_reentrancy_test(&env).await?; // Attempt reentrancy attack let initial_balance = victim.get_balance().await?; attacker.exploit().await?; let final_balance = victim.get_balance().await?; // Should not allow draining assert_eq!(initial_balance, final_balance); } }
Prevention: Use checks-effects-interactions pattern, reentrancy guards.
2. Integer Overflow/Underflow
Description: Arithmetic operations that exceed type bounds.
Detection with Starkbiter:
#![allow(unused)] fn main() { #[tokio::test] async fn test_overflow_vulnerability() { let env = Environment::builder().build().await?; let contract = deploy_vulnerable_contract(&env).await?; // Try to overflow let max_value = Felt::from(u128::MAX); let result = contract.add(max_value, Felt::ONE).await; // Should handle overflow safely assert!(result.is_err() || result.unwrap() != Felt::ZERO); } }
Prevention: Use checked arithmetic, Felt type bounds checking.
3. Access Control Issues
Description: Missing or incorrect permission checks.
Detection with Starkbiter:
#![allow(unused)] fn main() { #[tokio::test] async fn test_access_control() { let env = Environment::builder().build().await?; let owner = env.create_account().await?; let attacker = env.create_account().await?; let contract = deploy_with_owner(&env, &owner).await?; // Attacker tries privileged operation let result = contract.as_account(&attacker).privileged_function().await; // Should be rejected assert!(result.is_err()); } }
Prevention: Implement proper role-based access control.
4. Front-Running
Description: Attackers observe pending transactions and submit competing transactions with higher fees.
Detection with Starkbiter:
#![allow(unused)] fn main() { #[tokio::test] async fn test_frontrunning_vulnerability() { let env = Environment::builder().build().await?; let world = World::new(env); // Add frontrunner agent world.add_agent(Agent::new("frontrunner", FrontRunnerBehavior)); world.add_agent(Agent::new("victim", VictimBehavior)); world.run_for_blocks(100).await?; // Analyze if frontrunning occurred let metrics = world.get_metrics(); assert!(metrics.frontrunning_detected == false, "Vulnerable to frontrunning"); } }
Prevention: Use commit-reveal schemes, batch auctions.
5. Price Oracle Manipulation
Description: Attackers manipulate price oracles to exploit DeFi protocols.
Detection with Starkbiter:
#![allow(unused)] fn main() { #[tokio::test] async fn test_oracle_manipulation() { let env = Environment::builder().build().await?; let protocol = deploy_lending_protocol(&env).await?; let pool = deploy_dex_pool(&env).await?; // Take snapshot let snapshot = env.snapshot().await?; // Simulate large trade to manipulate price let whale = create_whale_account(&env).await?; pool.swap(&whale, large_amount).await?; // Try to exploit with manipulated price let profit = protocol.exploit_price_manipulation(&env).await?; // Restore env.restore(snapshot).await?; // Protocol should be resistant assert!(profit == Felt::ZERO, "Vulnerable to oracle manipulation"); } }
Prevention: Use TWAP oracles, multiple oracle sources, sanity checks.
6. Flash Loan Attacks
Description: Attackers use flash loans to manipulate markets or exploit protocols.
Detection with Starkbiter:
#![allow(unused)] fn main() { #[tokio::test] async fn test_flash_loan_attack() { let env = Environment::builder().build().await?; let world = World::new(env); // Setup protocol and flash loan attacker let protocol = setup_vulnerable_protocol(&world).await?; world.add_agent(Agent::new("attacker", FlashLoanAttacker::new())); let initial_tvl = protocol.get_tvl().await?; world.run_for_blocks(10).await?; let final_tvl = protocol.get_tvl().await?; // TVL should not be drained assert!(final_tvl >= initial_tvl * 99 / 100, "Vulnerable to flash loan attack"); } }
Prevention: Circuit breakers, time delays, borrowing limits.
7. Denial of Service
Description: Attackers prevent legitimate users from using the contract.
Detection with Starkbiter:
#![allow(unused)] fn main() { #[tokio::test] async fn test_dos_vulnerability() { let env = Environment::builder().build().await?; let contract = deploy_contract(&env).await?; // Attacker fills contract storage let attacker = env.create_account().await?; for i in 0..1000 { contract.add_item(&attacker, i).await?; } // Legitimate user should still be able to interact let user = env.create_account().await?; let result = contract.use_contract(&user).await; assert!(result.is_ok(), "Vulnerable to DoS"); } }
Prevention: Gas limits, rate limiting, bounded iterations.
8. Insufficient Validation
Description: Missing input validation allows invalid states.
Detection with Starkbiter:
#![allow(unused)] fn main() { #[tokio::test] async fn test_validation_vulnerability() { let env = Environment::builder().build().await?; let contract = deploy_contract(&env).await?; // Try invalid inputs let invalid_inputs = vec![ Felt::ZERO, Felt::from(u128::MAX), Felt::from(-1i128), ]; for input in invalid_inputs { let result = contract.process(input).await; assert!(result.is_err(), "Missing validation for: {:?}", input); } } }
Prevention: Comprehensive input validation, require statements.
Using the Corpus
Testing Your Contracts
- Review applicable vulnerability categories
- Implement detection tests for your contract
- Run tests with Starkbiter
- Fix identified issues
- Re-test
Contributing
If you discover a new vulnerability pattern:
- Document the vulnerability
- Create a detection test
- Submit a PR to this corpus
- Include mitigation strategies
Resources
Next Steps
- Testing Strategies - Advanced testing techniques
- Anomaly Detection - Detecting vulnerabilities
- Examples - See detection in action