An introduction to smart contracts (and beyond) on Cardano

Gianmarco Turchiano
29 min readSep 3, 2021

I got interested in blockchain-based development just a few months ago, when I decided to take my first steps in this environment by studying how smart contracts work and are made on Ethereum (by far the most popular general purpose blockchain network of them all). After getting fairly acquainted with both Ethereum and Solidity, I’ve later decided to enroll in the Plutus Pioneer Program, a free, official online course on how to develop for Cardano. What I was quite surprised to learn from this course is that, while Cardano is of course a competitor to Ethereum, these systems work in dramatically different ways, diverging both on the underlying infrastructure and on the way smart contracts are modelled.

Unspent Transaction Outputs

The first major difference is in the ways that these two platforms keep track of funds. Ethereum uses what is known as the “account model” (also sometimes referred to as the “balance model”), which basically boils down to having at any time just one singular on-chain record of how many ETH any particular address holds.

Differently, Cardano users hold their funds in so-called “Unspent Transaction Outputs”, or UTXOs for short, which are the foundation of the appropriately named “UTXO model”.

As the name suggests, UTXOs are the outputs of transactions, but they are actually the inputs as well: for a transaction to take place, one or more UTXOs have first to be selected as the inputs to be ���consumed”, or “redeemed”, or simply “spent” if you prefer.

A UTXO can be spent only and strictly once, and even if it holds much more funds than what is needed in a transaction, the whole UTXO always gets consumed in its full capacity. This of course doesn’t mean that if Alice has at her address a UTXO which holds 5 ADA, and wants to send 1 ADA to Bob, she will have to lose 4 ADA in the process. Simply put, this transaction will have Alice’s 5 ADA UTXO as its sole input, and will output a UTXO at Bob’s address holding 1 ADA, along with another UTXO holding 4 ADA at Alice’s very own address. The sum of the inputs will always be the same as the sum of the outputs.

Maybe though there was already a UTXO holding 2 ADA sitting at Bob’s address, meaning that now Bob actually owns 3 ADA. However, this information would not be directly reported anywhere on the blockchain (like it would in the context of an account-based system). The blockchain would simply tell us that Bob has two UTXOs, and if someone was interested in knowing how many ADA in total he holds, they would have to retrieve the values held in each one of the UTXOs from his address, in order to sum them up.

What if Bob wanted to send 3 ADA to Charlie? Since he doesn’t have any singular UTXO holding at least that amount, he would have to spend both of his UTXOs, which would constitute the inputs of a singular transaction. The only output would then be a UTXO at Charlie’s address, holding 3 ADA.

It goes without saying that when someone tries to spend a UTXO, they need to prove that they have the right to do so. This is done by signing the redeeming transaction with a private key which matches the UTXO’s address (which is basically just the hash of the holder’s public key, and as such is often specifically referred to as “public key hash address”). Alice would be unable to spend Bob’s or Charlie’s UTXOs, because she (supposedly) doesn’t know their private keys.

Smart Contracts revolve on a similar mechanism: scripts are just “validators”, functions that evaluate to true if the spending of a UTXO is approved, and to false otherwise. This means that instead of checking cryptographic signatures in transactions containing spending requests, we can potentially make up any sort of arbitrary conditions for which a UTXO can or cannot be spent. Those UTXOs that are locked by a validator sit at so-called “script addresses”.

Sending funds to a script address with a validator which unconditionally evaluates to true is basically the same as making a gift to some random lucky stranger, because spending UTXOs from that script address would always succeed. Basically, it would just be a matter of who’s the fastest grabber.

mkValidator :: () -> () -> ScriptContext -> Bool
mkValidator _ _ _ = True

Similarly, if a validator always evaluates to false, UTXOs at the corresponding script address would never be able to be spent, and the related funds would in fact be lost forever (i. e., burnt).

mkValidator :: () -> () -> ScriptContext -> Bool
mkValidator _ _ _ = False

It’s very important to understand that validators are supplied by the same users submitting the transactions to be validated. Users who want to spend UTXOs from a script address first need to somehow acquire the related validator, so that they can submit redeeming transactions with that script attached.

The hash of the validator must match with the script address of the UTXOs that we are trying to spend, since a script address is simply that very same script’s hash. Because of this, it’s impossible to cheat a contract by submitting a phony validator which would arbitrarily approve our spending, because that validator would have a completely different script address from the one we’re trying to fraudulently spend UTXOs from. Once a transaction which is trying to spend a UTXO at a certain script address is provided with the correct validator, the validator itself is then run by the nodes in the network in order to check for the validity of the spending.

Notice that a validator is executed once for each UTXO that we’re trying to spend, because they all need to be checked individually. So, spending two UTXOs from a script address would cost twice as much (in terms of transaction fees) as spending just one UTXO, even if it all happens in a single transaction.

