Mesh LogoMesh

Lesson 3: Aiken Contracts

Introduction to Aiken smart contract development on Cardano.

Learning Objectives

By the end of this lesson, you will be able to:

  • Understand how Cardano validators differ from traditional smart contracts
  • Explain the Transaction structure and its key components
  • Identify and create different types of scripts (minting, spending, withdrawing)
  • Write basic Aiken validators with parameters and redeemers
  • Use the Mesh CLI to scaffold an Aiken project

Prerequisites

Before starting this lesson, ensure you have:

Key Concepts

Validators vs Smart Contracts

Cardano contracts work differently from smart contracts on other blockchains. Instead of executing arbitrary code, Cardano uses validators - pure functions that return True or False to approve or reject transactions.

A validator examines the transaction context and decides whether the transaction is valid. If the validator returns True, the transaction proceeds. If it returns False, the transaction fails.

Why Validators?

Traditional Smart ContractsCardano Validators
Execute arbitrary codeReturn only True/False
State stored on-chainState in UTXOs via datums
Sequential executionParallel validation possible
Unpredictable costsDeterministic costs

Step 1: Set Up an Aiken Project

Use the Mesh CLI to create a new Aiken project with a template structure.

npx meshjs 03-aiken-contracts

Select the Aiken template when prompted.

The command creates this structure:

03-aiken-contracts/
  aiken-workspace/    # Main Aiken project (used in lessons)
  mesh/               # Equivalent Mesh off-chain code

Navigate to the Aiken workspace:

cd 03-aiken-contracts/aiken-workspace

Optional: Install Cardano-Bar Extension

If you use VS Code, install the Cardano-Bar extension for helpful code snippets.

Step 2: Understand the Transaction Structure

Every Aiken validator receives a Transaction object containing all information about the current transaction. Understanding this structure is essential for writing validators.

Refer to the Aiken standard library documentation for complete type definitions.

Key Transaction Fields

FieldTypeDescription
inputsList<Input>UTXOs being spent
outputsList<Output>UTXOs being created
reference_inputsList<Input>UTXOs referenced but not spent
mintValueAssets being minted or burned
extra_signatoriesList<Hash>Required signers' public key hashes
validity_rangeValidityRangeTime bounds for the transaction

Inputs and Outputs

Cardano transactions consume existing UTXOs (inputs) and create new UTXOs (outputs).

Input {
  output_reference: OutputReference,  // Points to previous tx output
  output: Output                      // The actual UTXO data
}

Output {
  address: Address,                   // Destination address
  value: Value,                       // ADA and tokens
  datum: Datum,                       // Optional data attachment
}

Common validation patterns:

  • Check if an input spends from a specific address
  • Check if an input contains a specific asset
  • Check if an output sends to a specific address
  • Check if datum contains expected values

Reference Inputs

reference_inputs are UTXOs included in the transaction for reading only - they are not spent. Use these to access data (like oracle prices) without consuming the UTXO.

Mint Field

The mint field lists assets being minted (positive quantities) or burned (negative quantities) in this transaction.

Extra Signatories

extra_signatories contains public key hashes that must sign the transaction. Use this to enforce authorization requirements.

Validity Range

validity_range specifies when the transaction is valid. Use this for time-locked contracts.

Step 3: Create a Minting Script

Minting scripts validate token creation and burning. The script runs whenever assets under its policy ID are minted or burned.

Create a file validators/mint.ak:

use cardano/assets.{PolicyId}
use cardano/transaction.{Transaction, placeholder}

validator always_succeed {
  mint(_redeemer: Data, _policy_id: PolicyId, _tx: Transaction) {
    True
  }

  else(_) {
    fail @"unsupported purpose"
  }
}

