Skip to main content

Stylus primitives

The Stylus SDK provides full support for Rust primitive types with automatic ABI encoding/decoding and Solidity type mappings. These primitives can be used in contract method signatures, storage, and as function parameters.

Boolean (bool)

Booleans in Rust map directly to Solidity's bool type.

Usage in Contract Methods

use stylus_sdk::prelude::*;

#[public]
impl MyContract {
pub fn is_valid(&self) -> bool {
true
}

pub fn toggle(&mut self, flag: bool) {
// Use the boolean value
if flag {
// Do something
}
}
}

Solidity Mapping

  • Rust type: bool
  • Solidity type: bool
  • Storage size: 1 byte
  • ABI signature: "bool"

Integers

The Stylus SDK supports both signed and unsigned integers with various bit sizes. All integer types from Rust's standard library and alloy-primitives are supported.

Unsigned Integers

Standard Rust Unsigned Integers

use stylus_sdk::prelude::*;

#[public]
impl MyContract {
// u8: 8-bit unsigned integer
pub fn get_byte(&self) -> u8 {
255
}

// u16: 16-bit unsigned integer
pub fn get_short(&self) -> u16 {
65535
}

// u32: 32-bit unsigned integer
pub fn get_int(&self) -> u32 {
4294967295
}

// u64: 64-bit unsigned integer
pub fn get_long(&self) -> u64 {
18446744073709551615
}

// u128: 128-bit unsigned integer
pub fn get_u128(&self) -> u128 {
340282366920938463463374607431768211455
}
}

Alloy Unsigned Integers

For larger integers and full compatibility with Solidity's uint types, use alloy_primitives::Uint:

use alloy_primitives::{U256, Uint};
use stylus_sdk::prelude::*;

#[public]
impl MyContract {
// U256: 256-bit unsigned integer (most common in Solidity)
pub fn get_balance(&self) -> U256 {
U256::from(1000000)
}

// Any bit size from 8 to 256 (in multiples of 8)
pub fn get_u160(&self) -> Uint<160, 3> {
Uint::<160, 3>::from(999)
}

pub fn get_u96(&self) -> Uint<96, 2> {
Uint::<96, 2>::from(123456)
}
}

Signed Integers

Standard Rust Signed Integers

use stylus_sdk::prelude::*;

#[public]
impl MyContract {
// i8: 8-bit signed integer
pub fn get_signed_byte(&self) -> i8 {
-128
}

// i16: 16-bit signed integer
pub fn get_signed_short(&self) -> i16 {
-32768
}

// i32: 32-bit signed integer
pub fn get_signed_int(&self) -> i32 {
-2147483648
}

// i64: 64-bit signed integer
pub fn get_signed_long(&self) -> i64 {
-9223372036854775808
}

// i128: 128-bit signed integer
pub fn get_signed_i128(&self) -> i128 {
-170141183460469231731687303715884105728
}
}

Alloy Signed Integers

use alloy_primitives::{Signed, I256};
use stylus_sdk::prelude::*;

#[public]
impl MyContract {
// I256: 256-bit signed integer
pub fn get_signed_balance(&self) -> I256 {
I256::try_from(-1000).unwrap()
}

// Any bit size from 8 to 256 (in multiples of 8)
pub fn get_i160(&self) -> Signed<160, 3> {
Signed::<160, 3>::try_from(-999).unwrap()
}
}

Integer Type Mappings

Rust TypeSolidity TypeBit SizeABI Signature
u8uint88 bits"uint8"
u16uint1616 bits"uint16"
u32uint3232 bits"uint32"
u64uint6464 bits"uint64"
u128uint128128 bits"uint128"
Uint<160, 3>uint160160 bits"uint160"
U256 / Uint<256, 4>uint256256 bits"uint256"
i8int88 bits"int8"
i16int1616 bits"int16"
i32int3232 bits"int32"
i64int6464 bits"int64"
i128int128128 bits"int128"
Signed<160, 3>int160160 bits"int160"
I256 / Signed<256, 4>int256256 bits"int256"

