Skip to main content
Minichain uses a gas system similar to Ethereum to prevent infinite loops and abuse. Every operation consumes gas, and transactions must specify a gas limit and gas price.

What is Gas?

Gas is a measure of computational work required to execute operations. It serves several purposes:

Prevent Infinite Loops

Programs must halt when gas runs out, preventing endless execution.

Fair Resource Allocation

Users pay for the resources they consume, preventing spam.

Incentivize Efficiency

Developers optimize code to minimize gas costs.

Prioritize Transactions

Higher gas prices get faster inclusion in blocks.

Gas Costs

Cost Tiers

Minichain defines five gas cost tiers:
pub struct GasCosts;

impl GasCosts {
    // Tier 1: Very cheap (simple register operations)
    pub const ZERO: u64 = 0;      // HALT, NOP
    pub const BASE: u64 = 2;      // ADD, SUB, AND, OR, MOV

    // Tier 2: Cheap (more complex ALU operations)
    pub const LOW: u64 = 3;       // MUL, comparison ops

    // Tier 3: Medium (division, shifts)
    pub const MID: u64 = 5;       // DIV, MOD, SHL, SHR

    // Tier 4: Memory operations
    pub const MEMORY_READ: u64 = 3;
    pub const MEMORY_WRITE: u64 = 3;
    pub const MEMORY_GROW_PER_BYTE: u64 = 1;

    // Tier 5: Storage (expensive!)
    pub const SLOAD: u64 = 100;           // Read from storage
    pub const SSTORE_SET: u64 = 20000;    // Write to empty slot
    pub const SSTORE_RESET: u64 = 5000;   // Overwrite existing slot

    // Control flow
    pub const JUMP: u64 = 8;
    pub const CALL: u64 = 700;
}

Complete Gas Table

Operation CategoryOpcodeGas CostNotes
Control Flow
HaltHALT0Stops execution
No-opNOP0Does nothing
JumpJUMP8Unconditional jump
Conditional JumpJUMPI8Jump if condition
CallCALL700Call another contract
ReturnRET8Return from call
RevertREVERT0Revert transaction
Arithmetic
Add/SubADD, SUB2Basic arithmetic
MultiplyMUL3Multiplication
DivideDIV5Division (more expensive)
ModuloMOD5Remainder operation
Add ImmediateADDI2Add constant
Bitwise
And/Or/XorAND, OR, XOR2Bitwise operations
NotNOT2Bitwise negation
Shift Left/RightSHL, SHR5Bit shifting
Comparison
Equal/Not EqualEQ, NE3Equality checks
Less/Greater ThanLT, GT3Comparisons
Less/Greater EqualLE, GE3Inclusive comparisons
Is ZeroISZERO3Check if zero
Memory (RAM)
Load 8-bitLOAD83Read byte from memory
Load 64-bitLOAD643Read word from memory
Store 8-bitSTORE83Write byte to memory
Store 64-bitSTORE643Write word to memory
Memory SizeMSIZE2Get memory size
Memory CopyMCOPY3 + sizeCopy memory region
Storage (Disk)
Storage LoadSLOAD100Read from persistent storage
Storage Store (new)SSTORE20,000Write to empty slot
Storage Store (update)SSTORE5,000Overwrite existing slot
Immediate/Move
Load ImmediateLOADI2Load constant into register
Move RegisterMOV2Copy between registers
Context
Get CallerCALLER2Get caller address
Get Call ValueCALLVALUE2Get sent value
Get AddressADDRESS2Get contract address
Get Block NumberBLOCKNUMBER2Get current block
Get TimestampTIMESTAMP2Get block timestamp
Get GasGAS2Get remaining gas
Debug
LogLOG8Emit log event
Storage operations (SLOAD/SSTORE) are 100x more expensive than memory operations! Use storage sparingly and prefer keeping data in memory during execution.

Gas Metering

Gas Meter

The GasMeter tracks gas consumption during execution:
pub struct GasMeter {
    remaining: u64,
    used: u64,
}

impl GasMeter {
    pub fn new(limit: u64) -> Self {
        Self {
            remaining: limit,
            used: 0,
        }
    }

    /// Consume gas, returning error if insufficient.
    pub fn consume(&mut self, amount: u64) -> Result<(), VmError> {
        if self.remaining < amount {
            return Err(VmError::OutOfGas {
                required: amount,
                remaining: self.remaining,
            });
        }
        self.remaining -= amount;
        self.used += amount;
        Ok(())
    }

    pub fn remaining(&self) -> u64 {
        self.remaining
    }

    pub fn used(&self) -> u64 {
        self.used
    }
}

Gas Consumption Flow

Every opcode execution follows this pattern:
// 1. Decode opcode
let opcode = Opcode::from_byte(bytecode[pc])?;

