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 available | Alternative |
|---|---|
std::collections::HashMap | Use StorageMap for on-chain state |
std::string::String | Use alloc::string::String with allocator |
std::vec::Vec | Use alloc::vec::Vec with allocator |
println!() / eprintln!() | Use miden::intrinsics::debug::breakpoint() for debugging |
std::io | Not 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.