Smart Contracts in Cardano

Claudio Hermida
Coinmonks
8 min readJul 20, 2024

--

Part III: Transaction Schemas

Claudio Hermida
claudio.hermida@gmail.com
https://www.linkedin.com/in/claudiohermida/

Abstract

The purpose of this note is to

  • Show how the eUTxO model of blockchain comes about from a consideration of handling state in functional programming
  • Exhibit Cardano’s validators as verifiable specifications of a contract-method input-output behaviour
  • Provide a putative answer to the question: what is a smart contract in Cardano?

This is third and final part of the three-part series covering these topics. The other parts are Part I and Part II. The whole article is presented in this YouTube video

https://www.youtube.com/watch?v=cUu-7FDV0wI?si=cSD8uurYBGDYs7vL

Introduction

In the first part Part I of this series we dealt with state in functional programming and its relevance to Cardano’s eutxo model, while in the second part Part II we set up the correspondence of formal specification of methods via pre and postconditions with (on-chain) validators for the corresponding redeemers.

In this final installment, we deal with the off-chain part of smart contract methods, namely the construction of transactions. To that end, we introduce the notion of transaction schema as a parameterized transaction body, and illustrate it with our guiding Vesting contract example.

We conclude by analyzing what it means to deploy a smart contract in Cardano, and consider the use of NFTs in this context.

Building Tx schemas

