Table of Contents

Aiken Library for Common Design Patterns in Cardano Smart Contracts

To help facilitate faster development of Cardano smart contracts, we present a collection of tried and tested modules and functions for implementing common design patterns.

Based on our design-patterns repository.

How to Use

Install the package with aiken:

aiken add anastasia-labs/aiken-design-patterns --version v1.8.0

And you’ll be able to import functions of various patterns:

use aiken_design_patterns/merkelized_validator
use aiken_design_patterns/multi_utxo_indexer
use aiken_design_patterns/linked_list
use aiken_design_patterns/linked_list/advanced
use aiken_design_patterns/linked_list/nested
use aiken_design_patterns/parameter_validation
use aiken_design_patterns/parameter_validation/advanced
use aiken_design_patterns/singular_utxo_indexer
use aiken_design_patterns/stake_validator
use aiken_design_patterns/tx_level_minter

Check out validators/examples to see how the exposed functions can be used.

How to Run Package Tests

Here are the steps to compile and run the included tests:

  1. Clone the repo and navigate inside:
git clone https://github.com/Anastasia-Labs/aiken-design-patterns
cd aiken-design-patterns
  1. Run the build command, which both compiles all the functions/examples and also runs the included unit tests:
aiken build
  1. Execute the test suite:
aiken check

Provided Patterns

Stake Validator

This pattern allows for delegating some computations to a given staking script.

The primary application for this is the so-called “withdraw zero trick,” which is most effective for validations against multiple script inputs in the transaction.

With a minimal spending logic (which is executed for each UTxO), and an arbitrary withdrawal logic (which is executed only once), a much more optimized script can be implemented.

The module offers three functions, primarily meant to be implemented under spending endpoints:

Use validate_withdraw_minimal if you don’t need to perform any validations on either the staking script’s redeemer or withdrawal Lovelace quantity.

All three functions go over the withdrawals list in the transaction. However, validate_withdraw and validate_withdraw_with_amount also traverse the redeemers field in order to let you validate against the redeemer (and the withdrawal quantity in case of the latter).

UTxO Indexers

The primary purpose of this pattern is to offer a more optimized and composable solution for a unique mapping between one input UTxO to one or many output UTxOs.

There are a total of 4 variations available:

Neither of the singular UTxO indexer patterns provides protection against the double satisfaction vulnerability, as this can be done in multiple ways depending on the contract. However, they require a dedicated argument as a reminder for the potential requirement of implementing a protection against this vulnerability.

Depending on the variation, the functions you can provide are:

Transaction Level Validator Minting Policy

Very similar to the stake validator, this design pattern couples the spend and minting endpoints of a validator.

In other words, spend logic only ensures the minting endpoint executes. It does so by looking at the mint field and making sure a non-zero amount of its asset (i.e. with a policy identical to the provided script hash) are getting minted/burnt.

The arbitrary logic is passed to the minting policy so that it can be executed a single time for a given transaction.

Validity Range Normalization

The datatype that models validity range in Cardano currently allows for values that are either meaningless, or can have more than one representation. For example, since the values are integers, the inclusive flag for each end is redundant for most cases and can be omitted in favor of a predefined convention (e.g. a value should always be considered inclusive).

In this module we present a custom datatype that essentially reduces the value domain of the original validity range to a smaller one that eliminates meaningless instances and redundancies.

The datatype is defined as follows:

pub type NormalizedTimeRange {
  ClosedRange { lower: Int, upper: Int }
  FromNegInf  {             upper: Int }
  ToPosInf    { lower: Int             }
  Always
  InvalidRange
}

The exposed function of the module (normalize_time_range), takes a ValidityRange and returns this custom datatype.

Merkelized Validator

Since transaction size is limited in Cardano, some validators benefit from a solution which allows them to delegate parts of their logic. This becomes more prominent in cases where such logic can greatly benefit from optimization solutions that trade computation resources for script sizes (e.g. table lookups can take up more space so that costly computations can be averted).

This design pattern offers an interface for off-loading such validations into an external observer/withdrawal script, so that the sizes of the scripts themselves can stay within the limits of Cardano.

Be aware that total size of reference scripts is currently limited to 200KiB (204800 bytes), and they also impose additional fees in an exponential manner. See here and here for more info.

The exposed delegated_compute function from merkelized_validator expects 6 arguments:

  1. The arbitrary input value for the underlying computation logic
  2. The hash of the withdrawal validator that performs the computation
  3. The Pairs of all redeemers within the current script context.
  4. Positional index of the withdrawal redeemer inside the redeemers list
  5. Validation function for coercing a Data to the format of the input expected by the staking script’s computation
  6. A similar validation function for coercing Data to the expected output of the computation

This function expects to find the given stake validator in the redeemers list, such that its redeemer is of type ComputationRedeemer (which carries the generic input argument(s) and the expected output(s)), makes sure provided input(s) match the ones given to the validator through its redeemer, and returns the output(s) (which are carried inside the withdrawal redeemer) so that you can safely use them.

For defining a withdrawal logic that carries out the computation, use the exposed computation_withdrawal_wrapper function. It expects 2 arguments:

  1. A redeemer of type ComputationRedeemer<a, b>. Note that a is the type of input argument(s), and b is the type of output argument(s)
  2. The computation itself. It has to take an argument of type a, and return a value of type b

It validates that the given input(s) and output(s) match correctly with the provided computation logic.

There are also ValidationRedeemer<a>, validation_withdrawal_wrapper and delegated_validation variants which can be used for validations that don’t return any outputs.

Parameter Validation

In some cases, validators need to be aware of instances of a parameterized script in order to have a more robust control over the flow of assets.

As a simple example, consider a minting script that needs to ensure the destination of its tokens can only be instances of a specific spending script, e.g. parameterized by users’ wallets.

Since each different wallet leads to a different script address, without verifying instances, instances can only be seen as arbitrary scripts from the minting script’s point of view.

This can be resolved by validating that an instance is the result of applying specific parameters to a given parameterized script.

The base aiken_design_patterns/parameter_validation module hashes parameters into fixed-length fragments. To validate them on-chain, some restrictions are needed:

  1. Parameters of the script must have constant lengths, which can be achieved by having them hashed
  2. Consequently, for each transaction, the resolved value of those parameters must be provided through the redeemer
  3. The dependent script must be provided with CBOR bytes of instances before and after the parameter(s)
  4. Wrapping instances’ logic in an outer function so that there’ll be single occurrences of each parameter

The module provides two sets of functions: one for applying parameter(s) in the dependent script (i.e. the minting script in the example above), and one for wrapping parameterized scripts.

After defining your parameterized scripts, you’ll need to generate instances of them with dummy data in order to obtain the required prefix value for your target script to utilize. Note that your prefix should be from a single CBOR encoded result.

See the base parameter-validation example for these helpers in use.

The advanced module supports a single arbitrary Data parameter. It lets a dependent validator prove that an output uses the exact instance produced by applying a known parameter value to a known parameterized script.

advanced.apply_param takes the script version, a precomputed Flat prefix, and the parameter. It serialises the parameter canonically, handles variable-length CBOR and Flat chunking, reconstructs the applied script, and returns its ScriptHash for comparison with the output credential.

See the advanced example and its prefix generator. Run the generator with pnpm --dir tools/advanced-parameter-prefix generate.

Linked List

Storing lists directly in datums is generally impractical: as the datum grows, the UTxO can become too expensive or impossible to spend.

A linked list stores the collection across many authenticated UTxOs. Each list element carries ADA, exactly one list NFT, an inline datum, and a link to its immediate successor.

Plutonomicon has a nice write-up of how this can be implemented with eUTxOs.

To provide an API as user-friendly as possible, the implementation handles structural linked-list validations and provides the data needed for custom application validations. This is why the API exposes primary list operations such as init, insert_ascending, and remove, rather than asking each contract to reassemble the structural checks from granular helpers.

The linked-list API is split across three modules:

See the generated docs pages above for module-specific details. They are long and elaborate many of the soft requirements in order to better guide agents.

List membership is authenticated by an asset under the list NFT policy, never by the payment credential alone. Anyone can create an output at the list payment credential without running either the spend script or the minting policy. An ADA-only UTxO, or a UTxO carrying only foreign-policy assets, is therefore not a list element even when it sits at that credential. It requires no linked-list structural validation, and no linked-list invariant may rely on its datum, value, continuation, or eventual spend. UTxOs at one credential remain independent: a transaction consumes only the inputs its builder explicitly selects, and an outside party cannot force another UTxO into a list transition. These UTxOs never need to be discovered, collected, spent, or cleaned up by the list protocol. Off-chain list discovery must authenticate the expected structural token and canonical element shape rather than treating every UTxO at the payment credential as state. The namespace-aware advanced and nested input scanners ignore inputs without list-policy assets completely. Only inputs carrying assets under the list policy enter their structural or non-structural classification.

