Minimal entrypoint contracts
This guide explains the low-level mechanics of Stylus contract entrypoints, helping you understand what happens behind the #[entrypoint] and #[public] macros. This knowledge is useful for advanced use cases, debugging, and building custom contract frameworks.
Overview
A Stylus contract at its core consists of:
user_entrypointfunction: The WASM export that Stylus calls- Router implementation: Routes function selectors to method implementations
- TopLevelStorage trait: Marks the contract's root storage type
- ArbResult type: Represents success/failure with encoded return data
Understanding ArbResult
ArbResult is the fundamental return type for Stylus contract methods:
pub type ArbResult = Result<Vec<u8>, Vec<u8>>;
Ok(Vec<u8>)- Success with ABI-encoded return dataErr(Vec<u8>)- Revert with ABI-encoded error data
Example:
use stylus_sdk::ArbResult;
// Success with no return data
fn no_return() -> ArbResult {
Ok(Vec::new())
}
// Success with encoded data
fn return_value() -> ArbResult {
let value: u32 = 42;
Ok(value.to_le_bytes().to_vec())
}
// Revert with error data
fn revert_with_error() -> ArbResult {
Err(b"InsufficientBalance".to_vec())
}
The user_entrypoint Function
The user_entrypoint function is the WASM export that Stylus calls when a transaction invokes the contract. The #[entrypoint] macro generates this function automatically.
Generated Structure
When you use #[entrypoint], the macro generates:
#[no_mangle]
pub extern "C" fn user_entrypoint(len: usize) -> usize {
let host = stylus_sdk::host::VM {
host: stylus_sdk::host::WasmVM{}
};
// Reentrancy check (unless reentrant feature enabled)
if host.msg_reentrant() {
return 1; // revert
}
// Ensure pay_for_memory_grow is referenced
// (costs 8700 ink, less than 1 gas)
host.pay_for_memory_grow(0);
// Read calldata
let input = host.read_args(len);
// Call the router
let (data, status) = match router_entrypoint::<MyContract, MyContract>(input, host.clone()) {
Ok(data) => (data, 0), // Success
Err(data) => (data, 1), // Revert
};
// Persist storage changes
host.flush_cache(false);
// Write return data
host.write_result(&data);
status
}
Key Points
- Signature:
extern "C" fn user_entrypoint(len: usize) -> usize - Input:
lenis the length of calldata to read - Output:
0for success,1for revert - Side effects: Reads calldata, writes return data, flushes storage cache
The Router Trait
The Router trait defines how function calls are dispatched to method implementations.
Trait Definition
pub trait Router<S, I = Self>
where
S: TopLevelStorage + BorrowMut<Self::Storage> + ValueDenier,
I: ?Sized,
{
type Storage;
/// Route a function call by selector
fn route(storage: &mut S, selector: u32, input: &[u8]) -> Option<ArbResult>;
/// Handle receive (plain ETH transfers, no calldata)
fn receive(storage: &mut S) -> Option<Result<(), Vec<u8>>>;
/// Handle fallback (unknown selectors or no receive)
fn fallback(storage: &mut S, calldata: &[u8]) -> Option<ArbResult>;
/// Handle constructor
fn constructor(storage: &mut S, calldata: &[u8]) -> Option<ArbResult>;
}
Routing Logic
The router_entrypoint function implements the routing logic:
pub fn router_entrypoint<R, S>(input: Vec<u8>, host: VM) -> ArbResult
where
R: Router<S>,
S: StorageType + TopLevelStorage + BorrowMut<R::Storage> + ValueDenier,
{
let mut storage = unsafe { S::new(U256::ZERO, 0, host) };
// No calldata - try receive, then fallback
if input.is_empty() {
if let Some(res) = R::receive(&mut storage) {
return res.map(|_| Vec::new());
}
if let Some(res) = R::fallback(&mut storage, &[]) {
return res;
}
return Err(Vec::new()); // No receive or fallback
}
// Extract selector (first 4 bytes)
if input.len() >= 4 {
let selector = u32::from_be_bytes(input[..4].try_into().unwrap());
// Check for constructor
if selector == CONSTRUCTOR_SELECTOR {
if let Some(res) = R::constructor(&mut storage, &input[4..]) {
return res;
}
}
// Try to route to a method
else if let Some(res) = R::route(&mut storage, selector, &input[4..]) {
return res;
}
}
// Try fallback
if let Some(res) = R::fallback(&mut storage, &input) {
return res;
}
Err(Vec::new()) // Unknown selector and no fallback
}
The TopLevelStorage Trait
The TopLevelStorage trait marks types that represent the contract's root storage.
Trait Definition
pub unsafe trait TopLevelStorage {}
Purpose
- Prevents storage aliasing during reentrancy
- Lifetime tracks all EVM state changes during contract invocation
- Must hold a reference when making external calls
- Automatically implemented by
#[entrypoint]
Safety
The trait is unsafe because:
- Type must truly be top-level to prevent storage aliasing
- Incorrectly implementing this trait can lead to undefined behavior
Building a Minimal Contract
Here's a minimal contract without using the high-level macros:
Step 1: Define Storage
#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
extern crate alloc;
use alloc::vec::Vec;
use stylus_sdk::{
abi::{Router, ArbResult},
storage::StorageType,
host::VM,
alloy_primitives::U256,
};
// Mark as top-level storage (normally done by #[entrypoint])
pub struct MyContract;
unsafe impl stylus_core::storage::TopLevelStorage for MyContract {}
impl StorageType for MyContract {
type Wraps<'a> = &'a Self where Self: 'a;
type WrapsMut<'a> = &'a mut Self where Self: 'a;
unsafe fn new(_slot: U256, _offset: u8, _host: VM) -> Self {
MyContract
}
fn load<'s>(self) -> Self::Wraps<'s> {
&self
}
fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
&mut self
}
}
Step 2: Implement Router
impl Router<MyContract> for MyContract {
type Storage = MyContract;
fn route(_storage: &mut MyContract, selector: u32, _input: &[u8]) -> Option<ArbResult> {
// Simple example: one method with selector 0x12345678
match selector {
0x12345678 => Some(Ok(Vec::new())),
_ => None, // Unknown selector
}
}
fn receive(_storage: &mut MyContract) -> Option<Result<(), Vec<u8>>> {
None // No receive function
}
fn fallback(_storage: &mut MyContract, _calldata: &[u8]) -> Option<ArbResult> {
None // No fallback function
}
fn constructor(_storage: &mut MyContract, _calldata: &[u8]) -> Option<ArbResult> {
None // No constructor
}
}
Step 3: Define Entrypoint
#[no_mangle]
pub extern "C" fn user_entrypoint(len: usize) -> usize {
let host = VM { host: stylus_sdk::host::WasmVM{} };
// Reentrancy check
if host.msg_reentrant() {
return 1;
}
// Reference pay_for_memory_grow
host.pay_for_memory_grow(0);
// Read input
let input = host.read_args(len);
// Route the call
let (data, status) = match stylus_sdk::abi::router_entrypoint::<MyContract, MyContract>(input, host.clone()) {
Ok(data) => (data, 0),
Err(data) => (data, 1),
};
// Flush storage
host.flush_cache(false);
// Write result
host.write_result(&data);
status
}
Function Selectors
Function selectors are 4-byte identifiers computed from the function signature.
Computing Selectors
use stylus_sdk::function_selector;
// Manual computation
const MY_FUNCTION: [u8; 4] = function_selector!("myFunction");
// With parameters
const TRANSFER: [u8; 4] = function_selector!("transfer", Address, U256);
// Constructor selector
const CONSTRUCTOR_SELECTOR: u32 =
u32::from_be_bytes(function_selector!("constructor"));
Using in Router
impl Router<MyContract> for MyContract {
type Storage = MyContract;
fn route(_storage: &mut MyContract, selector: u32, input: &[u8]) -> Option<ArbResult> {
const GET_VALUE: u32 = u32::from_be_bytes(function_selector!("getValue"));
const SET_VALUE: u32 = u32::from_be_bytes(function_selector!("setValue", U256));
match selector {
GET_VALUE => {
// Return encoded U256 value
let value = U256::from(42);
Some(Ok(value.to_be_bytes::<32>().to_vec()))
}
SET_VALUE => {
// Decode input and set value
if input.len() >= 32 {
// Process set_value logic
Some(Ok(Vec::new()))
} else {
Some(Err(Vec::new()))
}
}
_ => None,
}
}
fn receive(_storage: &mut MyContract) -> Option<Result<(), Vec<u8>>> {
None
}
fn fallback(_storage: &mut MyContract, _calldata: &[u8]) -> Option<ArbResult> {
None
}
fn constructor(_storage: &mut MyContract, _calldata: &[u8]) -> Option<ArbResult> {
None
}
}
Implementing Special Functions
Receive Function
Handles plain ETH transfers (no calldata):
fn receive(storage: &mut MyContract) -> Option<Result<(), Vec<u8>>> {
// Access msg_value via storage.vm().msg_value()
// Must return Ok(()) for success
Some(Ok(()))
}
Fallback Function
Handles unknown selectors or when no receive is defined:
fn fallback(storage: &mut MyContract, calldata: &[u8]) -> Option<ArbResult> {
// Can access full calldata
// Return Some to handle, None to revert
Some(Ok(Vec::new()))
}
Constructor
Called once during deployment with CONSTRUCTOR_SELECTOR:
fn constructor(storage: &mut MyContract, calldata: &[u8]) -> Option<ArbResult> {
// Initialize contract state
// calldata contains constructor parameters
Some(Ok(Vec::new()))
}
Complete Minimal Example
Here's a complete working minimal contract:
#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
extern crate alloc;
use alloc::vec::Vec;
use core::borrow::BorrowMut;
use stylus_sdk::{
abi::Router,
alloy_primitives::U256,
host::VM,
storage::StorageType,
ArbResult,
function_selector,
};
use stylus_core::{storage::TopLevelStorage, ValueDenier};
// Contract storage
pub struct MinimalContract;
// Mark as top-level storage
unsafe impl TopLevelStorage for MinimalContract {}
// Implement StorageType
impl StorageType for MinimalContract {
type Wraps<'a> = &'a Self where Self: 'a;
type WrapsMut<'a> = &'a mut Self where Self: 'a;
unsafe fn new(_slot: U256, _offset: u8, _host: VM) -> Self {
MinimalContract
}
fn load<'s>(self) -> Self::Wraps<'s> {
&self
}
fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
&mut self
}
}
// Implement ValueDenier (for non-payable check)
impl ValueDenier for MinimalContract {
fn deny_value(&self, _method_name: &str) -> Result<(), Vec<u8>> {
Ok(()) // Allow all for simplicity
}
}
// Implement BorrowMut
impl BorrowMut<MinimalContract> for MinimalContract {
fn borrow_mut(&mut self) -> &mut MinimalContract {
self
}
}
// Implement Router
impl Router<MinimalContract> for MinimalContract {
type Storage = MinimalContract;
fn route(_storage: &mut MinimalContract, selector: u32, _input: &[u8]) -> Option<ArbResult> {
const HELLO: u32 = u32::from_be_bytes(function_selector!("hello"));
match selector {
HELLO => Some(Ok(Vec::new())),
_ => None,
}
}
fn receive(_storage: &mut MinimalContract) -> Option<Result<(), Vec<u8>>> {
None
}
fn fallback(_storage: &mut MinimalContract, _calldata: &[u8]) -> Option<ArbResult> {
Some(Ok(Vec::new())) // Accept all unknown calls
}
fn constructor(_storage: &mut MinimalContract, _calldata: &[u8]) -> Option<ArbResult> {
Some(Ok(Vec::new()))
}
}
// Define user_entrypoint
#[no_mangle]
pub extern "C" fn user_entrypoint(len: usize) -> usize {
let host = VM { host: stylus_sdk::host::WasmVM{} };
if host.msg_reentrant() {
return 1;
}
host.pay_for_memory_grow(0);
let input = host.read_args(len);
let (data, status) = match stylus_sdk::abi::router_entrypoint::<MinimalContract, MinimalContract>(input, host.clone()) {
Ok(data) => (data, 0),
Err(data) => (data, 1),
};
host.flush_cache(false);
host.write_result(&data);
status
}
Why Use High-Level Macros?
While minimal contracts are educational, the #[entrypoint] and #[public] macros provide:
- Automatic selector generation from method names
- Type-safe parameter encoding/decoding using Alloy types
- Solidity ABI export for interoperability
- Storage trait implementations with caching
- Error handling with
Resulttypes - Payable checks for ETH-receiving functions
- Reentrancy protection by default
Recommended approach:
// Use macros for production contracts
#[storage]
#[entrypoint]
pub struct MyContract {
value: StorageU256,
}
#[public]
impl MyContract {
pub fn get_value(&self) -> U256 {
self.value.get()
}
pub fn set_value(&mut self, value: U256) {
self.value.set(value);
}
}
This generates all the low-level code automatically while providing a clean, type-safe interface.
Advanced Use Cases
Custom Routing Logic
Implement custom routing for multi-contract systems:
impl Router<MultiContract> for MultiContract {
type Storage = MultiContract;
fn route(storage: &mut MultiContract, selector: u32, input: &[u8]) -> Option<ArbResult> {
// Route to different modules based on selector range
match selector {
0x00000000..=0x0fffffff => ModuleA::route(storage, selector, input),
0x10000000..=0x1fffffff => ModuleB::route(storage, selector, input),
_ => None,
}
}
// ... other methods
}
Custom Entrypoint Logic
Add custom logic before/after routing:
#[no_mangle]
pub extern "C" fn user_entrypoint(len: usize) -> usize {
let host = VM { host: stylus_sdk::host::WasmVM{} };
// Custom pre-processing
let start_gas = host.evm_gas_left();
// Standard entrypoint logic
if host.msg_reentrant() {
return 1;
}
host.pay_for_memory_grow(0);
let input = host.read_args(len);
let (data, status) = match stylus_sdk::abi::router_entrypoint::<MyContract, MyContract>(input, host.clone()) {
Ok(data) => (data, 0),
Err(data) => (data, 1),
};
// Custom post-processing
let gas_used = start_gas - host.evm_gas_left();
// Log or handle gas usage
host.flush_cache(false);
host.write_result(&data);
status
}
Debugging Tips
Enable Debug Mode
#[cfg(feature = "debug")]
use stylus_sdk::console;
fn route(storage: &mut MyContract, selector: u32, input: &[u8]) -> Option<ArbResult> {
#[cfg(feature = "debug")]
console!("Selector: {:08x}", selector);
// Routing logic...
}
Check Selector Computation
#[test]
fn test_selectors() {
use stylus_sdk::function_selector;
let hello = u32::from_be_bytes(function_selector!("hello"));
assert_eq!(hello, 0x19ff1d21);
// Compare with Solidity: bytes4(keccak256("hello()"))
}
See Also
- Contracts - High-level contract development
- Global Variables - VM context methods
- Storage Types - Persistent storage