Skip to main content

Global variables and functions

Stylus contracts access blockchain context and utilities through the VM (Virtual Machine) interface via self.vm(). This provides access to message context, block information, transaction details, account data, cryptographic functions, and gas metering.

Accessing the VM

All public contract methods have access to the VM context through self.vm():

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

#[public]
impl MyContract {
pub fn get_context_info(&self) -> (Address, U256, u64) {
let vm = self.vm();
(
vm.msg_sender(), // Caller's address
vm.msg_value(), // ETH sent with call
vm.block_number(), // Current block number
)
}
}

The VM provides methods organized into several categories:

Message Context (msg)

Methods for accessing information about the current call.

msg_sender()

Gets the address of the account that called the program.

pub fn msg_sender(&self) -> Address

Equivalent to: Solidity's msg.sender

Example:

#[public]
impl Token {
pub fn transfer(&mut self, to: Address, amount: U256) -> bool {
let from = self.vm().msg_sender();
// Transfer from the caller to recipient
self._transfer(from, to, amount)
}
}

Important Notes:

  • For normal L2-to-L2 transactions, behaves like EVM's CALLER opcode
  • For L1-to-L2 retryable ticket transactions, the top-level sender's address will be aliased
  • In delegate calls, returns the original caller (not the delegating contract)

msg_value()

Gets the ETH value in wei sent to the program.

pub fn msg_value(&self) -> U256

Equivalent to: Solidity's msg.value

Example:

#[public]
impl PaymentContract {
#[payable]
pub fn deposit(&mut self) -> U256 {
let amount = self.vm().msg_value();
let sender = self.vm().msg_sender();

let balance = self.balances.get(sender);
self.balances.setter(sender).set(balance + amount);

amount
}
}

Note: Only functions marked with #[payable] can receive ETH. Non-payable functions will revert if msg_value() > 0.

msg_reentrant()

Checks whether the current call is reentrant.

pub fn msg_reentrant(&self) -> bool

Example:

#[public]
impl Vault {
pub fn withdraw(&mut self, amount: U256) {
if self.vm().msg_reentrant() {
// Handle reentrancy
panic!("Reentrant call detected");
}
// Withdrawal logic...
}
}

Note: By default, Stylus contracts prevent reentrancy unless the reentrant feature is enabled.

Transaction Context (tx)

Methods for accessing information about the current transaction.

tx_origin()

Gets the top-level sender of the transaction.

pub fn tx_origin(&self) -> Address

Equivalent to: Solidity's tx.origin

Example:

#[public]
impl Factory {
#[constructor]
pub fn constructor(&mut self) {
// Use tx_origin when deploying via a factory
let deployer = self.vm().tx_origin();
self.owner.set(deployer);
}
}

Important: Returns the original EOA (Externally Owned Account) that initiated the transaction, even through multiple contract calls.

tx_gas_price()

Gets the gas price in wei per gas, which on Arbitrum chains equals the basefee.

pub fn tx_gas_price(&self) -> U256

Equivalent to: Solidity's tx.gasprice

Example:

#[public]
impl Analytics {
pub fn record_gas_price(&mut self) {
let price = self.vm().tx_gas_price();
self.gas_prices.push(price);
}
}

tx_ink_price()

Gets the price of ink in EVM gas basis points.

pub fn tx_ink_price(&self) -> u32

Description: Stylus uses "ink" as its unit of computation. This method returns the conversion rate from ink to gas. See Ink and Gas for more information.

Example:

#[public]
impl Contract {
pub fn get_ink_price(&self) -> u32 {
self.vm().tx_ink_price()
}
}

Block Context (block)

Methods for accessing information about the current block.

block_number()

Gets a bounded estimate of the L1 block number at which the Sequencer sequenced the transaction.

pub fn block_number(&self) -> u64

Equivalent to: Solidity's block.number

Example:

#[public]
impl TimeLock {
pub fn lock_until(&mut self, blocks: u64) {
let unlock_block = self.vm().block_number() + blocks;
self.unlock_block.set(U256::from(unlock_block));
}

pub fn can_unlock(&self) -> bool {
let current = self.vm().block_number();
let unlock = self.unlock_block.get().try_into().unwrap_or(u64::MAX);
current >= unlock
}
}

