Skip to main content

Conversions between types

Stylus smart contracts often need to convert between different type representations: Rust native types, Alloy primitives, and Solidity types. The Stylus SDK provides comprehensive conversion mechanisms through the AbiType trait and various standard Rust conversion traits.

Understanding type relationships​

The Stylus SDK establishes a bidirectional relationship between Rust and Solidity types:

  • Alloy provides: Solidity types → Rust types mapping via SolType
  • Stylus SDK provides: Rust types → Solidity types mapping via AbiType

Together, these create a complete two-way type system for interoperability.

Key type mappings​

Rust/Alloy typeSolidity typeNotes
boolboolNative boolean
u8, u16, u32, u64, u128uint8 through uint128Native unsigned integers
i8, i16, i32, i64, i128int8 through int128Native signed integers
Uint<BITS, LIMBS>uintBITSArbitrary-sized unsigned integers (8-256 bits)
Signed<BITS, LIMBS>intBITSArbitrary-sized signed integers (8-256 bits)
U256uint256256-bit unsigned integer (most common)
Addressaddress20-byte Ethereum address
FixedBytes<N>bytesNFixed-size byte array
BytesbytesDynamic byte array
StringstringDynamic UTF-8 string
Vec<T>T[]Dynamic array
[T; N]T[N]Fixed-size array
(T1, T2, ...)(T1, T2, ...)Tuple types

Important: The SDK treats Vec<u8> as Solidity uint8[]. For Solidity bytes, use alloy_primitives::Bytes.

Converting numeric types​

Creating integers from literals​

use stylus_sdk::alloy_primitives::{U256, I256, U8, I8};

// From integer literals
let small: U8 = U8::from(1);
let large: U256 = U256::from(255);

// For signed integers
let positive: I8 = I8::unchecked_from(127);
let negative: I8 = I8::unchecked_from(-1);
let signed_large: I256 = I256::unchecked_from(0xff_u64);

Parsing from strings​

use stylus_sdk::alloy_primitives::I256;

// Parse decimal strings
let a = I256::try_from(20003000).unwrap();
let b = "100".parse::<I256>().unwrap();

// Parse hexadecimal strings
let c = "-0x138f".parse::<I256>().unwrap();

// Underscores are ignored for readability
let d = "1_000_000".parse::<I256>().unwrap();

// Arithmetic works as expected
let result = a * b + c - d;

Integer constants​

use stylus_sdk::alloy_primitives::I256;

let max = I256::MAX; // Maximum value
let min = I256::MIN; // Minimum value
let zero = I256::ZERO; // Zero
let minus_one = I256::MINUS_ONE; // -1

Converting between integer sizes​

use stylus_sdk::alloy_primitives::{Uint, Signed, U256};

// Between Alloy integer types (same bit-width)
let uint_value = Uint::<128, 2>::from(999);
let u128_value: u128 = uint_value.try_into()
.map_err(|_| "conversion error")
.unwrap();

// Between different bit-widths
let small = Uint::<8, 1>::from(100);
let large = U256::from(small);

The SDK uses the ConvertInt trait internally to enable conversions between Alloy’s Uint<BITS, LIMBS> types and Rust’s native integer types, such as u8, u16, u32, u64, and u128.

Converting addresses​

Creating addresses​

use stylus_sdk::alloy_primitives::{Address, address};

// From a 20-byte array
let addr1 = Address::from([0x11; 20]);

// Using the address! macro with checksummed string
let addr2 = address!("d8da6bf26964af9d7eed9e03e53415d37aa96045");

// From a byte slice
let bytes: [u8; 20] = [0xd8, 0xda, 0x6b, 0xf2, /* ... */];
let addr3 = Address::from(bytes);

Converting addresses to bytes​

use stylus_sdk::alloy_primitives::Address;

let addr = address!("d8da6bf26964af9d7eed9e03e53415d37aa96045");

// Get reference to underlying bytes
let bytes_ref: &[u8] = addr.as_ref();

// Use in byte concatenation
let data = [addr.as_ref(), other_data].concat();

Converting byte types​

Fixed-size bytes​

use stylus_sdk::alloy_primitives::FixedBytes;

// Create from array
let fixed = FixedBytes::<32>::new([0u8; 32]);

// Create from slice
let slice: &[u8] = &[1, 2, 3, 4];
let fixed = FixedBytes::<4>::from_slice(slice);

// Convert to slice
let bytes_ref: &[u8] = fixed.as_ref();

Dynamic bytes​

use stylus_sdk::abi::Bytes;

// Create from Vec<u8>
let bytes = Bytes::from(vec![1, 2, 3, 4]);

// Create empty
let empty = Bytes::new();

// Get reference to underlying data
let data: &[u8] = bytes.as_ref();

// Convert to Vec<u8>
let vec: Vec<u8> = bytes.to_vec();

Byte array conversions​

use stylus_sdk::alloy_primitives::U256;

// Convert U256 to big-endian bytes
let value = U256::from(12345);
let bytes_vec: Vec<u8> = value.to_be_bytes_vec();
let bytes_array: [u8; 32] = value.to_be_bytes();

// Convert from big-endian bytes
let from_slice = U256::try_from_be_slice(&bytes_vec).unwrap();

