🔒

Module M004

Advanced Input Validation

DirectEd x CATS Hackathon
Aiken Development Workshop Series

Duration: 2 hours

Format: 1 hour lecture + 1 hour exercises

SLIDE 2

Module Overview

In M003, you learned various validation techniques. Now we focus deep on one critical aspect: INPUT VALIDATION.

What You'll Learn

  • Filter inputs by address
  • Prevent double-satisfaction attacks
  • Validate input values (ADA & tokens)
  • Extract input datums
  • Implement secure patterns

Why This Matters

  • ⚠️ Most vulnerabilities = input issues
  • 🚨 Double-satisfaction = critical exploit
  • 💰 Wrong value checks = lost funds
  • 🛡️ Secure inputs = secure contracts
Critical: Input validation is where most smart contract vulnerabilities occur. Master this module to build production-ready validators!
SLIDE 3

Understanding Transaction Inputs

Inputs are the UTxOs being consumed in a transaction.

Input Structure

pub type Input { output_reference: OutputReference, // Unique ID (tx_hash + index) output: Output, // The actual UTxO data } pub type Output { address: Address, // Where it's locked value: Value, // ADA + tokens datum: Datum, // Attached data reference_script: Option<Script>, }

Why Multiple Inputs?

  • User wallet pays fees
  • Multiple UTxOs needed for balance
  • Script input + wallet inputs

Your Validator's Job

  • Find YOUR input
  • Count script inputs
  • Validate values
  • Prevent exploits
SLIDE 4

Finding the Current Input

Use find_input() to locate which UTxO your validator is checking.

Basic Pattern

use cardano/transaction.{find_input, OutputReference, Transaction} validator my_validator { spend( _datum: Option<Data>, _redeemer: Data, own_input_ref: OutputReference, // ← This identifies OUR input self: Transaction, ) -> Bool { // Find our specific input expect Some(own_input) = find_input(self.inputs, own_input_ref) // Now access its properties let our_address = own_input.output.address let our_value = own_input.output.value let our_datum = own_input.output.datum True } }
Key Function: find_input(inputs, ref) returns Option<Input>
SLIDE 5

Filtering Inputs by Address

Filter inputs to find how many are from your script address.

Manual Filtering

use aiken/collection/list fn filter_by_address( inputs: List<Input>, target_addr: Address, ) -> List<Input> { list.filter( inputs, fn(input) { input.output.address == target_addr } ) } // Usage let script_inputs = filter_by_address( self.inputs, our_address )

Using Vodka

use vodka/inputs.{inputs_at} // Much cleaner! let script_inputs = inputs_at( self.inputs, our_address ) // Check count when script_inputs is { [_single] -> True _ -> False } // Or just count let count = list.length(script_inputs)
Vodka Utility: inputs_at(inputs, address) returns List<Input>
SLIDE 6

The Double-Satisfaction Attack

The most dangerous validator vulnerability!

⚠️ CRITICAL VULNERABILITY ⚠️
A validator's logic can be satisfied MULTIPLE times in ONE transaction

What Happens?

💰💰

2 UTxOs
Same Validator

Each requires
5 ADA payment

💸

Attacker Pays
5 ADA ONCE

But validator checks
global outputs

🎉

BOTH Unlock!
Exploit Success

Attacker gets
2x value for 1x cost

SLIDE 7

Double-Satisfaction: Vulnerable Code

❌ VULNERABLE - DO NOT USE

pub type SaleDatum { beneficiary: ByteArray, // Seller's address price: Int, // Required payment } // This validator is EXPLOITABLE! validator vulnerable_sale { spend(datum_opt, _redeemer, _own_ref, self) { expect Some(datum) = datum_opt // Find all outputs to beneficiary let beneficiary_addr = address.from_verification_key(datum.beneficiary) let outputs_to_seller = outputs_at(self.outputs, beneficiary_addr) // Sum up total paid let total_paid = list.foldl( outputs_to_seller, assets.zero, fn(output, total) { assets.merge(output.value, total) } ) // Check if enough was paid lovelace_of(total_paid) >= datum.price // ⚠️ PROBLEM: Global check! } }
The Problem: Validator checks TOTAL outputs, not per-input. Multiple inputs can share the same output!
SLIDE 8

How the Exploit Works

Step-by-step attack scenario:

Setup

Bob locks 2 UTxOs at vulnerable validator:

  • UTxO #1: 20 SCOIN, requires 5 ADA to Bob
  • UTxO #2: 20 XCOIN, requires 10 ADA to Bob

Attack Transaction

Inputs: 1. UTxO #1: 20 SCOIN (validator checks: 5 ADA to Bob?) 2. UTxO #2: 20 XCOIN (validator checks: 10 ADA to Bob?) 3. Alice wallet: 15 ADA Outputs: 1. To Bob: 10 ADA ✓ 2. To Alice: 20 SCOIN + 20 XCOIN ✓ 3. Change: 5 ADA to Alice Result: - Validator runs for UTxO #1: Sees 10 ADA to Bob → PASS ✓ (needs 5) - Validator runs for UTxO #2: Sees SAME 10 ADA → PASS ✓ (needs 10) - Alice paid 10 ADA but got 20 SCOIN + 20 XCOIN!
Both validators see the SAME 10 ADA output and both pass their check!
SLIDE 9

Prevention: Single Script Input

Solution: Ensure only ONE input from your validator per transaction.

✅ SECURE - Use This Pattern

use vodka/inputs.{inputs_at} validator secure_sale { spend(datum_opt, _redeemer, own_ref, self) { expect Some(datum) = datum_opt // CRITICAL: Ensure only ONE script input expect Some(own_input) = find_input(self.inputs, own_ref) let our_address = own_input.output.address let script_inputs = inputs_at(self.inputs, our_address) // Must be exactly 1 expect [_single_input] = script_inputs // OR use pattern matching: // when script_inputs is { // [_] -> // Continue validation // _ -> False // Reject if 0 or 2+ inputs // } // Now safe to check outputs let beneficiary_addr = address.from_verification_key(datum.beneficiary) let outputs_to_seller = outputs_at(self.outputs, beneficiary_addr) let total_paid = /* sum outputs */ lovelace_of(total_paid) >= datum.price // ✓ Safe now! } }
SLIDE 10

Using single_script_input()

Vodka provides a utility that does both checks in one:

Vodka Utility

use vodka/inputs.{single_script_input} // Returns Some(Input) if: // 1. Input with own_ref exists // 2. It's the ONLY input from this script address // Returns None if 0 or 2+ inputs from script validator secure_validator { spend(_datum, _redeemer, own_ref, self) { // One-line double-satisfaction prevention! expect Some(_own_input) = single_script_input(self.inputs, own_ref) // Rest of validation... True } }

What It Checks

  • ✓ Input exists
  • ✓ Only 1 from script
  • ✓ Returns the input

When to Use

  • ✓ EVERY validator!
  • ✓ First check in spend
  • ✓ Before any logic
Best Practice: Start EVERY spend validator with this check!
SLIDE 11

Validating Input Values

Check that inputs contain the expected ADA and tokens.

Lovelace Validation

use cardano/assets.{lovelace_of} // Get our input expect Some(own_input) = find_input(self.inputs, own_ref) // Extract lovelace let input_lovelace = lovelace_of(own_input.output.value) // Validate minimum input_lovelace >= 10_000_000

Token Validation

use cardano/assets.{quantity_of} let policy = #"abc123..." let name = #"MyToken" // Get token quantity let token_qty = quantity_of( own_input.output.value, policy, name ) // Validate amount token_qty >= 100

Combined Example

validator value_checker { spend(_datum, _redeemer, own_ref, self) { expect Some(own_input) = find_input(self.inputs, own_ref) let value = own_input.output.value // Check ADA let has_ada = lovelace_of(value) >= 5_000_000 // Check tokens let has_tokens = quantity_of(value, policy, name) >= 50 // Both must pass has_ada && has_tokens } }
SLIDE 12

Extracting Input Datums

Access inline datums from transaction inputs.

Pattern for Datum Extraction

pub type MyDatum { owner: ByteArray, amount: Int, deadline: Int, } validator datum_validator { spend(_datum, _redeemer, own_ref, self) { // Get our input expect Some(own_input) = find_input(self.inputs, own_ref) // Extract inline datum expect InlineDatum(raw_datum) = own_input.output.datum // Cast to our type expect my_datum: MyDatum = raw_datum // Validate datum fields my_datum.amount >= 1_000_000 && my_datum.deadline > 0 } }
Pattern: InlineDatum(data) → cast to type → validate fields
SLIDE 13

Complete Secure Input Pattern

Combining all techniques for production-ready validators:

✅ Production Template

use vodka/inputs.{single_script_input} use cardano/assets.{lovelace_of, quantity_of} pub type SecureDatum { owner: ByteArray, minimum_lovelace: Int, required_token_policy: ByteArray, required_token_name: ByteArray, required_token_amount: Int, } validator production_ready { spend(datum_opt, _redeemer, own_ref, self) { expect Some(datum) = datum_opt // STEP 1: Prevent double-satisfaction (CRITICAL!) expect Some(own_input) = single_script_input(self.inputs, own_ref) // STEP 2: Validate input value (lovelace) let input_value = own_input.output.value let has_min_ada = lovelace_of(input_value) >= datum.minimum_lovelace // STEP 3: Validate input value (tokens) let token_qty = quantity_of( input_value, datum.required_token_policy, datum.required_token_name ) let has_min_tokens = token_qty >= datum.required_token_amount // STEP 4: Validate datum consistency expect InlineDatum(datum_raw) = own_input.output.datum expect input_datum: SecureDatum = datum_raw expect input_datum.owner == datum.owner // ALL checks must pass has_min_ada && has_min_tokens } }
SLIDE 14

Testing Input Validation

Test all scenarios to ensure your validator is secure:

✅ Single Script Input Tests

Test with exactly 1 script input (should pass)

❌ Multiple Script Input Tests

Test with 2+ script inputs (should fail - double-satisfaction)

✅ Valid Value Tests

Test with sufficient ADA and tokens (should pass)

❌ Insufficient Value Tests

Test with too little ADA or tokens (should fail)

✅ Datum Validation Tests

Test with valid and invalid datum fields

SLIDE 15

Mock Transaction for Testing

Building Mock Transactions

use mocktail.{ mocktail_tx, tx_in, tx_in_inline_datum, complete, mock_script_address, mock_tx_hash, mock_utxo_ref } // Test with single script input (should pass) fn mock_single_input_tx() -> Transaction { mocktail_tx() |> tx_in( True, mock_tx_hash(0), 0, from_lovelace(10_000_000), mock_script_address(0, None) // Our script ) |> tx_in_inline_datum(True, MyDatum { owner: #"abc", amount: 10_000_000 }) |> complete() } // Test with multiple script inputs (should fail) fn mock_double_input_tx() -> Transaction { mocktail_tx() |> tx_in(True, mock_tx_hash(0), 0, from_lovelace(10_000_000), mock_script_address(0, None)) |> tx_in(True, mock_tx_hash(0), 1, from_lovelace(10_000_000), mock_script_address(0, None)) // Same address! |> complete() } test test_passes_single_input() { let tx = mock_single_input_tx() my_validator.spend(Some(datum), Void, mock_utxo_ref(0, 0), tx) } test test_fails_double_input() { let tx = mock_double_input_tx() !my_validator.spend(Some(datum), Void, mock_utxo_ref(0, 0), tx) }
SLIDE 16

Hands-On Exercises

Time to build secure validators! 🔒

Exercise 1: Address Filtering (15 min)

Filter inputs by script address and ensure single input

Exercise 2: Prevent Double-Satisfaction (20 min)

Build payment validator with double-satisfaction prevention

Exercise 3: Value Validation (15 min)

Validate minimum ADA and token requirements

Exercise 4: Datum Validation (20 min)

Extract and validate inline datums from inputs

Exercise 5: Complete Secure Vault (30 min)

Combine ALL techniques into production-ready validator

SLIDE 17

Assignment M004

Build a Secure Token Vault

Requirements

  • Prevent double-satisfaction
  • Validate minimum ADA
  • Validate token quantities
  • Extract input datums
  • Support partial withdrawals
  • Track withdrawal history

Data Types

  • VaultDatum with 5+ fields
  • VaultRedeemer with 2 constructors
  • Owner signature required

Testing (12+ tests)

  • ✅ Single input scenarios
  • ❌ Double-satisfaction tests
  • ✅ Valid value tests
  • ❌ Insufficient value tests
  • ✅ Datum validation tests
  • ✅ Withdrawal scenarios

Submission

  • GitHub repository
  • Test results (all passing)
  • README with explanation
Deadline: Before Module M005
SLIDE 18

Common Issues & Solutions

❌ single_script_input returns None

Check: Input reference matches, address is correct, not multiple inputs

❌ quantity_of returns 0

Verify: Policy ID and asset name are correct hex-encoded ByteArrays

❌ Cannot extract inline datum

Use pattern matching: expect InlineDatum(data) = output.datum

✓ Debug with trace

trace @"Script inputs count" trace list.length(script_inputs)
SLIDE 19

Key Takeaways

You can now:

✅ Understand transaction input structure

✅ Prevent double-satisfaction attacks

✅ Filter inputs by script address

✅ Validate input values (ADA & tokens)

✅ Extract and validate input datums

✅ Implement production-ready input patterns

✅ Write comprehensive input validation tests

Next: Module M005 - Output Validation & State Machines 🔄
SLIDE 20

Critical Security Reminder

🛡️
⚠️ ALWAYS PREVENT DOUBLE-SATISFACTION ⚠️

Your Checklist

✅ Use single_script_input() in every validator
✅ Check it FIRST before any other validation
✅ Test with multiple script inputs (should fail)
✅ Validate input values, not just outputs
✅ Review code for global checks
Remember: Input security = Contract security
🔒

Excellent Work!

Module M004 Complete

You can now build secure validators with proper input validation!

Your contracts are now safe from double-satisfaction 🛡️

See you in M005! 🚀

/ 21