Hello Cardano CourseLessons3. Aiken Contracts

Lesson #03: Aiken Contracts

From lesson 3 to lesson 6, we will explore the core concepts of building Aiken smart contracts. Some materials are abstracted from Andamio’s AikenPBL.

Overview

  • Hello Cardano Course: Explains selected vital concepts of Aiken smart contract development.
  • AikenPBL: A complete end-to-end project-based learning course covering essential and basic concepts.

Aiken smart contract development is a specialized field. To dive deeper and start a career as a Cardano on-chain developer, we recommend completing both courses.

System Setup

Before we begin, let’s prepare our system for development. We will use Aiken for this course. Follow one of these guides to set up your system:

  1. Aiken Official Installation Guide
  2. Andamio’s AikenPBL Setup Guide

Set Up an Empty Aiken Project

Run the following command to create a new Aiken project using Mesh’s template:

npx meshjs 03-aiken-contracts

Select the Aiken template when prompted.

Select at CLI

After installation, a new folder 03-aiken-contracts will be created with the following structure:

03-aiken-contracts
├── aiken-workspace  // Main Aiken project folder used in lessons
└── mesh             // Folder for equivalent Mesh off-chain code (not used in lessons)

Optional: Install Cardano-Bar

If you use VSCode as your IDE, install the Cardano-Bar extension for code snippets to follow the course more easily.

Aiken script info

Understanding Transaction Context

Cardano contracts are not like traditional smart contracts on other blockchains. They are more like a set of rules governing how transactions are validated. Validator is a better term to describe Cardano contracts.

To build Cardano validators, we need to understand how transactions work. Refer to the Aiken documentation for details on the Transaction structure.

Aiken Tx

Inputs & Outputs

All Cardano transactions must have inputs and outputs:

  • Inputs: UTXOs being spent in the transaction.
  • Outputs: UTXOs being created in the transaction.

Refer to Aiken documentation for types:

Input Output

Key concepts:

  • An input references an output of a previous transaction, identified by output_reference.
  • Validators can check:
    • If an input spends from a specific address.
    • If an input spends a specific asset.
    • If an output sends to a specific address.
    • If an output sends a specific asset.
    • If input/output datum contains specific information.

Reference Inputs

reference_inputs in Transaction are inputs not spent but referenced in the validator. Useful for reading datum from a UTXO without spending it.

Mint

mint in Transaction lists assets being minted or burned. Useful for creating or burning tokens.

Signatures

extra_signatories in Transaction lists public key hashes required to sign the transaction. Useful for enforcing specific users to sign.

Time

validity_range in Transaction specifies the range of slots the transaction is valid for. Useful for enforcing time locks.

Types of Scripts

Refer to Aiken documentation for types of scripts in Cardano. Common types:

  • Minting
  • Spending
  • Withdrawing

Aiken script info

Minting Script

Minting script validation logic is triggered when assets are minted or burned under the script’s policy.

Example: /aiken-workspace/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)
}

This script compiles into a script with hash def68337867cb4f1f95b6b811fedbfcdd7780d10a95cc072077088ea, also called policy Id. It validates transactions minting or burning assets under this policy.

Parameters

Upgrade the script to allow minting/burning only when signed by a specific key:

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"
  }
}
  • owner_vkey: Public key hash of the owner allowed to mint/burn assets.
  • Use key_signed from vodka for validation.

Redeemer

Extend the policy to include a redeemer specifying the transaction action (minting or burning):

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"
  }
}

Spending Script

Spending script validation is triggered when a UTXO is spent in the transaction.

Example: /aiken-workspace/validators/spend.ak:

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"
  }
}

Datum

  • Datum: Data attached to UTXOs at script addresses.
  • Common design pattern: Use an oracle NFT (state thread token) to ensure UTXO uniqueness.

Withdrawing Script

Withdrawal script validation is triggered when withdrawing from a reward account.

Example: /aiken-workspace/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)
}

Handling Publishing

All withdrawal scripts must be registered on-chain before they can be used. This is done by publishing a registration certificate with the script hash as the stake credential. The publishing of the script is also validated by the publish function in the withdrawal script, which is triggered whenever the current withdrawal script is being registered or deregistered.

When withdrawal script is used?

For most Cardano users, we would just use a normal payment key to stake and withdraw rewards. However, it is very popular for Cardano DApps to build withdrawal scripts to enhance the efficiency of validation. We will cover this trick in lesson 5.