Speaking specifically about smart contracts, validators are actually the only type of scripts which will ever run on-chain. The rest of the logic of a contract is all about composing valid transactions and can therefore be handled by off-chain scripts without hindering our trust in the system.

— This is a very simple and basic example of off-chain codes.
— Users can call this function to submit a transaction that will create a UTXO holding “amount” ADA at the script address of “giftValidator”.
— This script will only ever run off the user’s machine.
give :: Integer -> Contract w GiftSchema Text ()
give amount = do
let tx = mustPayToTheScript () (Ada.lovelaceValueOf amount)
void $ submitTxConstraints giftValidator tx

When trying to redeem a UTXO from a script address, we can also attach a “redeemer” to the transaction. The redeemer is some crucial piece of information which we provide for example in order to prove that we possess some knowledge which entitles us to spend a UTXO. We could for example code a very silly validator which evaluates to true only if the redeemer is the number 42. So, if you know that the “key” is 42, you have the right to spend the UTXOs which are locked by that validator.

mkValidator :: () -> Integer -> ScriptContext -> Bool
mkValidator _ redeemer _ = redeemer == 42

An unrelated but important note is that this is actually a catastrophically unsecure approach: since users have to provide the validator themselves, the related source code is always completely transparent, which crucially is what makes us able to trust this whole business in the first place (we can read a validator, understand what it does, and only then make an informed decision on whether partaking in the corresponding system is actually a good idea).

So anyone could read that script, find out that the redeemer must equate to 42, and then effortlessly submit a valid spending request to that script address. So, in order to put in place something like this, it’d be a much better idea to compare hashes, instead of plain values.

Extending Bitcoin’s model

What I’ve been describing so far isn’t at all Cardano-specific. It’s actually more or less what Bitcoin, the cryptocurrency which first pioneered blockchain technology, does as well. Cardano uses Bitcoin’s very same model, but augments it in order to make smart contracts functionalities more powerful and expressive.

The problem with scripting on Bitcoin is that it is somewhat limited: it doesn’t allow loops, so it generally isn’t considered Turing-complete, it is completely stateless (aside from the amount of BTC that each UTXO contains), and what validators can check is quite narrow in scope, since scripts can see very little about the context in which a transaction takes place.

Cardano then proposes an “extended UTXO model”, or “eUTXO model”, which consists in providing three inputs to the validator script: the datum, the redeemer, and the context.

The datum, like the previously discussed redeemer, can be any possible kind of data, but instead of acting as a key for unlocking funds, it is used to store a state in a UTXO, optionally making the system stateful.

It isn’t actually accurate to state that a datum is stored in a UTXO, since UTXOs really only store datum hashes. The real datum is always stored in the transaction which created the UTXO. A transaction can create multiple UTXOs (because it can have many outputs), so it can potentially store more than one datum. We can use a datum hash to retrieve the corresponding datum from a transaction. However, for all intents and purposes, we can just think that a UTXO actually holds a datum.

An obvious way of using the datum is to make it so that the redeemer has to match it somehow. So, the key for spending a UTXO could be made dependent on that same UTXO’s state. For example, instead of hardcoding in the validator that the redeemer must equate to 42, we could store 42 in the datum of a UTXO, at a script address whose validator dynamically requires that the redeemer must have the exact same value as the datum.

mkValidator :: Integer -> Integer -> ScriptContext -> Bool
mkValidator datum redeemer _ = datum == redeemer

The datum is set for each individual UTXO by its creator, i. e. whoever sends funds to a script address. This means that at a script address there could be multiple UTXOs holding different states. Alice could send funds with a datum containing 31, while Bob could choose to store 78 instead.

If then Charlie tried to spend from this validator’s script address, submitting the number 31 as the redeemer, they would end up redeeming only the UTXO created by Alice. The UTXO created by Bob would remain unspent.

It’s worth noting that this validator isn’t much better than the previous one at keeping valid redeemers secret. That’s because everything on the blockchain is stored transparently and so Charlie could just query that script address for its UTXOs and then find out their corresponding states.

Once again, hashing our value and then storing that hash in the datum instead, would be a much wiser decision. However, it’s useful to keep in mind that we could also use a UTXO’s datum to just plainly host any kind of information on-chain, like oracles would need to do.

The context (which is always of type “ScriptContext”) enables us to check for any sort of conditions regarding the time, the fees, the signatories, but also both the inputs and the outputs.

The possibility of requiring certain properties from the UTXOs on both sides of a transaction is really powerful, for example allowing us to demand that an output must be at a specific address, holding a specific amount of funds.

