Skip to main content
Version: 0.13

Patterns & Security

Proven patterns for writing safe, correct Miden smart contracts, followed by security considerations specific to the Miden execution model. Each pattern is presented problem-first: the problem is stated, then the solution is shown with code. For a hands-on tutorial applying these patterns, see the Miden Bank Tutorial.

Common Patterns

Basic wallet

The simplest useful contract — receive and send assets:

#![no_std]
#![feature(alloc_error_handler)]

use miden::{component, output_note, Asset, NoteIdx};

#[component]
struct Wallet;

#[component]
impl Wallet {
pub fn receive_asset(&mut self, asset: Asset) {
self.add_asset(asset);
}

pub fn send_asset(&mut self, asset: Asset, note_idx: NoteIdx) {
let removed = self.remove_asset(asset);
output_note::add_asset(removed, note_idx);
}
}

All Miden contracts require #![no_std]. The remaining examples on this page omit the preamble (#![no_std], #![feature(alloc_error_handler)]) for brevity. See Toolchain & Project Structure for the full setup.

Counter

Track a numeric value in storage:

use miden::{component, felt, Felt, StorageMap, StorageMapAccess, Word};

#[component]
struct Counter {
#[storage(description = "counter storage map")]
count_map: StorageMap,
}

#[component]
impl Counter {
pub fn get_count(&self) -> Felt {
let key = Word::from_u64_unchecked(0, 0, 0, 1);
self.count_map.get(&key)
}

pub fn increment_count(&mut self) -> Felt {
let key = Word::from_u64_unchecked(0, 0, 0, 1);
let current: Felt = self.count_map.get(&key);
let new_value = current + felt!(1);
self.count_map.set(key, new_value);
new_value
}
}

Access control

Unlike Solidity, account component procedures cannot check "who is calling me." In Miden:

  • Note scripts can check who created the note via active_note::get_sender()
  • Account components rely on authentication components (Falcon512, ECDSA) which the transaction kernel invokes automatically in the epilogue

Note-based ownership check

Note scripts can restrict execution to notes from a specific sender. This mirrors how the protocol-level ownable standard works (miden-standards/asm/standards/access/ownable.masm):

use miden::*;

#[note_script]
mod owner_only_note {
use super::*;

/// A note that only executes if created by the expected owner account.
#[note]
struct OwnerOnlyNote;

impl OwnerOnlyNote {
pub fn run(self, _arg: Word, account: &mut Account) {
// Get the account that created this note
let sender = active_note::get_sender();

// Compare against the expected owner
// In practice, load this from account storage
let expected_prefix = Felt::new(0x1234); // placeholder
let expected_suffix = Felt::new(0x5678); // placeholder

assert_eq(sender.prefix, expected_prefix);
assert_eq(sender.suffix, expected_suffix);

// ... proceed with privileged operation
}
}
}

Authentication components

For most account-level access control, Miden uses authentication components rather than manual sender checks. The transaction kernel calls the account's auth procedure automatically during the transaction epilogue — if the signature is invalid, the entire transaction fails. See Authentication for the full pattern.

Rate limiting

Enforce cooldown periods between actions:

use miden::{component, felt, tx, Felt, Value, ValueAccess, Word};

const COOLDOWN_BLOCKS: u64 = 100;

#[component]
struct RateLimited {
#[storage(description = "last action block")]
last_action: Value,
}

#[component]
impl RateLimited {
pub fn rate_limited_action(&mut self) {
let state: Word = self.last_action.read();
let last_block = state[0].as_u64();
let current_block = tx::get_block_number().as_u64();

assert!(current_block.saturating_sub(last_block) >= COOLDOWN_BLOCKS);

self.last_action.write(Word::from([
tx::get_block_number(), felt!(0), felt!(0), felt!(0),
]));
}
}

Spending limits

Cap per-transaction and daily spending:

use miden::{component, felt, output_note, tx, Asset, Felt, NoteIdx, Value, ValueAccess, Word};

const BLOCKS_PER_DAY: u64 = 28800;

#[component]
struct LimitedWallet {
#[storage(description = "limits: [max_per_tx, daily_max, 0, 0]")]
limits: Value,

#[storage(description = "state: [daily_spent, last_reset, 0, 0]")]
state: Value,
}