Converting strings​

use alloc::string::{String, ToString};

// String conversions
let rust_string = "hello".to_string();
let bytes = rust_string.as_bytes();

// For Solidity string parameters in functions
// the String type is automatically handled by AbiType
pub fn process_string(&self, text: String) -> String {
text
}

Converting collections​

Dynamic arrays (Vec)​

use stylus_sdk::alloy_primitives::U256;
use alloc::vec::Vec;

// Vec is used directly as Solidity dynamic arrays
let numbers: Vec<U256> = vec![
U256::from(1),
U256::from(2),
U256::from(3),
];

// For Vec<u8>, note this maps to uint8[], not bytes
let uint8_array: Vec<u8> = vec![1, 2, 3];

Fixed-size arrays​

use stylus_sdk::alloy_primitives::U256;

// Fixed arrays map directly to Solidity fixed arrays
let fixed: [U256; 3] = [
U256::from(1),
U256::from(2),
U256::from(3),
];

// Nested arrays
let nested: [[u32; 2]; 4] = [[1, 2], [3, 4], [5, 6], [7, 8]];

ABI encoding and decoding​

Encoding types​

use stylus_sdk::abi::{encode, encode_params};
use stylus_sdk::alloy_primitives::{Address, U256};
use alloy_sol_types::{sol_data::*, SolType};

// Encode a single value
let value = U256::from(100);
let encoded = encode(&value);

// Encode tuple of parameters
type TransferParams = (Address, Uint<256>);
let params = (address, amount);
let encoded = TransferParams::abi_encode_params(&params);

Decoding types​

use stylus_sdk::abi::decode_params;
use stylus_sdk::alloy_primitives::{Address, U256};
use alloy_sol_types::{sol_data::*, SolType};

// Define the expected type structure
type TransferParams = (Address, Uint<256>);

// Decode from bytes
let decoded: (Address, U256) = TransferParams::abi_decode_params(&encoded_data)
.map_err(|_| "decode error")?;

Packed encoding​

Packed encoding is useful for hashing and signature verification:

use stylus_sdk::alloy_primitives::{Address, U256};
use alloy_sol_types::{sol_data::*, SolType};

// Method 1: Using SolType::abi_encode_packed
type DataTypes = (Address, Uint<256>, String, Bytes, Uint<256>);
let data = (target, value, func, bytes, timestamp);
let packed = DataTypes::abi_encode_packed(&data);

// Method 2: Manual concatenation
let packed_manual = [
target.as_ref(),
&value.to_be_bytes_vec(),
func.as_bytes(),
bytes.as_ref(),
&timestamp.to_be_bytes_vec(),
].concat();

Error type conversions​

Stylus error types can be converted using the Into trait:

use stylus_sdk::prelude::*;

sol! {
error InvalidParam();
error NotFound();
}

#[derive(SolidityError)]
pub enum MyError {
InvalidParam(InvalidParam),
NotFound(NotFound),
}

pub fn check_value(&self, value: U256) -> Result<(), MyError> {
if value == U256::ZERO {
return Err(InvalidParam {}.into());
}
Ok(())
}

Storage type conversions​

Storage types require special handling for persistence:

use stylus_sdk::prelude::*;
use stylus_sdk::alloy_primitives::U256;

#[storage]
pub struct Counter {
count: StorageU256,
}

#[public]
impl Counter {
// Get value from storage
pub fn get_count(&self) -> U256 {
self.count.get()
}

// Set value in storage
pub fn set_count(&mut self, value: U256) {
self.count.set(value);
}

// Increment using arithmetic
pub fn increment(&mut self) {
let current = self.count.get();
self.count.set(current + U256::from(1));
}
}

Best practices​

  1. Use try_from for fallible conversions: When converting between types where overflow is possible, use try_from instead of panicking conversions.

  2. Prefer native types when appropriate: Use Rust's native bool, u8-u128, and i8-i128 types when they match your needs exactly. They're more efficient and ergonomic.

  3. Be explicit about byte types: Remember that Vec<u8> maps to uint8[], not bytes. Use Bytes from stylus_sdk::abi for Solidity bytes type.

  4. Use the address! macro: For hardcoded addresses, use the address! macro which performs compile-time validation and checksumming.

  5. Handle conversion errors: Always handle potential errors from try_from, try_into, and parse operations rather than using unwrap in production code.

  6. Consider packed encoding for hashing: When preparing data for hashing or signature verification, packed encoding produces more compact representations.

Reference​

For complete implementation details, see:

  • [/stylus-sdk/src/abi/mod.rs](https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/stylus-sdk/src/abi/mod.rs): AbiType trait and encoding functions
  • [/stylus-sdk/src/abi/ints.rs](https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/stylus-sdk/src/abi/ints.rs): Integer type conversions
  • [/stylus-sdk/src/abi/impls.rs](https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/stylus-sdk/src/abi/impls.rs): Implementations for standard types
  • [/stylus-sdk/src/storage/traits.rs](https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/stylus-sdk/src/abi/traits.rs): Storage type conversion traits

External references​

Stylus uses the Alloy library for EVM primitives, so primitive conversion rules follow Alloy's conventions: