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 (
MAX_DEPOSIT_AMOUNT,MAX_BALANCE) - Used
assert!()for transaction validation - Compared token amounts with
u64business limits using.as_canonical_u64() - Added a deposit method skeleton with validation
- Verified the component builds and loads with its initial storage; later parts exercise the transaction guards
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_depositor_balance()│ │ + get_depositor_balance()│
│ │ │ + deposit() │ ◄── NEW (skeleton)
│ │ │ + MAX_DEPOSIT_AMOUNT │ ◄── NEW constant
│ │ │ + MAX_BALANCE │ ◄── NEW constant
└─────────────────────────┘ └─────────────────────────┘
Defining Constants
Constants in Miden Rust contracts work just like regular Rust constants:
/// Maximum allowed deposit amount per transaction.
///
/// This limit provides a safety constraint for the banking system.
///
/// Value: 1,000,000 tokens (arbitrary limit for demonstration)
const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000;
/// Maximum allowed balance per depositor per asset.
///
/// This matches `FungibleAsset::MAX_AMOUNT` (2^63 - 2^31) from the Miden protocol.
/// Felt arithmetic is modular (wraps at the Goldilocks prime), so without this guard
/// a cumulative balance could silently wrap around to zero. Validating the u64 result
/// of the addition against this bound prevents that overflow.
const MAX_BALANCE: u64 = 9_223_372_034_707_292_160; // 2^63 - 2^31
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:
fn initialize(&mut self) {
// Check not already initialized
let current: Word = self.initialized.get();
assert!(
current[0].as_canonical_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.set(initialized_word);
}
When an assertion fails:
- The Miden VM execution halts
- No valid proof can be generated
- The transaction is rejected
This is the primary mechanism for enforcing business rules in Miden contracts.
Comparing Felt Amounts with Business Limits
The current SDK's <, >, <=, and >= operators compare the canonical integer values of Felts. Direct comparisons are supported. Arithmetic on Felt is modular, however, so validate amounts before addition or subtraction can wrap around the field modulus.
Comparing two field elements:
// Direct Felt comparison uses canonical integer ordering.
if deposit_amount > felt!(1_000_000) {
// The amount exceeds the limit.
}
Comparing with our u64 constant:
// Convert the amount to compare it with the u64 business limit.
if deposit_amount.as_canonical_u64() > MAX_DEPOSIT_AMOUNT {
// The amount exceeds the limit.
}
Both comparisons have the same result. This tutorial uses .as_canonical_u64() to express quantity checks against u64 limits explicitly. Converting a value after field arithmetic has wrapped does not recover its original quantity.
Step 1: Add the Constant and Deposit Method
Update your contracts/bank-account/src/lib.rs to add the constants and a deposit method skeleton. Keep the storage struct from Part 1 and replace the Bank trait and implementation blocks with the code below. Declare deposit in the trait with #[account_procedure] before implementing it in #[component] impl Bank for BankStorage. Private helpers like require_initialized and balance_key remain in a separate plain impl BankStorage block:
const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000;
const MAX_BALANCE: u64 = 9_223_372_034_707_292_160; // 2^63 - 2^31
#[component]
trait Bank {
/// Initialize the bank account, enabling deposits.
#[account_procedure]
fn initialize(&mut self);
/// Get the bank-tracked balance for a depositor and specific asset type.
#[account_procedure]
fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt;
/// Deposit an asset into the bank for a specific depositor.
#[account_procedure]
fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset);
}
#[component]
impl Bank for BankStorage {
fn initialize(&mut self) {
let current: Word = self.initialized.get();
assert!(
current[0].as_canonical_u64() == 0,
"Bank already initialized"
);
let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]);
self.initialized.set(initialized_word);
}
/// Get the bank-tracked balance for a depositor and specific asset type.
fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt {
self.balances.get(BankStorage::balance_key(depositor, &asset))
}
/// Deposit assets into the bank.
/// For now, this just validates constraints - we'll add asset handling in Part 3.
fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) {
// NOTE: Initialization guard — enabled in Part 6 (Transaction Scripts)
// self.require_initialized();
// Extract the fungible amount from the asset
let deposit_amount = deposit_asset.value[0];
// ========================================================================
// CONSTRAINT: Maximum deposit amount check
// ========================================================================
assert!(
deposit_amount.as_canonical_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
}
}
/// Internal helpers that are not part of the component's exported WIT API.
impl BankStorage {
/// Derive the `balances` map key identifying a (depositor, faucet) pair:
/// `[depositor.prefix, depositor.suffix, faucet_prefix, faucet_suffix(+metadata)]`.
fn balance_key(depositor: AccountId, asset: &Asset) -> Word {
Word::from([
depositor.prefix,
depositor.suffix,
asset.key[3], // faucet id prefix
asset.key[2], // faucet id suffix folded with the asset metadata byte
])
}
/// Check that the bank is initialized.
fn require_initialized(&self) {
let current: Word = self.initialized.get();
assert!(
current[0].as_canonical_u64() == 1,
"Bank not initialized - deposits not enabled"
);
}
}
In v0.16 the fungible-asset ID Word is [asset_class_suffix, asset_class_prefix, faucet_suffix | metadata_byte, faucet_prefix]. The fungible asset class is empty, and the metadata byte contains the composition bits; the callback flag is part of the faucet account ID. Thus asset.key[2] is not the raw faucet suffix. The host/test side derives the same ID from FungibleAsset::new(faucet.id(), amt)?.to_id_word() indices [3]/[2] (not faucet.id().prefix()/suffix()).
The require_initialized() Guard
This helper is defined in the private impl BankStorage block and intentionally commented out in the deposit method until Part 6. When enabled, it will check initialization state:
fn require_initialized(&self) {
let current: Word = self.initialized.get();
assert!(
current[0].as_canonical_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:
cd contracts/bank-account
miden build
Optional: Verify Constraints Work
This is an optional self-check. If you create this test file, you can run it to verify the contract compiles with the constraint logic. The main runnable tests begin in Part 4.
use integration::helpers::{
build_project_in_dir, create_testing_account_from_package, AccountCreationConfig,
};
use miden_client::account::{
component::{InitStorageData, StorageValueName},
StorageSlotName,
};
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,
)?);
// The `initialized` value slot has no schema default, so
// `AccountComponent::from_package` requires it to be seeded (with a zero Word =
// uninitialized) or it errors with `InitValueNotProvided`. Only the `balances`
// map slot defaults to empty.
let initialized_slot =
StorageSlotName::new("bank_account::bank::initialized")
.expect("Valid slot name");
// Create an uninitialized bank account
let mut init_storage_data = InitStorageData::default();
init_storage_data.insert_value(
StorageValueName::from_slot_name(&initialized_slot),
Word::default(),
)?;
let bank_cfg = AccountCreationConfig {
init_storage_data,
..Default::default()
};
let bank_account =
create_testing_account_from_package(bank_package.clone(), bank_cfg)?;
// Verify the bank starts uninitialized
let initialized = bank_account.storage().get_item(&initialized_slot)?;
assert_eq!(
initialized[0].as_canonical_u64(),
0,
"Bank should start uninitialized"
);
println!("Bank account created with constraints!");
println!(" - MAX_DEPOSIT_AMOUNT: 1,000,000");
println!(" - require_initialized() defined (enabled in Part 6)");
println!(" - Initialization status: {}", initialized[0].as_canonical_u64());
println!("\nPart 2 constraints test passed!");
Ok(())
}
Run the test from the project root:
cargo test --package integration test_constraints_are_defined -- --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() defined (enabled in Part 6)
- 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, we'll write a real deposit-flow test. At that stage, the deposit works without initialization because the guard is still commented out. In Part 6 (Transaction Scripts), we'll enable the initialization guard and verify it works with a dedicated test.
Common Constraint Patterns
Balance Checks (Preview for Part 3)
fn require_sufficient_balance(&self, depositor: AccountId, asset: Asset, amount: Felt) {
let balance = self.get_depositor_balance(depositor, asset);
assert!(
balance.as_canonical_u64() >= amount.as_canonical_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
This optional extension assumes a paused: StorageValue<Word> slot has been added to the component and initialized to a zero Word. The Bank component in this tutorial does not include that slot.
fn require_not_paused(&self) {
let paused: Word = self.paused.get();
assert!(
paused[0].as_canonical_u64() == 0,
"Contract is paused"
);
}
Complete Code for This Part
Here's the full lib.rs after Part 2:
Click to expand full code
#![no_std]
#![feature(alloc_error_handler)]
#[macro_use]
extern crate alloc;
use miden::*;
use miden::Felt;
/// Maximum allowed deposit amount per transaction.
const MAX_DEPOSIT_AMOUNT: u64 = 1_000_000;
/// Maximum allowed balance per depositor per asset.
const MAX_BALANCE: u64 = 9_223_372_034_707_292_160; // 2^63 - 2^31
/// Storage layout for the bank account component.
#[component_storage]
struct BankStorage {
#[storage(description = "initialized")]
initialized: StorageValue<Word>,
#[storage(description = "balances")]
balances: StorageMap<Word, Felt>,
}
/// API of the bank account component.
#[component]
trait Bank {
/// Initialize the bank account, enabling deposits.
#[account_procedure]
fn initialize(&mut self);
/// Get the bank-tracked balance for a depositor and specific asset type.
#[account_procedure]
fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt;
/// Deposit an asset into the bank for a specific depositor.
#[account_procedure]
fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset);
}
#[component]
impl Bank for BankStorage {
fn initialize(&mut self) {
let current: Word = self.initialized.get();
assert!(
current[0].as_canonical_u64() == 0,
"Bank already initialized"
);
let initialized_word = Word::from([felt!(1), felt!(0), felt!(0), felt!(0)]);
self.initialized.set(initialized_word);
}
fn get_depositor_balance(&self, depositor: AccountId, asset: Asset) -> Felt {
self.balances.get(BankStorage::balance_key(depositor, &asset))
}
fn deposit(&mut self, depositor: AccountId, deposit_asset: Asset) {
// NOTE: Initialization guard — enabled in Part 6 (Transaction Scripts)
// self.require_initialized();
let deposit_amount = deposit_asset.value[0];
// CONSTRAINT: Maximum deposit amount check
assert!(
deposit_amount.as_canonical_u64() <= MAX_DEPOSIT_AMOUNT,
"Deposit amount exceeds maximum allowed"
);
// Balance tracking and asset handling added in Part 3
}
}
/// Internal helpers that are not part of the component's exported WIT API.
///
/// The `#[component]` macro exports only the methods of the `Bank` trait, so these
/// inherent methods stay private to the contract.
impl BankStorage {
/// Derive the `balances` map key identifying a (depositor, faucet) pair:
/// `[depositor.prefix, depositor.suffix, faucet_prefix, faucet_suffix(+metadata)]`.
fn balance_key(depositor: AccountId, asset: &Asset) -> Word {
Word::from([
depositor.prefix,
depositor.suffix,
asset.key[3],
asset.key[2],
])
}
/// Check that the bank is initialized.
fn require_initialized(&self) {
let current: Word = self.initialized.get();
assert!(
current[0].as_canonical_u64() == 1,
"Bank not initialized - deposits not enabled"
);
}
}
Key Takeaways
- Constants define immutable business rules at compile time
assert!()enforces constraints - failures reject the transaction- Use
.as_canonical_u64()to compare amounts withu64business limits; validate quantities before field arithmetic can wrap - Helper methods like
require_initialized()centralize validation logic - Failed assertions mean no valid proof can be generated
See the complete constraint implementation in contracts/bank-account/src/lib.rs.
Next Steps
Now that you can define and enforce business rules, let's learn how to handle assets in Part 3: Asset Management.