mkValidator :: PubKeyHash -> () -> ScriptContext -> Bool
mkValidator creatorAddress _ context = containsOwedAdaAmount `any` creatorAddressOutputValues
where
creatorAddressOutputValues :: [Value]
creatorAddressOutputValues = pubKeyOutputsAt creatorAddress $ scriptContextTxInfo context

owedAdaAmount :: Maybe Ada
owedAdaAmount = do
ownInputValue <- txOutValue . txInInfoResolved <$> findOwnInput context
Just $ fromValue ownInputValue `Ada.divide` 2

containsOwedAdaAmount :: Value -> Bool
containsOwedAdaAmount value = Just (fromValue value) >= owedAdaAmount

What this validator enforces is that when someone spends from the related script address, half of the ADA held in a UTXO that’s being redeemed has to go to the public key hash address stored in the corresponding datum. Someone sending funds to this script address would have to choose a public key hash address to store in the datum, while an aspiring spender would have to query the blockchain in order to find out the amount of ADA owed, and to whom they owe it, before submitting a transaction.

Parameterization

As previously stated, script addresses are just the corresponding scripts’ hashes. This means that all script addresses, for each and any possible validator which could ever be written, already exist. Furthermore, validators don’t “live” on the blockchain, waiting to be invoked, they are provided each time by the users submitting spending requests to script addresses.

So we don’t “deploy” a contract (certainly not in the Ethereum manner) once we’re done writing it. We don’t have to publish it on the blockchain and we don’t need to generate and then assign a script address to our newly coded validator: thanks to mathematics, it already, spontaneously exists (and always had existed, for that matter).

This particular concept becomes really powerful once we realize that in Haskell (which by the way is the general purpose, functional programming language that we use to write scripts on Cardano) functions are “curried”. Currying is a lambda calculus concept that basically enables us to generate new, further specialized versions of already existing functions.

Let’s consider an “add” function, which takes two numerical inputs and sums them up.

Prelude> add 2 3
5

Since this function is curried, we can pass to “add” just one of the arguments. The output of this operation would then be a new function, the further application of which would simply sum the value that we specified when defining it (2, in the example below) to its one and only required input.

Prelude> add2To = add 2
Prelude> add2To 3
5

This process of fixing values for some of the arguments of a function is called “partial function application”. In the context of Cardano development, this allows us to parameterize our scripts, potentially making them extremely flexible and generic.

As I’ve already hinted, a validator is derived from an Haskell function producing a boolean value from two arguments of any possible type (one being the datum and the other being the redeemer), along with a third one of type ScriptContext. However, we could define a function which takes more than these three standard inputs, and let users specify values for those additional parameters, in order to generate valid on-chain scripts via partial function application.

For example, if we wanted to make a generic version of the validator that approves spending only when it gets the number 42 as the redeemer, we could introduce a parameter called “number”.

mkValidator :: Integer -> () -> Integer -> ScriptContext -> Bool
mkValidator number _ redeemer _ = redeemer == number

This function, when parameterized with 42, is exactly equivalent to the unparameterized version that we first saw. For any possible “number” parameter, though, this function would generate a different validator, each one having its very own, distinct address.

This can be very useful. We could have a function for handling any blockchain-based crowdfunding campaign and make it so that each campaign is defined by a parameter of type “Campaign”, which bundles data such as the target amount, the deadline and the beneficiary’s public key hash address.

data Campaign = Campaign
{ beneficiary :: PubKeyHash
, targetAmount :: Ada
, deadline :: POSIXTime
}
mkValidator :: Campaign -> PubKeyHash -> () -> ScriptContext -> Bool
mkValidator campaign contributor _ context = transaction `txSignedBy` contributor ||
(transaction `txSignedBy` beneficiary campaign && campaignEndedSuccessfully)
where
transaction :: TxInfo
transaction = scriptContextTxInfo context
inputs :: [TxInInfo]
inputs = txInfoInputs transaction
ownInputsAdaSum :: [TxInInfo] -> Ada
ownInputsAdaSum [] = mempty
ownInputsAdaSum (input:inputs) =
let utxo = txInInfoResolved input
adaInUtxo = fromValue $ txOutValue utxo
utxoAddress = addressCredential $ txOutAddress utxo
campaignAddress = ScriptCredential $ ownHash context
in (if utxoAddress == campaignAddress then adaInUtxo else mempty) + ownInputsAdaSum inputs
campaignEndedSuccessfully :: Bool
campaignEndedSuccessfully =
from (deadline campaign) `contains` txInfoValidRange transaction
&& ownInputsAdaSum inputs >= targetAmount campaign

Someone wanting to launch a crowdfunding campaign would have to just settle on the campaign’s terms and communicate them to potential contributors. Literally no on-chain action would be required for someone to launch a crowdfunding campaign. People having the generic crowdfunding function at hand would then be able to find any specific campaign’s script address by inputting the corresponding parameters.

In order to donate to a campaign, contributors would have to simply create UTXOs (holding their donations) at the related script address. The validator will then allow the beneficiary to redeem the donations only if the deadline has passed and if the chosen target goal was reached (which is verified by summing up all the inputted UTXOs coming from the campaign’s script address).

Parameterization also helps to avoid off-chain filtering of UTXOs on the end user side. We could code that very same crowdfunding script without parameters, setting the Campaign type as the datum instead. Such a validator, though, would correspond to only one single script address, which might be hosting thousands of UTXOs for hundreds of different campaigns. We, as the organizer of only one of these campaigns, wouldn’t be able to spend all of the UTXOs from this address, so we would have to manually select those that we are actually entitled to. This would mean iterating over all of the UTXOs, checking for each one whether they contain our campaign’s parameters. Depending on the number of UTXOs to examine, this could require quite a bit of time.

With parameters, instead, each campaign has its own script address, and no filtering on the UTXOs would ever be necessary (if one of those UTXOs can be spent, all of them can).

Native Tokens

UTXOs can contain custom tokens as well. This means that Cardano lets users actually hold their tokens, exactly like they hold their ADA.

Conversely, tokens on Ethereum are handled by smart contracts (which follow various standards, such as the well known ERC-20 model for fungible tokens) that store in their state information on ownerships and balances. Ethereum’s native user balances are unaware of tokens outside of ETH (which is the only type of native token on Ethereum), because they are all a “fabrication” of smart contracts, which therefore have to be relied on for any kind of operation to take place. We can’t send tokens with normal spending transactions on Ethereum, we have to necessarily invoke functions from the related smart contracts. Since smart contracts are involved, some arbitrary on-chain code execution is required, which needlessly inflates transaction fees.

Meanwhile, Cardano treats user-defined tokens exactly like it treats ADA, to the point of calling them all “native tokens”. We can then spend tokens exactly like we spend ADA, using those same mechanisms that we’ve been describing so far.

The only limitation is that a UTXO can’t hold just custom tokens, it also needs to hold some minimum amount of ADA. This is necessary, because otherwise anyone could for example mint an absurd amount of worthless tokens (doing so is completely free of charge, putting transaction fees aside), allowing them to freely create numerous UTXOs. If this was possible, anyone on its own would be able to cause a significant increase of on-chain data to store, creating serious memory issues for the nodes in the network. Having this rule in place makes the execution of this kind of attack impossible (or, at the very least, economically impractical).

But how do we mint tokens? We submit transactions attaching so-called “minting policies”, which are scripts that evaluate to true if the minting of new tokens is approved, and to false otherwise. It’s indeed a very similar concept to that of validators. For example, this script will approve minting only when it receives a redeemer corresponding to the number 42.

mkPolicy :: Integer -> ScriptContext -> Bool
mkPolicy redeemer _ = redeemer == 42

As you can see, though, there’s no datum to speak of here. That’s because minting policies don’t approve the spending of UTXOs, which also means that there are no corresponding script addresses of sorts. Rather, the hash of one of these scripts represents the “currency symbol” (basically an ID) of the related tokens. A symbol can be assigned to an unlimited number of token types, each one identified by a unique name. So a minting policy can be used to mint whatever kind of tokens, unless the script doesn’t specifically enforce that only tokens with certain names can be minted.

mkPolicy :: () -> ScriptContext -> Bool
mkPolicy _ context = tokenNameIsFooOrBar `all` mintedTokens
where
mintedTokens :: [(CurrencySymbol, TokenName, Integer)]
mintedTokens = flattenValue $ txInfoMint $ scriptContextTxInfo context

tokenNameIsFooOrBar :: (CurrencySymbol, TokenName, Integer) -> Bool
tokenNameIsFooOrBar (_, tokenName, _) = tokenName == TokenName "FOO" || tokenName == TokenName "BAR"

It would be impossible to mint tokens that are not named “FOO” or “BAR” with this policy. Notice that FOO and BAR, while having the same currency symbol, would be two, well distinct kind of tokens. Similarly, FOO and BAR tokens minted with this policy would be completely different from tokens with the same names minted with the previous policy, because their currency symbols would be different.

Each UTXO holds funds in the form of a “Value”, which maps to every currency symbol another map that assigns each token name a corresponding amount. For example, a UTXO with 10 FOO, 30 BAR (both minted with the last policy we’ve seen) and 1 ADA would have the following Value.

Value (Map [(,Map [(“”,1000000)]),(35dedd2982a03cf39e7dce03c839994ffdec2ec6b04f1cf2d40e61a3,Map [(“BAR”,30),(“FOO”,10)])])

Lovelace, the smallest denomination of ADA (the equivalent of Satoshi for BTC and Wei for ETH), is itself a token type with no symbol (meaning that there’s just no possible minting policy script to make more of them) and no name. One million Lovelace is equal to 1 ADA.

Minting policies for NFTs (Non-Fungible Tokens) are basically scripts that will evaluate to true only once in history. We can easily enforce this by requiring that the input must be a very specific UTXO, since UTXOs are unique entities that stop existing forever once they get spent. We can make it so that the selected UTXO’s reference has to be set as a parameter of the minting policy.

mkPolicy :: TxOutRef -> () -> ScriptContext -> Bool
mkPolicy utxoReference _ context = isUtxoReference `any` transactionInputs
where
transactionInputs :: [TxInInfo]
transactionInputs = txInfoInputs $ scriptContextTxInfo context
isUtxoReference :: TxInInfo -> Bool
isUtxoReference input = txInInfoOutRef input == utxoReference

Fungible tokens could still be minted using this policy. For example, we could request the minting of 1 FOO and 100 BAR (notice that we can mint more that one token type at a time). While FOO would actually be an NFT, BAR would not, although there would never be any other units minted for both. If we want to be more strict, we have to enforce that the amount of tokens to be minted has to always be exactly 1.

mkPolicy :: TxOutRef -> () -> ScriptContext -> Bool
mkPolicy utxoReference () context = isUtxoReference `any` transactionInputs
&& tokenAmountIs1 `all` mintedTokens
where
transactionInputs :: [TxInInfo]
transactionInputs = txInfoInputs $ scriptContextTxInfo context
isUtxoReference :: TxInInfo -> Bool
isUtxoReference input = txInInfoOutRef input == utxoReference
mintedTokens :: [(CurrencySymbol, TokenName, Integer)]
mintedTokens = flattenValue $ txInfoMint $ scriptContextTxInfo context
tokenAmountIs1 :: (CurrencySymbol, TokenName, Integer) -> Bool
tokenAmountIs1 (_, _, amount) = amount == 1

This policy, however, would still allow the minting of 1 FOO and 1 BAR simultaneously, each one being a well distinct NFT from the other.

State machines

So far we’ve only seen very simple contracts, which just check whether a certain condition is met and then let the spender do whatever they want with the UTXOs which were approved to be spent. However, we would ideally want to realize more complex behaviours.

For instance, how should we go about implementing an auction which puts up for sale an NFT? This might seem relatively intuitive at first: just let users bid, by creating UTXOs at the auction’s script address. Then, once the auction’s deadline is reached, either the winner or the seller would have to submit a transaction which spends all of the bidding UTXOs, allowing the validator to go through them in order to check whether the highest bidder was correctly rewarded.

The problem with this approach is that users can decide which UTXOs to input to a transaction, meaning that someone could place a very low bid and then try to close the auction, claiming to be the winner by inputting only the UTXO that they created. The seller would similarly have an incentive to close the auction without inputting any of the bids, in case they weren’t satisfied with the highest bidder’s offer.

Setting up an auction as described would mean sacrificing our oh-so desired trustlessness. Remember that the only part of a contract which is trustlessly run on-chain is the validator, everything else (namely, the composition and submission of valid transactions) is handled by off-chain scripts that as such cannot be trusted. Off-chain scripts are just for the convenience of the end user and are not an essential part of the system. Anyone could code alternatives to those, even in order to exploit possible flaws in a contract (such as those that I’ve just described).

We need a way to ensure that no bids could ever be left behind. So, why don’t we just have the validator enforce that the number of inputs from the auction’s address has to be the same as the total amount of UTXOs at that same script address? Because to find out how many UTXOs there are we would have to fetch them and count them, one by one. We can easily do that off-chain, but on-chain it would be literally impossible, because the scope of script doesn’t go beyond the inputs and the outputs of the transaction being validated.

What we really need is a way of deciding what’s the highest bid after each bidding is submitted, and then keep a record of it, basically transitioning between states with each transaction.

bid ( highestBid, newBid ) → highestBid’

This is easily done on Ethereum, where smart contracts are unique “long lived” entities (they remain on the blockchain forever) that as such can retain state indefinitely, allowing state transitions to be repeatedly applied. In contrast, Cardano’s script addresses can host multiple UTXOs, which are individual “short lived” entities that hold state locally.

A UTXO ceases to exist (i. e., it gets spent) after its corresponding validator is run, so its state can be “computed” only once. However, a validator can enforce that one of the outputted UTXOs has to be at its own script address, maybe even holding a specific amount of funds and/or a specific datum.

So, while a spending request would still consume a script address UTXO (basically destroying it forever), it would also create a new one at the same script address, which might hold a state resulting from a computation on the previous state, establishing continuity between UTXOs. A series of subsequent UTXOs can then make up an abstract “medium to long lived” entity that provides the same possibilities that any smart contract on Ethereum would.

