🎨

Module M006

Minting Policies and NFT Development

DirectEd x CATS Hackathon
Aiken Development Workshop Series

Duration: 2 hours

Format: 1 hour lecture + 1 hour exercises

SLIDE 2

Module Overview

Until now, we've focused on spending validators. Now we shift to minting policies - validators that control token creation and destruction!

What You'll Learn

  • Minting policy structure
  • One-shot minting (OutputReference)
  • Oracle pattern for NFT collections
  • Parameterized minting policies
  • Burn functionality
  • Complete NFT system design

Why This Matters

  • 🎨 Create NFTs and tokens
  • 🔒 Control token supply
  • 📊 Build NFT collections
  • 🏗️ Design scalable systems
  • 💎 Ensure uniqueness
Goal: Master minting policies and build a complete NFT platform using the oracle pattern!
SLIDE 3

Spending Validators vs Minting Policies

Aspect Spending Validator Minting Policy
Purpose Controls when UTxOs can be spent Controls when tokens are minted/burned
Has Datum? ✅ Yes - state attached to UTxO ❌ No - no UTxO to attach to
Identifier Script Address PolicyId (script hash)
Parameters datum, redeemer, own_ref, tx redeemer, policy_id, tx
Real-World Analogy Bank vault (controls locked funds) Central bank (controls money supply)
Key Difference: Spending validators protect existing UTxOs, minting policies control token supply.
SLIDE 4

Minting Policy Structure

Minting policies have a different signature than spending validators.

Spending Validator

