Stylus compound types
Compound types allow you to group multiple values together 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 - 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 will 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.
Dynamic Bytes (Bytes)
use alloy_primitives::Bytes;
use stylus_sdk::prelude::*;
#[public]
impl MyContract {
// Return dynamic bytes
pub fn get_data(&self) -> Bytes {
Bytes::from(vec![1, 2, 3, 4, 5])
}
// Process bytes
pub fn get_length(&self, data: Bytes) -> usize {
data.len()
}
// Concatenate bytes
pub fn concat(&self, a: Bytes, b: Bytes) -> Bytes {
let mut result = a.to_vec();
result.extend_from_slice(&b);
Bytes::from(result)
}
}
Fixed Bytes (FixedBytes<N>)
use alloy_primitives::FixedBytes;
use stylus_sdk::prelude::*;
#[public]
impl MyContract {
// bytes32 (common for hashes)
pub fn get_hash(&self) -> FixedBytes<32> {
FixedBytes::<32>::ZERO
}
// bytes4 (common for selectors)
pub fn get_selector(&self) -> FixedBytes<4> {
FixedBytes::from([0x12, 0x34, 0x56, 0x78])
}
// bytes16
pub fn get_uuid(&self) -> FixedBytes<16> {
FixedBytes::<16>::from([
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
])
}
}
Complete Examples
Example 1: Complex Data Structures
#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
extern crate alloc;
use alloc::string::String;
use alloy_primitives::{Address, U256};
use alloy_sol_types::sol;
use stylus_sdk::prelude::*;
sol! {
#[derive(Debug, AbiType)]
struct Token {
string name;
string symbol;
uint8 decimals;
uint256 totalSupply;
}
#[derive(Debug, AbiType)]
struct Balance {
address owner;
uint256 amount;
}
}
sol_storage! {
#[entrypoint]
pub struct CompoundExample {
uint256 counter;
}
}
#[public]
impl CompoundExample {
// Return tuple
pub fn get_info(&self) -> (String, U256, bool) {
("Example".to_string(), U256::from(42), true)
}
// Return struct
pub fn get_token(&self) -> Token {
Token {
name: "MyToken".to_string(),
symbol: "MTK".to_string(),
decimals: 18,
totalSupply: U256::from(1000000),
}
}
// Return vector of structs
pub fn get_balances(&self) -> Vec<Balance> {
vec![
Balance {
owner: Address::ZERO,
amount: U256::from(100),
},
Balance {
owner: Address::ZERO,
amount: U256::from(200),
},
]
}
// Accept array
pub fn process_array(&self, data: [U256; 5]) -> U256 {
data.iter().sum()
}
// Accept vector and struct
pub fn batch_transfer(&mut self, recipients: Vec<Balance>) -> U256 {
recipients.iter().map(|b| b.amount).sum()
}
}
Example 2: Nested Data Structures
#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
extern crate alloc;
use alloc::string::String;
use alloy_primitives::{Address, U256};
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;
}
}
sol_storage! {
#[entrypoint]
pub struct NestedExample {}
}
#[public]
impl NestedExample {
pub fn create_user(&self) -> User {
User {
account: Address::ZERO,
name: "Alice".to_string(),
dogs: vec![
Dog {
name: "Rex".to_string(),
breed: "Labrador".to_string(),
},
Dog {
name: "Max".to_string(),
breed: "Beagle".to_string(),
},
],
}
}
pub fn get_dog_names(&self, user: User) -> Vec<String> {
user.dogs.into_iter().map(|dog| dog.name).collect()
}
pub fn count_dogs(&self, users: Vec<User>) -> U256 {
let total: usize = users.iter().map(|u| u.dogs.len()).sum();
U256::from(total)
}
}
Best Practices
1. Choose the Right Type
// Use tuples for simple groupings
pub fn get_basics(&self) -> (U256, Address, bool) { /* ... */ }
// Use structs for complex data with named fields
sol! {
#[derive(Debug, AbiType)]
struct UserProfile {
address account;
string name;
uint256 balance;
bool active;
}
}
// Use arrays for fixed-size collections
pub fn get_top_five(&self) -> [U256; 5] { /* ... */ }
// Use vectors for dynamic collections
pub fn get_all_users(&self) -> Vec<Address> { /* ... */ }
2. Memory Efficiency
use alloy_primitives::U256;
// Prefer fixed arrays when size is known
pub fn fixed_data(&self) -> [U256; 10] {
// More gas-efficient
[U256::ZERO; 10]
}
// Use vectors only when size varies
pub fn dynamic_data(&self, count: usize) -> Vec<U256> {
vec![U256::ZERO; count]
}
3. Struct Naming
use alloy_sol_types::sol;
sol! {
// Good: Clear, descriptive names
#[derive(Debug, AbiType)]
struct TokenMetadata {
string name;
string symbol;
uint8 decimals;
}
// Avoid: Ambiguous names
#[derive(Debug, AbiType)]
struct Data {
uint256 x;
uint256 y;
}
}
4. Vector vs Array
use alloy_primitives::{Address, U256};
// Use fixed arrays for known sizes
pub fn get_admins(&self) -> [Address; 3] {
// Three admin addresses
[Address::ZERO; 3]
}
// Use vectors for variable sizes
pub fn get_users(&self) -> Vec<Address> {
// Unknown number of users
vec![]
}
5. Nested Structures
use alloy_sol_types::sol;
sol! {
// Good: Reasonable nesting depth
#[derive(Debug, AbiType)]
struct User {
address account;
Profile profile;
}
#[derive(Debug, AbiType)]
struct Profile {
string name;
uint256 age;
}
// Avoid: Excessive nesting (gas inefficient)
#[derive(Debug, AbiType)]
struct DeepNesting {
Level1 l1;
}
#[derive(Debug, AbiType)]
struct Level1 {
Level2 l2;
}
#[derive(Debug, AbiType)]
struct Level2 {
Level3 l3;
}
#[derive(Debug, AbiType)]
struct Level3 {
uint256 value;
}
}
Type Conversion and Helpers
Converting Between Types
use alloy_primitives::{U256, Bytes};
// Vec<u8> to Bytes
let vec: Vec<u8> = vec![1, 2, 3];
let bytes = Bytes::from(vec);
// Bytes to Vec<u8>
let bytes = Bytes::from(vec![1, 2, 3]);
let vec: Vec<u8> = bytes.to_vec();
// Array to Vec
let arr: [U256; 3] = [U256::from(1), U256::from(2), U256::from(3)];
let vec: Vec<U256> = arr.to_vec();
// Vec to array (if size matches)
let vec = vec![U256::from(1), U256::from(2), U256::from(3)];
let arr: [U256; 3] = vec.try_into().unwrap();
Working with Iterators
use alloy_primitives::U256;
// Map over vector
let numbers = vec![U256::from(1), U256::from(2), U256::from(3)];
let doubled: Vec<U256> = numbers.iter().map(|n| n * U256::from(2)).collect();
// Filter vector
let evens: Vec<U256> = numbers.into_iter().filter(|n| n.byte(0) % 2 == 0).collect();
// Fold/reduce
let sum = numbers.iter().fold(U256::ZERO, |acc, n| acc + n);
Common Patterns
Batch Operations
use alloy_primitives::{Address, U256};
use alloy_sol_types::sol;
use stylus_sdk::prelude::*;
sol! {
#[derive(Debug, AbiType)]
struct Transfer {
address to;
uint256 amount;
}
}
#[public]
impl MyContract {
pub fn batch_transfer(&mut self, transfers: Vec<Transfer>) -> U256 {
let mut total = U256::ZERO;
for transfer in transfers {
// Process each transfer
total += transfer.amount;
}
total
}
}
Pagination
use alloy_primitives::U256;
use stylus_sdk::prelude::*;
#[public]
impl MyContract {
pub fn get_page(&self, items: Vec<U256>, page: usize, size: usize) -> Vec<U256> {
let start = page * size;
let end = start + size;
items.get(start..end.min(items.len()))
.unwrap_or(&[])
.to_vec()
}
}
See Also
- Primitives - Basic types (bool, integers, address, strings)
- Storage Types - Persistent storage for compound types
- Type Conversions - Converting between types