-
Notifications
You must be signed in to change notification settings - Fork 4
2. Developer Advocate : Coxygen Global Haskell Plutus Introduction 21082025
By Bernard Sibanda – Coxygen Global – 21-08-2025
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
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.
Core syntax
-
Variable:
x -
Abstraction:
λx. E(function takingx, returning expressionE) -
Application:
(f x)(applyftox)
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 -- 6Multiple arguments are encoded by nesting abstractions
λx. λy. x + y -- takes x, returns a function taking y
Haskell:
\x -> \y -> x + yCurrying: every function has one argument; multi-arg functions are chains:
add :: Int -> Int -> Int
-- really: add :: Int -> (Int -> Int)
add x y = x + yBecause of currying, you can partially apply:
inc :: Int -> Int
inc = add 1 -- fix the first argument
inc 5 -- 6Why 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.
You’ll frequently see signatures with a constraint to the left of =>:
(+) :: Num a => a -> a -> aRead this aloud as:
“
(+)has typea -> a -> a, such thatais an instance ofNum.”
Two common pieces of syntax you’ll meet:
-
C a => τ— constraintC amust 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”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 memptyLaws 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”).
-
Read aloud the signature
length :: Foldable t => t a -> Int
→ “
lengthtakes at aand returns anInt, such thattisFoldable.” -
Explain why
add 2is a function. → Becauseadd :: Int -> (Int -> Int). Supplying the firstIntyields a new functionInt -> Int. -
Name the three
Monoidlaws. → Left identity, Right identity, Associativity.
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.
In Haskell:
(.) :: (b -> c) -> (a -> b) -> (a -> c)
id :: a -> a- Composition
(.)is associative. -
idacts as left/right identity.
This mirrors the category axioms.
A Functor lifts functions into contexts.
Typeclass:
class Functor f where
fmap :: (a -> b) -> f a -> f bExample: List
fmap (+1) [1,2,3] -- [2,3,4]Example: Maybe
fmap (+1) (Just 5) -- Just 6
fmap (+1) Nothing -- NothingLaws:
- 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.
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 bExamples:
pure (+) <*> Just 3 <*> Just 4 -- Just 7
pure (+) <*> Nothing <*> Just 4 -- NothingApplicatives let us combine independent computations in a context.
👉 On-chain, you often combine multiple validations (mustPayToPubKey, mustValidateIn). Applicatives describe such independent constraints.
Monads allow sequential composition, where later steps depend on earlier results.
Typeclass:
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m bMaybe Monad Example:
Just 3 >>= (\x -> Just (x+2)) -- Just 5
Nothing >>= (\x -> Just (x+2)) -- NothingDo-notation:
do
x <- Just 3
y <- Just 5
pure (x + y) -- Just 8Laws:
- 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.
Let’s bridge Haskell abstractions with familiar constructs:
-
Python
- Functor ~
map()on lists. - Monad ~ chaining generators or async coroutines.
- Functor ~
-
JavaScript
- Functor ~
array.map(f). - Monad ~ Promises (
.then) handle sequencing with context.
- Functor ~
-
Java
- Functor ~
Optional.map(). - Monad ~
Optional.flatMap()sequences nullable operations.
- Functor ~
-
Rust
- Functor ~
Option::map,Result::map. - Monad ~
Option::and_then,Result::and_then.
- Functor ~
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!
-
State the Functor laws. → Identity and Composition.
-
Why do Applicatives exist if we have Monads? → Applicatives describe independent effects, while Monads allow dependent sequencing.
-
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.
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.
At Rust’s core is its ownership system, which prevents memory errors at compile-time.
Rules of ownership:
-
Each value has one owner.
-
At any time, you can have either:
- One mutable reference, or
- Any number of immutable references.
-
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.
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 + 5Takeaway:
Rust allows controlled mutation when performance or clarity demands it. Haskell enforces purity everywhere and isolates side effects in monads like IO.
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 = 10Option/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)); // NoneHere .and_then is flatMap / bind: a monadic operation.
- 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.
-
What are the three ownership rules in Rust? → One owner, either many immutable refs or one mutable ref, drop when scope ends.
-
Give one reason Rust is not purely functional. → It allows side effects and mutable variables.
-
Name a Rust type that behaves monadically. →
OptionandResult.
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.
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:
QuickCheckin Haskell.
prop_reverse :: [Int] -> Bool prop_reverse xs = reverse (reverse xs) == xs
- Library:
-
✅ 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.
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.
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.
- 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).
-
What is property-based testing? → Testing by asserting laws hold for many randomly generated inputs.
-
Name one advantage of typed λ-calculus. → Equational reasoning / types as proofs / safety guarantees.
-
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.
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.
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]
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):
-
Bids increase monotonically: Each new bid must be > previous highest.
-
Collateral preserved: Previous bidder refunded when outbid.
-
Deadline respected: Auction cannot be closed before deadline.
-
Single continuing output: Auction UTxO updated with new datum.
-
Cancel only valid if no bids.
-
Close distributes funds correctly:
- Asset → highest bidder.
- Bid amount → seller.
👉 These invariants are like Monoid laws for smart contracts—always true, regardless of inputs.
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.
Let’s walk through two traces.
Trace A – New Bid
-
Auction running, highest bid = 10 ADA (Alice).
-
Bob bids 15 ADA.
-
Validator checks:
-
15 > 10✅ - Refund Alice ✅
- Create continuing UTxO with datum
{highestBid=15, highestBidder=Bob}✅
-
-
Auction remains Active.
Trace B – Closing Auction
-
Auction deadline passed.
-
Charlie (highest bidder at 20 ADA).
-
Close transaction must:
- Pay seller 20 ADA. ✅
- Transfer asset to Charlie. ✅
- Remove auction UTxO. ✅
-
Auction moves to Closed state.
👉 Each trace respects invariants and shows how the state machine transitions deterministically.
-
What are the 4 main actions (redeemers) of the auction? →
Bid,Update,Cancel,Close. -
What invariant must hold for new bids? → Each new bid must be strictly greater than the previous.
-
When can the seller cancel the auction? → Only if no bids have been placed.
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.
-
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.
- 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).
- Small validators can be composed into larger contracts.
- Example:
mustPayToPubKey+mustValidateIncan 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.
-
The type system encodes invariants at compile-time.
-
Example:
-
IntegervsPOSIXTime→ cannot be mixed accidentally. -
Maybe Datumforces 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.
-
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.
-
Why does Plutus require purity? → To ensure determinism across nodes → consensus.
-
What advantage does currying give to validators? → Enables partial application and modular testing.
-
What does type safety prevent? → Mixing incompatible domains (like
IntegerandPOSIXTime), avoiding runtime errors.
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.
- English Auction contract, redeemers (
-
Part 6 🟢 Advantages of Plutus:
- Currying in validators, purity, modularity, type safety, equational reasoning.
- Plutus is Haskell-on-chain: Built from λ-calculus + type theory, inheriting purity and rigor.
- Contracts = State Machines: Auction contract shows transitions driven by redeemers.
- Invariants matter: Like Monoid laws, they guarantee safety across all states.
- Purity enables verification: Determinism + no hidden state → math can audit contracts.
- 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.
- λ-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.
- 📘 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
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.
Bernard Sibanda is a global Technology Entrepreneur, Web3 and Software Consultant with a deep focus on Cardano Blockchain, Midnight and Community building.
Key Positions:
- Founder, CTO, Developer Advocate cohort #1, Fullstake Developer, Cardano Ambassador, Catalyst Project Manager, DREP-WIMS:
- Co-founder of ABL Tech and Cardano Africa Live
- EBU-certified Plutus Pioneer (Plutus/Haskell)
- Cohort #1 Plutus Pioneer Developer
- Catalyst Community Reviewer & Funded Projects Manager
-
DRep for WIMS-Cardano (ID:
drep1yguj8zu48n99pv70yl6ckzt9hdgjy8yjnlqs2uyzcpafnjgu4vkul) - Intersect Developer Advocate
- Intersect Committe Member 2025-2026
- Cardano Marketer,Promoter and blogger
- Cardano Open Source Contributor
- Cardano communities and events organizer and builder
- Cardano Ambassador for South Africa
Official links:
- Stablecoins Dex
- Coxygen Global Universities
- WIMS Cardano Global
- Cardano Africa Live
- WIMS Cardano Videos
- Cardano Smart Contract Videos
- Fullstack IT Consulting
Social links: