Two HIGH validator-side bugs + several MED/LOW off-chain issues found in the subagent-driven audit on this branch. New validator hash: a8081acef26935d9b5f44b92052178e17301b6d6e6808c91c5b56f5d. ## HIGH-1: Deposit redeemer let depositors drain tokens aiken-escrow/validators/escrow.ak Deposit branch now requires `value_geq_value(new_value, in_value)` before computing net_added. Previously net_added could carry negative quantities (when new_value < in_value component-wise), letting a depositor write a matching new_d.deposits with reduced values and pocket the difference as wallet change. Latent under v1 ADA-only MCP usage but the validator must hold against all callers. ## HIGH-2: Empty/partial deposits enabled funds drain via Veto/Refund Veto and Refund branches now require `value_eq(deposits_to_value(d.deposits), in_value)` — the tracked deposits must account for the full locked value. Previously `refund_outputs_satisfy(_, [])` was vacuously true on empty deposits, so a driver could fire Veto/Refund on an escrow opened with `initial_contributor=None` (deposits=[], in_value>0) and pocket the input's lovelace as change. Defense in depth: escrow_open builder now refuses `initial_contributor=None`. New helper `deposits_to_value` folds deposit FlatValues into a Value via `assets.add` for the equality check. ## MED: off-chain fixes - escrow_open min-utxo bumped 1M → 2M (Conway-era inline-datum + script-address outputs need ~1.4-1.7 ADA, NOT the 1 ADA default). - escrow_settle_unsigned + escrow_refund_timeout_unsigned now derive `validity_lower_ms` via slot_to_posix_ms(network, slot) instead of Koios's `block_time*1000` — the chain reconstructs `lower` from the slot, so Koios's ~1s drift could pass off-chain preflight while the chain rejects at the strict-`>` boundary. - escrow_open_unsigned MCP tool no longer accepts (and silently discards) `fee_lovelace` — the unsigned-tx builder auto-estimates. ## LOW: defensive depth - escrow_veto + escrow_refund_timeout: `qty as u64` → `u64::try_from` so a corrupt or adversarial datum with negative i128 qty can't slip through with a wraparound. ## Tests - 36 escrow builder tests pass (added rejects_no_initial_contributor) - 132 dao tests pass under --features escrow_wip - aldabra-mcp release build clean ## Infra - Validator artifact files (plutus.json, validator.cbor.hex) regenerated. Dockerfile already wired to bake them at /etc/aldabra/escrow/ for MCP tools' validator_script_path arg. - Internal audit findings written up at audits/2026-05-09-escrow-internal-audit.md including the v2-deferred items (multi-asset spend-input, lovelace-not-cross-checked, etc.) Third-party audit still required before any mainnet deployment. |
||
|---|---|---|
| .. | ||
| validators | ||
| .gitignore | ||
| aiken.toml | ||
| plutus.json | ||
| README.md | ||
| validator.cbor.hex | ||
aiken-escrow
⚠️ WIP — UNAUDITED. Preprod testing only. Do NOT route mainnet funds through this validator. No third-party security review has been performed.
Two-party agreement-with-veto escrow validator (Plutus V3, Aiken
v1.1.21). The off-chain (Rust) side lives in crates/aldabra-dao behind
the escrow_wip feature flag.
Spec
audits/2026-05-09-escrow-spec.md documents the state machine, datum
shape, and redeemer invariants.
State machine:
Open ──(both sign Agree)──▶ Agreed{at} ──(lock_period elapsed, no veto)──▶ Settle (→ recipient)
│ │
│ └──(A or B fires Veto)─────────────▶ Refund (per-contributor)
│
└──(open_deadline passed, no agreement)─────────────────────────▶ Refund (per-contributor)
Build
cd aiken-escrow
aiken check # type check + tests
aiken build # produces plutus.json blueprint
The blueprint at plutus.json is consumed by aldabra's escrow builders
to construct script addresses + spending witnesses.
Threat model (out-of-scope for v1)
These are KNOWN gaps the validator does not protect against. They inform the WIP designation:
- Datum CBOR canonicality. The Deposit redeemer compares
cbor.serialise(expected) == cbor.serialise(new.deposits). If the Aiken stdlib's CBOR encoder is non-canonical for any input shape (e.g. map ordering), an attacker could submit a continuing output with the same logical content but byte-different and bypass the check. We mitigate by usingList<Deposit>(not Map) which has deterministic order, but external review should re-confirm. - Stake credential preservation on refund outputs. Refund outputs are derived from contributor PKHs as null-stake base addresses. If a contributor's wallet uses a custom stake credential, refund value bypasses their stake-delegation pool. Acceptable v1 tradeoff; documented in spec.
- Min-utxo per refund leg. Validator does not enforce min-utxo per refund output — assumes the off-chain builder has already ensured each deposit cleared min-utxo at deposit time. A pathological multi-asset deposit that splits below min-utxo on refund would brick the escrow until manual recovery.
- Multi-script-input attack. If a single tx spends multiple escrow UTxOs simultaneously with overlapping signers, the per-UTxO validator runs independently. Cross-UTxO consistency is not enforced.
Status
- Validator compiles (
aiken buildproducesplutus.json). - Off-chain codecs in
aldabra-dao::agora::escrow. - Off-chain unsigned-tx builders (5 paths).
- MCP tool wrappers.
- Preprod E2E (open → both deposit → agree → settle).
- Preprod E2E (open → agree → veto).
- Preprod E2E (open → refund-timeout).
- External audit.
- Mainnet release gate.