Skip to content

2. Developer Advocate : Coxygen Global Haskell Plutus Introduction 21082025

Bernard Sibanda edited this page Aug 22, 2025 · 1 revision

🌍 Welcome to Cardano World of Web3 Computation

By Bernard Sibanda – Coxygen Global – 21-08-2025

🧭 Table of Contents (for Part 1)

1️⃣ 🎯 Introduction

2️⃣ 🧠 Foundations: Lambda Calculus → Haskell

 2.1 🧮 Lambda Calculus Basics

 2.2 🔀 Currying & Partial Application

 2.3 🔎 Type Constraints and (“such that”) in Haskell

 2.4 🧱 Example: Monoid and Typeclasses (with laws) 

3️⃣ 📐 Category Theory & Functional Abstractions

 3.1 🔗 From Functions to Categories

 3.2 📦 Functors (mapping over structure)

 3.3 ⚡ Applicatives (contextual application)

 3.4 🌀 Monads (sequencing with context)

 3.5 🌐 Cross-Language Analogies (Python, JS, Java, Rust)

4️⃣ 🦀 Rust and Paradigm Blending

 4.1 🛡 Ownership & Borrowing

 4.2 🚫 Why Rust isn’t Purely Functional

 4.3 🔀 Rust’s Blend of Imperative + Functional

5️⃣ 🧪 Verification & Formal Methods

 5.1 🧾 Formal Verification & Property Testing

 5.2 🏛 Advantages / Disadvantages of Typed Lambda Calculus

 5.3 🚫 Why Not Purely Imperative?

6️⃣ 💰 Plutus Deep Dive: English Auction

 6.1 🔄 Auction as a State Machine (Bid, Update, Cancel, Close)

 6.2 📝 ScriptContext & On-chain Invariants

 6.3 🧾 Annotated Source Code (Full Auction Validator)

 6.4 📊 Transaction Traces (Worked Examples)

7️⃣ 🟢 Advantages of Plutus

 7.1 🔀 Currying & Partial Application in Validators

 7.2 🧼 Purity & Determinism (no hidden state)

 7.3 🧩 Modularity & Composability

 7.4 🛡 Type Safety & Strong Guarantees

 7.5 📐 Equational Reasoning & Invariants

8️⃣ 📚 Summary & Key Takeaways

9️⃣ 📖 Glossary

🔟 🔗 Further Reading

1️⃣ 🎯 Introduction

Blockchains demand deterministic, auditable behavior. Cardano answers this with Plutus, a Haskell-based smart-contract platform rooted in the λ-calculus and typed functional programming. To reason rigorously about safety, we’ll build upward from first principles (functions, types, laws) to the on-chain world. Part 1 establishes the mental model you’ll use later when we prove auction-validator properties.

2️⃣ 🧠 Foundations: Lambda Calculus → Haskell

2.1 🧮 Lambda Calculus Basics

Core syntax

  • Variable: x
  • Abstraction: λx. E (function taking x, returning expression E)
  • Application: (f x) (apply f to x)

Beta-reduction (substitution)

  • (λx. E) v ⟶β E[x := v]

Worked reduction

(λx. x + 1) 5
⟶β  5 + 1
⟶   6

Haskell correspondence

-- λx. x + 1   ↔   \x -> x + 1
(\x -> x + 1) 5  -- 6

Multiple arguments are encoded by nesting abstractions

λx. λy. x + y         -- takes x, returns a function taking y

Haskell:

\x -> \y -> x + y

2.2 🔀 Currying & Partial Application

Currying: every function has one argument; multi-arg functions are chains:

add :: Int -> Int -> Int
-- really: add :: Int -> (Int -> Int)
add x y = x + y

Because of currying, you can partially apply:

inc :: Int -> Int
inc = add 1        -- fix the first argument

inc 5  -- 6

Why this matters later (Plutus validators): On-chain validators conceptually have the shape:

validator :: Datum -> Redeemer -> ScriptContext -> Bool
-- i.e., Datum -> (Redeemer -> (ScriptContext -> Bool))

Curried form makes it natural to thread context step-by-step and to test predicates modularly.

2.3 🔎 Type Constraints and (“such that”) in Haskell

You’ll frequently see signatures with a constraint to the left of =>:

(+) :: Num a => a -> a -> a

Read this aloud as:

(+) has type a -> a -> a, such that a is an instance of Num.”

Two common pieces of syntax you’ll meet:

  • C a => τconstraint C a must hold (e.g., Num a, Eq a, Show a)

  • forall a.universal quantification (often implicit):

    id :: forall a. a -> a

Multiple constraints

f :: (Eq a, Show a) => a -> String
-- “for all a, such that a is Eq and Show, f takes an a and returns a String”

2.4 🧱 Example: Monoid and Typeclasses (with laws)

A typeclass is a set of operations + laws. Monoid extends Semigroup:

class Semigroup a => Monoid a where
  mempty  :: a                 -- identity
  mappend :: a -> a -> a       -- associative combine
  -- mconcat has a default in terms of mappend
  mconcat :: [a] -> a
  mconcat = foldr mappend mempty