Basically, we can set up contracts as on-chain “state machines”, to which users can request operations by submitting transactions that spend the UTXO holding the current state. Here’s the automaton for a very simple auction state machine.

A spending request to such a contract would have to contain a redeemer that describes the nature (and maybe even the details, in the form of arguments) of the solicited state transition. The transitions can be algebraically defined like so.

data AuctionTransition = Bid | Buy | Close

“AuctionTransition” will of course be the auction validator’s redeemer type. We can do the same for defining all the possible states that the contract can be in.

data AuctionState = NoBids | Bidding PubKeyHash

“AuctionState” will be the auction validator’s datum type. The “PubKeyHash” value wrapped by “Bidding” will be the address of the current highest bidder.

There’s no need to implement the “Over” state seen in the automaton above, because nothing else can happen once that state is reached. So, the Over state is really just the finishing state, which on-chain we can transition to by simply not outputting any other UTXO at the auction validator’s own script address: the state machine will just stop existing once the auction gets closed.

Notice that the validator itself cannot decide what will be the next state, it is just able to check whether a transition is valid, given the current state, and whether the next state is valid, given the transition. The validator will evaluate to false if the transition is not allowed by the current state, otherwise the UTXO will be correctly consumed, outputting a new UTXO at the same script address, holding the next valid state in its datum. It is the user causing the transition, they being the creator of the following state macchine UTXO, who is in charge of setting up correctly the next state.

For someone to be the first bidder, they would have to spend the initial UTXO, which would hold the “NoBids” datum, submitting the “Bid” redeemer. The output of this transaction would have to be a new UTXO at the auction’s script address, which would have to hold three things:

  1. A “Bidding” datum wrapping the public key hash address of the first bidder;
  2. The auctioned NFT, which was previously held in the UTXO that just got spent;
  3. The bidded amount of ADA, of course.

The same would go for outbidding someone else, with the added clause that the new highest bidder would also have to send back to the old highest bidder their previously bidded amount. Obviously, the new highest bid would have to be greater than the old highest bid.

For instabuying the NFT, one would have to spend the current UTXO, submitting the “Buy” redeemer. The auction would be over, so no new UTXO at the auction’s script address would have to be created, with the validator enforcing that the bidded amount (which also has to be greater than the “maxBid” parameter) must go the seller. Also, if there was already a highest bidder, they would have to be refunded in this case as well.

As you can see, there are no restrictions on the destination of the NFT, as a Buy transition can logically be requested only by the buyer themselves. Differently, the “Close” transition, which can be requested by anyone, enforces that the NFT has to go back to the seller, if there are no bids, or alternatively to the highest bidder, if there is one. In the latter case, the validator also demands that the bidded amount has to go to the seller. In both cases, no further outputs at the auction’s script address would be necessary, as the auction would be over.

It also goes without saying that Bid and Buy transitions can be requested only up until the deadline is reached, and that the Close transition can’t be applied if not after the deadline has passed.

data Auction = Auction
{ seller :: PubKeyHash
, auctionedNFT :: AssetClass
, deadline :: POSIXTime
, minBid :: Ada
, maxBid :: Ada
}
mkValidator :: Auction -> AuctionState -> AuctionTransition -> ScriptContext -> Bool
mkValidator auction state transition context =
case transition of
Bid -> not deadlineReached && nextUtxoHasAuctionedNFT && nextUtxoStoresCurrentBidder && newBid <= maxBid auction &&
case state of
NoBids -> newBid >= minBid auction
Bidding highestBidder -> Just newBid > highestBid && highestBidSentTo highestBidder
Buy -> not deadlineReached && sellerWasPaidMoreThanMaxBid &&
case state of
NoBids -> True
Bidding highestBidder -> highestBidSentTo highestBidder
Close -> deadlineReached &&
case state of
NoBids -> auctionedNFTSentTo $ seller auction
Bidding highestBidder -> auctionedNFTSentTo highestBidder && (highestBidSentTo $ seller auction)
where
transaction :: TxInfo
transaction = scriptContextTxInfo context
highestBid :: Maybe Ada
highestBid = fromValue . txOutValue . txInInfoResolved <$> findOwnInput context
nextUtxo :: TxOut
nextUtxo =
case getContinuingOutputs context of
[utxo] -> utxo
_ -> traceError "There should be exactly 1 continuing UTXO."
nextUtxoValue :: Value
nextUtxoValue = txOutValue nextUtxo
newBid :: Ada
newBid = fromValue nextUtxoValue
currentBidder :: PubKeyHash
currentBidder = head $ txInfoSignatories transaction
valueHasAuctionedNFT :: Value -> Bool
valueHasAuctionedNFT value = (value `assetClassValueOf` auctionedNFT auction) == 1
nextUtxoHasAuctionedNFT :: Bool
nextUtxoHasAuctionedNFT = valueHasAuctionedNFT nextUtxoValue
nextUtxoStoresCurrentBidder :: Bool
nextUtxoStoresCurrentBidder =
let nextState = do
datumHash <- txOutDatumHash nextUtxo
Datum d <- datumHash `findDatum` transaction
fromBuiltinData d
in currentBidder == case nextState of
Just (Bidding newHighestBidder) -> newHighestBidder
_ -> traceError "Next state wasn't set or was set incorrectly."
pubKeyOutputValuesAt :: PubKeyHash -> [Value]
pubKeyOutputValuesAt = (`pubKeyOutputsAt` transaction)
auctionedNFTSentTo :: PubKeyHash -> Bool
auctionedNFTSentTo publicKeyHashAddress = valueHasAuctionedNFT `any` pubKeyOutputValuesAt publicKeyHashAddress
highestBidSentTo :: PubKeyHash -> Bool
highestBidSentTo publicKeyHashAddress = (\value -> (Just $ fromValue value) == highestBid) `any` pubKeyOutputValuesAt publicKeyHashAddress
sellerWasPaidMoreThanMaxBid :: Bool
sellerWasPaidMoreThanMaxBid = (\value -> fromValue value > maxBid auction) `any` (pubKeyOutputValuesAt $ seller auction)
deadlineReached :: Bool
deadlineReached = from (deadline auction) `contains` txInfoValidRange transaction

