Skip to main content

Migration Guide

This guide provides a comprehensive comparison between Solidity (Ethereum's smart contract language) and Leo (Aleo's programming language for zero-knowledge applications). The document covers fundamental differences between these languages and offers practical examples to assist developers transitioning from Ethereum development to Aleo.

Execution Models

Leo and Solidity differ fundamentally in their execution approaches:

  • Ethereum/Solidity: On-chain execution where all computation occurs on the blockchain
  • Aleo/Leo: Off-chain execution with zero-knowledge proofs, where computation occurs privately and only the proof is verified on-chain

This difference influences many language design decisions that can be seen below.

Basic Structure

Headers

Programs begin differently in each language:

Solidity Contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract MyToken {
// Contract implementation
}

Leo Program:

// No license identifier required
// No pragma needed

program my_token.aleo {
// Program implementation
}

Key Differences:

  • Leo does not require license identifiers or pragma statements
  • Import statements in Leo use program IDs rather than file paths
  • Leo programs must have a .aleo suffix in their name
  • To define a contract in Solidity, contract is used as the keyword. In Leo, programs (equivalent to smart contracts in Solidity) are defined with the program keyword, followed by curly brackets {}

Constructor

Constructor in Solidity is optional. Leo does not have a constructor currently, but ARC-0006: Program Upgradability will introduce a must-have constructor in new programs in Leo. While constructor in Solidity only runs once, constructor in Leo is immutable and will run as part of a deployment or upgrade, thus is used to define the program upgradability logic.

Comments

Both languages support identical comment syntax:

// Single line comment in both languages

/*
* Multi-line comments
* work identically too
*/

Imports

Solidity's Import System:

// Import entire file
import "./MyContract.sol";

// Import specific symbols
import {Symbol1, Symbol2} from "./MyContract.sol";

// Import with alias
import * as MyAlias from "./MyContract.sol";

// Import from node_modules
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

Leo's Import System:

import credits.aleo;

program helloworld.aleo {
// Program implementation
}

Important: Leo imports must also be declared in program.json:

{
"program": "my_token.aleo",
"version": "0.0.0",
"description": "",
"license": "MIT",
"dependencies": [
{
"name": "credits.aleo",
"location": "network", // Importing from network
"network": "testnet"
},
{
"name": "board.aleo",
"location": "local", // Importing from local
"path": "../board"
}
]
}

Dependency Types:

  • Network dependencies: Programs deployed on Aleo networks (mainnet, testnet)
  • Local dependencies: Programs in local file system directories

Key Import Difference:

  • Solidity: Imports copy code directly into your contract at compile time, combining everything into one file
  • Leo: Imported programs remain separate entities - they are not merged with the current file, and each keeps its own unique program ID on the blockchain

Data & State

State Variables and Storage Models

Solidity and Leo differ significantly in how they handle state variables, storage, and data privacy.

Solidity's Approach:

contract Storage {
// State variables with visibility modifiers (access control only)
uint256 public constant FIXED_VALUE = 100; // Compile-time constant
uint256 public immutable RUNTIME_VALUE; // Set in constructor
uint256 public permanentData; // Auto-generates getter function
uint256 internal shared; // Accessible in derived contracts
uint256 private local = 42; // Inaccessible in derived contracts - still visible on-chain

constructor(uint256 _value) {
RUNTIME_VALUE = _value; // Can be set at construction
}

function processData(uint256 tempData) public {
uint256 memoryVar = tempData * 2; // Memory (temporary)
permanentData = memoryVar; // Persisted to storage
}
}

Leo's Approach:

program storage.aleo {
// Compile-time constants only
const FIXED_VALUE: u64 = 100u64;

// Permanant public state: accessible to everyone
mapping balances: address => u64;

// Permanant private state: stored in records (with cryptographic privacy)
record Token {
owner: address,
amount: u64,
}

// Transition parameters and returns are private by default unless marked public
async transition process_data(public amount: u64) -> (Token, Future) {
let amount_loc: u64 = amount * 2u64; // Local variable

let token: Token = Token {
owner: self.caller,
amount: amount_loc,
};

return (token, finalize_process_data(self.caller, amount_loc));
}

// Async function to handle on-chain state updates
// Parameters in async functions are automatically public since network nodes execute the computation
// Async function cannot return values
async function finalize_process_data(caller: address, amount_loc: u64) {
let current_balance: u64 = Mapping::get_or_use(balances, caller, 0u64);
Mapping::set(balances, caller, current_balance + amount_loc);
}
}

Key Differences:

  • Solidity Visibility: private, internal, public control code access but all data is visible on-chain
  • Leo Privacy: public vs private determines actual cryptographic privacy - whether data is stored on-chain or off-chain in records
  • Constants: Solidity has both constant and immutable, Leo only has compile-time const
  • Local Variables: The let keyword declares temporary computation variables (similar to Solidity's memory)
  • No Transient Storage: Leo does not support Solidity's transient storage concept

Getter Functions

The compiler of Solidity automatically creates getter functions for all public state variables. Leo does not have similar features, but instead provides direct API access to query mapping values from the node without requiring getter functions in the program code.

// Solidity: automatic getter generated
contract Example {
uint256 public value; // Automatically creates value() function as getter
}

Key Difference:

  • Solidity: Requires public state variables or explicit getter functions for external access
  • Leo: Mapping values are accessible via REST API endpoints without any program code:
    • Endpoint: GET /{network}/program/{programID}/mapping/{mappingName}/{mappingKey}
    • Example: GET /testnet3/program/credis.aleo/mapping/account/aleo1abc...
    • Reference: Get Mapping Value API

Deleting state variables

Solidity's Delete Operation:

contract DeleteExample {
uint256 public value = 100;
mapping(address => uint256) public balances;

function resetValue() public {
delete value; // Resets to initial value (0 for uint256)
}

function removeBalance(address user) public {
delete balances[user]; // Removes mapping entry, resets to initial value (0)
}
}

Leo's Mapping Operations:

program delete_example.aleo {
mapping balances: address => u64;

async transition remove_balance(user: address) -> Future {
return finalize_remove_balance(user);
}

async function finalize_remove_balance(user: address) {
// Check if mapping contains the key before removing
let exists: bool = Mapping::contains(balances, user);
if exists {
// Remove mapping entry
Mapping::remove(balances, user);
}
}
}

Key Differences:

  • Solidity: delete operator resets variables to their initial values (0 for numbers, false for booleans, empty for arrays)
  • Leo: Only supports removing entries from mappings using Mapping::remove()
  • No Direct Delete: Leo doesn't have a direct equivalent to Solidity's delete operator for other variable types
  • Mapping Focus: Leo's deletion functionality is focused on mapping entries, which is the primary state storage mechanism
  • Key Existence Check: Leo provides Mapping::contains() to check if a key exists in a mapping before operations

Data Types

The type systems differ in several ways:

Common Types (Similar Behavior):

// Solidity
bool flag = true;
uint32 smallNumber = 100;
address userAddr = 0x742d35...;
// Leo
let flag: bool = true;
let small_number: u32 = 100u32; // Must append type suffix (u32)
let user_addr: address = aleo1abc...;

Type Suffix Requirement: In Leo, all literals must explicitly append their type suffix.

Leo Special Types:

Leo introduces several unique cryptographic types that don't exist in Solidity:

// Leo's unique cryptographic types
let field_element: field = 123field; // Field elements for arithmetic
let group_element: group = group::GEN; // Group elements for elliptic curves
let scalar_value: scalar = 456scalar; // Scalar values for cryptographic operations
let user_signature: signature = sign1...; // Built-in signature type

Signature Verification: Leo's signature type uses the Schnorr signature scheme (different from Solidity's ECDSA), where signatures are generated using a nonce, challenge hash, and private key, then verified by reconstructing the challenge against the public key.

Leo Limitations:

  1. Integer Limitations:
// Solidity: supports up to 256 bits
uint256 bigNumber = 115792089237316195423570985008687907853269984665640564039457584007913129639935;
// Leo: maximum 128 bits
let big_number: u128 = 340282366920938463463374607431768211455u128;
// uint256 equivalent does not exist
  1. Types Not Supported in Leo:
// Solidity types that Leo does not support
string memory text = "Hello World"; // String type not available in Leo
bytes memory data = hex"1234"; // Bytes type not available in Leo
uint256[] memory dynamicArray; // Dynamic arrays not available in Leo
mapping(string => uint256) stringMap; // String keys in mappings not available in Leo
enum Status { Active, Inactive } // Enum type not available in Leo
// Leo alternatives or workarounds
let text_hash: field = 123456field; // Convert text into Leo supported types such as Field
let static_array: [u32; 5] = [1u32, 2u32, 3u32, 4u32, 5u32]; // Fixed-size arrays only
mapping hash_map: field => u64; // Use field for complex key types
const ACTIVE: u8 = 0u8; // Use constants instead of enums
const INACTIVE: u8 = 1u8;
  1. Static Arrays in Leo:
// Solidity: supports both dynamic and static arrays
uint256[5] public staticArray;

function updateArray() public {
staticArray[0] = 42; // Direct element modification
}
// Leo: only supports static array 
transition simple_array() -> [u32; 3] {
// Create a fixed-size array of 3 elements
let numbers: [u32; 3] = [1u32, 2u32, 3u32];

// Modify elements using explicit index type (u8)
numbers[0u8] = 10u32;
numbers[1u8] = 20u32;

return numbers;
}

Key Static Array Features:

  • Fixed Size: Array size must be known at compile time
  • Element Access: Use explicit index types (e.g., array[0u8])
  • No Dynamic Operations: No push(), pop(), or resizing operations

Type Conversions

Solidity's Implicit Conversions:

uint8 small = 100;
uint256 big = small; // Automatic conversion

// Solidity allows truncation
uint16 large = 300;
uint8 truncated = uint8(large); // Only the rightmost 8 bits are kept

Leo's Explicit Conversions:

let small: u8 = 100u8;
let big: u32 = small as u32; // Explicit casting required

// Leo fails on truncation (checked casting)
let large: u32 = 300u32;
let truncated: u8 = large as u8; // Program fails - no truncation allowed unless able to fit into smaller size

// Safe conversions to larger types
let safe: u64 = small as u64; // No truncation

Key Differences:

  • Leo requires explicit casting with the as keyword
  • Truncation Behavior: Solidity allows truncation during casting, Leo fails on potential data loss
  • Type Promotion: Solidity automatically promotes smaller types in operations, Leo requires explicit casting

Automatic Type Promotion Examples:

// Solidity: automatic type promotion
uint8 small = 100;
uint16 medium = 200;
uint16 result = small + medium; // uint8 automatically promoted to uint16
// Leo: explicit casting required
let small: u8 = 100u8;
let medium: u16 = 200u16;
let result: u16 = (small as u16) + medium; // Must explicitly cast u8 to u16
// let invalid: u16 = small + medium; // This would fail - type mismatch

Reference vs Value Types

Solidity distinguishes between value types and reference types, requiring explicit data location specification:

contract TypeSystem {
uint256[] storageArray; // Permanently stored

function processData(uint256[] memory memoryArray) public {
uint256[] storage localRef = storageArray; // Reference to storage
uint256[] memory localCopy = storageArray; // Copy to memory

localRef[0] = 100; // Modifies storage
localCopy[0] = 200; // Modifies memory copy only
}

function externalCall(uint256[] calldata data) external {
// Calldata is read-only, cannot be modified
}
}

Leo only supports value types - all data is copied when passed around:

program value_types.aleo {
transition process_data(input_array: [u32; 5]) -> [u32; 5] {
// All data is copied by value
let local_array: [u32; 5] = input_array; // Creates a copy

// Can modify individual elements directly
local_array[0u8] = input_array[0u8] + 1u32;
local_array[1u8] = input_array[1u8] + 2u32;

return local_array;
}
}

Key Differences:

  • Leo Limitation: Only value types, no reference types
  • Memory Management: Leo handles memory management automatically based on type semantics, eliminating the need for explicit storage location specifications.

Functions

Function Types

Leo completely reimagines function architecture. Instead of Solidity's visibility modifiers, Leo uses different function types:

Solidity's Approach:

contract Example {
uint256 private data;

// External function - called from outside
function publicFunction(uint256 input) external returns (uint256) {
return internalHelper(input);
}

// Internal helper - accessible in inherited contracts
function internalHelper(uint256 input) internal view returns (uint256) {
return input * 2;
}

// Private function - only accessible within this contract
function privateFunction() private pure returns (uint256) {
return 42;
}
}

contract Child is Example {
function useParent(uint256 input) public returns (uint256) {
return internalHelper(input); // Can access internal functions
// return privateFunction(); // Cannot access private functions
}
}

Leo's Approach:

program example.aleo {
// Transition: externally callable, off-chain execution
// All inputs and outputs are private by default, requiring explicit 'public' keyword for on-chain visibility
transition public_function(input: u32, public pub_input2: u32) -> (public u32, u32) {
let result: u32 = internal_helper(input);
let pub_result: u32 = result + pub_input2;
return (pub_result, result);
}

// Function: helper for transitions (similar to pure functions)
// Can only compute - no state access, no external calls
function internal_helper(input: u32) -> u32 {
let doubled: u32 = inline_multiplier(input, 2u32);
return doubled;
}

// Inline: body inserted at call sites (similar to pure functions)
// Can only compute - no state access, no external calls
inline inline_multiplier(a: u32, b: u32) -> u32 {
return a * b;
}
}

Key Differences:

  • Externally Callable Functions: In Leo, only transition and async transition functions can be called externally. All other functions (function and inline) are internal and can only be called from within the program
  • Leo's Computation-Only Functions: Both function and inline in Leo are similar to Solidity's pure functions - they can only perform computations and cannot access state variables or make external calls
  • Visibility in Leo: Unlike Solidity's visibility modifiers (public, private, internal), Leo's visibility refers to data privacy - whether data is publicly visible on-chain or kept private off-chain
  • Function Overloading: Unlike Solidity which supports function overloading (multiple functions with the same name but different parameter types), Leo does not allow function overloading. Each function name must be unique within a program, requiring developers to use distinct names for functions with different parameter types.

Returns