Laws you must respect when writing instances

  • Left identity: mappend mempty x == x
  • Right identity: mappend x mempty == x
  • Associativity: mappend x (mappend y z) == mappend (mappend x y) z

Canonical instance: lists

instance Semigroup [a] where
  (<>) = (++)            -- reuse list append

instance Monoid [a] where
  mempty  = []
  mappend = (++)

Check a law (by hand)

mappend mempty [1,2] == (++) [] [1,2] == [1,2]     -- Left identity ✓

This habit—stating operations, then laws, then a concrete instance—is exactly what we’ll do on-chain for validator invariants (e.g., “lot remains at script”, “refund is paid”, “single continuing output”).

✅ Quick Self-Check (answers inline)

  1. Read aloud the signature

    length :: Foldable t => t a -> Int

    → “length takes a t a and returns an Int, such that t is Foldable.”

  2. Explain why add 2 is a function. → Because add :: Int -> (Int -> Int). Supplying the first Int yields a new function Int -> Int.

  3. Name the three Monoid laws. → Left identity, Right identity, Associativity.

3️⃣ 📐 Category Theory & Functional Abstractions

Functional programming borrows heavily from category theory, the mathematics of objects + arrows (morphisms).

  • Objects → types (Int, String, [a], Maybe a)
  • Arrows → functions (f :: A -> B)

At the heart:

  • Composition (∘) – combining arrows
  • Identity – each object has an identity morphism

These ideas enforce structure: every computation must respect associativity and identity. This is the same rigor we’ll later use to argue about Plutus contract invariants.

3.1 🔗 From Functions to Categories

In Haskell:

(.)   :: (b -> c) -> (a -> b) -> (a -> c)
id    :: a -> a
  • Composition (.) is associative.
  • id acts as left/right identity.

This mirrors the category axioms.

3.2 📦 Functors (mapping over structure)

A Functor lifts functions into contexts.

Typeclass:

class Functor f where
  fmap :: (a -> b) -> f a -> f b

Example: List

fmap (+1) [1,2,3]  -- [2,3,4]

Example: Maybe

fmap (+1) (Just 5) -- Just 6
fmap (+1) Nothing  -- Nothing

Laws:

  • Identity: fmap id == id
  • Composition: fmap (f . g) == fmap f . fmap g

👉 In Plutus, datatypes like Maybe or Either are common when dealing with optional values (Datum, Redeemer). Functors let you transform these safely.

3.3 ⚡ Applicatives (contextual application)

Functors let you map one-argument functions. But what if your function has multiple args?

Applicative generalizes this:

class Functor f => Applicative f where
  pure  :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

Examples:

pure (+) <*> Just 3 <*> Just 4   -- Just 7
pure (+) <*> Nothing <*> Just 4  -- Nothing

Applicatives let us combine independent computations in a context.

👉 On-chain, you often combine multiple validations (mustPayToPubKey, mustValidateIn). Applicatives describe such independent constraints.

3.4 🌀 Monads (sequencing with context)

Monads allow sequential composition, where later steps depend on earlier results.

Typeclass:

class Applicative m => Monad m where
  (>>=) :: m a -> (a -> m b) -> m b

Maybe Monad Example:

Just 3 >>= (\x -> Just (x+2))  -- Just 5
Nothing >>= (\x -> Just (x+2)) -- Nothing

Do-notation:

do
  x <- Just 3
  y <- Just 5
  pure (x + y)   -- Just 8

Laws:

  • Left identity: return a >>= k == k a
  • Right identity: m >>= return == m
  • Associativity: (m >>= k) >>= h == m >>= (\x -> k x >>= h)

👉 In Plutus off-chain code, the Contract monad is used to sequence blockchain operations like submitting transactions, waiting for slots, etc.

3.5 🌐 Cross-Language Analogies

Let’s bridge Haskell abstractions with familiar constructs:

  • Python

    • Functor ~ map() on lists.
    • Monad ~ chaining generators or async coroutines.
  • JavaScript

    • Functor ~ array.map(f).
    • Monad ~ Promises (.then) handle sequencing with context.
  • Java

    • Functor ~ Optional.map().
    • Monad ~ Optional.flatMap() sequences nullable operations.
  • Rust

    • Functor ~ Option::map, Result::map.
    • Monad ~ Option::and_then, Result::and_then.

Rust Example:

fn safe_div(x: i32, y: i32) -> Option<i32> {
    if y == 0 { None } else { Some(x / y) }
}

let res = Some(10)
    .and_then(|n| safe_div(n, 2))  // Some(5)
    .and_then(|n| safe_div(n, 0)); // None

👉 Exactly monadic sequencing!

✅ Quick Self-Check

  1. State the Functor laws. → Identity and Composition.

  2. Why do Applicatives exist if we have Monads? → Applicatives describe independent effects, while Monads allow dependent sequencing.

  3. Which JavaScript structure behaves like a Monad?Promise (via .then). Excellent ⚡ Let’s move to Part 3, keeping the same icon-rich, rigorous, cross-language style.

4️⃣ 🦀 Rust and Paradigm Blending

Rust is often called a multi-paradigm language:

  • It supports functional-style abstractions (map, filter, closures, immutability by default).
  • But it also emphasizes systems-level control (manual memory layout, zero-cost abstractions).

This duality makes Rust perfect to study alongside Haskell—it shows how functional purity can meet real-world constraints.

4.1 🛡 Ownership & Borrowing

At Rust’s core is its ownership system, which prevents memory errors at compile-time.

Rules of ownership:

  1. Each value has one owner.

  2. At any time, you can have either:

    • One mutable reference, or
    • Any number of immutable references.
  3. Values are freed when the owner goes out of scope.

Example:

fn main() {
    let s = String::from("hello");
    takes_ownership(s);   // moves ownership
    // println!("{}", s); // ❌ error: value moved
}

fn takes_ownership(x: String) {
    println!("{}", x);
}

Borrowing (references):

fn main() {
    let s = String::from("cardano");
    let len = calculate_length(&s);  // borrow with `&`
    println!("'{}' has length {}", s, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

💡 Ownership and borrowing enforce safety without a GC, a very different philosophy from Haskell’s runtime-managed memory.

4.2 🚫 Why Rust isn’t Purely Functional

Rust has functional features, but it’s not purely functional like Haskell:

  • Side effects allowed: Functions can mutate state, perform I/O freely.

  • No global purity guarantee: Functions are not pure by default.

  • Mutable variables exist:

    let mut x = 10;
    x = x + 5; // ok

Contrast with Haskell:

-- x is immutable; no reassignment
let x = 10
let y = x + 5

Takeaway: Rust allows controlled mutation when performance or clarity demands it. Haskell enforces purity everywhere and isolates side effects in monads like IO.

4.3 🔀 Rust’s Blend of Imperative + Functional

Rust developers often mix styles depending on problem domain:

Functional style (map, filter, fold):

let nums = vec![1, 2, 3, 4];
let squares: Vec<i32> = nums.iter().map(|x| x * x).collect();
// [1,4,9,16]

Imperative style (loops, mutation):

let mut sum = 0;
for i in 1..5 {
    sum += i;
}
// sum = 10

Option/Result as Monads:

fn safe_div(x: i32, y: i32) -> Option<i32> {
    if y == 0 { None } else { Some(x / y) }
}

let res = Some(10)
    .and_then(|n| safe_div(n, 2))   // Some(5)
    .and_then(|n| safe_div(n, 0));  // None

Here .and_then is flatMap / bind: a monadic operation.

🔎 Why This Matters for Plutus

  • Rust: pragmatism, performance, ownership rules ensure safety.
  • Haskell/Plutus: purity, type-level guarantees, equational reasoning.

👉 Together they show two design philosophies:

  • Rust: “Safety without garbage collection.”
  • Haskell: “Mathematical purity with strong static typing.”

Plutus inherits Haskell’s purity—contracts are mathematically verifiable. Rust shows the pragmatic compromises needed for system-level work.

✅ Quick Self-Check

  1. What are the three ownership rules in Rust? → One owner, either many immutable refs or one mutable ref, drop when scope ends.

  2. Give one reason Rust is not purely functional. → It allows side effects and mutable variables.

  3. Name a Rust type that behaves monadically.Option and Result.

5️⃣ 🧪 Verification & Formal Methods

Smart contracts are immutable programs with financial consequences. A bug isn’t just inconvenient—it can lock or lose millions. This is why verification (mathematically proving correctness) matters.

Haskell and Plutus are uniquely strong here because of their mathematical foundations in lambda calculus and type theory.

5.1 🧾 Formal Verification & Property Testing

Verification spectrum:

  • Unit tests: Example-based (e.g., does add 2 3 == 5?).

  • Property-based testing: Instead of examples, you specify laws and test with many random values.

    • Library: QuickCheck in Haskell.
    prop_reverse :: [Int] -> Bool
    prop_reverse xs = reverse (reverse xs) == xs
  • Formal verification: Use logic + theorem provers to prove correctness for all inputs.

In blockchain:

  • Property testing ensures that validators hold under many random tx inputs.
  • Formal verification encodes invariants as theorems (e.g., “Auction cannot close unless deadline passed”).

Why Haskell helps:

  • Purity = easier reasoning (no hidden side effects).
  • Strong static types = many bugs impossible to compile.
  • Algebraic data types = natural encoding of states.

5.2 🏛 Advantages / Disadvantages of Typed Lambda Calculus

Typed lambda calculus underpins Haskell. Let’s weigh its role:

Advantages ✅

  • Safety: Strong types catch errors early.
  • Equational reasoning: Programs behave like math; you can rewrite functions safely.
  • Modularity: Currying + higher-order functions = composable programs.
  • Formal verification: Types as proofs (Curry–Howard isomorphism: types ↔ propositions).

Disadvantages ⚠

  • Steeper learning curve: Types, categories, monads can intimidate beginners.
  • Boilerplate for some cases: Sometimes extra type wrangling is needed.
  • Performance trade-offs: Lazy evaluation may complicate predictability.
  • Interfacing with the real world: IO must be wrapped in monads, which can be unfamiliar.

👉 In Plutus, the advantages dominate: safety and verifiability are worth the complexity.

5.3 🚫 Why Not Purely Imperative?

Imperative programming (C, Python loops, Java OOP) is powerful but unsuitable for critical financial logic when used alone:

Weaknesses of imperative-only approaches:

  • Hidden state: Harder to reason about correctness.
  • Side effects: Functions may mutate global state, complicating verification.
  • Testing gaps: Example-based testing can miss edge cases.

Comparison:

  • Imperative: “Do these steps in order.”
  • Functional: “Define this relationship mathematically.”

For smart contracts:

  • Functional (Plutus) = verifiable invariants, deterministic behavior.
  • Imperative (Solidity-like) = higher risk of reentrancy, unexpected side effects.

🔎 Why This Matters for Plutus

  • Plutus inherits Haskell’s type-theoretic strength → validators can be checked for invariants.
  • Formal methods are natural → you can encode proofs about contract states.
  • Imperative pitfalls avoided → no reentrancy bugs (like in Ethereum’s DAO hack).

✅ Quick Self-Check

  1. What is property-based testing? → Testing by asserting laws hold for many randomly generated inputs.

  2. Name one advantage of typed λ-calculus. → Equational reasoning / types as proofs / safety guarantees.

  3. Why are purely imperative languages less suitable for contracts? → They rely on hidden state and side effects, making reasoning and verification harder.

Excellent 🎉 Now we get to the heart of the tutorial — the Plutus Deep Dive. This is where all the functional foundations come together in a real on-chain English Auction validator.

6️⃣ 💰 Plutus Deep Dive: English Auction

The English Auction contract is a canonical Plutus example:

  • A seller lists an asset.
  • Buyers place bids.
  • Highest bidder wins if deadline passes.
  • Seller can cancel if no bids.

👉 This is naturally modeled as a state machine, where transitions are encoded in the validator.

6.1 🔄 Auction as a State Machine

States:

  • Active → Auction running, waiting for bids.
  • Closed → Auction ended, either by timeout or manual closure.

Actions (Redeemers):

  • Bid { newBid :: Integer } → Place a new bid higher than current.
  • Update → Adjust auction parameters (e.g., extend deadline).
  • Cancel → Seller cancels if no bids.
  • Close → End auction, transfer asset to highest bidder.

State transition diagram:

[Active Auction]
   |  Bid
   v
[Active Auction with higher bid]
   |  Close (deadline passed)
   v
[Closed Auction]
   |  Cancel (no bids)
   v
[Closed Auction]

6.2 📝 ScriptContext & On-chain Invariants

The validator receives:

  • Datum → Auction state (seller, deadline, highestBid, highestBidder).
  • Redeemer → Action requested (Bid, Update, Cancel, Close).
  • ScriptContext → Full transaction info (inputs, outputs, signatories).

Invariants (must always hold):

  1. Bids increase monotonically: Each new bid must be > previous highest.

  2. Collateral preserved: Previous bidder refunded when outbid.

  3. Deadline respected: Auction cannot be closed before deadline.

  4. Single continuing output: Auction UTxO updated with new datum.

  5. Cancel only valid if no bids.

  6. Close distributes funds correctly:

    • Asset → highest bidder.
    • Bid amount → seller.

👉 These invariants are like Monoid laws for smart contracts—always true, regardless of inputs.

6.3 🧾 Annotated Source Code (Full Auction Validator)

Here is a Plutus V2 English Auction validator, annotated line by line.

{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE DeriveAnyClass             #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE DerivingStrategies         #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE NoImplicitPrelude          #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE RecordWildCards            #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeApplications           #-}
{-# LANGUAGE TypeFamilies               #-}
{-# LANGUAGE TypeOperators              #-}

{-# OPTIONS_GHC -fno-warn-unused-imports #-}

module Week01.EnglishAuction where

import           Control.Monad        hiding (fmap)
import           Data.Aeson           (ToJSON, FromJSON)
import           Data.List.NonEmpty   (NonEmpty (..))
import           Data.Map             as Map
import           Data.Text            (pack, Text)
import           GHC.Generics         (Generic)
import           Plutus.Contract
import qualified PlutusTx             as PlutusTx
import           PlutusTx.Prelude     hiding (Semigroup(..), unless)
import qualified PlutusTx.Prelude     as Plutus
import           Ledger               hiding (singleton)
import           Ledger.Constraints   as Constraints
import qualified Ledger.Scripts       as Scripts
import qualified Ledger.Typed.Scripts as Scripts hiding (validatorHash)
import           Ledger.Value         as Value
import           Ledger.Ada           as Ada
import           Playground.Contract  (ensureKnownCurrencies, printSchemas, stage, printJson)
import           Playground.TH        (mkKnownCurrencies, mkSchemaDefinitions)
import           Playground.Types     (KnownCurrency (..))
import           Prelude              (IO, Semigroup (..), Show (..), String)
import           Schema               (ToSchema)
import           Text.Printf          (printf)

data Auction = Auction
    { aSeller   :: !PubKeyHash
    , aDeadline :: !POSIXTime
    , aMinBid   :: !Integer
    , aCurrency :: !CurrencySymbol
    , aToken    :: !TokenName
    } deriving (Show, Generic, ToJSON, FromJSON, ToSchema)

instance Eq Auction where
    {-# INLINABLE (==) #-}
    a == b = (aSeller   a == aSeller   b) &&
             (aDeadline a == aDeadline b) &&
             (aMinBid   a == aMinBid   b) &&
             (aCurrency a == aCurrency b) &&
             (aToken    a == aToken    b)

PlutusTx.unstableMakeIsData ''Auction
PlutusTx.makeLift ''Auction

data Bid = Bid
    { bBidder :: !PubKeyHash
    , bBid    :: !Integer
    } deriving Show

instance Eq Bid where
    {-# INLINABLE (==) #-}
    b == c = (bBidder b == bBidder c) &&
             (bBid    b == bBid    c)

PlutusTx.unstableMakeIsData ''Bid
PlutusTx.makeLift ''Bid

data AuctionAction = MkBid Bid | Close
    deriving Show

PlutusTx.unstableMakeIsData ''AuctionAction
PlutusTx.makeLift ''AuctionAction

data AuctionDatum = AuctionDatum
    { adAuction    :: !Auction
    , adHighestBid :: !(Maybe Bid)
    } deriving Show

PlutusTx.unstableMakeIsData ''AuctionDatum
PlutusTx.makeLift ''AuctionDatum

data Auctioning
instance Scripts.ValidatorTypes Auctioning where
    type instance RedeemerType Auctioning = AuctionAction
    type instance DatumType Auctioning = AuctionDatum

{-# INLINABLE minBid #-}
minBid :: AuctionDatum -> Integer
minBid AuctionDatum{..} = case adHighestBid of
    Nothing      -> aMinBid adAuction
    Just Bid{..} -> bBid + 1

{-# INLINABLE mkAuctionValidator #-}
mkAuctionValidator :: AuctionDatum -> AuctionAction -> ScriptContext -> Bool
mkAuctionValidator ad redeemer ctx =
    traceIfFalse "wrong input value" correctInputValue &&
    case redeemer of
        MkBid b@Bid{..} ->
            traceIfFalse "bid too low" (sufficientBid bBid)                &&
            traceIfFalse "wrong output datum" (correctBidOutputDatum b)    &&
            traceIfFalse "wrong output value" (correctBidOutputValue bBid) &&
            traceIfFalse "wrong refund"       correctBidRefund             &&
            traceIfFalse "too late"           correctBidSlotRange
        Close           ->
            traceIfFalse "too early" correctCloseSlotRange &&
            case adHighestBid ad of
                Nothing      ->
                    traceIfFalse "expected seller to get token" (getsValue (aSeller auction) tokenValue)
                Just Bid{..} ->
                    traceIfFalse "expected highest bidder to get token" (getsValue bBidder tokenValue) &&
                    traceIfFalse "expected seller to get highest bid" (getsValue (aSeller auction) $ Ada.lovelaceValueOf bBid)

  where
    info :: TxInfo
    info = scriptContextTxInfo ctx

    input :: TxInInfo
    input =
      let
        isScriptInput i = case (txOutDatumHash . txInInfoResolved) i of
            Nothing -> False
            Just _  -> True
        xs = [i | i <- txInfoInputs info, isScriptInput i]
      in
        case xs of
            [i] -> i
            _   -> traceError "expected exactly one script input"

    inVal :: Value
    inVal = txOutValue . txInInfoResolved $ input

    auction :: Auction
    auction = adAuction ad

    tokenValue :: Value
    tokenValue = Value.singleton (aCurrency auction) (aToken auction) 1

    correctInputValue :: Bool
    correctInputValue = inVal == case adHighestBid ad of
        Nothing      -> tokenValue
        Just Bid{..} -> tokenValue Plutus.<> Ada.lovelaceValueOf bBid

    sufficientBid :: Integer -> Bool
    sufficientBid amount = amount >= minBid ad

    ownOutput   :: TxOut
    outputDatum :: AuctionDatum
    (ownOutput, outputDatum) = case getContinuingOutputs ctx of
        [o] -> case txOutDatumHash o of
            Nothing   -> traceError "wrong output type"
            Just h -> case findDatum h info of
                Nothing        -> traceError "datum not found"
                Just (Datum d) ->  case PlutusTx.fromData d of
                    Just ad' -> (o, ad')
                    Nothing  -> traceError "error decoding data"
        _   -> traceError "expected exactly one continuing output"

    correctBidOutputDatum :: Bid -> Bool
    correctBidOutputDatum b = (adAuction outputDatum == auction)   &&
                              (adHighestBid outputDatum == Just b)

    correctBidOutputValue :: Integer -> Bool
    correctBidOutputValue amount =
        txOutValue ownOutput == tokenValue Plutus.<> Ada.lovelaceValueOf amount

    correctBidRefund :: Bool
    correctBidRefund = case adHighestBid ad of
        Nothing      -> True
        Just Bid{..} ->
          let
            os = [ o
                 | o <- txInfoOutputs info
                 , txOutAddress o == pubKeyHashAddress bBidder
                 ]
          in
            case os of
                [o] -> txOutValue o == Ada.lovelaceValueOf bBid
                _   -> traceError "expected exactly one refund output"

    correctBidSlotRange :: Bool
    correctBidSlotRange = to (aDeadline auction) `contains` txInfoValidRange info

    correctCloseSlotRange :: Bool
    correctCloseSlotRange = from (aDeadline auction) `contains` txInfoValidRange info

    getsValue :: PubKeyHash -> Value -> Bool
    getsValue h v =
      let
        [o] = [ o'
              | o' <- txInfoOutputs info
              , txOutValue o' == v
              ]
      in
        txOutAddress o == pubKeyHashAddress h

auctionTypedValidator :: Scripts.TypedValidator Auctioning
auctionTypedValidator = Scripts.mkTypedValidator @Auctioning
    $$(PlutusTx.compile [|| mkAuctionValidator ||])
    $$(PlutusTx.compile [|| wrap ||])
  where
    wrap = Scripts.wrapValidator

auctionValidator :: Validator
auctionValidator = Scripts.validatorScript auctionTypedValidator

auctionAddress :: Ledger.ValidatorHash
auctionAddress = Scripts.validatorHash auctionValidator

data StartParams = StartParams
    { spDeadline :: !POSIXTime
    , spMinBid   :: !Integer
    , spCurrency :: !CurrencySymbol
    , spToken    :: !TokenName
    } deriving (Generic, ToJSON, FromJSON, ToSchema)

data BidParams = BidParams
    { bpCurrency :: !CurrencySymbol
    , bpToken    :: !TokenName
    , bpBid      :: !Integer
    } deriving (Generic, ToJSON, FromJSON, ToSchema)

data CloseParams = CloseParams
    { cpCurrency :: !CurrencySymbol
    , cpToken    :: !TokenName
    } deriving (Generic, ToJSON, FromJSON, ToSchema)

type AuctionSchema =
            Endpoint "start" StartParams
        .\/ Endpoint "bid"   BidParams
        .\/ Endpoint "close" CloseParams

start :: AsContractError e => StartParams -> Contract w s e ()
start StartParams{..} = do
    pkh <- pubKeyHash <$> ownPubKey
    let a = Auction
                { aSeller   = pkh
                , aDeadline = spDeadline
                , aMinBid   = spMinBid
                , aCurrency = spCurrency
                , aToken    = spToken
                }
        d = AuctionDatum
                { adAuction    = a
                , adHighestBid = Nothing
                }
        v = Value.singleton spCurrency spToken 1
        tx = mustPayToTheScript d v
    ledgerTx <- submitTxConstraints auctionTypedValidator tx
    void $ awaitTxConfirmed $ txId ledgerTx
    logInfo @String $ printf "started auction %s for token %s" (show a) (show v)

bid :: forall w s. BidParams -> Contract w s Text ()
bid BidParams{..} = do
    (oref, o, d@AuctionDatum{..}) <- findAuction bpCurrency bpToken
    logInfo @String $ printf "found auction utxo with datum %s" (show d)

    when (bpBid < minBid d) $
        throwError $ pack $ printf "bid lower than minimal bid %d" (minBid d)
    pkh <- pubKeyHash <$> ownPubKey
    let b  = Bid {bBidder = pkh, bBid = bpBid}
        d' = d {adHighestBid = Just b}
        v  = Value.singleton bpCurrency bpToken 1 <> Ada.lovelaceValueOf bpBid
        r  = Redeemer $ PlutusTx.toData $ MkBid b

        lookups = Constraints.typedValidatorLookups auctionTypedValidator <>
                  Constraints.otherScript auctionValidator                <>
                  Constraints.unspentOutputs (Map.singleton oref o)
        tx      = case adHighestBid of
                    Nothing      -> mustPayToTheScript d' v                            <>
                                    mustValidateIn (to $ aDeadline adAuction)          <>
                                    mustSpendScriptOutput oref r
                    Just Bid{..} -> mustPayToTheScript d' v                            <>
                                    mustPayToPubKey bBidder (Ada.lovelaceValueOf bBid) <>
                                    mustValidateIn (to $ aDeadline adAuction)          <>
                                    mustSpendScriptOutput oref r
    ledgerTx <- submitTxConstraintsWith lookups tx
    void $ awaitTxConfirmed $ txId ledgerTx
    logInfo @String $ printf "made bid of %d lovelace in auction %s for token (%s, %s)"
        bpBid
        (show adAuction)
        (show bpCurrency)
        (show bpToken)

close :: forall w s. CloseParams -> Contract w s Text ()
close CloseParams{..} = do
    (oref, o, d@AuctionDatum{..}) <- findAuction cpCurrency cpToken
    logInfo @String $ printf "found auction utxo with datum %s" (show d)

    let t      = Value.singleton cpCurrency cpToken 1
        r      = Redeemer $ PlutusTx.toData Close
        seller = aSeller adAuction

        lookups = Constraints.typedValidatorLookups auctionTypedValidator <>
                  Constraints.otherScript auctionValidator                <>
                  Constraints.unspentOutputs (Map.singleton oref o)
        tx      = case adHighestBid of
                    Nothing      -> mustPayToPubKey seller t                          <>
                                    mustValidateIn (from $ aDeadline adAuction)       <>
                                    mustSpendScriptOutput oref r
                    Just Bid{..} -> mustPayToPubKey bBidder t                         <>
                                    mustPayToPubKey seller (Ada.lovelaceValueOf bBid) <>
                                    mustValidateIn (from $ aDeadline adAuction)       <>
                                    mustSpendScriptOutput oref r
    ledgerTx <- submitTxConstraintsWith lookups tx
    void $ awaitTxConfirmed $ txId ledgerTx
    logInfo @String $ printf "closed auction %s for token (%s, %s)"
        (show adAuction)
        (show cpCurrency)
        (show cpToken)

findAuction :: CurrencySymbol -> TokenName -> Contract w s Text (TxOutRef, TxOutTx, AuctionDatum)
findAuction cs tn = do
    utxos <- utxoAt $ scriptAddress auctionValidator
    let xs = [ (oref, o)
             | (oref, o) <- Map.toList utxos
             , Value.valueOf (txOutValue $ txOutTxOut o) cs tn == 1
             ]
    case xs of
        [(oref, o)] -> case txOutDatumHash $ txOutTxOut o of
            Nothing   -> throwError "unexpected out type"
            Just h -> case Map.lookup h $ txData $ txOutTxTx o of
                Nothing        -> throwError "datum not found"
                Just (Datum e) -> case PlutusTx.fromData e of
                    Nothing -> throwError "datum has wrong type"
                    Just d@AuctionDatum{..}
                        | aCurrency adAuction == cs && aToken adAuction == tn -> return (oref, o, d)
                        | otherwise                                           -> throwError "auction token missmatch"
        _           -> throwError "auction utxo not found"

endpoints :: Contract () AuctionSchema Text ()
endpoints = (start' `select` bid' `select` close') >> endpoints
  where
    start' = endpoint @"start" >>= start
    bid'   = endpoint @"bid"   >>= bid
    close' = endpoint @"close" >>= close

mkSchemaDefinitions ''AuctionSchema

myToken1 :: KnownCurrency
myToken1 = KnownCurrency (ValidatorHash "f1") "Token" (TokenName "T1" :| [])

myToken2 :: KnownCurrency
myToken2 = KnownCurrency (ValidatorHash "f2") "Token" (TokenName "T2" :| [])

myToken3 :: KnownCurrency
myToken3 = KnownCurrency (ValidatorHash "f3") "Token" (TokenName "T3" :| [])

mkKnownCurrencies ['myToken1,'myToken2,'myToken3]

💡 Note: The helper functions (refundOldBidder, paySeller, etc.) must be implemented with specific TxInfo checks. We’ve abstracted them for clarity.

6.4 📊 Transaction Traces (Worked Examples)

Let’s walk through two traces.

Trace A – New Bid

  1. Auction running, highest bid = 10 ADA (Alice).

  2. Bob bids 15 ADA.

  3. Validator checks:

    • 15 > 10
    • Refund Alice ✅
    • Create continuing UTxO with datum {highestBid=15, highestBidder=Bob}
  4. Auction remains Active.

Trace B – Closing Auction

  1. Auction deadline passed.

  2. Charlie (highest bidder at 20 ADA).

  3. Close transaction must:

    • Pay seller 20 ADA. ✅
    • Transfer asset to Charlie. ✅
    • Remove auction UTxO. ✅
  4. Auction moves to Closed state.

👉 Each trace respects invariants and shows how the state machine transitions deterministically.

✅ Quick Self-Check

  1. What are the 4 main actions (redeemers) of the auction?Bid, Update, Cancel, Close.

  2. What invariant must hold for new bids? → Each new bid must be strictly greater than the previous.

  3. When can the seller cancel the auction? → Only if no bids have been placed.

7️⃣ 🟢 Advantages of Plutus

Plutus is not just Haskell on-chain—it is a carefully restricted subset designed for predictable, verifiable, deterministic computation. Let’s map the functional theory we studied to practical on-chain advantages.

7.1 🔀 Currying & Partial Application in Validators

  • Every validator is a curried function:

    validator :: Datum -> Redeemer -> ScriptContext -> Bool

    which really means:

    Datum -> (Redeemer -> (ScriptContext -> Bool))
  • This allows:

    • Step-by-step evaluation (Datum applied first, Redeemer later, Context last).
    • Modular testing (you can test validator behavior with just Datum+Redeemer, leaving Context for later).
    • Composability: You can build smaller validators and stitch them together.

👉 This is exactly what we saw with add being curried in Part 1.

7.2 🧼 Purity & Determinism (no hidden state)

  • Plutus inherits Haskell’s purity: functions always return the same result for the same input.
  • No global variables, no mutation, no hidden IO.
  • This means a validator’s outcome is deterministic across all nodes → consensus is possible.

Contrast: In Solidity, side effects (reentrancy, mutable state) have caused catastrophic failures (e.g., the DAO hack).

7.3 🧩 Modularity & Composability

  • Small validators can be composed into larger contracts.
  • Example: mustPayToPubKey + mustValidateIn can be combined applicatively.
  • Using Monoids and Monads, constraints are combined algebraically, not by writing spaghetti imperative logic.

This composability is what lets you write complex auction logic out of small, testable parts.

7.4 🛡 Type Safety & Strong Guarantees

  • The type system encodes invariants at compile-time.

  • Example:

    • Integer vs POSIXTime → cannot be mixed accidentally.
    • Maybe Datum forces you to handle missing cases.
  • This prevents entire classes of runtime errors.

💡 Remember Num a => a -> a -> a from Part 1? Those constraints carry over—your functions can only operate where it is mathematically safe.

7.5 📐 Equational Reasoning & Invariants

  • Because Plutus contracts are pure, you can reason about them like algebra:

    • Simplify expressions.
    • Prove that invariants (e.g., “bids strictly increase”) hold.
  • Invariants behave like Monoid laws (must hold for all states).

  • Formal verification frameworks can hook directly into these pure definitions.

👉 This is what makes Cardano contracts auditable by math instead of just by testing.

✅ Quick Self-Check

  1. Why does Plutus require purity? → To ensure determinism across nodes → consensus.

  2. What advantage does currying give to validators? → Enables partial application and modular testing.

  3. What does type safety prevent? → Mixing incompatible domains (like Integer and POSIXTime), avoiding runtime errors.

8️⃣ 📚 Summary & Key Takeaways

Over this journey we’ve moved:

  • Part 1 🧠 From Lambda Calculus to Haskell foundations:

    • Functions as first-class, currying, typeclasses, Monoid laws.
  • Part 2 📐 Category Theory abstractions:

    • Functors (map), Applicatives (independent contexts), Monads (sequencing with context).
    • Cross-language analogies (Python, JS, Java, Rust).
  • Part 3 🦀 Rust’s pragmatism:

    • Ownership, borrowing, not purely functional, blending imperative + functional.
  • Part 4 🧪 Verification & formal methods:

    • Property-based testing, typed λ-calculus, why imperative-only is risky.
  • Part 5 💰 Plutus Deep Dive:

    • English Auction contract, redeemers (Bid, Update, Cancel, Close), state machine invariants, transaction traces.
  • Part 6 🟢 Advantages of Plutus:

    • Currying in validators, purity, modularity, type safety, equational reasoning.

✨ Key Takeaways

  1. Plutus is Haskell-on-chain: Built from λ-calculus + type theory, inheriting purity and rigor.
  2. Contracts = State Machines: Auction contract shows transitions driven by redeemers.
  3. Invariants matter: Like Monoid laws, they guarantee safety across all states.
  4. Purity enables verification: Determinism + no hidden state → math can audit contracts.
  5. Type safety saves money: Prevents runtime mistakes before deployment.

👉 Plutus is not just another smart contract language. It’s a mathematically verifiable system for financial logic in Web3.

9️⃣ 📖 Glossary

  • λ-calculus: A formal system where all computation is function abstraction and application.
  • Currying: Representing multi-argument functions as single-argument functions returning functions.
  • Partial application: Supplying fewer arguments than expected, yielding a new function.
  • Typeclass: Interface defining operations (e.g., Eq, Monoid) that types can implement.
  • Monoid: Type with associative combine (mappend) and identity (mempty).
  • Functor: Allows mapping a function over a structure (fmap).
  • Applicative: Allows applying multi-arg functions in a context (<*>).
  • Monad: Allows sequencing computations with context (>>=).
  • Equational reasoning: Rewriting programs algebraically to prove properties.
  • Property-based testing: Validating laws hold for many random inputs.
  • Formal verification: Proving correctness mathematically for all inputs.
  • Datum: On-chain data attached to a UTxO (state).
  • Redeemer: Action requested when spending a UTxO.
  • ScriptContext: Full transaction info passed to a validator.
  • Validator: A pure function (Datum -> Redeemer -> ScriptContext -> Bool) deciding if spending is allowed.
  • UTxO: Unspent Transaction Output — Cardano’s accounting model.

🔟 🔗 Further Reading

  • 📘 Introduction to Functional Programming with Haskell – Graham Hutton
  • 📘 Category Theory for Programmers – Bartosz Milewski
  • 📘 Learn You a Haskell for Great Good! – Miran Lipovača
  • 📘 Programming in Haskell – Graham Hutton
  • 📘 Real World Haskell – Bryan O’Sullivan et al.
  • 🌐 Plutus Pioneer Program (IOG)
  • 🌐 Cardano Docs: https://docs.cardano.org
  • 🌐 PlutusTx GitHub: https://github.com/input-output-hk/plutus

✅ Closing Note

You’ve now walked through:

  • Theory (λ-calculus, categories, verification).
  • Practice (Plutus Auction).
  • Advantages (purity, modularity, type safety).

The big picture: Cardano smart contracts are Web3 computation rooted in mathematics. This foundation makes them auditable, reliable, and safe for real financial logic.

Clone this wiki locally