This is actually a very crude and messy way of implementing state machines. Plutus (the platform powering scripting on Cardano) offers much better alternatives that abstract away most of the boilerplate seen here. Unfortunately, these other solutions go beyond the scope of this article.

An impressive demonstration of the power and flexibility of state machines is provided by Marlowe, a simple domain specific language (DSL) for modeling financial instruments as smart contracts. Marlowe’s interpreter is basically an on-chain state machine, meaning that what makes Marlowe smart contracts possible is a lower level Plutus smart contract. What Marlowe basically provides is a handy layer of abstraction for setting up specific kinds of state machines, with each UTXO holding a Marlowe script in its datum, and with each transition gradually consuming the code as its effects are realized.

Deployment

While I’ve now repeated several times that smart contracts on Cardano don’t need to be deployed, that’s not entirely true if an initial amount of funds and/or an initial state is needed for making the system usable by third parties.

State machines, for example, are just UTXOs that continuously get spent in order to create new UTXOs, causing state transitions in the process. UTXOs don’t spontaneously come to be, though, so a state machine needs first to be “booted up”, by creating an initial UTXO holding the starting state.

Doing so is really simple: we just send some amount of ADA (maybe a required amount for the corresponding system to start operating, or alternatively just the minimum amount possible, which currently should be exactly 1) to our state machine’s script address.

Problem is that this is actually TOO simple. We can put restrictions on who can spend UTXOs at some script address, but we can’t do the same when it comes to random people sending funds instead. This means that anyone can create UTXOs at whichever address, because why would we ever want to put restrictions on someone giving us money anyway?

In the context of the crowdfunding example that we previously discussed, it really wouldn’t make any sense, but when it comes to script addresses that are meant to host a state machine, this could be seriously problematic: anyone could create another UTXO at the same address at which we already deployed ours, possibly defrauding users.

Let’s consider an oracle, for example, holding important information in the datum of a UTXO at a certain script address. By default there wouldn’t be anything stopping a third party from deploying an alternative, phony oracle at the same address, suggesting fake data and maybe even charging money for it. Users wouldn’t have any way to reliably detect which one is the genuine oracle.

We need a way to uniquely and undeniably identify the UTXO corresponding to the real oracle, so that users can then easily ignore all of the others. To do so, in order to create the initial UTXO, we have to send an NFT to the oracle’s script address. NFTs are by definition unique, so they make for great IDs.

After choosing an NFT to bind to our system, we enforce in the validator that spending is approved only if among the outputs of a transaction there’s a UTXO at the validator’s own script address holding that specific NFT. We check the outputs because when changing state the NFT must remain at our script address (otherwise no valid UTXO would no longer exist). Being any NFT unique, this condition also implicitly checks whether that same NFT was in the validated inputs as well.

nextUtxo :: TxOut
nextUtxo =
case getContinuingOutputs context of
[utxo] -> utxo
_ -> traceError "There should be exactly 1 continuing UTXO."
nextUtxoValue :: Value
nextUtxoValue = txOutValue nextUtxo
valueHasAuctionedNFT :: Value -> Bool
valueHasAuctionedNFT value = (value `assetClassValueOf` auctionedNFT auction) == 1
nextUtxoHasAuctionedNFT :: Bool
nextUtxoHasAuctionedNFT = valueHasAuctionedNFT nextUtxoValue