Note: All Solidity uint/int types from uint8/int8 to uint256/int256 (in 8-bit increments) are supported through Uint<BITS, LIMBS> and Signed<BITS, LIMBS>.

Address

Ethereum addresses are represented by the Address type from alloy_primitives.

Basic Usage

use alloy_primitives::Address;
use stylus_sdk::prelude::*;

#[public]
impl MyContract {
pub fn get_owner(&self) -> Address {
Address::ZERO
}

pub fn is_owner(&self, account: Address) -> bool {
account == self.vm().msg_sender()
}

pub fn transfer_ownership(&mut self, new_owner: Address) {
// Address validation and logic
if new_owner == Address::ZERO {
// Handle error
}
}
}

Address Constants

use alloy_primitives::Address;

// Zero address (0x0000000000000000000000000000000000000000)
let zero = Address::ZERO;

// Parse from string
let addr = Address::parse_checksummed("0x1234567890123456789012345678901234567890", None).unwrap();

// Create from bytes
let bytes: [u8; 20] = [0; 20];
let addr = Address::from(bytes);

Solidity Mapping

  • Rust type: Address (from alloy_primitives)
  • Solidity type: address
  • Storage size: 20 bytes (160 bits)
  • ABI signature: "address"

String

Rust String types map to Solidity string type.

String Literals

use stylus_sdk::prelude::*;
use alloc::string::String;

#[public]
impl MyContract {
pub fn get_name(&self) -> String {
String::from("MyToken")
}

pub fn greet(&self, name: String) -> String {
format!("Hello, {}!", name)
}
}

Solidity Mapping

  • Rust type: String (from alloc::string)
  • Solidity type: string
  • Storage: Dynamic (heap-allocated)
  • ABI signature: "string"
  • ABI export:
    • As argument: "string calldata"
    • As return: "string memory"

Note: Strings in Solidity are UTF-8 encoded byte arrays. When using strings in Stylus:

  • Use alloc::string::String for owned strings
  • Strings are dynamically sized and stored in memory/calldata
  • For storage, use StorageString (see Storage Types)

Bytes

The SDK provides two types for working with byte data:

1. Dynamic Bytes (Bytes)

For variable-length byte arrays (Solidity bytes):

use alloy_primitives::Bytes;
use stylus_sdk::prelude::*;

#[public]
impl MyContract {
pub fn get_data(&self) -> Bytes {
Bytes::from(vec![1, 2, 3, 4])
}

pub fn process_data(&mut self, data: Bytes) -> usize {
data.len()
}
}

2. Fixed Bytes (FixedBytes)

For fixed-length byte arrays (Solidity bytesN):

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
}

// bytes2
pub fn get_signature(&self) -> FixedBytes<2> {
FixedBytes::new([0x12, 0x34])
}

// Any size from 1 to 32
pub fn get_bytes8(&self) -> FixedBytes<8> {
FixedBytes::<8>::from([1, 2, 3, 4, 5, 6, 7, 8])
}
}

Common FixedBytes Aliases

use alloy_primitives::{B256, B160, B128};

// B256 is FixedBytes<32> (bytes32)
let hash: B256 = B256::ZERO;

// B160 is FixedBytes<20> (bytes20)
let data: B160 = B160::ZERO;

// B128 is FixedBytes<16> (bytes16)
let value: B128 = B128::ZERO;

Bytes Type Mappings

Rust TypeSolidity TypeDescription
BytesbytesDynamic byte array
Vec<u8>uint8[]Array of bytes (NOT bytes!)
FixedBytes<N>bytesNFixed-size byte array (N = 1-32)
B256 / FixedBytes<32>bytes3232-byte array (hashes)
B160 / FixedBytes<20>bytes2020-byte array
B128 / FixedBytes<16>bytes1616-byte array

Important Distinction:

  • Vec<u8> maps to Solidity uint8[] (array of unsigned integers)
  • Bytes maps to Solidity bytes (dynamic byte array)
  • For Solidity bytes, always use alloy_primitives::Bytes

