Skip to main content
Version: 0.13 (unstable)

Part 2: Constants and Constraints

In this section, you'll learn how to define business rules using constants and enforce them with assertions. We'll implement deposit limits and see how failed constraints cause transactions to be rejected.

What You'll Build in This Part

By the end of this section, you will have:

  • Defined constants for business rules
  • Used assert!() for transaction validation
  • Learned safe Felt comparison with .as_u64()
  • Added a deposit method skeleton with validation
  • Verified constraints work by testing that invalid operations fail

Building on Part 1

In Part 1, we set up the Bank's storage structure. Now we'll add business rules:

Part 1:                          Part 2:
┌──────────────────┐ ┌──────────────────┐
│ Bank │ │ Bank │
│ ─────────────────│ ──► │ ─────────────────│
│ + initialize() │ │ + initialize() │
│ + get_balance() │ │ + get_balance() │
│ │ │ + deposit() │ ◄── NEW (skeleton)
│ │ │ + MAX_DEPOSIT │ ◄── NEW constant
└──────────────────┘ └──────────────────┘

Defining Constants

Constants in Miden Rust contracts work just like regular Rust constants:

contracts/bank-account/src/lib.rs
/// Maximum allowed deposit amount per transaction.
///
/// Value: 1,000,000 tokens (arbitrary limit for demonstration)
const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000;

Use constants for:

  • Business rule limits (max amounts, timeouts)
  • Magic numbers that need documentation
  • Values used in multiple places

Constants are compiled into the contract code and cannot change. Use storage slots for values that need to be modified at runtime.

The assert!() Macro

The assert!() macro validates conditions during transaction execution:

contracts/bank-account/src/lib.rs
pub fn initialize(&mut self) {
// Check not already initialized
let current: Word = self.initialized.read();
assert!(
current[0].as_u64() == 0,
"Bank already initialized"
);

// Set initialized flag to 1
let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]);
self.initialized.write(initialized_word);
}

When an assertion fails:

  1. The Miden VM execution halts
  2. No valid proof can be generated
  3. The transaction is rejected

This is the primary mechanism for enforcing business rules in Miden contracts.

Safe Felt Comparisons

Never use <, >, <=, or >= operators directly on Felt values. They produce incorrect results due to field element ordering.

Wrong approach:

// DON'T DO THIS - produces incorrect results
if deposit_amount > felt!(1_000_000) {
// This comparison is unreliable!
}

Correct approach:

// CORRECT - convert to u64 first
if deposit_amount.as_u64() > MAX_DEPOSIT_AMOUNT {
// This works correctly
}

The .as_u64() method extracts the underlying 64-bit integer from a Felt, allowing standard Rust comparisons.

Step 1: Add the Constant and Deposit Method

Update your contracts/bank-account/src/lib.rs to add the constant and a deposit method skeleton:

contracts/bank-account/src/lib.rs
/// Maximum allowed deposit amount per transaction.
///
/// Value: 1,000,000 tokens (arbitrary limit for demonstration)
const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000;

#[component]
impl Bank {
/// Initialize the bank account, enabling deposits.
pub fn initialize(&mut self) {
let current: Word = self.initialized.read();
assert!(
current[0].as_u64() == 0,
"Bank already initialized"
);

let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]);
self.initialized.write(initialized_word);
}

/// Get the balance for a depositor.
pub fn get_balance(&self, depositor: AccountId) -> Felt {
let key = Word::from([depositor.prefix, depositor.suffix, felt!(0), felt!(0)]);
self.balances.get(&key)
}

/// Check that the bank is initialized.
fn require_initialized(&self) {
let current: Word = self.initialized.read();
assert!(
current[0].as_u64() == 1,
"Bank not initialized - deposits not enabled"
);
}

/// Deposit assets into the bank.
/// For now, this just validates constraints - we'll add asset handling in Part 3.
pub fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) {
// ========================================================================
// CONSTRAINT: Bank must be initialized
// ========================================================================
self.require_initialized();

// Extract the fungible amount from the asset
let deposit_amount = deposit_asset.inner[0];

// ========================================================================
// CONSTRAINT: Maximum deposit amount check
// ========================================================================
assert!(
deposit_amount.as_u64() <= MAX_DEPOSIT_AMOUNT,
"Deposit amount exceeds maximum allowed"
);

// We'll add balance tracking and asset handling in Part 3
// For now, just validate the constraints
}
}

The require_initialized() Guard

We use a helper method to check initialization state:

fn require_initialized(&self) {
let current: Word = self.initialized.read();
assert!(
current[0].as_u64() == 1,
"Bank not initialized - deposits not enabled"
);
}

This pattern:

  • Centralizes the initialization check
  • Provides a clear error message
  • Can be reused across multiple methods

How Assertions Affect Proving

When an assertion fails in the Miden VM:

Transaction Execution Flow:
┌─────────────────────┐
│ User submits TX │
└──────────┬──────────┘

┌─────────────────────┐
│ VM executes code │
└──────────┬──────────┘

┌──────┴──────┐
│ Assertion? │
└──────┬──────┘
Pass │ Fail
┌──────┴──────┐
▼ ▼
┌────────┐ ┌────────────┐
│ Prove │ │ TX Rejected│
│ Success│ │ No Proof │
└────────┘ └────────────┘

Key points:

  • Failed assertions prevent proof generation
  • No state changes occur if the transaction fails
  • Error messages help with debugging