Note: See Block Numbers and Time for more information on how this value is determined on Arbitrum.

block_timestamp()

Gets a bounded estimate of the Unix timestamp at which the Sequencer sequenced the transaction.

pub fn block_timestamp(&self) -> u64

Equivalent to: Solidity's block.timestamp

Example:

#[public]
impl Auction {
pub fn place_bid(&mut self, amount: U256) {
let now = self.vm().block_timestamp();
let deadline = self.deadline.get().try_into().unwrap_or(0);

if now > deadline {
panic!("Auction ended");
}

// Process bid...
}
}

Note: See Block Numbers and Time for more information on how this value is determined on Arbitrum.

block_basefee()

Gets the basefee of the current block.

pub fn block_basefee(&self) -> U256

Equivalent to: Solidity's block.basefee

Example:

#[public]
impl FeeTracker {
pub fn current_basefee(&self) -> U256 {
self.vm().block_basefee()
}
}

block_coinbase()

Gets the coinbase of the current block.

pub fn block_coinbase(&self) -> Address

Equivalent to: Solidity's block.coinbase

Important: On Arbitrum chains, this is the L1 batch poster's address, which differs from Ethereum where the validator determines the coinbase.

Example:

#[public]
impl Contract {
pub fn get_batch_poster(&self) -> Address {
self.vm().block_coinbase()
}
}

block_gas_limit()

Gets the gas limit of the current block.

pub fn block_gas_limit(&self) -> u64

Equivalent to: Solidity's block.gaslimit

Example:

#[public]
impl Contract {
pub fn check_gas_limit(&self) -> bool {
let limit = self.vm().block_gas_limit();
limit > 30_000_000
}
}

Chain Context

Methods for accessing chain-specific information.

chain_id()

Gets the unique chain identifier of the Arbitrum chain.

pub fn chain_id(&self) -> u64

Equivalent to: Solidity's block.chainid

Example:

#[public]
impl MultiChain {
pub fn verify_chain(&self, expected_chain: u64) -> bool {
self.vm().chain_id() == expected_chain
}
}

Common Arbitrum Chain IDs:

  • Arbitrum One: 42161
  • Arbitrum Nova: 42170
  • Arbitrum Sepolia (testnet): 421614

Account Information

Methods for querying account details.

contract_address()

Gets the address of the current program.

pub fn contract_address(&self) -> Address

Equivalent to: Solidity's address(this)

Example:

#[public]
impl Contract {
pub fn this_address(&self) -> Address {
self.vm().contract_address()
}

pub fn this_balance(&self) -> U256 {
let addr = self.vm().contract_address();
self.vm().balance(addr)
}
}

balance(address)

Gets the ETH balance in wei of the account at the given address.

pub fn balance(&self, account: Address) -> U256

Equivalent to: Solidity's address.balance

Example:

#[public]
impl BalanceChecker {
pub fn get_balance(&self, account: Address) -> U256 {
self.vm().balance(account)
}

pub fn has_sufficient_balance(&self, account: Address, required: U256) -> bool {
self.vm().balance(account) >= required
}
}

code(address)

Gets the code from the account at the given address.

pub fn code(&self, account: Address) -> Vec<u8>

Equivalent to: Solidity's address.code (similar to EXTCODECOPY opcode)

Example:

#[public]
impl Contract {
pub fn is_contract(&self, account: Address) -> bool {
self.vm().code(account).len() > 0
}
}

code_size(address)

Gets the size of the code in bytes at the given address.

pub fn code_size(&self, account: Address) -> usize

Equivalent to: Solidity's EXTCODESIZE opcode

Example:

#[public]
impl Contract {
pub fn get_code_size(&self, account: Address) -> usize {
self.vm().code_size(account)
}
}

code_hash(address)

Gets the code hash of the account at the given address.

pub fn code_hash(&self, account: Address) -> B256

Equivalent to: Solidity's EXTCODEHASH opcode

Example:

#[public]
impl Contract {
pub fn verify_code(&self, account: Address, expected_hash: B256) -> bool {
self.vm().code_hash(account) == expected_hash
}
}

