Modular Contract Design Patterns
The reference implementation (OptimFinance/leviathan) is written in Aiken, Cardano's purpose-built smart contract language, and follows Aiken's standard project layout:
leviathan/
├── validators/ # entry points: spend / mint / withdraw handlers
├── lib/
│ └── account/ # shared, non-validator logic (the Optim Account module)
├── aiken.toml # project + dependency manifest
├── aiken.lock
└── plutus.json # compiled blueprint (CIP-57)This separation is not incidental — it is the core modularity pattern the whole system relies on, and it maps directly onto the requirements from the previous chapter.
1. Validators vs. Library Code
Validators (validators/*.ak) are kept thin. A validator's job is limited to:
locating its own input/output pair in the script context (SR-4),
dispatching to a redeemer-specific handler,
returning a boolean.
Library modules (lib/account/*.ak) hold all the actual business logic: signature verification, datum parsing/validation, chain-continuity checks, oracle lookups. This split exists for three concrete reasons:
Testability: Aiken's
testblocks can exercise library functions directly with constructed fixtures, without needing to assemble a fullScriptContext. Validators, by contrast, are awkward to unit-test in isolation.Reuse across script boundaries: the same "verify this intent signature" or "check chain continuity" logic is needed by more than one validator (e.g., both the Optim Account spend validator and the chain-extension validator need signature verification). Putting it in
lib/accountavoids duplicated, potentially divergent copies of security-critical logic.Auditability: security reviewers can audit
lib/accountonce as the trusted computing base, rather than re-deriving the same invariants across every validator file.
2. Datum Design: Carrying Chain State, Not Just Balance
A recurring anti-pattern in naive eUTxO scaling designs is treating the datum as a pure balance record. Leviathan's account and chain-extension datums instead carry provenance and continuity metadata, because the validator for transaction N+1 has to be able to prove it is a legitimate continuation of transaction N without any off-chain trust:
Modeling the datum this way turns FR-3 (chain-extension continuity) into a local, per-transaction check: the validator for step N+1 only needs to confirm predecessor_hash == hash(datum_at_N) and sequence_no == sequence_no_at_N + 1. It never needs to re-walk or re-verify the entire chain history — this is what keeps chain extension cheap regardless of chain depth.
3. Redeemer Routing Pattern
Rather than one validator per action, Leviathan validators use a sum-typed redeemer with one variant per distinct authorized action, dispatched via when...is. This keeps the number of distinct script hashes (and therefore the number of addresses users/solvers need to reason about) small, while still giving each action path its own, independently auditable branch:
The Revert branch is deliberately kept as simple and conservative as possible — it should be the least complex code path in the validator, since it is the one that has to remain correct even when every other assumption about sequencer liveness has failed.
4. Shared Utility Modules
Cross-cutting concerns are factored into small, single-purpose library modules rather than folded into lib/account wholesale:
lib/account/auth.ak
Signature/multisig verification over canonical intent serialization (FR-4)
lib/account/chain.ak
Chain continuity + sequence-number checks (FR-3)
lib/account/oracle.ak
Reference-input based time-oracle lookups (FR-6)
lib/account/params.ak
Versioned protocol-parameter reads (SR-5)
lib/account/double_satisfaction.ak
Per-input/output correlation helpers (SR-3)
This granularity matters for upgradeability: because Aiken compiles to on-chain script hashes deterministically from source, changing any line in a shared module changes every validator that imports it. Keeping modules narrowly scoped means a change to, say, the oracle-lookup convention doesn't force a redeploy (and re-audit) of validators that never touch time-sensitive logic.
5. Portal (Entry/Exit) as an Isolated Boundary Contract
The portal validator is deliberately not part of the lib/account dependency graph in the other direction — account and chain-extension logic must not import anything from the portal module, and the portal module's logic must be independently auditable without understanding the rest of the chain-extension machinery. This is a defense-in-depth pattern: the highest-value, highest-blast-radius contract (the one actually holding L1 custody of deposited funds) is kept as small and dependency-free as the requirements allow.
6. Composability Rule: No Same-Transaction Coupling
Per FR-8, no validator emits an output whose spending conditions require it to be consumed within the same transaction that created it. This is what preserves the property the whitepaper calls "restoring composability" — any solver, dApp, or user wallet can construct the next link in a chain independently, without needing bespoke integration with whatever produced the previous link, as long as it satisfies the public datum/redeemer interface.
Last updated