Mint Logic

Understanding the smart contract - How to mint NFTs on Cardano (in accordance to CIP-68 standard) using Aiken smart contracts. Cardano interactions powered by Anvil API

You've chosen the Smart Contract approach for CIP-68 - excellent choice for projects needing more validation control and complex business rules!

Don't worry about Aiken syntax! This guide focuses on what the smart contract validates and why, not how to write Aiken code. You can successfully use CIP-68 smart contracts through the Anvil API without being an Aiken expert.

Ready to implement? Skip to the practical minting example and return here when you want to understand the underlying logic.

What the Smart Contract Does

While the main guide explained what CIP-68 achieves (updatable NFT metadata), this guide explains how the smart contract ensures everything works correctly with the CIP-68 NFT standard.

The Smart Contract's Job:

  • βœ… Enforces CIP-68 Rules: Ensures reference tokens (100) and user tokens (222) are always created together

  • βœ… Validates Authorization: Only authorized admins can mint new token pairs

  • βœ… Guarantees Metadata Integrity: Reference tokens contain valid CIP-68 metadata structure

  • βœ… Enables Future Updates: Reference tokens can be spent later to update metadata

How It Works: The Validation Flow

Every CIP-68 mint using this smart contract follows the same validation sequence: Build Transaction β†’ Validate Smart Contract β†’ Submit Valid Transaction β†’ Outputs.

Quick Setup Process:

  1. Upload & Parameterize: Deploy the smart contract with admin keys via POST /blueprints endpoints

  2. Build Transactions: Use the parameterized contract in POST /transactions/build calls

  3. Let Validation Happen: Smart contract automatically enforces all CIP-68 rules

  4. Submit Valid Transaction: Validated transaction is submitted to the Cardano network via the POST /transactions/submit endpoint.

Smart Contract Validation Flow

The diagram below shows how the CIP-68 smart contract processes your transaction. Follow the arrows to understand the validation sequence:

How to read this diagram: Start with "Build Transaction" (what you provide), follow the arrows through "Validate Smart Contract" (validation checks), and end at "Submit Valid Transaction" (what gets created). Each validation step must pass for the transaction to succeed.

1. Build Transaction

Your transaction requires these inputs: For more information on transaction building, see the transaction building guide.

Required Transaction Inputs:

  • πŸ’° UTxOs (ADA for fees) - Sufficient ADA for transaction fees and minimum UTxO requirements

  • 🏷️ Reference NFT (100) - Token sent to smart contract address containing metadata

  • 🎨 User Token (222) - Token sent to user as the actual NFT

  • πŸ”‘ Admin Signature - Parameterized admin key authorization

  • πŸ“‹ Blueprint with Applied Parameters - Smart contract with embedded admin key

2. POST /transactions/build (API Processing)

Once you build your transaction, it gets processed through the Anvil API's transaction builder endpoint:

POST /transactions/build

What happens at this step:

  • Your transaction details are received by the Anvil API

  • The API loads the parameterized smart contract from preloadedScripts

  • Transaction structure is validated for basic requirements

  • The smart contract validation logic is triggered

  • If validation passes, a transaction is built and returned

Key Point: The smart contract validation (next section) happens during this API call, not separately.

Want to understand transaction building in detail? This overview focuses on smart contract validation, but for comprehensive transaction construction details, see the transaction building guide.

3. Validate Smart Contract

The smart contract performs three validation checks:

βœ… Check Admin Signature

let is_signed = list.has(self.extra_signatories, signer)

Ensures only the parameterized admin can authorize mints.

βœ… Validate CIP-68 Tokens

The contract performs comprehensive validation of CIP-68 dual-token creation, metadata, and exact mint matching:

validator cip68examples(signer: VerificationKeyHash) {
  mint(redeemer: MintRedeemer, policy_id: PolicyId, self: Transaction) {
    // Step 1: Generate expected tokens based on redeemer instructions
    let expected_mint =
      generate_mint(redeemer.assetnames, policy_id, zero, self)
        |> tokens(policy_id)

    // Step 2: Get what's actually being minted in this transaction
    let currently_minting = self.mint |> tokens(policy_id)

    // Step 3: Ensure both admin signature AND exact token match
    and {
      is_signed?,                              // Admin must sign
      (expected_mint == currently_minting)?,   // Mint exactly what's expected, nothing more/less
    }
  }
}

How this validation works:

  1. Expected vs Actual: The contract calculates what tokens should be minted based on the redeemer, then compares against what's actually being minted

  2. No Surprises: If someone tries to mint extra tokens or different quantities, the validation fails

  3. Dual Validation: Both admin signature AND exact token matching must pass