In Leo, return types are specified using an arrow (->) syntax, similar to Rust, which differs from Solidity's returns keyword.

Key Differences:

  • Return Type Syntax: Leo only requires the type for returns, not identifiers (e.g., -> u32 vs Solidity's returns (uint256 value))
  • Return Value Handling: In Solidity, multiple return values can be selectively ignored using commas (e.g., (index, , ) = f();), while Leo requires all return values to be caught with exact types
  • Parameter Handling: Similarly, Solidity allows unused parameters to be omitted in function declarations, while Leo requires all parameters to be explicitly declared with their types

Modifiers

Solidity's Modifiers: Modifiers in Solidity are usually used to amend the semantics of functions. Similar to inline in Leo, the body is inlined at each call site.

contract ModifierExample {
address public owner;

modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_; // Continue with function execution
}

modifier validAmount(uint256 amount) {
require(amount > 0, "Amount must be positive");
_;
}

function withdraw(uint256 amount) public onlyOwner validAmount(amount) {
// Function body executes after modifiers
payable(msg.sender).transfer(amount);
}
}

Leo's Approach: Leo doesn't have modifiers, but similar functionality can be achieved using inline functions:

program modifier_example.aleo {
// Inline functions provide similar behavior to modifiers
inline only_owner(caller: address, owner: address) {
assert_eq(caller, owner);
}

inline valid_amount(amount: u64) {
assert(amount > 0u64);
}

transition withdraw(amount: u64, owner: address) -> u64 {
// Inline functions are called explicitly (like modifiers)
only_owner(self.caller, owner);
valid_amount(amount);

// Function logic continues
return amount;
}
}

Async Functions

Leo introduces async functions for on-chain execution:

program defi_example.aleo {
mapping user_balances: address => u64;

// Async transition: can call async functions
async transition deposit_and_update(public amount: u64) -> Future {
// Off-chain computation
let new_record: Token = Token {
owner: self.caller,
amount: amount,
};

// Return future for on-chain execution
return finish_deposit(self.caller, amount);
}

// Async function: executes on-chain, can access mappings
async function finish_deposit(user: address, amount: u64) {
let current_balance: u64 = user_balances.get_or_use(user, 0u64);
user_balances.set(user, current_balance + amount);
}
}

Key Concepts:

  • Async Pattern: Leo introduces async transitions and async functions for on-chain execution - transitions that call async functions must be declared as async transition
  • Future Objects: Async functions return Future objects that execute on-chain at a later point in time, allowing access to mapping values that regular transitions cannot access

Call Restrictions

Leo enforces strict rules about function call hierarchy:

Call Flow: transitionfunctioninline | transitioninline | transitionexternal_transition

Call Flow Rules:

  • A transition can only call a function, inline, or external transition.
  • A function can only call an inline.
  • An inline can only call another inline.
  • Direct/indirect recursive calls are not allowed.

Recursion: While Solidity allows function recursion (functions calling themselves directly or indirectly), Leo prohibits all forms of recursive calls.

program call_hierarchy.aleo {
// Transition can call: function, inline, external transitions
transition main_entry(input: u32) -> u32 {
let result1: u32 = helper_function(input); // Valid
let result2: u32 = inline_helper(input); // Valid
let result3: u32 = external_program.aleo/some_transition(input); // Valid
return result1 + result2 + result3;
}

// Function can call: inline only
function helper_function(input: u32) -> u32 {
let processed: u32 = inline_helper(input); // Valid
// let invalid: u32 = another_function(input); // Invalid
return processed;
}

// Inline can call: other inline only
inline inline_helper(input: u32) -> u32 {
let doubled: u32 = inline_doubler(input); // Valid
return doubled;
}

inline inline_doubler(input: u32) -> u32 {
return input * 2u32;
}
}

Fallback and Receive Functions

Solidity's Fallback and Receive:

contract FallbackExample {
// Fallback function - called when no other function matches
fallback() external {
// Handle unmatched function calls
}

// Receive function - called when receiving ETH
receive() external payable {
// Handle incoming ETH
}

// Regular function
function regularFunction() public {
// Function implementation
}
}

Leo's Approach: Leo does not have fallback or receive functions:

program fallback_example.aleo {
// No fallback function - all calls must match a valid function signature
transition regular_function() {
// Function implementation
}

// To receive Aleo Credits, use credits.aleo program
async transition receive_credits(public amount: u64) -> Future {
return credits.aleo/transfer_public(self.caller, self.address, amount);
}
}

Key Differences:

  • No Fallback: Leo requires all function calls to match a valid function signature
  • No Receive: Programs don't need special functions to receive Aleo Credits
  • Program Addresses: Each Leo program has a unique address (same as user-controlled addresses)
  • Private Records: Programs cannot spend private records sent to them (records are effectively "burnt")
  • Credit Transfers: Use credits.aleo program's transfer functions to send/receive credits