Having set up validators for our methods, we must produce transactions to execute them. There are various frameworks for transaction building: MeshJS (https://meshjs.dev/) caters to JavaScript enthusiasts, offering React integration, while Atlas (https://atlas-app.io/) favors the more Cardano-native Haskell environment. In keeping with our functional programming emphasis we will use this latter as reference, although the concepts we expose are framework agnostic.

One attractive feature of Atlas is that it provides a modular way of building transactions, piece by piece so to speak: add an input, an output, a validity range, a signatory, etc. All these pieces are called skeletons: they live in a GYTxSkeleton monad with a monoid structure to combine such pieces via <>. A skeleton is fed to a transaction builder, like gyTxBuilder in GYTxMonadNode, along with relevant parameters for interacting with a node, such as network id and data providers. Such a transaction builder will attempt to complete the transaction body using a coin selection algorithm to select relevant fee and collateral input eutxos, and well as balancing the transaction by sending unspent input value to a change address.

We call transaction schema any function that produces a transaction body TxBody as output, that is

Transaction Schema = parameterized Transaction Body

So a composition of transaction builder with transaction-skeleton constructors would form such a schema. In the Atlas framework, a transaction body has type GYTxBody.

Let us see the relevant transaction schemas for our example Vesting contract. First, let us recall the original formulation of the Claim method in Solidity

function claim() public {
require(msg.sender == beneficiary, "Only the beneficiary can call this function.");
require(block.timestamp >= deadline, "Deadline has not passed yet.");
require(!consumed, "Funds have already been released.");
consumed = true;
payable(beneficiary).transfer(amount);
}

For the formulation in Atlas/Haskell, we use inline datumsPeyton Jones 2021a (full code given in our github repo):

-- implement business logic of Claim method

The code is rather verbose as there are a number of helper functions to extract data nested into records with lists of etc etc. Finally, we have to build transaction bodies out of the skeletons, sign and submit. Here are the essential details (for the full-fledged version of a similar contract see Brujnes 2023b)

-- build transaction to Claim vested amount:
beneficiary <- extractBeneficiary oref
txBody <- runGYTxMonadNode networkId providers beneficiary collateral $
claimVestingBeneficiary oref
tid <- gySubmitTx providers $ signTx txBody [skey]

Smart Contracts in Cardano

We could sum up our considerations of Cardano smart contracts with the slogan

A Cardano smart contract is a set of transaction schemas and their associated validators

We have already established how such a set of schemas and validators must ‘share’ the data types Datum and Redeemer, in formulations compatible with the off-chain and on-chain setups, which might involve different languages. Essentially, Datum is a product of the various components of local state of the contract (besides its Value):

data Datum = Datum { var_1 :: Type_1 , var_2 :: Type_2 , … , var_n :: Type_n }

while Redeemer is a sum type, whose various alternatives correspond essentially to the methods signatures:

data Redeemer = method_1 Type_11 … Type_1p | … | method_m Type_m1 … Type_mq

The correspondence of “methods” to redeemer cases need not be 1–1: some methods interact with various ‘pieces of state’ which will be represented by several inputs, each to be consumed by a possibly different redeemer. And conversely, a complex transaction involving several inputs implements a business logic that may correspond to a combination of several “methods” in the OOP sense.

Deploying contracts

In account-based models, a smart contract is an object or instance of a class. Deploying a contract amounts to creating a new instance of the class and assigning it to an account address, which will hold the local state of that object/instance. The initial state is set upon deployment invoking the constructor method in the contract. We can actually deploy several instances of the same contract class, each residing at a different account address; such account addresses hold the respective local states of those instances.

The analogous operation of deploying a contract in Cardano involves two steps:

  • Generate a script address that is going to hold the local state of the contract via eutxos. It is generated by hashing the Plutus validator script.
  • Build and submit a transaction with an output (eutxo) targeted at the script address, encoding the intial state of the instance in its Value and Datum. This is analogous to executing the constructor method of the contract.

In our example Vesting contract, let us recall the original constructor in Solidity

contract VestingContract {
...
constructor(
address _beneficiary,
uint _deadline
) payable {
beneficiary = _beneficiary;
benefactor = msg.sender;
amount = msg.value;
deadline = _deadline;
consumed = false;
}
...
}

Here is the relevant code in Atlas/Haskell (recall that we use inline datum):

-- set up initial state to deploy Vesting contract
placeVesting :: GYTxQueryMonad m => GYPubKeyHash -> GYPubKeyHash -> GYTime -> GYValue -> m (GYTxSkeleton 'PlutusV2)
placeVesting benefactor beneficiary deadline valueAmount =
return $ mustHaveOutput $ GYTxOut --specify an eutxo with inline datum
{ gyTxOutAddress = vestingAddress -- script address from mKVestingValidator
, gyTxOutValue = valueAmount
, gyTxOutDatum = Just (datumFromPlutusData $
VestingDatum benefactor beneficiary
{timeToPlutus deadline) (toLovelace $ fromValue valueAmount)
, GYTxOutUseInlineDatum)
, gyTxOutRefS = Nothing
}

We may also emulate the declaration of the contract code, which is a transaction that loads the code by itself on-chain (considered as a library of methods), as follows:

Build an eutxo with a reference scriptCIP-33, so that the ‘contract class’ is on-chain and we can refer to it whenever it is needed for execution (validation).

The reason for putting the code in an eutxo is that the very simple model of ledger in Cardano, namely as a set or collection of utxos, implies that if we want to associate any information to an address, we must build a utxo targeted at that address, containing that info.

Reference Script CIP-33

A reference-script utxo carries the code of the validator script in a special new field. It was also introduced by Peyton Jones 2021b implemented in the Vasil hard-fork. It greatly simplifies the size of transactions which invoke this validator, as we can just refer to the given utxo rather than pass the whole piece of code each time we refer to it.↩︎

In order to accommodate the possibility of several instances of the same contract coexising in the blockchain, we mint an NFT which is incorporated in the value of the initializing eutxo and passed along as we update contract state via new eutxos. This NFT uniquely identifies the instance; it is the actual counterpart of the account address of the contract object. For a good example of this use of NFTs see Brujnes 2023a.

On NFT threat tokens

https://developers.cardano.org/docs/smart-contracts

Contract instances: When you have contracts designed to run in multiple steps, the UTXO that represents the current state of a specific instance/invocation of that script is something you need to be able to keep track of. There is no standard for how to do this as of now, but one way to accomplish this is to be to create a minting-policy that only allows minting of thread token NFTs to the script’s address, and then use the NFTs as thread-tokens by having the validator script enforce such NFTs be moved with each transaction

Here is a brief summary of related concepts in both blockchain models:

References

Peyton Jones, Michael. 2021a. “Inline Datums.” https://cips.cardano.org/cip/CIP-32/.

— — — . 2021b. “Reference Scripts.” https://cips.cardano.org/cip/CIP-33/.

Brujnes, Lars. 2023a. “Stablecoin with Oracle.” https://github.com/input-output-hk/plutus-pioneer-program/tree/fourth-iteration/code/Week09.

— — — . 2023b. “Vesting App.” https://github.com/brunjlar/atlas-examples/tree/main/vesting/app.

--

--

Claudio Hermida
Coinmonks

PhD computer science, category theorist, web 3 enthusiast