🔐

Module M003

Smart Contracts with Validation Logic

DirectEd x CATS Hackathon
Aiken Development Workshop Series

Duration: 2 hours

Format: 1 hour lecture + 1 hour exercises

SLIDE 2

Module Overview

In M001 and M002, you learned to write validators and test them with mock transactions. Now it's time to make your validators actually DO something!

What You'll Learn

  • Validate redeemer values
  • Check datum fields
  • Verify signatures
  • Enforce time constraints
  • Inspect inputs & outputs
  • Combine validation rules

From This Module

  • Unlocked doors → Locked doors with keys
  • Accept everything → Check specific rules
  • Static logic → Dynamic validation
  • Simple tests → Real-world patterns
Goal: Transform validators from "always succeed" to "check rules and enforce security"
SLIDE 3

What is Validation Logic?

Validation logic is the set of rules that determines whether a transaction can spend a UTxO or mint tokens.

💰

Your Funds
Locked at Script

🔐

Validator
(Checks Rules)

Rules Pass
Transaction Succeeds

Rules Fail
Transaction Rejected

If validator returns False or raises an error, the entire transaction fails and nothing changes on the blockchain.
SLIDE 4

The Validator Function Signature

Every spending validator receives four parameters:

validator my_validator { spend( datum: Option<MyDatum>, // Data locked with UTxO redeemer: MyRedeemer, // Arguments from spender input: OutputReference, // Which UTxO being spent self: Transaction, // Complete transaction context ) -> Bool { // Your validation logic here True // or False } }

Parameters

  • datum: State/config
  • redeemer: Unlock arguments
  • input: UTxO reference
  • self: Full transaction

Return Value

  • True: Transaction valid ✅
  • False: Transaction invalid ❌
  • fail/error: Also rejects ❌
SLIDE 5

Redeemer Validation

A redeemer is the argument passed when spending from a validator. Think of it as providing the "reason" or "proof" to unlock.

Simple Redeemer

pub type SimpleRedeemer { Unlock Cancel Extend } validator simple_check { spend(..., redeemer, ...) { when redeemer is { Unlock -> True Cancel -> True Extend -> False } } }

Redeemer with Data

pub type ActionRedeemer { Withdraw { amount: Int } Deposit { amount: Int } Transfer { to: ByteArray } } validator action_validator { spend(..., redeemer, ...) { when redeemer is { Withdraw { amount } -> amount <= 100 Deposit { .. } -> True Transfer { to, .. } -> to == #"aabbcc" } } }
Key Concept: Redeemers let you define different "actions" or "modes" for your validator, each with its own rules.
SLIDE 6

Datum Validation

A datum is custom data stored with a UTxO at a script address. It represents the "state" or "configuration" of the locked funds.

Why Datums Matter

  • Store who can unlock funds
  • Track state like bids or schedules
  • Hold configuration (deadlines, amounts)
  • Enable stateful smart contracts
pub type VestingDatum { beneficiary: ByteArray, amount: Int, deadline: Int, } validator vesting_validator { spend(datum, redeemer, _, self) { // Extract datum expect Some(vesting_datum) = datum when redeemer is { Claim -> { let VestingDatum { beneficiary, .. } = vesting_datum list.has(self.extra_signatories, beneficiary) } Cancel -> { let VestingDatum { amount, .. } = vesting_datum amount < 1000 } } } }
Pattern: Use expect Some(datum) = datum to extract, then destructure fields with pattern matching.
SLIDE 7

Parameterized Validators

Parameters are values set at compile time, making validators reusable with different configurations.

Think of it like a house blueprint:

📋

Same Blueprint
(Validator Code)

🔧

Different Parameters
(Owner, Admin, etc.)

🏠

Different Addresses
(Unique Instances)

pub type ValidatorParams { owner: ByteArray, admin: ByteArray, } validator access_control(params: ValidatorParams) { spend(_, redeemer, _, self) { when redeemer is { OwnerAction -> list.has(self.extra_signatories, params.owner) AdminAction -> list.has(self.extra_signatories, params.admin) } } }
Benefit: One validator, multiple instances with different owners/configs!
SLIDE 8

Signature Verification

Check WHO signed the transaction using the extra_signatories field.

Common Patterns

// Single signature list.has(signatories, owner) // ANY of multiple (OR) list.has(signatories, owner1) || list.has(signatories, owner2) // ALL of multiple (AND) list.has(signatories, owner1) && list.has(signatories, owner2) // At least N of M list.count(signatories, fn(s) { list.has(required, s) }) >= n

Example: Multi-Sig

pub type OwnershipDatum { owner: ByteArray, co_owner: ByteArray, } validator ownership { spend(datum, redeemer, ..) { expect Some(d) = datum when redeemer is { SingleOwner -> list.has(sigs, d.owner) || list.has(sigs, d.co_owner) BothOwners -> list.has(sigs, d.owner) && list.has(sigs, d.co_owner) } } }
Remember: extra_signatories is a list of public key hashes that signed the transaction.
SLIDE 9

Time-Based Validation

Time on Cardano is represented as an interval, not a single timestamp.

Why Intervals?

  • Transactions don't execute instantly
  • Blocks created every ~20 seconds
  • Can't know exact millisecond of execution
  • Intervals provide flexibility + security

Vodka Time Utilities

use vodka/time.{ is_after, is_before, is_within } // After timestamp is_after(validity_range, 1000) // Before timestamp is_before(validity_range, 2000) // Within range is_within(validity_range, time)

Example

pub type TimeLockDatum { unlock_after: Int, lock_until: Int, } validator timelock { spend(datum, redeemer, _, self) { expect Some(d) = datum when redeemer is { ClaimEarly -> is_before( self.validity_range, d.lock_until ) ClaimLate -> is_after( self.validity_range, d.unlock_after ) } } }
SLIDE 10

Input & Output Validation

Check transaction structure: How many? From where? To where?

Input Validation

use vodka/inputs.{ inputs_at, single_script_input } // Ensure only one script input expect Some(_) = single_script_input( self.inputs, input_ref ) // Get all inputs at address let my_inputs = inputs_at(self.inputs, addr) // Check input value input_lovelace >= 10_000_000

Output Validation

use vodka/outputs.{ outputs_at, single_output_at } // Ensure exactly one output expect Some(out) = single_output_at( self.outputs, recipient_addr ) // Check output value lovelace_of(out.value) >= 5_000_000 // Verify no datum on wallet expect NoDatum = out.datum
Security: Always check for single script input to prevent double-satisfaction attacks!
SLIDE 11

Combining Validation Techniques

Real-world validators combine multiple checks for comprehensive security.

pub type VaultDatum { owner: ByteArray, unlock_time: Int, minimum_withdrawal: Int, } pub type VaultRedeemer { Withdraw Cancel } validator comprehensive_vault { spend(datum, redeemer, input_ref, self) { expect Some(vault_datum) = datum when redeemer is { Withdraw -> { // Check 1: Signed by owner let signed = list.has(self.extra_signatories, vault_datum.owner) // Check 2: Time passed let time_ok = is_after(self.validity_range, vault_datum.unlock_time) // Check 3: Single script input let single_input = single_script_input(self.inputs, input_ref) != None // Check 4: Output meets minimum let output_ok = check_minimum_output(self.outputs, vault_datum.minimum_withdrawal) // ALL must be true signed && time_ok && single_input && output_ok } Cancel -> { list.has(self.extra_signatories, vault_datum.owner) } } } }
Use && to combine checks: ALL conditions must pass for transaction to succeed.
SLIDE 12

Testing Validation Logic

Comprehensive tests ensure your validation logic is correct and secure.

✅ Test Passing Cases

Valid redeemers, correct signatures, proper timing, balanced transactions

❌ Test Failing Cases

Invalid redeemers, wrong signatures, bad timing, unbalanced transactions

⚠️ Test Edge Cases

Boundary values, empty lists, None datums, maximum/minimum amounts

🔍 Test All Redeemer Paths

Every redeemer constructor should have passing and failing tests

Best Practice: For N redeemer constructors, write at least 2N tests (1 pass + 1 fail per constructor).
SLIDE 13

Common Validation Patterns

Access Control

Check owner signature before allowing actions

list.has( self.extra_signatories, datum.owner )

Time Locks

Enforce deadlines or unlock times

is_after( self.validity_range, datum.deadline )

Value Checks

Ensure minimum values or amounts

lovelace_of(value) >= minimum_amount

Multi-Sig

Require multiple signatures

list.has(sigs, owner1) && list.has(sigs, owner2)

Mix and Match!

Combine these patterns to create sophisticated smart contracts: Time Lock + Access Control, Multi-Sig + Value Checks, etc.

SLIDE 14

Hands-On Exercises

Time to build! 🛠️

Exercise 1: Password Validator (20 min)

Build redeemer-based validation with password checking

Exercise 2: Owner-Based Access (20 min)

Implement datum + signature validation

Exercise 3: Time-Locked Withdrawal (25 min)

Combine signature and time validation

Exercise 4: Input/Output Check (25 min)

Validate transaction structure and values

Exercise 5: Comprehensive Vault (30 min)

Combine ALL validation techniques!

SLIDE 15

Assignment M003

Build a Comprehensive Validator

Choose One Scenario

  1. Option A: Escrow Contract
  2. Option B: Vesting Contract
  3. Option C: Auction Contract

Must Include

  • Datum with 3+ fields
  • Redeemer with 2+ constructors
  • Signature validation
  • Time-based validation
  • Input/output validation

Testing Requirements

  • 8+ comprehensive tests
  • All redeemer paths tested
  • Passing cases
  • Failing cases
  • Edge cases

Submission

  • GitHub repository URL
  • Test results (all passing)
  • Brief explanation
Deadline: Before Module M004
SLIDE 16

Common Issues & Solutions

❌ Pattern matching fails on datum

Use expect Some(d) = datum first, then destructure

❌ Signature check always fails

Add signatories to mock tx: tx_extra_signatories([#"aabbcc"])

❌ Time validation not working

Set validity range: set_validity_range(after(1000))

✓ Use trace for debugging

trace @"Checking condition" trace my_value // Run with: aiken check -m trace
SLIDE 17

Key Takeaways

You can now:

✅ Validate redeemers with pattern matching

✅ Extract and validate datum fields

✅ Create parameterized validators

✅ Verify transaction signatures

✅ Implement time-based validation

✅ Validate inputs and outputs

✅ Combine multiple validation techniques

Next: Module M004 - Advanced Input Validation 🔒
🔐

Outstanding Work!

Module M003 Complete

You can now build secure validators with real logic!

Practice combining validation techniques 🛡️

See you in M004! 🚀

/ 18