Import the base linked_list module alongside any variant module you call. Keep variant-specific operations in their variant modules; in particular, nested datums must not be passed to the base update/read helpers.

use aiken_design_patterns/linked_list
use aiken_design_patterns/linked_list/advanced
// or: use aiken_design_patterns/linked_list/nested

Usage Guideline

Contracts using these modules must keep all authenticated list elements controlled by one spend script/payment credential and one list NFT minting policy:

  1. Define the spend script datum as an applied alias of the module’s Element type. For base and advanced lists this is Element<RootType, NodeType>; for nested lists this is Element<RootType, InnerRootType, NodeType>.
  2. Ensure that the UTxO produced by init goes to that spend script credential.
  3. Implement structural spend branches so they only succeed through spend_for_adding_or_removing_an_element.
  4. Implement non-structural continuation branches through spend_for_updating_elements_data.
  5. Implement the list minting policy so every structural init, insert, remove, fold, and deinit mint/burn branch uses the matching linked-list mint helper where the module provides one. Custom nested remove logic must prove the same structural invariants directly in its minting-policy branch. The spend-side structural gate and mint-policy validation are a paired API: the spend gate permits the list UTxO spend when a list-policy mint/burn is present, and the mint policy proves the exact list transition.
  6. Pass the complete, unmodified ScriptContext.transaction.inputs list in ledger order to every linked-list helper inputs argument. Do not pass a filtered, reordered, reconstructed, or redeemer-provided list. Exact structural-input counts and namespace-aware input collection are guaranteed only across the supplied list.
  7. Treat the list policy, not the payment credential, as the authentication boundary. UTxOs without any asset under that policy are outside the list state, including when they are at the list payment credential; do not validate or protect them as list elements. Transaction builders select inputs explicitly, so hostile outputs at that credential neither participate in nor block a valid list transition. They require no cleanup. Off-chain indexers must authenticate the structural list token and canonical element shape, not infer membership from the address alone.
  8. Every Output argument passed to a linked-list mint helper must be selected from the script context transaction outputs. Helpers authenticate the selected outputs as list UTxOs, but intentionally leave the selection method to the caller. A contract may pick by redeemer-provided output index, filter ScriptContext.transaction.outputs, use list.find, or use another deterministic method. What matters is that the final Output value comes from the transaction outputs, not from redeemer data or a locally constructed value.
  9. Reserve the asset <list_nft_policy_id, root_key> globally across every redeemer branch of the list policy. A one-time init is the only branch that may mint it, and it mints exactly one into the canonical root at the list payment credential. Structural and update continuations keep it there; deinit is the only branch that may burn it. Every application-specific same-policy branch must also reject changes to root_key. Under this policy-wide invariant, a root_key token at another payment credential is unreachable and must not be treated as valid external state.
  10. Choose root and node NFT names so their namespaces are disjoint: use a non-empty node key prefix, non-empty node keys, and a root key that cannot equal node_key_prefix ++ node_key. The library assumes this convention instead of adding repeated on-chain checks to every operation; agents wiring a contract must treat this as a deployment precondition, not as something recovered by the helpers later.

These rules preserve the invariant that linked-list NFTs cannot leave the list spend script/payment credential; they make no claim about UTxOs at that credential which carry no list-policy asset. Continued anchors are checked by full address equality; newly minted nodes share the anchor payment credential, which lets callers choose staking parts for new nodes. If a callback does not receive a produced element address directly, the corresponding Output is an argument the caller supplied to the helper and can be captured by the callback. The structural spend gate only requires a list-policy mint/burn to occur; it is not standalone authorization. The paired minting policy must only accept structural mint/burns through a matching linked-list mint helper where one is provided, or through custom validation that proves the same invariants. Example validators in this repository demonstrate API wiring, but they do not replace contract-specific authorization or state-transition invariants.

License

MIT license:

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Search Document