#[component]
impl LimitedWallet {
pub fn send(&mut self, asset: Asset, note_idx: NoteIdx) {
let amount = asset.inner[0].as_u64();

// Check per-tx limit
let limits: Word = self.limits.read();
assert!(amount <= limits[0].as_u64());

// Check daily limit with auto-reset
let state: Word = self.state.read();
let daily_spent = state[0].as_u64();
let last_reset = state[1].as_u64();
let current_block = tx::get_block_number().as_u64();
let blocks_since_reset = current_block.saturating_sub(last_reset);

let effective_daily = if blocks_since_reset >= BLOCKS_PER_DAY { 0 } else { daily_spent };
assert!(effective_daily + amount <= limits[1].as_u64());

// Update state
let new_reset = if blocks_since_reset >= BLOCKS_PER_DAY {
tx::get_block_number()
} else {
Felt::from_u64_unchecked(last_reset)
};
self.state.write(Word::from([
Felt::from_u64_unchecked(effective_daily + amount),
new_reset, felt!(0), felt!(0),
]));

// Execute transfer
let removed = self.remove_asset(asset);
output_note::add_asset(removed, note_idx);
}
}

Security

Assertions and error handling

Miden doesn't support error strings or Result types in contract execution. Use assertions:

// Good — clear, simple assertions
assert!(amount > felt!(0));
assert!(amount.as_u64() <= 1_000_000);

// Also available — SDK assertion functions
use miden::{assert_eq, assertz};
assert_eq(a, b); // Fails if a != b
assertz(flag); // Fails if flag != 0

When an assertion fails, proof generation fails and the transaction is rejected before reaching the network.

Replay protection

Always increment the nonce when modifying account state (see Authentication for the full pattern):

// The auth component should call incr_nonce()
let new_nonce = self.incr_nonce();

Without nonce management, the same transaction proof could be submitted multiple times.

Safe arithmetic

Use saturating_sub to prevent underflow:

// Good — won't underflow
let elapsed = current_block.saturating_sub(last_block);

// Dangerous — could underflow if current < last
let elapsed = current_block - last_block;

For Felt arithmetic, values wrap modulo the prime field (no overflow panic), but the result may not be what you expect if you're treating Felts as integers.

Anti-patterns

Don't use integer division with Felt

// WRONG — Felt division computes multiplicative inverse, not integer division
let half = amount / felt!(2);

// RIGHT — convert to u64 for integer division
let half = Felt::from_u64_unchecked(amount.as_u64() / 2);

Don't compare Felts with > or < directly

// WRONG — Felt comparison is over field elements, not integers
if amount > felt!(100) { ... }

// RIGHT — convert to u64 for numeric comparison
if amount.as_u64() > 100 { ... }

Don't store secrets in contract code

Contract code is visible on-chain. Never embed private keys, seeds, or other secrets.

Don't skip nonce management

Every state-changing transaction must increment the nonce to prevent replay attacks.

For the complete function reference, see the Cheatsheet.

#![no_std] Environment

All Miden contracts run without the standard library. This means:

Standard library alternatives

Not availableAlternative
std::collections::HashMapUse StorageMap for on-chain state
std::string::StringUse alloc::string::String with allocator
std::vec::VecUse alloc::vec::Vec with allocator
println!() / eprintln!()Use miden::intrinsics::debug::breakpoint() for debugging
std::ioNot available
Error strings in assert!()Use assert!(condition) without messages

Using the allocator

If you need heap allocation (Vec, String, etc.), add the bump allocator:

#![no_std]
#![feature(alloc_error_handler)]

extern crate alloc;
use alloc::vec::Vec;

#[global_allocator]
static ALLOC: miden::BumpAlloc = miden::BumpAlloc::new();

#[cfg(not(test))]
#[panic_handler]
fn my_panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}

#[cfg(not(test))]
#[alloc_error_handler]
fn my_alloc_error(_info: core::alloc::Layout) -> ! {
loop {}
}

BumpAlloc is a bump allocator — it grows memory but never frees it. This is fine for short-lived transaction execution.