Skip to content

fix: open_cdp for iETH/iBTC/iADA — pin the local UPLC evaluator past a 0.2.20 false-negative - #52

Merged
adacapo21 merged 2 commits into
mainfrom
fix/indy-137-uplc-evaluator
Aug 23, 2026
Merged

fix: open_cdp for iETH/iBTC/iADA — pin the local UPLC evaluator past a 0.2.20 false-negative#52
adacapo21 merged 2 commits into
mainfrom
fix/indy-137-uplc-evaluator

Conversation

@adacapo21

Copy link
Copy Markdown
Member

Fixes INDY-137.

open_cdp with ADA collateral failed for iETH, iBTC and iADA — always with
Spend[n] … Trace <expected> CDP output is invalid — while iUSD, iSOL, iEUR and iJPY
built fine. Reported by an external integrator against the published 0.4.0.

The transactions were valid all along. The exact bytes the local evaluator refused were
accepted by the ledger evaluator (Ogmios via Blockfrost), which returned execution units
for every script including spend:1.

Cause

@lucid-evolution/uplc 0.2.20 has a bug in local script evaluation, fixed upstream in
0.2.22. It arrives three levels down: indigo-mcp@indigo-labs/indigo-sdk
@lucid-evolution/lucid@0.4.29@lucid-evolution/uplc@0.2.20.

Isolated by capturing the exact arguments handed to eval_phase_two_raw and replaying them
against both versions:

0.2.20  ->  REJECTED  "… Trace <expected> CDP output is invalid"
0.2.22  ->  ACCEPTED  5 redeemers evaluated

The trigger is CBOR length encoding. cdp_creator builds its expected CDP datum by
embedding the collateral_asset field copied verbatim from the collateral-asset reference
input, and 0.2.20 compares that sub-term byte-sensitively rather than structurally:

validator's copy   d879 82 40 40        definite-length
SDK's encoding     d879 9f 40 40 ff     indefinite-length

The on-chain collateral-asset entries split exactly on that encoding — indefinite for the
four assets that worked, definite for the three that did not. Forcing the encoding flips the
local result in both directions across all seven.

Changes

package.json — pin the evaluator:

"pnpm": { "overrides": { "@lucid-evolution/uplc": ">=0.2.22" } }

scripts/build.sh — the override alone is not enough. The WASM copy step scanned
node_modules with wildcards, so find matched both 0.2.20 and 0.2.22 and whichever came
last won; the build kept shipping the 0.2.20 WASM against 0.2.22 JS glue. The CML pattern had
the same hazard, with two copies present and the wildcard matching a different scope than the
comment claimed. Each WASM is now resolved through the same dependency chain the bundle
imports.

Second commit, independent: @indigo-labs/indigo-sdk 0.3.28 → 0.5.9 and
@lucid-evolution/lucid 0.4.29 → 0.6.1. Not required for the fix — open_cdp already works
on 0.3.28 once the evaluator is pinned — but it brings the SDK current. The API change is
mechanical: currentSlot was dropped from every builder signature, so the argument comes out
at each call site; lucid moves in step or two distinct LucidEvolution types coexist. The
Pyth wiring test's argument positions shift down by one.

The override is kept even though lucid 0.6.1 already requires 0.2.22, because @qbtlabs/x402
still pulls lucid 0.4.29 into the tree.

Verification

  • All seven assets build unsigned CBOR through the MCP protocol (stdio, mainnet, build-only).
  • tsc --noEmit clean · 144/144 tests · Prettier clean.
  • npm packed the result and installed the tarball into a clean directory with no repo and
    no overrides: all seven still build. uplc is bundled rather than a runtime dependency, and
    the shipped WASM is the 960980-byte 0.2.22 build (the broken one is 1005786), so the fix
    reaches consumers.
  • Contracts confirmed unmodified: aiken build --trace-level compact (v1.1.21) reproduced all
    48 validators in smart-contracts-aiken's plutus.json byte-for-byte, and the deployed
    script equals that blueprint applied to its parameters.

Follow-ups (not in this PR)

  • A release is required before this helps downstream — the integrator is on
    @indigoprotocol/indigo-mcp@0.4.0.
  • redeem_cdp remains broken by an SDK guard that does not account for the Pyth
    DeferredValidation path; present in every published version through 0.5.9.
  • leverage_cdp is unusable for every asset: findAllRobs and findTreasuryOref scan
    enterprise script addresses while the real UTxOs carry staking credentials.
  • Amount parameters accept display units and fail late with a misleading message.

open_cdp failed for iETH, iBTC and iADA with ADA collateral, always with
`Spend[n] … Trace <expected> CDP output is invalid`, while iUSD, iSOL, iEUR
and iJPY built fine. The transactions were valid all along: the exact bytes
the local evaluator refused were accepted by the ledger evaluator (Ogmios via
Blockfrost), which returned execution units for every script.

The cause is @lucid-evolution/uplc 0.2.20, fixed upstream in 0.2.22. It
arrives three levels down, via @indigo-labs/indigo-sdk ->
@lucid-evolution/lucid@0.4.29. Isolated by capturing the arguments passed to
eval_phase_two_raw and replaying them against both versions: 0.2.20 rejects,
0.2.22 accepts.

The trigger is CBOR length encoding. The cdp_creator validator builds its
expected CDP datum by embedding the collateral_asset field copied from the
collateral-asset reference input, and 0.2.20 compares that sub-term
byte-sensitively rather than structurally. The on-chain collateral-asset
entries split exactly on that encoding: indefinite-length for the four assets
that worked, definite-length for the three that did not.

The override alone is not enough. The WASM copy step scanned node_modules with
wildcards, so `find` matched both 0.2.20 and 0.2.22 and whichever came last
won; the build kept shipping the 0.2.20 WASM against 0.2.22 JS glue. The CML
pattern had the same hazard, with two copies present and a wildcard matching a
different scope than the comment claimed. Resolve each WASM through the same
dependency chain the bundle imports instead.

No SDK change is needed: open_cdp works on the unchanged 0.3.28 once the
evaluator is fixed.

Verified: all seven assets build unsigned CBOR through the MCP protocol; the
packed tarball installed into a clean directory with no repo and no overrides
still builds all seven, so the fix reaches consumers.

Refs INDY-137
Independent of the evaluator fix in the previous commit — open_cdp already
works on 0.3.28 once the evaluator is pinned. This brings the SDK up to the
current release so later work starts from it.

The API change is mechanical: `currentSlot` was dropped from every builder
signature, since the SDK now derives it from lucid. Removed the argument at
each call site along with the local bindings that fed it. `@lucid-evolution/lucid`
moves in step, otherwise two distinct `LucidEvolution` types coexist and every
build closure fails to typecheck.

The Pyth wiring test asserts argument positions, which all shift down by one,
and depositCdp drops from 10 arguments to 9.

The uplc override stays: @qbtlabs/x402 still pulls lucid 0.4.29, so without it
a 0.2.20 evaluator can still reach the bundle.

Verified: all seven assets build unsigned CBOR through the MCP protocol,
tsc --noEmit clean, 144 tests pass.
@adacapo21
adacapo21 merged commit cb8c30c into main Aug 23, 2026
4 checks passed
@adacapo21 adacapo21 mentioned this pull request Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant