Skill v1.0.1
currentAutomated scan100/100+3 new
version: "1.0.1" name: optimize-contract description: Optimize Scalus/Cardano smart contracts for execution budget (CPU steps and memory). Analyzes @Compile annotated validators for performance issues — expensive patterns, unnecessary allocations, redundant traversals, missed short-circuits. Provides concrete Scalus rewrites with budget impact estimates. Use when reviewing on-chain code performance or when /optimize-contract is invoked. Requires explicit path argument.
Smart Contract Optimization Review
Analyze Scalus/Cardano smart contracts for execution budget optimization opportunities.
Prerequisites — Before You Optimize
- Establish a baseline. Run the validator's tests with budget assertions (
assertBudgetEqualsorassertBudgetWithin) on representative inputs — simple case, worst case, and typical case. - Identify actual hot paths. Don't guess — measure. Use
EvalTestDslbudget assertions to find which code paths dominate the budget. - Optimize surgically. Change one thing at a time and re-measure. Small, targeted changes are safer than sweeping rewrites.
- Re-benchmark after every significant change. Budget is the only ground truth.
Target Code Identification
Find on-chain code by searching for:
- Objects/classes with
@Compileannotation - Objects extending
Validator,DataParameterizedValidator, orParameterizedValidator - Objects compiled with
PlutusV3.compile(),PlutusV2.compile(), orPlutusV1.compile()
Search patterns:
grep -rn "@Compile" --include="*.scala" <path>grep -rn "extends Validator" --include="*.scala" <path>grep -rn "extends DataParameterizedValidator" --include="*.scala" <path>
Workflow
- Discovery: Find all
@Compileannotated code in specified path - Profile: Identify existing budget tests; note current memory/steps
- Analysis: Check each validator against the optimization checklist below
- Prioritize: Rank findings by estimated budget impact (high/medium/low)
- Rewrite: Propose concrete code changes with before/after
- Verify: Run
sbtn quickor specific test to confirm budget improvement - Report: Generate structured report with budget deltas
Optimization Checklist
For detailed patterns with Scalus code examples, see references/patterns.md.
High Impact — Data Structures & Traversals
| ID | Pattern | Problem | Fix | |
|---|---|---|---|---|
| O001 | Multiple list traversals | Separate filter then map then length | Combine into single foldLeft | |
| O002 | foldRight on lists | Not tail-recursive, builds thunks | Use foldLeft with reverse if order matters | |
| O003 | list.flatten | O(n*m) via nested foldRight + ++ | Accumulate with foldLeft and prepend | |
| O004 | list.distinct | O(n^2) — foldLeft with exists | Use SortedMap or deduplicate at source | |
| O005 | list :+ elem (append) | O(n) per append | Use elem +: list (prepend) and reverse once | |
| O006 | Reconstructing Value | Full Value maintains invariants expensively | Use SortedMap or PairList directly when possible | |
| O007 | AssocMap anywhere | get scans to hit-or-end; union is O(n*m) (get per left key, exists per right key) | Use SortedMap: get stops early via Ord, union is one linear merge | |
| O008 | list.map(f).filter(p) | Two traversals, intermediate list | Single foldLeft combining map + filter | |
| O009 | list.length == 0 / map.size == 0 | O(n) traversal; SortedMap.size is O(n) although inline | Use isEmpty on List, SortedMap, AssocMap: one nullList, O(1) | |
| O010 | AssocMap.fromList on large input | O(n^2) dedup | Pre-sort and use SortedMap.fromStrictlyAscendingList |
High Impact — Short-Circuiting & Ordering
| ID | Pattern | Problem | Fix | |||||
|---|---|---|---|---|---|---|---|---|
| O011 | Expensive checks before cheap ones | Wasted budget on failing txs | Put cheapest/most-likely-to-fail checks first | |||||
| O012 | Late require for invalid input | Work done before validation | Fail fast — validate inputs at the top | |||||
| O013 | Linear condition chains | Average n/2 evaluations for n conditions | Structure as binary decision tree | |||||
| O014 | No short-circuit in &&/` | ` | Evaluating both sides always | Scalus &&/` | ` DO short-circuit — ensure expensive side is second | |||
| O015 | list.exists after construction | Building list just to search it | Inline the search into the fold that builds the data |
Medium Impact — Data Representation
| ID | Pattern | Problem | Fix | |
|---|---|---|---|---|
| O016 | === on a BigInt/ByteString behind a type variable | Lowers to equalsData, not equalsInteger/equalsByteString: 1 761 779 vs 832 313 cpu, 2.1x (measured) | Make the key type concrete at the comparison site (a BigInt-keyed or ByteString-keyed helper, not a generic K) | |
| O017 | Constructing tuples/records to return | Allocation + destructuring overhead | Use continuation-passing or accumulator parameters | |
| O018 | List[(A, B)] map operations | ~12 builtins per element | Use PairList — ~4 builtins per element via fstPair/sndPair | |
| O019 | Pattern matching for Data access | Constructs intermediate Scala objects | Use Data builtins directly when structure is known | |
| O020 | Hand-written equalsData(a.toData, b.toData) or a.toData == b.toData | Buys nothing: for every Data-backed type === already lowers to equalsData; both spellings pin to 901 mem / 1 653 665 cpu on a Value (measured) | Derive Eq and write a === b; never compare TxInfo-scale structures whole |
Medium Impact — Computation
| ID | Pattern | Problem | Fix | |
|---|---|---|---|---|
| O021 | pow(2, n) | Generic exponentiation loop | Use exp2(n) — single builtin via byte shift | |
| O022 | Manual log2 via division loop | O(log n) divisions | Use log2(n) — single builtin via integerToByteString | |
| O023 | Recomputing same expression | Duplicated subexpressions | Use let bindings; V3 optimizer has CSE but don't rely on it | |
| O024 | generateErrorTraces = true in prod | Trace strings bloat script and budget | Set generateErrorTraces = false for production builds | |
| O025 | Complex pure computations on-chain | Expensive on-chain work | Move computation off-chain, pass result as redeemer, verify on-chain |
Medium Impact – Stdlib idioms (measured where a number is given)
| ID | Pattern | Problem | Fix | |
|---|---|---|---|---|
| O031 | out.datum.inlineOrFail[T](msg) === expected | Decodes, then compares field-wise: 461 lovelace | out.hasInlineDatum(expected): one equalsData on the wrapped datum, 286 lovelace. Use inlineOrFail only to read fields | |
| O032 | xs.exists(_ === x) | exists is find(p).isDefined: allocates an Option for a Boolean; a fixed per-call tax of 326 483 cpu (miss) / 564 996 cpu (hit) on V3 | xs.contains(x): an intrinsic, no Option, no Eq closure. For a non-equality predicate use forall or a hand fold, not exists | |
| O033 | xs.filter(p).length | 2 traversals plus k mkCons; filter is a non-tail foldRight; no pass fuses them | xs.count(p): one tail-recursive foldLeft, no allocation | |
| O034 | filter(p).length === BigInt(1) then .head, or count(p) === BigInt(1) | 2 passes (or 1 pass plus a second scan for the element); the guard and the lookup are separate | xs.findUniqueOrFail(p, msg): one pass, returns the element, fails on 0 or 2+. Against count(p) === BigInt(1) on inputs: fee 3 175 vs 3 307 (3 inputs), 6 289 vs 6 804 (10 inputs) |
Low Impact — Micro-Optimizations
| ID | Pattern | Problem | Fix | |
|---|---|---|---|---|
| O026 | Small recursive helpers | Call overhead per recursion | Unroll first 1-2 iterations for common small cases | |
| O027 | Non-tail-recursive numeric loops | Stack growth | Rewrite with accumulator parameter | |
| O028 | Redundant FromData/ToData conversions | Serialization round-trips | Keep data in Data form between operations | |
| O029 | list.reverse.foldLeft | Extra O(n) reverse pass | Use foldRight if list is small, or build in correct order | |
| O030 | Building closures in inner loops | Allocation per iteration | Lift closure outside loop if captures don't change |
Key Scalus Optimization Principles
1. Don't Compute, Verify
The most impactful optimization: move work off-chain.
Instead of computing a result on-chain, have the off-chain code compute it and pass it as a redeemer field. The validator only checks correctness.
// Expensive: compute on-chainval sqrtResult = radicand.sqRoot// Cheap: verify pre-computed resultval sqrtResult = redeemer.sqrtValuerequire(sqrtResult * sqrtResult <= radicand)require((sqrtResult + 1) * (sqrtResult + 1) > radicand)
2. Fail Fast
Put cheapest and most-likely-to-fail validations first. Every require that fails early saves the budget of all subsequent code.
// Good: cheap check firstrequire(isSignedBy(txInfo, admin), "not admin")require(expensiveValueCheck(txInfo), "value mismatch")// Bad: expensive check firstrequire(expensiveValueCheck(txInfo), "value mismatch")require(isSignedBy(txInfo, admin), "not admin")
3. Traverse Once
Never traverse a list twice when once will do. Combine filter + map + count into a single fold.
// Bad: three traversalsval filtered = items.filter(_.isValid)val mapped = filtered.map(_.amount)val total = mapped.foldLeft(BigInt(0))(_ + _)// Good: single traversalval total = items.foldLeft(BigInt(0)) { (acc, item) =>if item.isValid then acc + item.amount else acc}
4. Use PairList for Map Operations
PairList uses raw UPLC pair builtins (~4 ops/element) vs List[(A, B)] (~12 ops/element).
// Expensivemap.toList.map { case (k, v) => (k, f(v)) }// Cheap — 3x fewer builtinsmap.toPairList.mapValues(f)
5. Write ===, and Keep Key Types Concrete
For every Data-backed type a === b already lowers to one equalsData builtin. Hand-written equalsData(a.toData, b.toData) or a.toData == b.toData produces identical UPLC (measured: both pin to 901 mem / 1 653 665 cpu on a Value). The cost that is real: === on a BigInt or ByteString behind a type variable emits equalsData instead of equalsInteger / equalsByteString, 1 761 779 vs 832 313 cpu (2.1x, measured).
// Same UPLC, worse to read: do not write thisrequire(equalsData(toData(outputDatum), toData(inputDatum)))// Write this (derive Eq on the datum type)require(outputDatum === inputDatum)// Generic key: equalsData on every step, 2.1x slowerdef lookup[K: Eq](key: K, entries: List[(K, BigInt)]): Option[BigInt] =entries.find(_._1 === key).map(_._2)// Concrete key: equalsIntegerdef lookup(key: BigInt, entries: List[(BigInt, BigInt)]): Option[BigInt] =entries.find(_._1 === key).map(_._2)
For a continuing datum, compare without decoding: out.hasInlineDatum(expected) costs 286 lovelace against 461 for out.datum.inlineOrFail[T](msg) === expected (measured, O031).
6. Leverage Ledger Invariants
The ledger guarantees: inputs are sorted by TxOutRef, values are ordered by policy ID, outputs never contain negative quantities, minted values exclude ADA. Align your algorithms with these invariants instead of re-validating them.
7. Build Caches for Repeated Lookups
If you check membership in the same set multiple times, build a decision closure once.
// Bad: O(n) per checkrequire(signatories.exists(_ === admin1))require(signatories.exists(_ === admin2))// Better: single traversal, check bothval (hasAdmin1, hasAdmin2) = signatories.foldLeft((false, false)) { case ((a1, a2), sig) =>(a1 || sig === admin1, a2 || sig === admin2)}require(hasAdmin1 && hasAdmin2)
8. Use Cheap Builtins for Math
log2 and exp2 use integerToByteString/shiftByteString — much cheaper than iterative computation.
// Cheapval bits = x.log2val powerOf2 = n.exp2// Expensiveval bits = manualLog2Loop(x)val powerOf2 = pow(BigInt(2), n)
Compiler Options That Affect Budget
given Options = Options(// Use V3 lowering for better optimizations (CSE, CaseConstr)targetLoweringBackend = TargetLoweringBackend.SirToUplcV3Lowering,// Disable error traces for production — saves budget on every requiregenerateErrorTraces = false,// Enable UPLC optimizer pipelineoptimizeUplc = true)
Measuring Budget
Use EvalTestDsl for precise budget measurement:
import scalus.testing.dsl.EvalTestDsl.*// Exact budget match — catches regressions AND improvementseval(compiled).onVM(PlutusV3.makePlutusV3VM()).expectSuccess().assertBudgetEquals(memory = 129528, steps = 37_067868)// Upper bound — for tests where budget fluctuates slightly between buildseval(compiled).onVM(PlutusV3.makePlutusV3VM()).expectSuccess().assertBudgetWithin(memory = 140000, steps = 40_000000)
Format convention: Always use named parameters and _ at million boundary: ExUnits(memory = 129528, steps = 37_067868)
Output Format
Use clickable file_path:line_number format for all code locations.
Finding Format
### [IMPACT] ID: Optimization Name**Location:** `full/path/to/File.scala:LINE`**Estimated savings:** ~X% memory, ~Y% steps (or: high/medium/low)**Current code** (`full/path/to/File.scala:LINE-LINE`):
// actual code from file
**Optimized code:**
// proposed optimization
**Rationale:** Why this is faster and what budget cost it avoids.---
Summary Table
## Summary| ID | Impact | Location | Pattern | Est. Savings ||----|--------|----------|---------|-------------|| O-01 | High | `path/File.scala:123` | Multiple traversals → single fold | ~30% steps || O-02 | Medium | `path/File.scala:87` | inlineOrFail === x → hasInlineDatum(x) | 286 vs 461 lovelace || O-03 | Low | `path/File.scala:200` | Unroll small recursion | ~2% steps |**Current budget:** ExUnits(memory = X, steps = Y)**Estimated budget after optimization:** ExUnits(memory = X', steps = Y')
Interactive Workflow
For each finding:
- Display issue with location and proposed optimization
- Prompt: "Apply optimization? [y/n/s/d]"
- y: Apply change, re-run budget test
- n: Skip, log as "declined"
- s: Skip without logging
- d: Show detailed budget breakdown
- After all findings: run
sbtn quickto verify correctness - Generate summary report with actual budget deltas (before/after)
Reference
For detailed optimization patterns with Scalus code examples, see:
references/patterns.md— Full pattern catalog with before/after code and budget estimates