βœ… Validate Metadata

The contract ensures each reference token output contains valid CIP-68 metadata:

// Extract and validate datum from reference token output
expect Some(datum) = find_datum(output, self)
expect _: Cip68Metadata = datum

Metadata Structure:

pub type Cip68Metadata {
  metadata: Pairs<Data, Data>,
  version: Int,
  extra_datum: MetadataUpdateExpiration,
}

What this validates:

  • Datum Presence: Reference token outputs must contain a datum

  • Structure Validation: Datum must conform to CIP-68 metadata specification

  • Version Control: Metadata includes version information for future updates

The generate_mint function handles all token validation:

This function processes each asset name from the redeemer and validates the corresponding transaction outputs:

pub fn generate_mint(
  tokens: List<(ByteArray, Int)>,  // List of (asset_name, output_index) pairs
  policy_id: PolicyId,
  mint: Value,                     // Accumulator for expected mint value
  self: Transaction,
) -> Value {
  when tokens is {
    [] -> mint  // Base case: return accumulated mint value
    [(assetname, index), ..rest] -> {
      // Step 1: Generate the CIP-68 token pair names
      let (ref_tok, user_tok) = generate_ref_and_user_token(assetname)
      
      // Step 2: Find and validate the reference token output
      expect Some(output) = self.outputs |> at(index)  // Must exist at specified index
      expect Some(datum) = find_datum(output, self)     // Must have a datum
      expect _: Cip68Metadata = datum                   // Datum must be valid CIP-68 metadata
      
      // Step 3: Verify reference token goes to script address with correct value
      let output_value = without_lovelace(output.value)
      let expected_output_value = zero |> add(policy_id, ref_tok, 1)  // Exactly 1 reference token
      expect output_value == expected_output_value                    // No extra tokens in output
      expect
        when output.address.payment_credential is {
          Script(hash) -> hash == policy_id  // Must go to this script's address
          _ -> False                         // Not a script address = fail
        }
      
      // Step 4: Add both tokens to mint and process remaining assets
      generate_mint(
        rest,
        policy_id,
        mint
          |> add(policy_id, ref_tok, 1)   // Add reference token to expected mint
          |> add(policy_id, user_tok, 1), // Add user token to expected mint
        self,
      )
    }
  }
}

What each validation step ensures:

  • Output Existence: The transaction must have an output at the specified index

  • Metadata Presence: Reference token output must contain valid CIP-68 metadata

  • Address Validation: Reference token must be sent to the smart contract address

  • Value Precision: Output contains exactly 1 reference token, no extras

  • Recursive Processing: All asset names in the redeemer are validated

CIP-68 Token Generation:

/// (100) Reference Token Prefix
pub const prefix_100: ByteArray = #"000643b0"
/// (222) Non-Fungible Token Prefix  
pub const prefix_222: ByteArray = #"000de140"

pub fn generate_ref_and_user_token(assetname: ByteArray) -> (ByteArray, ByteArray) {
  (prefix_100 |> concat(assetname), prefix_222 |> concat(assetname))
}

Metadata Structure:

pub type Cip68Metadata {
  metadata: Pairs<Data, Data>,
  version: Int,
  extra_datum: MetadataUpdateExpiration,
}

What this comprehensive validation ensures:

  • Token Pairs: Creates Reference (100) and User (222) tokens for each asset name

  • Metadata Validation: Reference tokens must have valid CIP-68 metadata datum

  • Output Placement: Reference tokens sent to script address, user tokens to specified address

  • Exact Matching: Only specified tokens are minted, no extras allowed

  • Label Prefixes: Proper CIP-68 label encoding (100 = 000643b0, 222 = 000de140)

4. Submit Valid Transaction

Once all validations pass, the transaction is submitted to the Cardano network and CIP-68 token pairs are created on-chain.

5. Outputs

The successful transaction creates both tokens as validated above:

  • 🏷️ Reference NFT (100) - Sent to smart contract address with metadata datum

  • 🎨 User Token (222) - Sent to user's wallet as the actual NFT

Security Model

  • Admin Authorization: Admin key hash is embedded in the contract; only they can authorize mints

  • Customer Control: Customers control their UTxOs and receive user tokens (222) directly

  • Metadata Protection: Reference tokens (100) are locked at script address with validated metadata

Next Steps

Now that you understand the smart contract validation logic, you can:

  • Ready to implement? See the practical minting example for step-by-step code

  • Want to build custom applications? Use this validation knowledge to create applications that interact with CIP-68 contracts

Technical References

Last updated

Was this helpful?