// 2. Consume gas BEFORE execution
let cost = match opcode {
    Opcode::ADD => GasCosts::BASE,
    Opcode::MUL => GasCosts::LOW,
    Opcode::DIV => GasCosts::MID,
    Opcode::SLOAD => GasCosts::SLOAD,
    Opcode::SSTORE => {
        // Dynamic cost based on storage state
        if storage_slot_is_empty {
            GasCosts::SSTORE_SET
        } else {
            GasCosts::SSTORE_RESET
        }
    }
    _ => GasCosts::BASE,
};
gas_meter.consume(cost)?;

// 3. Execute operation
execute_opcode(opcode)?;

// 4. Move to next instruction
pc += opcode.instruction_size();
Gas is consumed before execution, not after. This prevents operations from executing if they would run out of gas mid-execution.

Out of Gas Error

When gas runs out, execution halts immediately:
#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum VmError {
    #[error("Out of gas: required {required}, remaining {remaining}")]
    OutOfGas { required: u64, remaining: u64 },
    // ... other errors
}

// Example:
// Trying to execute SLOAD (cost: 100) with only 50 gas remaining
// Error: Out of gas: required 100, remaining 50

Transaction Gas

Gas Limit and Gas Price

Transactions specify two gas-related fields:
pub struct Transaction {
    // ... other fields
    /// Maximum gas to use.
    pub gas_limit: u64,
    /// Price per unit of gas.
    pub gas_price: u64,
    // ...
}
Gas Limit:
  • Maximum gas the transaction can consume
  • Set by sender based on estimated execution cost
  • Unused gas is refunded
  • Minimum: 21,000 for simple transfers
Gas Price:
  • Amount sender pays per unit of gas
  • Higher gas price = faster inclusion in blocks
  • Total cost = gas_used * gas_price

Transaction Cost Calculation

impl Transaction {
    /// Calculate the maximum cost of this transaction.
    pub fn max_cost(&self) -> u64 {
        self.value
            .saturating_add(self.gas_limit.saturating_mul(self.gas_price))
    }
}

// Example:
// value = 1000
// gas_limit = 50000
// gas_price = 2
// max_cost = 1000 + (50000 * 2) = 101,000
Pre-Execution Balance Check:
// Sender must have enough for value + max gas cost
let max_cost = tx.max_cost();
if account.balance < max_cost {
    return Err(ValidationError::InsufficientBalance {
        required: max_cost,
        available: account.balance,
    });
}

Gas Refund Mechanism

// 1. Deduct max gas cost upfront
let max_gas_cost = tx.gas_limit * tx.gas_price;
account.debit(tx.value + max_gas_cost)?;

// 2. Execute transaction
let result = vm.execute(bytecode, tx.gas_limit)?;

// 3. Refund unused gas
let gas_used = result.gas_used;
let gas_refund = (tx.gas_limit - gas_used) * tx.gas_price;
account.credit(gas_refund);

// 4. Charge only for gas actually used
let actual_cost = tx.value + (gas_used * tx.gas_price);
Unused gas is always refunded! If you set gas_limit = 100,000 but only use 50,000 gas, you only pay for 50,000.

Base Transaction Costs

Transfer Cost

Simple value transfers have a fixed cost:
// Base cost for a transfer transaction
const BASE_TX_GAS: u64 = 21_000;

let tx = Transaction::transfer(
    from,
    to,
    amount,
    nonce,
    gas_price,
);

assert_eq!(tx.gas_limit, 21_000);

Deployment Cost

Contract deployments pay per byte of bytecode:
const DEPLOYMENT_GAS_PER_BYTE: u64 = 200;

// Calculate deployment cost
let bytecode_size = bytecode.len() as u64;
let deployment_gas = bytecode_size * DEPLOYMENT_GAS_PER_BYTE;
let total_gas = BASE_TX_GAS + deployment_gas;

// Example: 100-byte contract
// total_gas = 21,000 + (100 * 200) = 41,000

Contract Call Cost

Contract calls include base cost plus execution:
const BASE_CALL_GAS: u64 = 21_000;

// Total cost = base + execution
let total_gas = BASE_CALL_GAS + execution_gas;

Memory Expansion Cost

Growing memory incurs additional gas:
impl GasMeter {
    /// Calculate gas for memory expansion.
    pub fn memory_expansion_cost(current_size: usize, new_size: usize) -> u64 {
        if new_size <= current_size {
            return 0;
        }
        let expansion = (new_size - current_size) as u64;
        expansion * GasCosts::MEMORY_GROW_PER_BYTE
    }
}

// Example: Growing memory from 1024 to 2048 bytes
let expansion_cost = GasMeter::memory_expansion_cost(1024, 2048);
// expansion_cost = (2048 - 1024) * 1 = 1024 gas

Storage vs Memory Cost Comparison

Let’s compare the costs of storage vs memory operations:
; Write to storage (expensive)
LOADI R0, 42
LOADI R1, 0
SSTORE R1, R0        ; Cost: 20,000 gas (new slot)

; Read from storage
LOADI R1, 0
SLOAD R2, R1         ; Cost: 100 gas

; Total: 20,100 gas
Cost Comparison:
OperationStorageMemoryRatio
Write (new)20,00036,667x
Write (update)5,00031,667x
Read100333x
Storage is 1,667x to 6,667x more expensive than memory! Only use storage for data that must persist between transactions.