So even if someone created UTXOs at our script address, users could easily find the only genuine one, by checking which UTXO holds the intended NFT. And even if they accidentally tried to spend a third party created UTXO, they wouldn’t be able to do so, because the validator requires a UTXO to hold the NFT in order to be spent.

Some systems might naturally involve NFTs (just think of the previous NFT auction example), but for those that do not (such as an oracle) we can mint and use NFTs whose only task is to identify the correct UTXO for using the related state machine.

Determinism Vs. Concurrency

While very powerful, the problem with state machines is that on their own they are fundamentally “single-threaded”, narrowing the throughput of a contract to exactly one transaction per block. On Cardano this would mean that only one user every 20 seconds (more or less) would be able to access the services provided by a state machine. This might be good enough for contracts between a limited number of parties, but it’s of course not very scalable for wider scope systems.

This constraint is due to the fact that a UTXO can be spent only once. So, if someone wanted to spend a state machine’s UTXO and someone else beat them to the punch, the first user wouldn’t have any other UTXO to spend, meaning that they would have to wait until the next block.

While this is a big inconvenience, these properties of the UTXO model make the whole system completely deterministic: instead of composing one solid, dense entity which can be simulatenously accessed and manipulated by multiple actors, states (and funds as well) on Cardano are divided in unique, well distinct units (UTXOs) which only one actor at a time can work with. This helps to avoid the “resource contention” problem, which in blockchains can lead a transaction to unexpectedly fail, because the state it was supposed to operate on was concurrently modified by another transaction.

Since no concurrent operation can interfere with the spending of a UTXO, we can accurately foresee everything about the effects of a transaction before it even gets submitted to the network. That’s the reason we are able to check the outputs of a transaction while it’s still being validated.

Furthermore, this allows users to simulate the execution of scripts locally, inputting the UTXOs that they’re interested in spending. Then they can submit a transaction that will request the spending only of those inputs on which the off-chain validation succeeded. Precisely because of determinism, if validation succeeds off-chain, it will succeed on-chain too, as long as the targeted UTXOs don’t get spent by someone else before the transaction gets validated (which is easily checked without having the nodes running any scripts).

This means that users won’t get charged fees for useless on-chain script executions that ended up with their spending attempt being rejected. Basically, a transaction can safely fail without having the user pay for it.

Also, an endless loop being executed off-chain will timeout at some point, resulting in automatic failure. This means that honest users won’t ever accidentally submit scripts that will never terminate.

But what about malicious actors which might be bypassing off-chain checks in order to submit scripts such as the following? How does Cardano deal with the dreaded halting problem that famously forced Bitcoin to get rid of loops?

mkValidator :: () -> () -> ScriptContext -> Bool
mkValidator a b c = mkValidator a b c

Off-chain script executions are also meant to calculate the corresponding transaction fees, for which “collateral” UTXOs from the public key hash address submitting the transaction are then selected. Those UTXOs cover the expected transaction fees, and if a script fails on-chain (which is something that would never happen while behaving honestly), the collateral UTXOs are spent to pay for the fees.

Moreover, scripts are broken into steps, on which fees are cumulatively calculated on. If during on-chain execution the calculated fees exceed the expected transaction fees, the process is simply aborted. So, it is guaranteed that the on-chain execution of a script containing an endless loop will eventually fail.

This is all well and good, but what about concurrency? One basic solution to that problem is to set up “concurrent state machines”, creating many different UTXOs, each one representing a thread. Users can then spend any of these UTXOs to interact with the state machine, meaning that the contract allows for as many transactions per block as there are “thread UTXOs”. At some point, all threads will have to be gathered by a transaction spending all of the corresponding UTXOs.

Since all threads are deployed at the same time, the expected total number of UTXOs is constant and can therefore be hardcoded in the validator. Validators can then enforce that, for threads to be reunited, a certain number of inputs from their own script address is required.

Closing thoughts

This lengthy article was made by patching together my Plutus Pioneer Program notes. At first I really struggled with some of these novel (at least to me) concepts, so I thought that an introductory article explaining them in simple terms could be useful to those coming from Ethereum (or other similar environments), who are now moving their first steps on Cardano.

If you’re learning about Cardano and Plutus just now, don’t let some of these more foreign concepts scare you off. I also really do advise you to follow the Plutus Pioneer Program: you don’t even need to enroll or to wait until the next cohort (which I think will happen in October or November 2021), the lectures are both on IOHK’s and the teacher’s (Lars Brünjes) YouTube channels, and you can freely watch them now.

--

--