Note: The code hash of an account without code will be the empty hash: keccak("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470.

Gas and Metering

Methods for accessing gas and ink metering information.

evm_gas_left()

Gets the amount of gas left after paying for the cost of this hostio.

pub fn evm_gas_left(&self) -> u64

Equivalent to: Solidity's gasleft()

Example:

use stylus_sdk::call::Call;

#[public]
impl Contract {
pub fn complex_operation(&mut self, target: ITarget) {
let gas_before = self.vm().evm_gas_left();

// Use half the remaining gas for external call
let config = Call::new_mutating(self)
.gas(gas_before / 2);

target.do_work(self.vm(), config);
}
}

evm_ink_left()

Gets the amount of ink remaining after paying for the cost of this hostio.

pub fn evm_ink_left(&self) -> u64

Description: Returns remaining computation units in "ink". See Ink and Gas for more information on Stylus's compute pricing.

Example:

#[public]
impl Contract {
pub fn check_ink(&self) -> u64 {
self.vm().evm_ink_left()
}
}

ink_to_gas(ink)

Computes the units of gas per a specified amount of ink.

pub fn ink_to_gas(&self, ink: u64) -> u64

Example:

#[public]
impl Contract {
pub fn convert_ink_to_gas(&self, ink: u64) -> u64 {
self.vm().ink_to_gas(ink)
}
}

gas_to_ink(gas)

Computes the units of ink per a specified amount of gas.

pub fn gas_to_ink(&self, gas: u64) -> u64

Example:

#[public]
impl Contract {
pub fn convert_gas_to_ink(&self, gas: u64) -> u64 {
self.vm().gas_to_ink(gas)
}
}

Cryptographic Functions

The SDK provides cryptographic utilities through the crypto module and VM methods.

keccak()

Efficiently computes the keccak256 hash of the given preimage.

use stylus_sdk::crypto;

pub fn keccak<T: AsRef<[u8]>>(bytes: T) -> B256

Equivalent to: Solidity's keccak256()

Example:

use stylus_sdk::crypto;
use alloy_primitives::{Address, FixedBytes, U256};

#[public]
impl Contract {
pub fn hash_data(&self, data: Vec<u8>) -> FixedBytes<32> {
crypto::keccak(data)
}

pub fn verify_hash(&self, data: Vec<u8>, expected: FixedBytes<32>) -> bool {
crypto::keccak(data) == expected
}

// Hash multiple values together
pub fn hash_packed(&self, addr: Address, amount: U256) -> FixedBytes<32> {
let packed = [
addr.as_ref(),
&amount.to_be_bytes_vec(),
].concat();
crypto::keccak(packed)
}
}

native_keccak256()

VM method for computing keccak256 hash (alternative to crypto::keccak).

pub fn native_keccak256(&self, input: &[u8]) -> B256

Example:

#[public]
impl Contract {
pub fn hash_via_vm(&self, data: Vec<u8>) -> B256 {
self.vm().native_keccak256(&data)
}
}

Note: crypto::keccak() is the recommended approach as it's more ergonomic.

Event Logging

Methods for emitting events to the blockchain.

log(event)

Emits a typed Solidity event.

pub fn log<T: SolEvent>(&self, event: T)

Example:

use alloy_sol_types::sol;

sol! {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}

#[public]
impl Token {
pub fn transfer(&mut self, to: Address, value: U256) -> bool {
let from = self.vm().msg_sender();

// Transfer logic...
self._transfer(from, to, value);

// Emit event
self.vm().log(Transfer { from, to, value });

true
}
}

raw_log(topics, data)

Emits a raw log with custom topics and data.

pub fn raw_log(&self, topics: &[B256], data: &[u8]) -> Result<(), &'static str>

Example:

use alloy_primitives::{B256, FixedBytes};

#[public]
impl Contract {
pub fn emit_custom_log(&self) {
let topic = B256::from([1u8; 32]);
let topics = &[topic];
let data = b"custom data";

self.vm().raw_log(topics, data).unwrap();
}
}

Note: Maximum of 4 topics allowed. The first topic is typically the event signature hash.

Storage Operations

Methods for interacting with contract storage.

storage_load_bytes32(key)

Reads a 32-byte value from permanent storage.

pub fn storage_load_bytes32(&self, key: U256) -> B256

Equivalent to: Solidity's SLOAD opcode

Note: Storage is cached for efficiency. Use the SDK's storage types instead of direct storage access.

flush_cache(clear)

Persists dirty values in the storage cache to the EVM state trie.

pub fn flush_cache(&self, clear: bool)

Parameters:

  • clear: If true, drops the cache entirely after flushing

Note: Typically handled automatically by the SDK. Manual cache flushing is rarely needed.

Memory Management

pay_for_memory_grow(pages)

Pays for memory growth in WASM pages.

pub fn pay_for_memory_grow(&self, pages: u16)

Note: The #[entrypoint] macro handles this automatically. Manual calls are not recommended and will unproductively consume gas.

Complete Example

Here's a comprehensive example using various global variables and functions:

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

use alloy_primitives::{Address, U256, FixedBytes};
use alloy_sol_types::sol;
use stylus_sdk::{crypto, prelude::*};

sol! {
event Action(
address indexed caller,
uint256 value,
uint256 timestamp,
bytes32 data_hash
);
}

sol_storage! {
#[entrypoint]
pub struct ContextExample {
mapping(address => uint256) balances;
uint256 total_deposits;
uint256 creation_block;
address owner;
}
}

#[public]
impl ContextExample {
#[constructor]
pub fn constructor(&mut self) {
// Use tx_origin for factory deployments
self.owner.set(self.vm().tx_origin());
self.creation_block.set(U256::from(self.vm().block_number()));
}

#[payable]
pub fn deposit(&mut self, data: Vec<u8>) {
// Message context
let caller = self.vm().msg_sender();
let amount = self.vm().msg_value();

// Block context
let timestamp = self.vm().block_timestamp();
let block_num = self.vm().block_number();

// Require minimum value
if amount < U256::from(1000) {
panic!("Insufficient deposit");
}

// Update balances
let balance = self.balances.get(caller);
self.balances.setter(caller).set(balance + amount);
self.total_deposits.set(self.total_deposits.get() + amount);

// Hash the data
let data_hash = crypto::keccak(&data);

// Emit event
self.vm().log(Action {
caller,
value: amount,
timestamp: U256::from(timestamp),
data_hash,
});
}

pub fn get_contract_info(&self) -> (Address, U256, u64, u64) {
(
self.vm().contract_address(), // Contract's address
self.vm().balance(self.vm().contract_address()), // Contract's balance
self.vm().chain_id(), // Chain ID
self.vm().block_number(), // Current block
)
}

pub fn verify_signature(&self, message: Vec<u8>, expected_hash: FixedBytes<32>) -> bool {
let hash = crypto::keccak(message);
hash == expected_hash
}

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

pub fn time_since_creation(&self) -> u64 {
let current_block = self.vm().block_number();
let creation_block: u64 = self.creation_block.get().try_into().unwrap_or(0);
current_block.saturating_sub(creation_block)
}
}

Summary of Available Methods

Message Context

  • msg_sender()Address - Caller's address
  • msg_value()U256 - ETH sent with call
  • msg_reentrant()bool - Is reentrant call

Transaction Context

  • tx_origin()Address - Original transaction sender
  • tx_gas_price()U256 - Gas price
  • tx_ink_price()u32 - Ink price

Block Context

  • block_number()u64 - Block number
  • block_timestamp()u64 - Block timestamp
  • block_basefee()U256 - Base fee
  • block_coinbase()Address - Batch poster
  • block_gas_limit()u64 - Gas limit

Chain Context

  • chain_id()u64 - Chain identifier

Account Information

  • contract_address()Address - This contract's address
  • balance(Address)U256 - Account balance
  • code(Address)Vec<u8> - Account code
  • code_size(Address)usize - Code size
  • code_hash(Address)B256 - Code hash

Gas and Metering

  • evm_gas_left()u64 - Remaining gas
  • evm_ink_left()u64 - Remaining ink
  • ink_to_gas(u64)u64 - Convert ink to gas
  • gas_to_ink(u64)u64 - Convert gas to ink

Cryptographic Functions

  • crypto::keccak(bytes)B256 - Keccak256 hash
  • native_keccak256(&[u8])B256 - Keccak256 via VM

Event Logging

  • log<T: SolEvent>(event) - Emit typed event
  • raw_log(&[B256], &[u8]) - Emit raw log

See Also