Cryptography & Built-ins

Hash Functions

While Solidity provides basic hash functions, Leo offers an extensive cryptographic toolkit:

Solidity's Limited Options:

function hashData(bytes memory data) public pure returns (bytes32) {
return keccak256(data); // Most common
// sha256(data); // Also available
// ripemd160(data); // Less common
}

Leo's Extensive Options:

transition hash_examples(data: u32) -> (field, field, field) {
// Poseidon hashes (ZK-friendly)
let poseidon_hash: field = Poseidon4::hash_to_field(data);

// BHP hashes
let bhp_hash: field = BHP256::hash_to_field(data);

// Traditional hashes
let keccak_hash: field = Keccak256::hash_to_field(data);
let sha3_hash: field = SHA3_256::hash_to_field(data);

return (poseidon_hash, bhp_hash, keccak_hash);
}

// Commitment schemes
transition commit_example(value: u32, randomness: field) -> (field, field) {
let pedersen_commit: field = Pedersen64::commit_to_field(value, randomness);
let bhp_commit: field = BHP256::commit_to_field(value, randomness);
return (pedersen_commit, bhp_commit);
}

Random Numbers

Solidity has no built-in randomness and must rely on 3rd party solutions like Chainlink VRF or external oracles.

Leo's Built-in Solution:

async transition secure_random() -> Future {
return finalize_random();
}

async function finalize_random() {
// Supports ChaCha random - only available in async functions
let random_value: u32 = ChaCha::rand_u32();
}

Built-in Properties and Global Variables

Solidity's Extensive Built-ins:

contract GlobalAccess {
function getBlockInfo() public view returns (uint256, uint256, address, uint256) {
return (
block.number, // Current block number
block.timestamp, // Current block timestamp
msg.sender, // Immediate caller
tx.origin // Transaction originator
);
}

function getNetworkInfo() public view returns (uint256, uint256) {
return (
block.chainid, // Current chain ID
gasleft() // Remaining gas
);
}
}

Leo's Limited Built-ins:

program global_access.aleo {
async transition get_block_info() -> Future {
return finalize_get_info();
}

async function finalize_get_info() {
// Available within finalize scope only
let current_height: u32 = block.height; // Equivalent to block.number
let network: u32 = network.id; // Equivalent to block.chainid
}

transition get_caller_info() {
let immediate_caller: address = self.caller; // Equivalent to msg.sender
let origin_caller: address = self.signer; // Equivalent to tx.origin
let program_address: address = self.address; // Equivalent to address(this)
}
}

Key Differences:

  • Limited Scope: Leo's block properties only available in async functions (finalize scope)
  • No Timestamp: Leo doesn't provide block timestamp at the moment until ARC-0040 is implemented
  • No Gas Tracking: Leo doesn't expose gas/fee information to programs
  • Program Context: Leo provides self.address for the current program address

Currency Denominations

Solidity's Ether Units:

contract EtherUnits {
function demonstrateUnits() public pure returns (uint256, uint256, uint256) {
uint256 oneWei = 1;
uint256 oneGwei = 1 gwei; // 1e9 wei
uint256 oneEther = 1 ether; // 1e18 wei

return (oneWei, oneGwei, oneEther);
}
}

Leo's Credit Units:

program credit_units.aleo {
transition demonstrate_units() -> (u64, u64, u64) {
let one_microcredit: u64 = 1u64;
let one_millicredit: u64 = 1000u64; // 1e3 microcredits
let one_credit: u64 = 1000000u64; // 1e6 microcredits

return (one_microcredit, one_millicredit, one_credit);
}
}

Key Differences:

  • Aleo Credits: microcredits (base), millicredits (1e3), credits (1e6)
  • Ethereum: wei (base), gwei (1e9), ether (1e18)
  • No Built-in Units: Leo doesn't have built-in unit literals like Solidity

Address Functionality and Native Token Transfers

Solidity's Address Members: Address in Solidity has members to access its associated states such as:

contract AddressExample {
function demonstrateAddressMembers(address payable target) public view returns (uint256, bytes32) {
// Access address properties
uint256 balance = target.balance; // Balance in Wei
bytes memory code = target.code; // Code at address (can be empty)
bytes32 codeHash = target.codehash; // The codehash of the address

return (balance, codeHash);
}

function transferMethods(address payable target, uint256 amount) public {
// Different transfer methods
target.transfer(amount); // Reverts on failure, 2300 gas stipend
bool success = target.send(amount); // Returns false on failure, 2300 gas stipend

// Low-level calls with adjustable gas
(bool callSuccess, bytes memory data) = target.call{value: amount}("");
(bool delegateSuccess, bytes memory delegateData) = target.delegatecall("");
(bool staticSuccess, bytes memory staticData) = target.staticcall("");
}
}