Step 2: Build and Verify

Build the updated contract:

>_ Terminal
cd contracts/bank-account
miden build

Try It: Verify Constraints Work

Let's write a test to verify our constraints work correctly. This test verifies that depositing without initialization fails:

integration/tests/part2_constraints_test.rs
use integration::helpers::{
build_project_in_dir, create_testing_account_from_package, AccountCreationConfig,
};
use miden_client::account::{StorageMap, StorageSlot};
use miden_client::Word;
use std::{path::Path, sync::Arc};

/// Test that our constraint logic is set up correctly
#[tokio::test]
async fn test_constraints_are_defined() -> anyhow::Result<()> {
// Build the bank account contract to verify it compiles with constraints
let bank_package = Arc::new(build_project_in_dir(
Path::new("../contracts/bank-account"),
true,
)?);

// Create an uninitialized bank account
let bank_cfg = AccountCreationConfig {
storage_slots: vec![
StorageSlot::Value(Word::default()), // initialized = 0
StorageSlot::Map(StorageMap::with_entries([])?),
],
..Default::default()
};

let bank_account =
create_testing_account_from_package(bank_package.clone(), bank_cfg).await?;

// Verify the bank starts uninitialized
let initialized = bank_account.storage().get_item(0)?;
assert_eq!(
initialized[0].as_int(),
0,
"Bank should start uninitialized"
);

println!("Bank account created with constraints!");
println!(" - MAX_DEPOSIT_AMOUNT: 1,000,000");
println!(" - require_initialized() guard in place");
println!(" - Initialization status: {}", initialized[0].as_int());
println!("\nPart 2 constraints test passed!");

Ok(())
}

Run the test from the project root:

>_ Terminal
cargo test --package integration part2_constraints -- --nocapture
Expected output
   Compiling integration v0.1.0 (/path/to/miden-bank/integration)
Finished `test` profile [unoptimized + debuginfo] target(s)
Running tests/part2_constraints_test.rs

running 1 test
Bank account created with constraints!
- MAX_DEPOSIT_AMOUNT: 1,000,000
- require_initialized() guard in place
- Initialization status: 0

Part 2 constraints test passed!
test test_constraints_are_defined ... ok

test result: ok. 1 passed; 0 failed; 0 ignored

In Part 4, when we have the deposit note script, we'll write a full test that verifies:

  1. Depositing without initialization fails
  2. Depositing amounts over MAX_DEPOSIT_AMOUNT fails

For now, the constraint logic is in place and we've verified the contract compiles.

Common Constraint Patterns

Balance Checks (Preview for Part 3)

fn require_sufficient_balance(&self, depositor: AccountId, amount: Felt) {
let balance = self.get_balance(depositor);
assert!(
balance.as_u64() >= amount.as_u64(),
"Insufficient balance"
);
}

This pattern is mandatory for any operation that subtracts from a balance. Miden uses field element (Felt) arithmetic, which is modular. Without this check, subtracting more than the balance would NOT cause an error - instead, the value would silently wrap around to a large positive number, effectively allowing unlimited withdrawals. See Common Pitfalls for more details.

State Checks

fn require_not_paused(&self) {
let paused: Word = self.paused.read();
assert!(
paused[0].as_u64() == 0,
"Contract is paused"
);
}

Complete Code for This Part

Here's the full lib.rs after Part 2:

Click to expand full code
contracts/bank-account/src/lib.rs
#![no_std]
#![feature(alloc_error_handler)]

#[macro_use]
extern crate alloc;

use miden::*;

/// Maximum allowed deposit amount per transaction.
const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000;

/// Bank account component that tracks depositor balances.
#[component]
struct Bank {
#[storage(slot(0), description = "initialized")]
initialized: Value,

#[storage(slot(1), description = "balances")]
balances: StorageMap,
}

#[component]
impl Bank {
/// Initialize the bank account, enabling deposits.
pub fn initialize(&mut self) {
let current: Word = self.initialized.read();
assert!(
current[0].as_u64() == 0,
"Bank already initialized"
);

let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]);
self.initialized.write(initialized_word);
}

/// Get the balance for a depositor.
pub fn get_balance(&self, depositor: AccountId) -> Felt {
let key = Word::from([depositor.prefix, depositor.suffix, felt!(0), felt!(0)]);
self.balances.get(&key)
}

/// Check that the bank is initialized.
fn require_initialized(&self) {
let current: Word = self.initialized.read();
assert!(
current[0].as_u64() == 1,
"Bank not initialized - deposits not enabled"
);
}

/// Deposit assets into the bank.
pub fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) {
// CONSTRAINT: Bank must be initialized
self.require_initialized();

let deposit_amount = deposit_asset.inner[0];

// CONSTRAINT: Maximum deposit amount check
assert!(
deposit_amount.as_u64() <= MAX_DEPOSIT_AMOUNT,
"Deposit amount exceeds maximum allowed"
);

// Balance tracking and asset handling added in Part 3
}
}

Key Takeaways

  1. Constants define immutable business rules at compile time
  2. assert!() enforces constraints - failures reject the transaction
  3. Always use .as_u64() for Felt comparisons, never direct operators
  4. Helper methods like require_initialized() centralize validation logic
  5. Failed assertions mean no valid proof can be generated

See the complete constraint implementation in the miden-bank repository.

Next Steps

Now that you can define and enforce business rules, let's learn how to handle assets in Part 3: Asset Management.