test test_always_succeed_minting_policy() {
  let data = Void
  always_succeed.mint(data, #"", placeholder)
}

Understanding the Code

ElementDescription
validator always_succeedDeclares a validator named always_succeed
mint(...)Handler for minting/burning operations
_redeemer: DataUser-provided data (underscore means unused)
_policy_id: PolicyIdThe policy ID being validated
_tx: TransactionThe full transaction context
else(_)Fallback for unsupported script purposes

Build and test:

aiken build
aiken check

Add Parameters

Make the script more useful by requiring a specific signature:

use aiken/crypto.{VerificationKeyHash}
use cardano/assets.{PolicyId}
use cardano/transaction.{Transaction}
use vodka/extra/list.{key_signed}

validator minting_policy(owner_vkey: VerificationKeyHash) {
  mint(_redeemer: Data, _policy_id: PolicyId, tx: Transaction) {
    key_signed(tx.extra_signatories, owner_vkey)
  }

  else(_) {
    fail @"unsupported purpose"
  }
}

The owner_vkey parameter is baked into the compiled script at deployment time, creating a unique policy ID for each owner.

Add Redeemer Logic

Extend the policy to handle different actions:

use aiken/crypto.{VerificationKeyHash}
use cardano/assets.{PolicyId}
use cardano/transaction.{Transaction}
use vodka/extra/list.{key_signed}
use vodka/extra/validity_range.{valid_before}
use vodka/extra/value.{check_policy_only_burn}

pub type MyRedeemer {
  MintToken
  BurnToken
}

validator minting_policy(
  owner_vkey: VerificationKeyHash,
  minting_deadline: Int,
) {
  mint(redeemer: MyRedeemer, policy_id: PolicyId, tx: Transaction) {
    when redeemer is {
      MintToken -> {
        let before_deadline = valid_before(tx.validity_range, minting_deadline)
        let is_owner_signed = key_signed(tx.extra_signatories, owner_vkey)
        before_deadline? && is_owner_signed?
      }
      BurnToken -> check_policy_only_burn(tx.mint, policy_id)
    }
  }

  else(_) {
    fail @"unsupported purpose"
  }
}

Redeemer Types

RedeemerConditions
MintTokenMust be before deadline AND signed by owner
BurnTokenOnly burning allowed (no new minting)

The ? operator after boolean expressions enables tracing - if validation fails, it reports which condition failed.

Step 4: Create a Spending Script

Spending scripts validate when UTXOs at a script address are spent.

Create a file validators/spend.ak:

use cardano/assets.{PolicyId}
use cardano/transaction.{OutputReference, Transaction}
use vodka/extra/list.{inputs_with_policy}

pub type Datum {
  oracle_nft: PolicyId,
}

validator hello_world {
  spend(
    datum_opt: Option<Datum>,
    _redeemer: Data,
    _input: OutputReference,
    tx: Transaction,
  ) {
    when datum_opt is {
      Some(datum) ->
        when inputs_with_policy(tx.reference_inputs, datum.oracle_nft) is {
          [_ref_input] -> True
          _ -> False
        }
      None -> False
    }
  }

  else(_) {
    fail @"unsupported purpose"
  }
}

Understanding Spending Scripts

ParameterDescription
datum_optOptional datum attached to the UTXO being spent
redeemerUser-provided data for this spend action
inputReference to the UTXO being spent
txFull transaction context

This example requires a reference input containing an "oracle NFT" to unlock funds.

Common Datum Pattern: Oracle NFT

A common pattern uses an NFT as a "state thread token" to ensure UTXO uniqueness:

pub type Datum {
  oracle_nft: PolicyId,  // NFT that must be referenced
}

This pattern:

  1. Creates a unique NFT (only one exists)
  2. Stores the NFT at an oracle address with data in the datum
  3. Requires referencing this UTXO to spend from the validator

Step 5: Create a Withdrawal Script

Withdrawal scripts validate stake reward withdrawals and are commonly used for validation delegation (covered in Lesson 5).

Create a file validators/withdraw.ak:

use aiken/crypto.{VerificationKeyHash}
use cardano/address.{Credential, Script}
use cardano/certificate.{Certificate}
use cardano/transaction.{Transaction, placeholder}

validator always_succeed(_key_hash: VerificationKeyHash) {
  withdraw(_redeemer: Data, _credential: Credential, _tx: Transaction) {
    True
  }

  publish(_redeemer: Data, _certificate: Certificate, _tx: Transaction) {
    True
  }

  else(_) {
    fail @"unsupported purpose"
  }
}

test test_always_succeed_withdrawal_policy() {
  let data = Void
  always_succeed.withdraw("", data, Script(#""), placeholder)
}

Withdrawal Script Requirements

All withdrawal scripts must include a publish handler because:

  1. The script must be registered on-chain before use
  2. Registration creates a certificate
  3. The publish function validates registration/deregistration

When to Use Withdrawal Scripts

Most users stake and withdraw using regular payment keys. However, dApps use withdrawal scripts to:

  • Centralize validation logic across multiple validators
  • Implement the "withdraw 0 trick" for efficient validation (see Lesson 5)

Key Concepts Explained

Script Types Summary

TypeTriggered WhenCommon Use
MintingAssets minted/burned under the policyToken creation, NFT minting
SpendingUTXO at script address is spentEscrow, vesting, DeFi protocols
WithdrawingStake rewards withdrawnValidation delegation

The else Handler

Every validator needs an else handler for unsupported purposes:

else(_) {
  fail @"unsupported purpose"
}

This prevents the script from being used for unintended operations.

Vodka Library

The examples use vodka, a utility library for common Aiken operations:

  • key_signed: Check if a key hash is in the signatories list
  • valid_before: Check if transaction is valid before a time
  • inputs_with_policy: Find inputs containing a specific policy
  • check_policy_only_burn: Verify only burning occurs

Exercises

  1. Extend the minting script: Add a third redeemer action TransferOwnership that allows changing the owner key hash (hint: you need to track state).

  2. Time-bounded spending: Create a spending script that only allows spending after a certain time stored in the datum.

  3. Multi-oracle validation: Modify the spending script to require references to multiple oracle NFTs.

Next Steps

You have learned:

  • How Cardano validators work
  • The Transaction structure and its components
  • How to write minting, spending, and withdrawal scripts
  • How to use parameters and redeemers

In the next lesson, you learn how to test Aiken contracts using mock transactions.

On this page