Skip to main content
Smart contracts in Minichain are programs that run on the register-based VM. This guide covers best practices, patterns, and techniques for writing efficient and secure contracts.

Contract Structure

A well-structured contract follows this pattern:
; Contract description and purpose
.entry main

; Constants
.const STORAGE_SLOT_COUNTER 0
.const STORAGE_SLOT_OWNER 1

main:
    ; Your contract logic here
    HALT

; Helper functions (if using CALL/RET)
helper_function:
    ; Function implementation
    RET

Understanding VM Components

Registers

The VM provides 16 general-purpose 64-bit registers (R0-R15):
1

All registers start at 0

At the beginning of contract execution, all registers are initialized to zero.
2

No calling convention

Unlike traditional architectures, there’s no enforced calling convention. You define how to use registers.
3

Plan register usage

Document which registers serve which purpose in your contract.
Example register allocation:
; Register allocation:
; R0 - Storage keys
; R1 - Storage values / temp
; R2 - Arithmetic operations
; R3 - Caller address
; R4 - Call value
; R5 - Jump targets
; R6-R15 - Scratch space

main:
    CALLER R3           ; Get caller address
    CALLVALUE R4        ; Get sent value
    ; ... rest of logic

Memory

Contracts have access to linear memory (up to 1MB):
; Allocate memory by storing to it
LOADI R0, 0             ; Address 0
LOADI R1, 42            ; Value to store
STORE64 R0, R1          ; Store 64-bit value at address 0

LOAD64 R2, R0           ; Load back from address 0

; Byte-level access
LOADI R3, 8             ; Address 8
LOADI R4, 0xFF          ; Byte value
STORE8 R3, R4           ; Store single byte
LOAD8 R5, R3            ; Load single byte
Memory addresses are byte-addressable. Use LOAD64/STORE64 for 64-bit values and LOAD8/STORE8 for bytes.

Storage

Persistent storage survives across contract calls:
.const COUNTER_SLOT 0

main:
    ; Load from storage
    LOADI R0, COUNTER_SLOT
    SLOAD R1, R0            ; R1 = storage[0]
    
    ; Increment
    LOADI R2, 1
    ADD R1, R1, R2          ; R1 = R1 + 1
    
    ; Store back
    SSTORE R0, R1           ; storage[0] = R1
    HALT
Storage operations (SLOAD/SSTORE) are expensive in terms of gas. Minimize storage access by using registers and memory for temporary values.

Context Information

Contracts can access execution context:
main:
    ; Who called this contract?
    CALLER R0
    LOG R0                  ; Debug: print caller address
    
    ; How much value was sent?
    CALLVALUE R1
    LOG R1
    
    ; What's my address?
    ADDRESS R2
    LOG R2
    
    ; Block context
    BLOCKNUMBER R3
    TIMESTAMP R4
    
    ; Remaining gas
    GAS R5
    
    HALT

Control Flow

Conditional Jumps

Implement conditional logic using comparison and jump instructions:
.entry main

main:
    CALLER R0               ; Get caller
    LOADI R1, 0x1234...     ; Expected owner address
    
    ; Check if caller is owner
    EQ R2, R0, R1           ; R2 = (R0 == R1) ? 1 : 0
    LOADI R3, authorized
    JUMPI R2, R3            ; Jump if R2 != 0
    
    ; Caller is not owner
    REVERT                  ; Revert transaction

authorized:
    ; Caller is owner, proceed
    LOADI R4, 42
    ; ... rest of logic
    HALT

Loops

Implement loops using labels and conditional jumps:
.entry main

main:
    LOADI R0, 0             ; Counter
    LOADI R1, 10            ; Max value

loop:
    ; Loop body
    LOG R0                  ; Print counter
    
    ; Increment
    LOADI R2, 1
    ADD R0, R0, R2
    
    ; Check condition
    LT R3, R0, R1           ; R3 = (R0 < R1)
    LOADI R4, loop
    JUMPI R3, R4            ; Continue if R0 < R1
    
    HALT

Functions

Use CALL and RET for subroutines:
.entry main

main:
    LOADI R0, 5
    LOADI R1, 10
    LOADI R2, add_numbers
    CALL R2                 ; Call function
    ; Result in R3
    LOG R3
    HALT

add_numbers:
    ADD R3, R0, R1          ; R3 = R0 + R1
    RET                     ; Return to caller
The VM does not provide a call stack for automatic return address management. If you use CALL, ensure you have a strategy for handling return addresses (e.g., storing in a register).

Best Practices

Storage operations are expensive. Load values once, operate on them in registers, then store once:
; Good: Load once, operate, store once
SLOAD R0, R1
ADD R0, R0, R2
ADD R0, R0, R3
SSTORE R1, R0

; Bad: Multiple storage operations
SLOAD R0, R1
ADD R0, R0, R2
SSTORE R1, R0
SLOAD R0, R1
ADD R0, R0, R3
SSTORE R1, R0
Add comments explaining register allocation:
; Register allocation:
; R0 - Current counter value
; R1 - Storage slot key
; R2 - Temporary arithmetic
; R3 - Caller address
Define constants for magic numbers:
.const STORAGE_COUNTER 0
.const STORAGE_OWNER 1
.const MAX_VALUE 1000

main:
    LOADI R0, STORAGE_COUNTER  ; Not: LOADI R0, 0
Check conditions and revert on invalid input:
main:
    CALLVALUE R0
    LOADI R1, 0
    EQ R2, R0, R1           ; Check value is 0
    LOADI R3, valid
    JUMPI R2, R3
    REVERT                  ; Revert if value sent

valid:
    ; Continue execution
Assembly arithmetic wraps on overflow. Add explicit checks if needed:
; Check for overflow before adding
LOADI R0, 0xFFFFFFFFFFFFFFFF  ; Max u64
LOADI R1, 1
ADD R2, R0, R1                ; R2 wraps to 0

Common Patterns

Access Control

.const STORAGE_OWNER 1

main:
    CALLER R0
    LOADI R1, STORAGE_OWNER
    SLOAD R2, R1            ; Load owner from storage
    
    EQ R3, R0, R2           ; Check if caller is owner
    LOADI R4, authorized
    JUMPI R3, R4
    REVERT                  ; Not authorized

authorized:
    ; Protected logic here
    HALT

Counter

.const STORAGE_COUNTER 0

main:
    LOADI R0, STORAGE_COUNTER
    SLOAD R1, R0            ; Load counter
    LOADI R2, 1
    ADD R1, R1, R2          ; Increment
    SSTORE R0, R1           ; Store back
    LOG R1                  ; Emit new value
    HALT

Conditional Storage Update

.const STORAGE_VALUE 0
.const MAX_VALUE 100

main:
    LOADI R0, STORAGE_VALUE
    SLOAD R1, R0            ; Current value
    
    LOADI R2, MAX_VALUE
    LT R3, R1, R2           ; Check if less than max
    LOADI R4, can_update
    JUMPI R3, R4
    REVERT                  ; Value too high

can_update:
    LOADI R5, 10
    ADD R1, R1, R5          ; Add 10
    SSTORE R0, R1           ; Store
    HALT

Debugging

Use the LOG instruction to debug your contracts:
main:
    LOADI R0, 42
    LOG R0                  ; Prints register value
    
    CALLER R1
    LOG R1                  ; Print caller address
    
    HALT
The CLI provides execution traces showing register values and gas consumption at each step. Use minichain call --trace for detailed debugging.

Next Steps

Deploy Contracts

Learn how to deploy your contracts to the blockchain

Call Contracts

Interact with deployed contracts

Examples

See complete contract examples

VM Instructions

Complete instruction reference

Build docs developers (and LLMs) love