validator my_spending { spend( datum: Option<MyDatum>, redeemer: MyRedeemer, own_ref: OutputReference, tx: Transaction ) -> Bool { // Has datum! True } }

Minting Policy

validator my_mint { mint( redeemer: MyRedeemer, policy_id: PolicyId, tx: Transaction ) -> Bool { // No datum! True } }

Key Points

  • No datum parameter - minting policies don't lock UTxOs
  • PolicyId parameter - the hash of this minting script
  • Purpose is always Mint - not Spend
  • Access tx.mint field - to see what's being minted/burned
SLIDE 5

Token Identity: PolicyId + AssetName

Every token on Cardano has a unique identity made of two parts:

PolicyId

Hash of minting policy

d5e6bf0500378d4f0da4e8dde6 becec7621cd8cbf5cbb9b87013 d4cc

Identifies the "collection"

+

AssetName

Token identifier (ByteArray)

"MyNFT001" or "" (empty) or #"0064"

Identifies specific token

Full Token Identity

d5e6bf0500378d4f0da4e8dde6becec7621cd8cbf5cbb9b87013d4cc.MyNFT001
SLIDE 6

Accessing the Mint Field

The tx.mint field shows what tokens are being created or destroyed.

use cardano/assets.{PolicyId, tokens} validator example_mint { mint(redeemer: Data, policy_id: PolicyId, tx: Transaction) -> Bool { // Access the mint field let minted_tokens = tx.mint // Extract tokens for THIS policy let my_tokens = assets.tokens(minted_tokens, policy_id) // my_tokens is now a Dict<AssetName, Int> // where Int is the quantity: // Positive = minting (creating) // Negative = burning (destroying) // Zero = nothing (shouldn't happen) True } }

Minting (Creating)

if quantity > 0 { // This is a mint // New tokens created // Must appear in outputs }

Burning (Destroying)

if quantity < 0 { // This is a burn // Tokens destroyed // Must exist in inputs }
SLIDE 7

The Uniqueness Problem

For NFTs, we need guaranteed uniqueness - each token minted only ONCE, ever.

⚠️ THE CHALLENGE ⚠️
How do we ensure a minting policy only succeeds ONE TIME?

❌ This Doesn't Work

validator simple_nft { mint(redeemer, policy_id, tx) { // Check owner signature let authorized = list.has( tx.extra_signatories, owner_key ) authorized // Problem! } }

Problem: Owner can sign multiple transactions, minting the same token name repeatedly!

✅ The Solution

Use OutputReference - a UTxO that can only be spent ONCE.

  • Every UTxO has unique OutputReference
  • UTxO can only be spent once
  • Once spent, can never be used again
  • Guarantees one-time minting!
SLIDE 8

Understanding OutputReference

An OutputReference is a unique identifier for every UTxO on Cardano.

OutputReference Structure

pub type OutputReference { transaction_id: TransactionId, // Hash of tx that created it output_index: Int, // Position in tx outputs (0, 1, 2, ...) } // Example: OutputReference { transaction_id: "abc123def456...", output_index: 0 }

Why This Guarantees Uniqueness

  • Every transaction has a unique hash (transaction_id)
  • Each output in that transaction has a unique index
  • Therefore: Every OutputReference is globally unique
  • A UTxO can only be spent once (by definition)
  • Once spent, that OutputReference can never be used again
Analogy: Like a concert ticket with a unique barcode - once scanned, it can never be used again!
SLIDE 9

One-Shot Minting Policy

Parameterize minting policy with a specific OutputReference that must be spent.

use cardano/transaction.{OutputReference, Transaction, Input} use cardano/assets.{PolicyId} // Parameterized with a specific UTxO reference validator one_shot(utxo_ref: OutputReference) { mint(_redeemer: Data, policy_id: PolicyId, tx: Transaction) -> Bool { // Check that the specific UTxO is being spent in this transaction let has_utxo = list.any( tx.inputs, fn(input: Input) { input.output_reference == utxo_ref } ) // If UTxO is present, allow minting // Since UTxO can only be spent once, minting can only happen once! has_utxo } }
How it works: The UTxO can only be spent once → Policy can only succeed once → Guaranteed one-time minting!
SLIDE 10

One-Shot Minting Workflow

Step 1: Choose a UTxO

Alice has a UTxO she owns at tx_hash "abc123..." output index 0

Step 2: Compile Policy

Compile one_shot policy with that specific OutputReference as parameter

Step 3: Create Minting Transaction

Transaction MUST include that UTxO as input + mint operation

Step 4: Policy Validates

✓ UTxO "abc123#0" is in inputs → Minting succeeds!

Step 5: Try Again?

❌ UTxO "abc123#0" was already spent! Cannot include it again → Minting FAILS

SLIDE 11

The Oracle Pattern

How do we create an NFT collection with sequential numbering?

The Challenge

We want: "MyCoolNFT (0)", "MyCoolNFT (1)", "MyCoolNFT (2)", ...

Problem: How do we track the counter? Minting policies have no datum!

Oracle Pattern Solution

1. Oracle NFT (One-Shot Minting Policy)

Minted exactly once, serves as reference token

↓ Locked in ↓
2. Oracle Validator (Spending Validator)

Holds Oracle NFT + datum with collection state (count, price, admin)

↓ Referenced by ↓
3. Collection NFT Policy (Parameterized Minting Policy)

Checks Oracle via reference input, mints with current count

SLIDE 12

Oracle NFT: One-Time Reference Token

The Oracle NFT is minted exactly once and serves as a reference token.

use cardano/transaction.{OutputReference, Transaction, Input} use cardano/assets.{PolicyId} pub type OracleAction { Mint Burn } validator oracle_nft(utxo_ref: OutputReference) { mint(redeemer: OracleAction, policy_id: PolicyId, tx: Transaction) -> Bool { let tokens = assets.tokens(tx.mint, policy_id) when redeemer is { Mint -> { // Must spend the specific UTxO (one-shot) let has_utxo = list.any( tx.inputs, fn(input: Input) { input.output_reference == utxo_ref } ) // Must mint exactly 1 token with empty name let correct_mint = { expect [(name, quantity)] = tokens |> dict.to_pairs() name == "" && quantity == 1 } has_utxo && correct_mint } Burn -> { // For burning: all quantities must be negative list.all(tokens |> dict.values(), fn(q) { q < 0 }) } } } }
SLIDE 13

Oracle Validator: Managing State

The Oracle validator holds the Oracle NFT and tracks collection state in its datum.

Oracle Datum

pub type OracleDatum { count: Int, lovelace_price: Int, fee_address: Address, } // Example: OracleDatum { count: 5, // 5 NFTs minted lovelace_price: 5_000_000, fee_address: admin_addr }

What Oracle Does

  • ✅ Locks Oracle NFT
  • ✅ Tracks mint count
  • ✅ Enforces fee payment
  • ✅ Increments counter
  • ✅ Prevents value changes
  • ✅ Admin can stop
Key Validation: On each mint, counter increments by exactly 1, fee is paid, Oracle NFT stays locked.
SLIDE 14

Reference Inputs (Plutus V2)

Reference inputs let you READ a UTxO without spending it!

Normal Inputs

  • UTxO is consumed (spent)
  • Removed from blockchain
  • Validator must approve
  • Only one tx can spend it

Reference Inputs

  • UTxO is READ ONLY
  • Stays on blockchain
  • No validator approval needed
  • Multiple txs can reference

Why This Matters for Oracle

Collection minting policy can READ the Oracle state (current count) via reference input, while the Oracle validator handles the actual spending and state update.

pub type Transaction { inputs: List<Input>, // UTxOs being spent reference_inputs: List<Input>, // UTxOs being read (not spent!) outputs: List<Output>, // ... }
SLIDE 15

Collection NFT Minting Policy

The actual NFT collection policy references the Oracle to get the current count.

pub type CollectionParams { collection_name: ByteArray, // e.g., "MyCoolNFT" oracle_nft_policy: PolicyId, // Oracle NFT policy ID } validator collection_nft(params: CollectionParams) { mint(redeemer: CollectionAction, policy_id: PolicyId, tx: Transaction) -> Bool { when redeemer is { Mint -> { // 1. Find Oracle NFT in reference inputs expect Some(oracle_input) = list.find( tx.reference_inputs, fn(input: Input) { // Check if this input contains Oracle NFT // (implementation details...) } ) // 2. Extract Oracle datum to get current count expect InlineDatum(oracle_datum_data) = oracle_input.output.datum expect oracle_datum: OracleDatum = oracle_datum_data let current_count = oracle_datum.count // 3. Validate token name: "CollectionName (N)" let expected_name = make_token_name(params.collection_name, current_count) // 4. Ensure exactly 1 token with correct name expect [(token_name, quantity)] = tokens |> dict.to_pairs() token_name == expected_name && quantity == 1 } Burn -> { /* burn logic */ } } } }
SLIDE 16

Complete NFT Minting Flow

Setup: Mint Oracle NFT

One-time transaction mints Oracle NFT, locks it at Oracle address with initial datum (count: 0)

User Mints NFT #1

Inputs: Oracle UTxO (spending), User wallet
Reference Inputs: Oracle UTxO (read count = 0)
Outputs: Oracle back (count = 1), NFT "Collection (0)" to user, 5 ADA fee
Mint: +1 "Collection (0)"

User Mints NFT #2

Same pattern: Read count = 1, mint "Collection (1)", update to count = 2

Continue...

Each mint reads current count, creates sequential NFT, increments Oracle

SLIDE 17

Dynamic Token Name Generation

Token names are generated dynamically using the collection name + count.

Token Name Format

// Helper function for token name generation fn make_token_name(collection_name: ByteArray, count: Int) -> ByteArray { // Result: "CollectionName (N)" bytearray.concat( collection_name, bytearray.concat( " (", bytearray.concat( string.from_int(count), ")" ) ) ) } // Examples: make_token_name("MyCoolNFT", 0) → "MyCoolNFT (0)" make_token_name("MyCoolNFT", 5) → "MyCoolNFT (5)" make_token_name("GameItem", 42) → "GameItem (42)"
Why this format? Human-readable, sequential, easy to parse off-chain, follows NFT conventions.
SLIDE 18

Preventing Double-Satisfaction in Oracle

Remember double-satisfaction from M004? It can happen here too!

⚠️ If Oracle validator doesn't enforce single input, an attacker could include Oracle UTxO twice and mint TWO NFTs with the same count!

❌ Vulnerable

validator oracle { spend(datum, redeemer, _, tx) { // Just check count incremented // BUT: Multiple Oracle inputs? // Both see same updated output! count_incremented } }

Two Oracle inputs could both validate with same output!

✅ Secure

use vodka/inputs.{single_script_input} validator oracle { spend(datum, redeemer, own_ref, tx) { // CRITICAL: Single input check! expect Some(_) = single_script_input( tx.inputs, own_ref ) // Now safe to validate count_incremented } }
SLIDE 19

Parameter Dependency Flow

The three validators are connected through parameters.

Layer 1: Oracle NFT Policy

Parameter: utxo_ref (OutputReference)
Output: PolicyId (used by Oracle validator)

↓ PolicyId flows to ↓

Layer 2: Oracle Validator

Parameter: oracle_nft_policy (PolicyId from Layer 1)
Output: Script Address (where Oracle NFT is locked)

↓ PolicyId flows to ↓

Layer 3: Collection NFT Policy

Parameters: collection_name + oracle_nft_policy (PolicyId from Layer 1)
Uses: References Oracle to read state

Compile Order: Oracle NFT → Oracle Validator → Collection NFT Policy
SLIDE 20

Burn Functionality

Minting policies can also handle burning (destroying tokens).

Minting vs Burning

Minting quantity > 0
Burning quantity < 0
// Mint 1 token tx.mint = { policy: { "NFT": 1 } } // Burn 1 token tx.mint = { policy: { "NFT": -1 } }

Burn Validation

when redeemer is { Mint -> { // Minting logic quantity == 1 } Burn -> { // All quantities negative list.all( tokens |> dict.values(), fn(q) { q < 0 } ) } }
Important: Tokens must exist in transaction inputs to be burned!
SLIDE 21

Testing Minting Policies

Comprehensive testing ensures your NFT system is secure and functional.

✅ Oracle NFT Tests

• One-shot mint succeeds with UTxO
• Mint fails without UTxO
• Burn succeeds

✅ Oracle Validator Tests

• Counter increments correctly
• Fee payment enforced
• Value preservation
• Single input validation
• Admin stop with burn

✅ Collection NFT Tests

• Correct token name passes
• Wrong token name fails
• Missing Oracle reference fails
• Burn succeeds

✅ Integration Tests

• Complete mint workflow
• Sequential minting
• State consistency

SLIDE 22

Building Mock Minting Transactions

use mocktail.{ mocktail_tx, tx_in, tx_mint, tx_reference_input, complete } // Test Oracle NFT one-shot minting test oracle_nft_one_shot_succeeds() { let utxo_ref = mock_utxo_ref(0, 0) let policy_id = mock_policy_id(0) let tx = mocktail_tx() |> tx_in( True, utxo_ref, // The special UTxO (one-shot) from_lovelace(5_000_000), mock_pub_key_address(0, None), NoDatum, zero(), ) |> tx_mint(policy_id, "", 1) // Mint 1 Oracle NFT with empty name |> complete() oracle_nft.mint(utxo_ref, Mint, policy_id, tx) } // Test Collection NFT with Oracle reference test collection_mint_with_oracle() { let oracle_datum = OracleDatum { count: 3, ... } let tx = mocktail_tx() |> tx_reference_input( // Reference input for reading Oracle state oracle_utxo_ref, oracle_value, oracle_address, InlineDatum(oracle_datum), ) |> tx_mint(collection_policy, "TestNFT (3)", 1) // Expected name with count |> complete() collection_nft.mint(params, Mint, collection_policy, tx) }
SLIDE 23

Hands-On Exercises

Time to build your NFT system! 🎨

Exercise 1: Simple Authorized Minting (15 min)

Build basic minting policy with signature authorization

Exercise 2: One-Shot NFT Minting (20 min)

Implement OutputReference-based one-time minting

Exercise 3: Token Name Validation (20 min)

Create policy that validates token name prefix

Exercise 4: Simple Oracle System (Challenge - 40 min)

Build mini Oracle NFT + Oracle validator with counter

SLIDE 24

Assignment M006

Build a Complete NFT Minting System

Choose Your Theme

  1. Option A: Art Gallery Collection
  2. Option B: Gaming Assets Collection
  3. Option C: Membership Passes

Three Validators Required

  • Oracle NFT (one-shot minting)
  • Oracle Validator (state management)
  • Collection NFT (parameterized policy)

Testing (12+ tests)

  • ✅ Oracle NFT tests (3)
  • ✅ Oracle validator tests (5)
  • ✅ Collection NFT tests (4)
  • All redeemer paths tested

Submission

  • GitHub repository
  • Parameter dependency diagram
  • Test results (all passing)
  • README with explanation
Deadline: Before Module M007
SLIDE 25

Common Issues & Solutions

❌ Cannot find OutputReference in inputs

Ensure mock transaction includes the exact UTxO ref used as parameter

❌ PolicyId not found in minted tokens

Use correct policy_id when calling assets.tokens(tx.mint, policy_id)

❌ Datum type mismatch in Oracle

Use proper casting: expect output_datum: OracleDatum = output_datum_data

❌ Token name doesn't match expected format

Test token name generation function separately first

✓ Use trace for debugging

trace @"Current count" trace oracle_datum.count
SLIDE 26

Key Takeaways

You can now:

✅ Understand minting policies vs spending validators

✅ Implement one-shot minting with OutputReference

✅ Understand the Oracle pattern for state management

✅ Create parameterized minting policies

✅ Use reference inputs effectively

✅ Build complete NFT collection systems

✅ Connect multiple validators through parameters

Next: Module M007 - Advanced DApp Architecture 🏗️
SLIDE 27

Complete NFT System Architecture

🏗️

Three Validators Working Together

Oracle NFT Policy

• One-shot minting (OutputReference)
• Mints exactly 1 reference token
• Can be burned by admin

↓ locks in ↓

Oracle Validator

• Holds Oracle NFT + state datum
• Tracks count, price, admin address
• Enforces single input (prevent double-satisfaction)
• Increments counter, validates fees

↓ referenced by ↓

Collection NFT Policy

• Parameterized with collection name + Oracle policy
• References Oracle via reference input
• Reads current count from Oracle datum
• Generates token name: "Collection (N)"
• Mints exactly 1 NFT with sequential name

SLIDE 28

Production NFT Platform Considerations

Building a real NFT platform requires additional features:

Metadata

  • CIP-25 standard
  • Image links
  • Descriptions
  • Attributes

Royalties

  • CIP-27 standard
  • Creator fees
  • Secondary sales
  • Automatic enforcement

Access Control

  • Whitelisting
  • Mint deadlines
  • Supply caps
  • Pause/resume

Off-Chain Components

  • Frontend minting UI
  • Transaction builder
  • Metadata server
  • IPFS integration

Advanced Features

  • Batch minting
  • Reveal mechanisms
  • Trait generation
  • Rarity systems
🎨

Amazing Work!

Module M006 Complete

You can now build complete NFT systems on Cardano!

Master the Oracle pattern 🏗️

Create production-ready NFT platforms 💎

See you in M007! 🚀

/ 29