Real-World Examples

Example 1: Counter Contract

Let’s analyze the gas cost of a simple counter:
.entry main

main:
    LOADI R0, 0          ; Load slot 0          - 2 gas
    SLOAD R1, R0         ; Read counter         - 100 gas
    LOADI R2, 1          ; Load constant 1      - 2 gas
    ADD R1, R1, R2       ; Increment            - 2 gas
    SSTORE R0, R1        ; Save counter         - 5,000 gas (update)
    HALT                 ; Stop                 - 0 gas

; Total execution: ~5,106 gas
; Plus base transaction cost: 21,000 gas
; Total: ~26,106 gas

Example 2: Complex Calculation

Compare computing in memory vs storage:
; Calculate: (a + b) * (c - d)
; All data in registers/memory

LOADI R0, 10         ; a = 10           - 2 gas
LOADI R1, 20         ; b = 20           - 2 gas
LOADI R2, 5          ; c = 5            - 2 gas
LOADI R3, 3          ; d = 3            - 2 gas

ADD R4, R0, R1       ; R4 = a + b       - 2 gas
SUB R5, R2, R3       ; R5 = c - d       - 2 gas
MUL R6, R4, R5       ; R6 = result      - 3 gas

HALT                 ; Stop             - 0 gas

; Total: 15 gas

Example 3: Gas Limit Selection

// Estimate gas for contract call
let estimated_gas = 30_000;

// Add 20% buffer for safety
let gas_limit = (estimated_gas as f64 * 1.2) as u64;

// Create transaction
let tx = Transaction::call(
    from,
    contract,
    calldata,
    0,
    nonce,
    gas_limit,  // 36,000
    gas_price,
);

// If actual usage is 28,000:
// - Gas used: 28,000
// - Gas refunded: 36,000 - 28,000 = 8,000
// - Final cost: 28,000 * gas_price

Gas Optimization Strategies

Bad:
; Reading storage multiple times
LOADI R0, 0
SLOAD R1, R0        ; 100 gas
; ... use R1
SLOAD R1, R0        ; 100 gas (redundant!)
Good:
; Read once, reuse register
LOADI R0, 0
SLOAD R1, R0        ; 100 gas
; ... use R1 multiple times
Bad:
; Using storage as scratch space
LOADI R0, 0
LOADI R1, 42
SSTORE R0, R1       ; 20,000 gas (wasteful!)
SLOAD R2, R0        ; 100 gas
Good:
; Use registers for temporary values
LOADI R1, 42        ; 2 gas
MOV R2, R1          ; 2 gas
Bad:
; Multiple small storage writes
SSTORE R0, R1       ; 5,000 gas
; ... some computation
SSTORE R0, R2       ; 5,000 gas
; ... more computation
SSTORE R0, R3       ; 5,000 gas
; Total: 15,000 gas
Good:
; Compute final value, write once
; ... all computation in registers
SSTORE R0, R3       ; 5,000 gas
; Total: 5,000 gas
Bad:
; Inefficient: x / 2 using DIV
LOADI R1, 2
DIV R0, R0, R1      ; 5 gas
Good:
; Efficient: x / 2 using shift
LOADI R1, 1
SHR R0, R0, R1      ; 5 gas (same cost, but more idiomatic)
Bad:
; Always do all work
; ... expensive computation (1000 gas)
; ... check condition
JUMPI end           ; Waste if condition false
Good:
; Check condition first
; ... check condition
JUMPI end           ; Exit early
; ... expensive computation (only if needed)
end:

Gas Profiling

Minichain provides execution traces to analyze gas usage:
// Enable tracing
let mut vm = Vm::new_with_context(
    bytecode,
    gas_limit,
    caller,
    address,
    value,
    block_number,
    timestamp,
);

let tracer = Tracer::new();
vm.set_tracer(Some(tracer));

// Execute
let result = vm.execute()?;

// Analyze trace
let trace = vm.get_tracer().unwrap();
for step in trace.steps() {
    println!(
        "PC: {:4} | Opcode: {:?} | Gas: {} | Remaining: {}",
        step.pc,
        step.opcode,
        step.gas_cost,
        step.gas_remaining,
    );
}

println!("Total gas used: {}", result.gas_used);

Best Practices

1

Set appropriate gas limits

Add 20-30% buffer to estimated gas for safety.
2

Optimize storage access

Minimize SLOAD/SSTORE operations. Use registers and memory.
3

Profile before deploying

Use tracing to identify gas hotspots.
4

Consider gas price

Higher gas price = faster inclusion, but higher cost.
5

Handle out-of-gas gracefully

Check gas remaining before expensive operations.

Next Steps

Virtual Machine

Learn how the VM executes opcodes

Writing Assembly

Write gas-efficient smart contracts

Opcode Reference

Complete opcode and gas cost reference

Transaction Types

Understand transaction gas requirements

Build docs developers (and LLMs) love