Bytes ABI Encoding

// Bytes type
// ABI signature: "bytes"
// As argument: "bytes calldata"
// As return: "bytes memory"

// FixedBytes<N> type
// ABI signature: "bytesN" where N is 1-32
// Example: FixedBytes<32> -> "bytes32"

Hex String Literals

When working with hex data, you can use hex literals:

use alloy_primitives::{hex, Address, FixedBytes, Bytes};

// Hex bytes
let data = hex!("deadbeef");

// Address from hex
let addr = Address::from(hex!("1234567890123456789012345678901234567890"));

// FixedBytes from hex
let hash = FixedBytes::<32>::from(hex!(
"0000000000000000000000000000000000000000000000000000000000000000"
));

// Dynamic Bytes from hex
let bytes = Bytes::from(hex!("aabbccdd"));

Complete Example

Here's a comprehensive example showing all primitive types:

#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
extern crate alloc;

use alloc::string::String;
use alloy_primitives::{Address, Bytes, FixedBytes, U256};
use stylus_sdk::prelude::*;

sol_storage! {
#[entrypoint]
pub struct PrimitiveExample {
bool initialized;
uint256 count;
address owner;
}
}

#[public]
impl PrimitiveExample {
// Boolean
pub fn is_initialized(&self) -> bool {
self.initialized.get()
}

// Unsigned integers (native Rust)
pub fn get_u8(&self) -> u8 {
255
}

pub fn get_u256(&self) -> U256 {
self.count.get()
}

// Signed integers
pub fn get_signed(&self) -> i32 {
-42
}

// Address
pub fn get_owner(&self) -> Address {
self.owner.get()
}

pub fn set_owner(&mut self, new_owner: Address) {
self.owner.set(new_owner);
}

// String
pub fn get_name(&self) -> String {
String::from("PrimitiveExample")
}

// Dynamic bytes
pub fn get_data(&self) -> Bytes {
Bytes::from(vec![1, 2, 3, 4])
}

// Fixed bytes
pub fn get_hash(&self) -> FixedBytes<32> {
FixedBytes::<32>::ZERO
}

// Multiple parameters
pub fn complex_function(
&mut self,
flag: bool,
amount: U256,
recipient: Address,
data: Bytes
) -> bool {
// Function logic
true
}
}

Best Practices

  1. Use U256 for token amounts: Solidity commonly uses uint256 for token balances and amounts.

    use alloy_primitives::U256;

    pub fn transfer(&mut self, amount: U256) {
    // amount is uint256 in Solidity
    }
  2. Use Address for account addresses: Always use alloy_primitives::Address for Ethereum addresses.

    use alloy_primitives::Address;

    pub fn get_balance(&self, account: Address) -> U256 {
    // Query balance
    }
  3. Use Bytes for dynamic byte data: For Solidity bytes, use alloy_primitives::Bytes, not Vec<u8>.

    use alloy_primitives::Bytes;

    pub fn process(&self, data: Bytes) {
    // data maps to Solidity bytes
    }
  4. Use FixedBytes for hashes and signatures: For fixed-size byte data like hashes.

    use alloy_primitives::FixedBytes;

    pub fn verify(&self, hash: FixedBytes<32>) -> bool {
    // hash maps to Solidity bytes32
    true
    }
  5. Check for zero addresses: Always validate addresses before use.

    use alloy_primitives::Address;

    pub fn set_admin(&mut self, admin: Address) {
    if admin == Address::ZERO {
    // Handle error
    }
    }

Type Conversion

Between Integer Types

use alloy_primitives::U256;

// Native to U256
let amount: u64 = 1000;
let big_amount = U256::from(amount);

// U256 to native (with bounds checking)
let big_value = U256::from(1000);
let small_value: u64 = big_value.to::<u64>();

Address Conversions

use alloy_primitives::Address;

// From bytes
let bytes: [u8; 20] = [0; 20];
let addr = Address::from(bytes);

// To bytes
let addr = Address::ZERO;
let bytes: [u8; 20] = addr.into();

See Also