Leo's Approach: Leo address does not have member functions. Aleo credits are defined as a program (credits.aleo) on Aleo, therefore to make transfers and access balances, it uses the same method as querying any programs on Aleo, either publicly with mappings or privately by scanning owned records.

import credits.aleo;

program address_example.aleo {
// Access balance through credits.aleo program mappings (public balance)
async transition get_public_balance(user: address) -> Future {
return finalize_get_balance(user);
}

async function finalize_get_balance(user: address) {
// Query public balance from credits.aleo account mapping
let balance: u64 = credits.aleo/account.get_or_use(user, 0u64);
}

// Transfer credits using credits.aleo program functions
async transition transfer_public_credits(
to: address,
amount: u64
) -> Future {
return credits.aleo/transfer_public(self.caller, to, amount);
}

transition transfer_private_credits(
input: credits.aleo/credits,
to: address,
amount: u64
) -> (credits.aleo/credits, credits.aleo/credits) {
return credits.aleo/transfer_private(input, to, amount);
}
}

Key Differences:

  • No Address Members: Leo addresses don't have .balance, .code, .codehash properties
  • No Built-in Transfer Methods: No .transfer(), .send(), .call() methods on addresses
  • Program-Based Transfers: Native token transfers go through the credits.aleo program
  • Public vs Private: Balances can be public (mappings) or private (records)
  • Unified Interface: All program interactions use the same call syntax, whether for tokens or other functionality

Mathematical Operations and Overflow Handling

Solidity's Approach:

contract MathOperations {
function checkedMath(uint256 a, uint256 b) public pure returns (uint256) {
return a + b; // Reverts on overflow by default (since 0.8.0)
}

function uncheckedMath(uint256 a, uint256 b) public pure returns (uint256) {
unchecked {
return a + b; // Wraps on overflow
}
}

function modularMath(uint256 a, uint256 b, uint256 n) public pure returns (uint256, uint256) {
return (addmod(a, b, n), mulmod(a, b, n));
}
}

Leo's Approach:

program math_operations.aleo {
transition checked_math(a: u64, b: u64) -> u64 {
return a + b; // Reverts on overflow by default
}

transition wrapped_math(a: u64, b: u64) -> u64 {
return a.add_wrap(b); // Wraps on overflow using wrapped arithmetic
}

transition modular_math(a: u64, b: u64, n: u64) -> (u64, u64) {
let add_result: u64 = (a + b) % n;
let mul_result: u64 = (a * b) % n;
return (add_result, mul_result);
}
}

Key Differences:

  • Wrapped Operations: Leo uses .add_wrap(), .sub_wrap(), .mul_wrap() instead of unchecked blocks
  • No Built-in Modular Math: Leo doesn't have addmod/mulmod built-ins
  • Explicit Operators: Leo requires explicit use of wrapped operators

Time and Temporal Operations

Solidity's Time Support:

contract TimeOperations {
function timeUnits() public pure returns (uint256, uint256, uint256) {
return (
1 seconds, // 1
1 minutes, // 60 seconds
1 hours // 3600 seconds
);
}

function getCurrentTime() public view returns (uint256) {
return block.timestamp;
}
}

Leo's Limitation: Leo does not support time units and timestamps (at the moment):

  • No Time Units: No equivalent to seconds, minutes, hours, etc.
  • No Timestamps: No access to block timestamps at the moment until ARC-0040 is implemented.
  • No Time-based Logic: Developers must implement time logic externally or rely on block height

Error Handling

Solidity's Error Handling:

contract ErrorHandling {
ExternalContract public externalContract;
error InsufficientBalance(uint256 requested, uint256 available);

function transfer(uint256 amount) public {
require(amount > 0, "Amount must be positive");

if (balance < amount) {
revert InsufficientBalance(amount, balance);
}

try externalContract.riskyCall() returns (string memory) {
// Success path
} catch Error(string memory reason) {
// Handle with reason
} catch {
// Handle without reason
}
}
}

Leo's Error Handling:

program error_handling.aleo {
transition transfer(amount: u64, balance: u64) -> u64 {
// Simple assertions (no custom messages due to lack of string support)
assert(amount > 0u64);
assert_neq(amount, 0u64); // Alternative syntax
assert(balance >= amount);

// No try/catch - all external calls must succeed
let result: u64 = external_program.aleo/safe_operation(amount);
return result;
}
}

Loops & Conditionals

Solidity's Control Flow:

function controlFlow(uint256[] memory items) public pure returns (uint256) {
uint256 sum = 0;

// While loops
uint256 i = 0;
while (i < items.length) {
if (items[i] > 10) {
continue; // Skip this item
}

sum += items[i];

if (sum > 100) {
break; // Exit early
}

i++;
}

// For loops
for (uint256 j = 0; j < items.length; j++) {
// Process items
}

return sum;
}

