Stylus compound types
Compound types allow you to group multiple values in Stylus contracts. The SDK provides full support for tuples, structs, arrays, and vectors with automatic ABI encoding/decoding and Solidity type mappings.
Tuples
Tuples group multiple values of different types together. They map directly to Solidity tuples.
Basic tuples
use alloy_primitives::{Address, U256, Bytes};
use stylus_sdk::prelude::*;
#[public]
impl MyContract {
// Return multiple values as a tuple
pub fn get_data(&self) -> (U256, Address, bool) {
(U256::from(100), Address::ZERO, true)
}
// Accept tuple as parameter
pub fn process_tuple(&mut self, data: (U256, U256, U256)) -> U256 {
let (a, b, c) = data;
a + b + c
}
// Nested tuples
pub fn nested(&self) -> ((U256, U256), bool) {
((U256::from(1), U256::from(2)), true)
}
}
Tuple destructuring
use alloy_primitives::U256;
use stylus_sdk::prelude::*;
#[public]
impl MyContract {
pub fn calculate(&self) -> (U256, U256) {
let values = (U256::from(100), U256::from(200));
// Destructure the tuple
let (first, second) = values;
// Return new tuple
(first * U256::from(2), second * U256::from(2))
}
// Pattern matching with tuples
pub fn match_tuple(&self, data: (bool, U256)) -> U256 {
match data {
(true, value) => value * U256::from(2),
(false, value) => value,
}
}
}
Tuple type mappings
| Rust Type | Solidity Type | ABI Signature |
|---|---|---|
(U256,) | (uint256) | "(uint256)" |
(U256, Address) | (uint256, address) | "(uint256,address)" |
(bool, U256, Bytes) | (bool, uint256, bytes) | "(bool,uint256,bytes)" |
((U256, U256), bool) | ((uint256, uint256), bool) | "((uint256,uint256),bool)" |
Tuple Limitations:
- Tuples support up to 24 elements
- Tuples are always returned as
memoryin Solidity - An empty tuple
()represents no return value
Structs
Structs define custom data types with named fields. Use the sol! macro to define Solidity-compatible structs.
Defining structs with sol!
use alloy_primitives::{Address, U256};
use alloy_sol_types::sol;
use stylus_sdk::prelude::*;
sol! {
#[derive(Debug, AbiType)]
struct User {
address account;
uint256 balance;
string name;
}
#[derive(Debug, AbiType)]
struct Token {
string name;
string symbol;
uint8 decimals;
}
}
#[public]
impl MyContract {
pub fn get_user(&self) -> User {
User {
account: Address::ZERO,
balance: U256::from(1000),
name: "Alice".to_string(),
}
}
pub fn process_user(&mut self, user: User) -> U256 {
// Access struct fields
user.balance
}
pub fn get_token_info(&self) -> Token {
Token {
name: "MyToken".to_string(),
symbol: "MTK".to_string(),
decimals: 18,
}
}
}
Nested structs
Structs can contain other structs, enabling complex data structures:
use alloy_primitives::Address;
use alloy_sol_types::sol;
use stylus_sdk::prelude::*;
sol! {
#[derive(Debug, AbiType)]
struct Dog {
string name;
string breed;
}
#[derive(Debug, AbiType)]
struct User {
address account;
string name;
Dog[] dogs;
}
}
#[public]
impl MyContract {
pub fn create_user(&self) -> User {
let dogs = vec![
Dog {
name: "Rex".to_string(),
breed: "Labrador".to_string(),
},
Dog {
name: "Max".to_string(),
breed: "Beagle".to_string(),
},
];
User {
account: Address::ZERO,
name: "Alice".to_string(),
dogs,
}
}
pub fn get_dog_count(&self, user: User) -> u256 {
user.dogs.len() as u256
}
}
Struct best practices
-
Always use
#[derive(AbiType)]for structs that are to be used in contract interfaces:sol! {#[derive(Debug, AbiType)]struct MyData {uint256 value;address owner;}} -
Add
Debugderive for easier debugging:sol! {#[derive(Debug, AbiType)]struct Config {bool enabled;uint256 timeout;}} -
Use descriptive field names that match Solidity conventions:
sol! {#[derive(Debug, AbiType)]struct VestingSchedule {address beneficiary;uint256 startTime;uint256 cliffDuration;uint256 totalAmount;}}
Arrays
Arrays are fixed-size collections of elements. Stylus supports both Rust arrays and Solidity-style arrays.
Fixed-size arrays
use alloy_primitives::U256;
use stylus_sdk::prelude::*;
#[public]
impl MyContract {
// Return a fixed-size array
pub fn get_numbers(&self) -> [U256; 5] {
[
U256::from(1),
U256::from(2),
U256::from(3),
U256::from(4),
U256::from(5),
]
}
// Accept fixed-size array as parameter
pub fn sum_array(&self, numbers: [U256; 5]) -> U256 {
numbers.iter().fold(U256::ZERO, |acc, &x| acc + x)
}
// Nested arrays
pub fn matrix(&self) -> [[u32; 2]; 3] {
[[1, 2], [3, 4], [5, 6]]
}
}
Array operations
use alloy_primitives::{Address, U256};
use stylus_sdk::prelude::*;
#[public]
impl MyContract {
// Iterate over array
pub fn process_addresses(&self, addresses: [Address; 10]) -> U256 {
let mut count = U256::ZERO;
for addr in addresses.iter() {
if *addr != Address::ZERO {
count += U256::from(1);
}
}
count
}
// Array of booleans
pub fn check_flags(&self, flags: [bool; 8]) -> bool {
flags.iter().all(|&f| f)
}
}
Array type mappings
| Rust Type | Solidity Type | Description |
|---|---|---|
[U256; 5] | uint256[5] | 5-element uint256 array |
[bool; 10] | bool[10] | 10-element bool array |
[Address; 3] | address[3] | 3-element address array |
[[u32; 2]; 4] | uint32[2][4] | Nested array (4x2 matrix) |
[FixedBytes<32>; 2] | bytes32[2] | 2-element bytes32 array |
Vectors
Vectors are dynamic arrays that can grow or shrink at runtime. They map to Solidity dynamic arrays.
Basic vector usage
use alloy_primitives::{Address, U256, Bytes};
use stylus_sdk::prelude::*;
#[public]
impl MyContract {
// Return a vector
pub fn get_numbers(&self) -> Vec<U256> {
vec![U256::from(1), U256::from(2), U256::from(3)]
}
// Accept vector as parameter
pub fn sum_vec(&self, numbers: Vec<U256>) -> U256 {
numbers.iter().fold(U256::ZERO, |acc, x| acc + *x)
}
// Vector of addresses
pub fn get_addresses(&self) -> Vec<Address> {
vec![Address::ZERO, Address::ZERO]
}
// Vector of bytes
pub fn get_data_list(&self) -> Vec<Bytes> {
vec![
Bytes::from(vec![1, 2, 3]),
Bytes::from(vec![4, 5, 6]),
]
}
}
Vector operations
use alloy_primitives::U256;
use stylus_sdk::prelude::*;
#[public]
impl MyContract {
// Filter vector
pub fn filter_even(&self, numbers: Vec<U256>) -> Vec<U256> {
numbers
.into_iter()
.filter(|n| n.byte(0) % 2 == 0)
.collect()
}
// Map over vector
pub fn double_values(&self, numbers: Vec<U256>) -> Vec<U256> {
numbers
.into_iter()
.map(|n| n * U256::from(2))
.collect()
}
// Find in vector
pub fn contains_value(&self, numbers: Vec<U256>, target: U256) -> bool {
numbers.contains(&target)
}
// Get vector length
pub fn get_length(&self, items: Vec<U256>) -> U256 {
U256::from(items.len())
}
}
Vectors of structs
use alloy_primitives::Address;
use alloy_sol_types::sol;
use stylus_sdk::prelude::*;
sol! {
#[derive(Debug, AbiType)]
struct Transaction {
address from;
address to;
uint256 amount;
}
}
#[public]
impl MyContract {
pub fn get_transactions(&self) -> Vec<Transaction> {
vec![
Transaction {
from: Address::ZERO,
to: Address::ZERO,
amount: U256::from(100),
},
Transaction {
from: Address::ZERO,
to: Address::ZERO,
amount: U256::from(200),
},
]
}
pub fn total_amount(&self, txs: Vec<Transaction>) -> U256 {
txs.iter()
.fold(U256::ZERO, |acc, tx| acc + tx.amount)
}
}
Vector type mappings
| Rust Type | Solidity Type | ABI Signature | Storage |
|---|---|---|---|
Vec<U256> | uint256[] | "uint256[] memory" | Dynamic |
Vec<Address> | address[] | "address[] memory" | Dynamic |
Vec<bool> | bool[] | "bool[] memory" | Dynamic |
Vec<Bytes> | bytes[] | "bytes[] memory" | Dynamic |
Vec<MyStruct> | MyStruct[] | "MyStruct[] memory" | Dynamic |
Important Notes:
- Vectors are always returned as
memoryin Solidity, never ascalldata Vec<u8>maps touint8[], notbytes(useBytesfor Soliditybytes)- Vectors have dynamic size and consume more gas than fixed arrays
Bytes Types
The SDK provides Bytes for dynamic byte arrays and FixedBytes<N> for fixed-size byte arrays.