Leo's Control Flow:

transition control_flow(items: [u32; 5]) -> u32 {
let sum: u32 = 0u32;

// For loops only (no while, do-while)
for i: u32 in 0u32..5u32 {
sum += items[i as u8];
// No continue and break statement available
}

// Ternary operator works identically
let result: u32 = sum > 100u32 ? 100u32 : sum;
return result;
}

Important Limitation: Leo currently executes all branches of conditional statements, then select the correct result. This differs from typical conditional execution where only one branch runs. This behavior can cause unexpected issues, especially with operations that can halt (like division by zero). ARC-0004 proposes flagged operations to address this limitation, enabling proper if-then-else semantics. More details can be found in the Leo limitations documentation.

Cross-Program Calls

Solidity's Dynamic Approach:

interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
}

contract DynamicCalls {
function transferTokens(address tokenAddress, address to, uint256 amount) public {
// Dynamic contract interaction using interface
IERC20 token = IERC20(tokenAddress);
bool success = token.transfer(to, amount);
require(success, "Transfer failed");

// Low-level calls
(bool callSuccess, bytes memory data) = tokenAddress.call(
abi.encodeWithSignature("transfer(address,uint256)", to, amount)
);
require(callSuccess, "Call failed");
}
}

Leo's Static Approach:

import credits.aleo;

program static_calls.aleo {
async transition transfer_credits(
input: credits.aleo/credits,
to: address,
amount: u64
) -> (credits.aleo/credits, Future) {
// Static, compile-time known calls only
let tuple: (credits.aleo/credits, Future) = credits.aleo/transfer_private(
input,
to,
amount
);

return (tuple.0, f_transfer(tuple.1));
}

async function f_transfer(f: Future) {
f.await();
}

// Can query public state from other programs within finalize scope
async transition get_external_balance(user: address) -> Future {
return f_get_external_balance(user);
}

async function f_get_external_balance(user: address) {
let balance: u64 = credits.aleo/account.get(user);
}
}

Inheritance

Solidity's Inheritance System:

abstract contract Animal {
string public name;

constructor(string memory _name) {
name = _name;
}

function speak() public virtual returns (string memory);
}

contract Dog is Animal {
constructor(string memory _name) Animal(_name) {}

function speak() public pure override returns (string memory) {
return "Woof!";
}

function wagTail() public pure returns (string memory) {
return "Wagging tail";
}
}

contract Cat is Animal {
constructor(string memory _name) Animal(_name) {}

function speak() public pure override returns (string memory) {
return "Meow!";
}

function purr() public pure returns (string memory) {
return "Purring";
}
}

Leo's Composition Approach:

Leo does not support inheritance like Solidity does. Instead, Leo uses composition and program imports to achieve similar functionality.

program animal_behaviors.aleo {
struct AnimalData {
name: field,
species: u8, // 1 for dog, 2 for cat
}

function speak(animal: AnimalData) -> field {
// Different sounds for different species
return animal.species == 1u8 ? 100field : 200field; // Dog: "Woof!", Cat: "Meow!"
}

function species_behavior(animal: AnimalData) -> bool {
// Dogs wag tail, cats purr
return animal.species == 1u8 ? true : false; // Dog: wag tail, Cat: purr
}
}
import animal_behaviors.aleo;

program my_pets.aleo {
// Struct from imported program must be redefined with exact members
struct AnimalData {
name: field,
species: u8,
}

record Pet {
data: AnimalData,
}

transition create_dog(name: field) -> Pet {
return Pet {
data: AnimalData {
name: name,
species: 1u8, // Dog
},
};
}

transition create_cat(name: field) -> Pet {
return Pet {
data: AnimalData {
name: name,
species: 2u8, // Cat
},
};
}

transition make_sound(pet: Pet) -> field {
// Get sound from animal_behaviors
return animal_behaviors.aleo/speak(pet.data);
}

transition check_behavior(pet: Pet) -> bool {
// Check species-specific behavior
return animal_behaviors.aleo/species_behavior(pet.data);
}
}

Abstract Contracts and Interfaces

Solidity's Abstract Types:

// Abstract contract - some functions not implemented
abstract contract Animal {
string public name;

constructor(string memory _name) {
name = _name;
}

function speak() public virtual returns (string memory); // Must be implemented

function eat() public pure returns (string memory) {
return "Eating..."; // Default implementation
}
}

// Interface - no functions implemented
interface IERC20 {
function totalSupply() external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
// All functions must be external
}

contract Token is IERC20 {
function totalSupply() external pure override returns (uint256) {
return 1000000;
}

function transfer(address to, uint256 amount) external pure override returns (bool) {
return true;
}
}

Leo's Approach: Leo does not support abstract contracts or interfaces since it lacks inheritance:

  • No Abstract Contracts: Cannot define partially implemented contracts
  • No Interfaces: Cannot define contract interfaces for implementation
  • Alternative: Use composition and program imports for modularity
  • Future Consideration: Interfaces may be supported once dynamic dispatch is implemented

Libraries

Solidity's Libraries:

library SafeMath {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
}

contract UsingLibrary {
using SafeMath for uint256;

function calculate(uint256 a, uint256 b) public pure returns (uint256) {
return a.add(b); // Library function attached to type
}
}

Leo's Alternative: Leo doesn't have dedicated libraries but developers can create stateless programs that function similarly:

// math_library.aleo - Stateless program acting as library
program math_library.aleo {
transition safe_add(a: u64, b: u64) -> u64 {
let result: u64 = a + b;
assert(result >= a); // Check for overflow
return result;
}
}
import math_library.aleo;

program using_library.aleo {
transition calculate(a: u64, b: u64) -> u64 {
return math_library.aleo/safe_add(a, b);
}
}

Key Differences:

  • No Delegatecall: Leo doesn't support executing external code in current context
  • No Type Attachment: Cannot attach functions to types like Solidity's using directive
  • Stateless Programs: Use separate programs instead of libraries

Events and Logging

Solidity's Event System:

contract EventExample {
event Transfer(address indexed from, address indexed to, uint256 value);
event Debug(string message, uint256 value);

function transfer(address to, uint256 amount) public {
// Emit events for external monitoring
emit Transfer(msg.sender, to, amount);
emit Debug("Transfer executed", amount);
}
}

Leo's Limitation: Leo does not support events or logging:

  • No Events: Cannot emit events for external monitoring
  • No Logging: No built-in logging facilities
  • Alternative: Use program outputs or external state tracking for monitoring
  • Best Practice: Document important state changes in comments

ABI Encoding and Decoding

Solidity's ABI Functions:

contract ABIExample {
function encodeData(uint256 value, string memory text) public pure returns (bytes memory) {
return abi.encode(value, text);
}

function encodePacked(uint256 a, uint256 b) public pure returns (bytes memory) {
return abi.encodePacked(a, b);
}

function decodeData(bytes memory data) public pure returns (uint256, string memory) {
return abi.decode(data, (uint256, string));
}
}

Leo's Limitation: Leo does not have ABI encoding/decoding functionality:

  • No Bytes Type: Leo doesn't support bytes type needed for ABI operations
  • No Dynamic Encoding: Cannot dynamically encode/decode complex data structures
  • Alternative: Use fixed-size data structures and manual serialization if needed
  • Static Calls: All cross-program calls use statically defined interfaces

Contract Creation and Dynamic Deployment

Solidity's Contract Creation:

contract Factory {
function createContract(uint256 initialValue) public returns (address) {
// Create new contract with constructor parameters
MyContract newContract = new MyContract(initialValue);
return address(newContract);
}

function createWithSalt(uint256 initialValue, bytes32 salt) public returns (address) {
// Create deterministic address using CREATE2
MyContract newContract = new MyContract{salt: salt}(initialValue);
return address(newContract);
}
}

Leo's Limitation: Leo programs cannot create new programs:

  • No Dynamic Creation: Programs cannot deploy other programs
  • Static Deployment: All programs must be deployed through external tools
  • No CREATE2: No deterministic address generation but programs on Aleo already have developer-defined human-readable names.
  • Fixed Architecture: Program relationships must be established at deployment time

Scoping Rules

Solidity and Leo Similarities: Both languages follow similar scoping rules:

// Solidity
contract ScopeExample {
uint256 globalVar = 100;

function scopeDemo() public view returns (uint256) {
uint256 localVar = 200;
{
uint256 blockVar = 300;
return globalVar + localVar + blockVar; // All accessible
}
// blockVar not accessible here
return globalVar + localVar;
}
}
// Leo
program scope_example.aleo {
transition scope_demo() -> u32 {
let local_var: u32 = 200u32;

{
let block_var: u32 = 300u32;
local_var = local_var + block_var; // Both accessible
}

// block_var not accessible here
return local_var;
}
}

Key Similarity:

  • Variables are visible from declaration until the end of their containing {} block

Inline Assembly

Solidity's Inline Assembly:

contract AssemblyExample {
function efficientHash(uint256 a, uint256 b) public pure returns (uint256 result) {
assembly {
let hash := keccak256(add(a, b), 0x40)
result := hash
}
}
}

Leo's Limitation: Leo does not support inline assembly:

  • No Yul-like Integration: Cannot insert low-level assembly code
  • Separate IR: Aleo Instructions exist as a separate language, not embeddable in Leo.
  • Standalone Programs: Aleo Instructions can be written as standalone programs in .aleo files, enabling fine-grained control over program execution and circuit design at a low level. For more details about Aleo Instructions, see the Aleo Instructions Overview.