From 78ed92304e0580a5e9f5d2b05fb445e621d9532e Mon Sep 17 00:00:00 2001 From: Kayos Date: Sat, 9 May 2026 11:38:45 -0700 Subject: [PATCH] feat(escrow_wip): aiken validator + plutus.json blueprint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠ WIP — UNAUDITED. Plutus V3, Aiken v1.1.21. Preprod-only. Five-redeemer two-party agreement-with-veto escrow validator. Mirrors the off-chain codecs at crates/aldabra-dao/src/agora/escrow.rs. Validator script hash: 223aa7ace4a98ff5b8f8988c1c07b846c046de1a2bc9e8dc77411486 Compiled UPLC size: 7902 bytes. Datum: ProductIsData (Constr 0 [a, b, recipient, deadline, lock, state, deposits]). Redeemer: Constr 0..4 (Deposit | Agree | Veto | Settle | Refund). DepositEntry.value uses concrete Pairs> since cardano/assets.Value is opaque (datums require concrete types). Pairs encode as Plutus Map at the dataType layer — matches the off-chain EscrowValue codec's PlutusData::Map(KeyValuePairs) emission. Build: cd aiken-escrow && aiken build (produces plutus.json blueprint). Threat-model gaps explicitly documented in aiken-escrow/README.md: - CBOR canonicality of Pairs serialisation (validator equality check) - Stake-credential null'd on refund outputs (intentional — protects pool delegation privacy at cost of stake reward routing) - No min-utxo enforcement on refund legs (off-chain builder's job) - No multi-script-input cross-UTxO consistency External audit gates mainnet deployment. --- aiken-escrow/.gitignore | 3 + aiken-escrow/README.md | 73 ++++++ aiken-escrow/aiken.toml | 18 ++ aiken-escrow/plutus.json | 200 ++++++++++++++++ aiken-escrow/validators/escrow.ak | 380 ++++++++++++++++++++++++++++++ 5 files changed, 674 insertions(+) create mode 100644 aiken-escrow/.gitignore create mode 100644 aiken-escrow/README.md create mode 100644 aiken-escrow/aiken.toml create mode 100644 aiken-escrow/plutus.json create mode 100644 aiken-escrow/validators/escrow.ak diff --git a/aiken-escrow/.gitignore b/aiken-escrow/.gitignore new file mode 100644 index 0000000..c5528e6 --- /dev/null +++ b/aiken-escrow/.gitignore @@ -0,0 +1,3 @@ +# Aiken build artifacts +build/ +aiken.lock diff --git a/aiken-escrow/README.md b/aiken-escrow/README.md new file mode 100644 index 0000000..d365725 --- /dev/null +++ b/aiken-escrow/README.md @@ -0,0 +1,73 @@ +# 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 + +```bash +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 using `List` (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 + +- [x] Validator compiles (`aiken build` produces `plutus.json`). +- [x] 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. diff --git a/aiken-escrow/aiken.toml b/aiken-escrow/aiken.toml new file mode 100644 index 0000000..7b66b09 --- /dev/null +++ b/aiken-escrow/aiken.toml @@ -0,0 +1,18 @@ +name = "sulkta-coop/escrow" +version = "0.0.0" +compiler = "v1.1.21" +plutus = "v3" +license = "Apache-2.0" +description = "Aiken contracts for project 'sulkta-coop/escrow'" + +[repository] +user = "sulkta-coop" +project = "escrow" +platform = "github" + +[[dependencies]] +name = "aiken-lang/stdlib" +version = "v3.1.0" +source = "github" + +[config] diff --git a/aiken-escrow/plutus.json b/aiken-escrow/plutus.json new file mode 100644 index 0000000..c29d950 --- /dev/null +++ b/aiken-escrow/plutus.json @@ -0,0 +1,200 @@ +{ + "preamble": { + "title": "sulkta-coop/escrow", + "description": "Aiken contracts for project 'sulkta-coop/escrow'", + "version": "0.0.0", + "plutusVersion": "v3", + "compiler": { + "name": "Aiken", + "version": "v1.1.21+42babe5" + }, + "license": "Apache-2.0" + }, + "validators": [ + { + "title": "escrow.escrow.spend", + "datum": { + "title": "datum", + "schema": { + "$ref": "#/definitions/escrow~1EscrowDatum" + } + }, + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/escrow~1EscrowRedeemer" + } + }, + "compiledCode": "590f6c01010029800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cdc3a400530080024888966002600460106ea800e3300130093754007370e90024dc3a40013008375400891111991192cc004c01401226464b30013016002801c590131bae3014001301037540171598009804802456600260206ea802e0031640451598009803002456600260206ea802e00316404515980099b87480180122b3001301037540170018b20228acc004cdc3a401000915980098081baa00b800c5901145900e201c4038807100e0acc004c010c038dd5000c4c8cc8966002600e60226ea800a330013015301237540052301630170019180b180b980b980b980b980b980b800c8c058c05cc05c0064446466446600400400244b3001001801c4c8c96600266e440180062b30013371e00c0031375a60340050054061133004004301e00340606eb8c060004c06c005019191919800800803112cc004006007132325980099b910080018acc004cdc7804000c4dd5980d801401501944cc010010c07c00d0191bae3019001301c0014068297adef6c6014800244646600200200644b30010018a518acc004c00cc064006266004004603400314a080a101748c010cc054c010cc054dd4800a5eb80cc055300103d87a80004bd704888c8cc00400401089660020031004899801980d00099801001180d800a03091194c0040060070024004444b30010028800c4c8cc8a600200d301e0059919800800802912cc00400626603c66ec0dd48021ba60034bd6f7b63044c8cc896600266e4401c00a2b30013371e00e0051325980099baf4c101a000374c003100289981119bb037520106e9800400901e194c0040066eacc08001200e800888966002005100189919914c00401a605200b32330010010052259800800c4cc0a4cdd81ba9004375000697adef6c6089919912cc004cdc8803801456600266e3c01c00a264b30013370e00266e052000007880144cc0b4cdd81ba9008375066e0000401c0090291bad302a00389981619bb0375200e6ea001801102844cc0b000ccc0140140050281bae3027001302c002302a00140a08030dd718110009bad30230013025002408d133021337606ea401cdd3003002203a89981080199802802800a03a375c60380026042004603e00280e90061bae301700137566030002603400480c24602c602e602e602e0032301630173017301730173017301730170019180b180b980b980b980b980b800cc044dd5006c8c058c05cc05cc05cc05c006446466002002006446600600260040052232330010013758603060326032603260326032603260326032602a6ea800c896600200314a115980099b8f375c603200200714a3133002002301a001405080ba4646600200200444b30010018a5eb82264664466446600400400244b30010018801c4c8cc074dd39980e9ba90053301d301a0013301d301b0014bd7019801801980f801180e800a036375660300066eb8c054004cc00c00cc068008c0600050164dd7a6103d8798000488888888888888888a60026eacc044c090dd5180898121baa012981398121baa3011302437540252232330010010022259800800c528c566002646464660286eacc060c0acdd50019191980b1bab302d0022337126eb4c0b800660020090029bae302d00140606eb8c0ac006600200d4bd6f7b6304896600266ebcc0bcc0b0dd5001001c4cc048004dd5980c98161baa0028800a054404460226eb8c0b0c0a4dd50009815800c4cc008008c0b0006294102620529192cc004c07cc094dd5000c4c05ccc0a0c0a4c098dd5000a5eb822980103d87a800040906050604a6ea8c0a0c094dd5180498129baa0019119912cc0040060051598009816000c4c8c966002604060526ea8006264b30013020302a375400313232323232323298009bac30350019bae30350079bae30350069bae30350059bad30350049bad3035003488888966002607800f1323322598009818001456600260766ea800e0031640f1159800981a00144c8c96600260820050038b207c375a607e00260766ea800e2c81c9039181c1baa001133019007225980080144c0b0cc0f4dd39981e8089981e9814181d9baa0134bd7025eb82264b30013031303b37540031323322598009821800c4cc07cdd59821000912cc00400a2600e608a01113232330233756608600444b300100289802982480344c8cc88c010c130014dd718228009bad3046001304800241186eb8c104004c1100090424590401bae30400013041001303c37540031640e8607c00481e0c0ec0222c81c8606a0026068002606600260640026062002606000260566ea80062c8148c0b4c0a8dd5000c530103d87a800040a0602860526ea8004c0ac00600481490290a6103d87a800032330010010032259800800c52f5c1133225980099baf302d302a375400400b13302c00233004004001899802002000a050302b001302c00140a4911112cc004c07802e264b30013007300d302a3754033159800acc004cdc78009bae302d302a375403314a313371e0026eb8c05cc0a8dd500ca0508acc004cc024088006264b30013020302a3754003132325980099b8f375c6060605a6ea8004dd7181818169baa01c8acc004cdc79bae301a302d37540026eb8c068c0b4dd500e456600266e3cdd7180c18169baa001375c6030605a6ea80722b30013370e6eb4c048c0b4dd50009bad3012302d375403915980099b87375a601c605a6ea8004dd6980718169baa01c8acc004c028c040c0b4dd5000c4ca600244466008006464b30013371e0020091337606ea4004dd3001c4cdd81ba9001303300240c46eb8c0c400644646600200200644b30010018801c4cc0d0c0d4004cc008008c0d8005033488c8cc00400400c896600200314c0103d87a80008992cc004cdc78021bae3032001898121981a9819800a5eb82266006006606e0048188c0d4005033244466e3cdd99ba798009111919800800802112cc0040062009133003303900133002002303a00140dd3758603a60626ea808264660020026020660306eacc0d4c0d801cc8cc00400403c896600200314bd6f7b63044c8cc0dccdd8181a0009ba632330010013756606c00444b30010018a5eb7bdb1822646607466ec0c0dc004dd419b8148000dd6981c00099801801981e001181d000a070330030033039002303700140d444b30010018a5eb7bdb182265300130380019bae303700199801801981c00124453001375c6074007375a6074607600733007001002488966002605e60726ea800633001004802ccc02cdd5981e981d1baa0013303c337606ea400cdd400125eb7bdb18100c44cc02c010cc0f0cdd81ba9005374c6607866ec0dd48019ba80024bd6f7b63025eb7bdb18103806eb0c0d80050342444b300132330010010032259800800c528456600266e3cdd7181c981b1baa303900100d8a51899801001181d000a06840dd132330010010032259800800c52f5c1133038325980099b8f375c6074606e6ea800403a26050660726074606e6ea8004cc0e4dd34c0040126eacc090c0dcdd5000c88c8cc8966002606260766ea800a33001004801e60020033756607e60786ea800a44646644b3001303630403754005132330150052325980099b8f001006899bb037520026ea000e266ec0dd480098220012084375c608400266e00dd6982218209baa0020018998090021982199bb037520066ea00052f5bded8c081f8c8cc00400400c896600200314c0103d87a80008992cc004cdc78021bae304100189819998221821000a5eb82266006006608c0048200c1100050421bad303f003375c607a004805900e44cc034010cc0f8cdd81ba9003374c00297adef6c6040e8660140040026eacc0e800cdd7181c001200c4bd7044005035181c80099801001181d000a06e8991919800800802112cc0040062007133039303a00133002002303b00140e06606c604a6606c6ea402ccc0d8dd3000a5eb812f5c081906eccc074c0c4dd5002888c8cc00400400c896600200314bd6f7b63044cc0ccc00cc0d0004cc008008c0d400503245902b45902b45902b45902b45902b45902b18178009bac302e302b37540031640a4660046eb0c054c0a8dd5011002c590284590284590281bae302c302937540491325980098118064566002600e601a60546ea80662b300133009022375c605a60546ea80662b300133009022375c602e60546ea8066264b30013020302a37540031325980099b89001375a602260586ea806e264b30013022302c3754003132332259800acc004cc01c00403226600e01800314a081722b30013371e6eb8c0ccc0c0dd50011bae30333030375403f15980099b8f375c603a60606ea8008dd7180e98181baa01f8acc004cdc79bae301b303037540046eb8c06cc0c0dd500fc56600266e1cdd6980a98181baa002375a602a60606ea807e2b30013370e6eb4c044c0c0dd50011bad30113030375403f15980099b8f3766603860606ea8008dd9980e18181baa01f899baf30133030375400466e95200233032375000a97ae08b205c8b205c8b205c8b205c8b205c8b205c8b205c30310013756606260640026eb0c0c0c0b4dd5000c5902b198021bac3017302c375404800f1640a86eb4c0b8c0acdd5000c59029192cc004c090c0a8dd5000c4c070cc0b4c0b8c0acdd5000a5eb82298103d87a800040a4605a60546ea8c05cc0a8dd5180718151baa0228b20508b20508b20508acc004c0800322b3001302330293754601a60546ea80662b300159800998048111bae302d302a375403314a3133009022375c602e60546ea806502844cc010dd6180a98151baa0223758602c60546ea80662c81422c81422b30013370e900300644c966002604860546ea8006264b30013021302b375400315980099b88337006eb4c0bcc0b0dd50011bad300d302c37540366eb4c0bcc0b0dd5000c4c8cc01260026eb0c060c0b4dd5012d2f5bded8c1225980099baf3032302f375400400713301500137566038605e6ea800a20028169014004980a1bae3017302c37540371640a91640a860080471640a4601a60546ea80662b30013007300d302a375403313259800981018151baa0018acc004cdc41bad3010302b37540346eb4c0b8c0acdd5000c4cc014dd6180b18159baa0233758602e60566ea806a2c814a2c8148c00c08a2c8141028205040a0446466002002601400444b30010018a518acc004c8c8cdc49bad3032303300198008034dd718190014dd71819000a03230320013758606000313300200230310018a5040ac8171027116404064660020026eb0c050c044dd5004912cc004006298103d87a80008992cc004cdd7980b18099baa001006898021980a800a5eb82266006006602e0048088c054005013180998081baa002374a900045900d18080021808180880222c8038601000260066ea802229344d95900101", + "hash": "223aa7ace4a98ff5b8f8988c1c07b846c046de1a2bc9e8dc77411486" + }, + { + "title": "escrow.escrow.else", + "redeemer": { + "schema": {} + }, + "compiledCode": "590f6c01010029800aba2aba1aba0aab9faab9eaab9dab9a488888896600264653001300800198041804800cdc3a400530080024888966002600460106ea800e3300130093754007370e90024dc3a40013008375400891111991192cc004c01401226464b30013016002801c590131bae3014001301037540171598009804802456600260206ea802e0031640451598009803002456600260206ea802e00316404515980099b87480180122b3001301037540170018b20228acc004cdc3a401000915980098081baa00b800c5901145900e201c4038807100e0acc004c010c038dd5000c4c8cc8966002600e60226ea800a330013015301237540052301630170019180b180b980b980b980b980b980b800c8c058c05cc05c0064446466446600400400244b3001001801c4c8c96600266e440180062b30013371e00c0031375a60340050054061133004004301e00340606eb8c060004c06c005019191919800800803112cc004006007132325980099b910080018acc004cdc7804000c4dd5980d801401501944cc010010c07c00d0191bae3019001301c0014068297adef6c6014800244646600200200644b30010018a518acc004c00cc064006266004004603400314a080a101748c010cc054c010cc054dd4800a5eb80cc055300103d87a80004bd704888c8cc00400401089660020031004899801980d00099801001180d800a03091194c0040060070024004444b30010028800c4c8cc8a600200d301e0059919800800802912cc00400626603c66ec0dd48021ba60034bd6f7b63044c8cc896600266e4401c00a2b30013371e00e0051325980099baf4c101a000374c003100289981119bb037520106e9800400901e194c0040066eacc08001200e800888966002005100189919914c00401a605200b32330010010052259800800c4cc0a4cdd81ba9004375000697adef6c6089919912cc004cdc8803801456600266e3c01c00a264b30013370e00266e052000007880144cc0b4cdd81ba9008375066e0000401c0090291bad302a00389981619bb0375200e6ea001801102844cc0b000ccc0140140050281bae3027001302c002302a00140a08030dd718110009bad30230013025002408d133021337606ea401cdd3003002203a89981080199802802800a03a375c60380026042004603e00280e90061bae301700137566030002603400480c24602c602e602e602e0032301630173017301730173017301730170019180b180b980b980b980b980b800cc044dd5006c8c058c05cc05cc05cc05c006446466002002006446600600260040052232330010013758603060326032603260326032603260326032602a6ea800c896600200314a115980099b8f375c603200200714a3133002002301a001405080ba4646600200200444b30010018a5eb82264664466446600400400244b30010018801c4c8cc074dd39980e9ba90053301d301a0013301d301b0014bd7019801801980f801180e800a036375660300066eb8c054004cc00c00cc068008c0600050164dd7a6103d8798000488888888888888888a60026eacc044c090dd5180898121baa012981398121baa3011302437540252232330010010022259800800c528c566002646464660286eacc060c0acdd50019191980b1bab302d0022337126eb4c0b800660020090029bae302d00140606eb8c0ac006600200d4bd6f7b6304896600266ebcc0bcc0b0dd5001001c4cc048004dd5980c98161baa0028800a054404460226eb8c0b0c0a4dd50009815800c4cc008008c0b0006294102620529192cc004c07cc094dd5000c4c05ccc0a0c0a4c098dd5000a5eb822980103d87a800040906050604a6ea8c0a0c094dd5180498129baa0019119912cc0040060051598009816000c4c8c966002604060526ea8006264b30013020302a375400313232323232323298009bac30350019bae30350079bae30350069bae30350059bad30350049bad3035003488888966002607800f1323322598009818001456600260766ea800e0031640f1159800981a00144c8c96600260820050038b207c375a607e00260766ea800e2c81c9039181c1baa001133019007225980080144c0b0cc0f4dd39981e8089981e9814181d9baa0134bd7025eb82264b30013031303b37540031323322598009821800c4cc07cdd59821000912cc00400a2600e608a01113232330233756608600444b300100289802982480344c8cc88c010c130014dd718228009bad3046001304800241186eb8c104004c1100090424590401bae30400013041001303c37540031640e8607c00481e0c0ec0222c81c8606a0026068002606600260640026062002606000260566ea80062c8148c0b4c0a8dd5000c530103d87a800040a0602860526ea8004c0ac00600481490290a6103d87a800032330010010032259800800c52f5c1133225980099baf302d302a375400400b13302c00233004004001899802002000a050302b001302c00140a4911112cc004c07802e264b30013007300d302a3754033159800acc004cdc78009bae302d302a375403314a313371e0026eb8c05cc0a8dd500ca0508acc004cc024088006264b30013020302a3754003132325980099b8f375c6060605a6ea8004dd7181818169baa01c8acc004cdc79bae301a302d37540026eb8c068c0b4dd500e456600266e3cdd7180c18169baa001375c6030605a6ea80722b30013370e6eb4c048c0b4dd50009bad3012302d375403915980099b87375a601c605a6ea8004dd6980718169baa01c8acc004c028c040c0b4dd5000c4ca600244466008006464b30013371e0020091337606ea4004dd3001c4cdd81ba9001303300240c46eb8c0c400644646600200200644b30010018801c4cc0d0c0d4004cc008008c0d8005033488c8cc00400400c896600200314c0103d87a80008992cc004cdc78021bae3032001898121981a9819800a5eb82266006006606e0048188c0d4005033244466e3cdd99ba798009111919800800802112cc0040062009133003303900133002002303a00140dd3758603a60626ea808264660020026020660306eacc0d4c0d801cc8cc00400403c896600200314bd6f7b63044c8cc0dccdd8181a0009ba632330010013756606c00444b30010018a5eb7bdb1822646607466ec0c0dc004dd419b8148000dd6981c00099801801981e001181d000a070330030033039002303700140d444b30010018a5eb7bdb182265300130380019bae303700199801801981c00124453001375c6074007375a6074607600733007001002488966002605e60726ea800633001004802ccc02cdd5981e981d1baa0013303c337606ea400cdd400125eb7bdb18100c44cc02c010cc0f0cdd81ba9005374c6607866ec0dd48019ba80024bd6f7b63025eb7bdb18103806eb0c0d80050342444b300132330010010032259800800c528456600266e3cdd7181c981b1baa303900100d8a51899801001181d000a06840dd132330010010032259800800c52f5c1133038325980099b8f375c6074606e6ea800403a26050660726074606e6ea8004cc0e4dd34c0040126eacc090c0dcdd5000c88c8cc8966002606260766ea800a33001004801e60020033756607e60786ea800a44646644b3001303630403754005132330150052325980099b8f001006899bb037520026ea000e266ec0dd480098220012084375c608400266e00dd6982218209baa0020018998090021982199bb037520066ea00052f5bded8c081f8c8cc00400400c896600200314c0103d87a80008992cc004cdc78021bae304100189819998221821000a5eb82266006006608c0048200c1100050421bad303f003375c607a004805900e44cc034010cc0f8cdd81ba9003374c00297adef6c6040e8660140040026eacc0e800cdd7181c001200c4bd7044005035181c80099801001181d000a06e8991919800800802112cc0040062007133039303a00133002002303b00140e06606c604a6606c6ea402ccc0d8dd3000a5eb812f5c081906eccc074c0c4dd5002888c8cc00400400c896600200314bd6f7b63044cc0ccc00cc0d0004cc008008c0d400503245902b45902b45902b45902b45902b45902b18178009bac302e302b37540031640a4660046eb0c054c0a8dd5011002c590284590284590281bae302c302937540491325980098118064566002600e601a60546ea80662b300133009022375c605a60546ea80662b300133009022375c602e60546ea8066264b30013020302a37540031325980099b89001375a602260586ea806e264b30013022302c3754003132332259800acc004cc01c00403226600e01800314a081722b30013371e6eb8c0ccc0c0dd50011bae30333030375403f15980099b8f375c603a60606ea8008dd7180e98181baa01f8acc004cdc79bae301b303037540046eb8c06cc0c0dd500fc56600266e1cdd6980a98181baa002375a602a60606ea807e2b30013370e6eb4c044c0c0dd50011bad30113030375403f15980099b8f3766603860606ea8008dd9980e18181baa01f899baf30133030375400466e95200233032375000a97ae08b205c8b205c8b205c8b205c8b205c8b205c8b205c30310013756606260640026eb0c0c0c0b4dd5000c5902b198021bac3017302c375404800f1640a86eb4c0b8c0acdd5000c59029192cc004c090c0a8dd5000c4c070cc0b4c0b8c0acdd5000a5eb82298103d87a800040a4605a60546ea8c05cc0a8dd5180718151baa0228b20508b20508b20508acc004c0800322b3001302330293754601a60546ea80662b300159800998048111bae302d302a375403314a3133009022375c602e60546ea806502844cc010dd6180a98151baa0223758602c60546ea80662c81422c81422b30013370e900300644c966002604860546ea8006264b30013021302b375400315980099b88337006eb4c0bcc0b0dd50011bad300d302c37540366eb4c0bcc0b0dd5000c4c8cc01260026eb0c060c0b4dd5012d2f5bded8c1225980099baf3032302f375400400713301500137566038605e6ea800a20028169014004980a1bae3017302c37540371640a91640a860080471640a4601a60546ea80662b30013007300d302a375403313259800981018151baa0018acc004cdc41bad3010302b37540346eb4c0b8c0acdd5000c4cc014dd6180b18159baa0233758602e60566ea806a2c814a2c8148c00c08a2c8141028205040a0446466002002601400444b30010018a518acc004c8c8cdc49bad3032303300198008034dd718190014dd71819000a03230320013758606000313300200230310018a5040ac8171027116404064660020026eb0c050c044dd5004912cc004006298103d87a80008992cc004cdd7980b18099baa001006898021980a800a5eb82266006006602e0048088c054005013180998081baa002374a900045900d18080021808180880222c8038601000260066ea802229344d95900101", + "hash": "223aa7ace4a98ff5b8f8988c1c07b846c046de1a2bc9e8dc77411486" + } + ], + "definitions": { + "ByteArray": { + "dataType": "bytes" + }, + "Int": { + "dataType": "integer" + }, + "List": { + "dataType": "list", + "items": { + "$ref": "#/definitions/escrow~1DepositEntry" + } + }, + "aiken/crypto/VerificationKeyHash": { + "title": "VerificationKeyHash", + "dataType": "bytes" + }, + "escrow/AssetEntries": { + "title": "AssetEntries", + "dataType": "map", + "keys": { + "$ref": "#/definitions/ByteArray" + }, + "values": { + "$ref": "#/definitions/Int" + } + }, + "escrow/DepositEntry": { + "title": "DepositEntry", + "anyOf": [ + { + "title": "DepositEntry", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "contributor", + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + }, + { + "title": "value", + "$ref": "#/definitions/escrow~1FlatValue" + } + ] + } + ] + }, + "escrow/EscrowDatum": { + "title": "EscrowDatum", + "anyOf": [ + { + "title": "EscrowDatum", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "party_a", + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + }, + { + "title": "party_b", + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + }, + { + "title": "recipient", + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + }, + { + "title": "open_deadline_ms", + "$ref": "#/definitions/Int" + }, + { + "title": "lock_period_ms", + "$ref": "#/definitions/Int" + }, + { + "title": "state", + "$ref": "#/definitions/escrow~1EscrowState" + }, + { + "title": "deposits", + "$ref": "#/definitions/List" + } + ] + } + ] + }, + "escrow/EscrowRedeemer": { + "title": "EscrowRedeemer", + "anyOf": [ + { + "title": "Deposit", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "contributor", + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + } + ] + }, + { + "title": "Agree", + "dataType": "constructor", + "index": 1, + "fields": [] + }, + { + "title": "Veto", + "dataType": "constructor", + "index": 2, + "fields": [] + }, + { + "title": "Settle", + "dataType": "constructor", + "index": 3, + "fields": [] + }, + { + "title": "Refund", + "dataType": "constructor", + "index": 4, + "fields": [] + } + ] + }, + "escrow/EscrowState": { + "title": "EscrowState", + "anyOf": [ + { + "title": "Open", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "Agreed", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "title": "agreed_at_ms", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "escrow/FlatValue": { + "title": "FlatValue", + "dataType": "map", + "keys": { + "$ref": "#/definitions/ByteArray" + }, + "values": { + "$ref": "#/definitions/escrow~1AssetEntries" + } + } + } +} \ No newline at end of file diff --git a/aiken-escrow/validators/escrow.ak b/aiken-escrow/validators/escrow.ak new file mode 100644 index 0000000..800930f --- /dev/null +++ b/aiken-escrow/validators/escrow.ak @@ -0,0 +1,380 @@ +// ⚠️ WIP — UNAUDITED. EXPERIMENTAL. DO NOT USE WITH MAINNET FUNDS. +// +// Aldabra escrow validator — v1 (Plutus V3 / Aiken v1.1.x) +// +// Status: feature-flagged behind `--features escrow_wip` in the off-chain +// crates. Tested only on preprod_test2 by Sulkta-Coop. No third-party audit +// has been performed. Do NOT deploy to mainnet, do NOT route real value +// through this script until external review is complete. +// +// Two-party agreement-with-veto escrow. Spec: audits/2026-05-09-escrow-spec.md +// +// State machine: +// Open ─(both sign Agree)─▶ Agreed{at} ─(lock elapsed, no veto)─▶ Settle (→ recipient) +// │ +// └─(A or B fires Veto)──────────▶ Refund (per-contributor) +// Open ─(open_deadline passed)──────────────────────────────────▶ Refund (per-contributor) +// +// Five redeemers: Deposit / Agree / Veto / Settle / Refund. + +use aiken/collection/list +use aiken/collection/pairs +use aiken/crypto.{VerificationKeyHash} +use aiken/interval.{Finite} +use aiken/cbor +use cardano/address.{Address, VerificationKey} +use cardano/assets.{Value, flatten, merge, negate, quantity_of, zero} +use cardano/transaction.{ + Transaction, Output, OutputReference, InlineDatum, find_input, +} + +// ----- types ----- +// +// FlatValue mirrors Plutus's on-chain `Map PolicyId (Map AssetName Int)` +// using concrete `Pairs` since the cardano/assets `Value` type is opaque +// (datums require concrete types). Convert to/from `Value` at script +// boundaries via `assets.flatten` + manual reduction. + +pub type AssetEntries = Pairs + +pub type FlatValue = Pairs + +pub type DepositEntry { + contributor: VerificationKeyHash, + value: FlatValue, +} + +pub type EscrowState { + Open + Agreed { agreed_at_ms: Int } +} + +pub type EscrowDatum { + party_a: VerificationKeyHash, + party_b: VerificationKeyHash, + recipient: VerificationKeyHash, + open_deadline_ms: Int, + lock_period_ms: Int, + state: EscrowState, + deposits: List, +} + +pub type EscrowRedeemer { + Deposit { contributor: VerificationKeyHash } + Agree + Veto + Settle + Refund +} + +// ----- helpers ----- + +fn signed_by(tx: Transaction, pkh: VerificationKeyHash) -> Bool { + list.has(tx.extra_signatories, pkh) +} + +fn pkh_to_base_address(pkh: VerificationKeyHash) -> Address { + Address { + payment_credential: VerificationKey(pkh), + stake_credential: None, + } +} + +/// Convert opaque on-chain Value into FlatValue (Pairs form). Iterates +/// `assets.flatten` triples and groups by policy. +fn value_to_flat(v: Value) -> FlatValue { + list.foldr( + flatten(v), + [], + fn(triple, acc) { + let (policy, name, qty) = triple + // append (name, qty) under existing policy entry, else add new + when pairs.get_first(acc, policy) is { + Some(entries) -> { + let updated = list.concat(entries, [Pair(name, qty)]) + insert_pair(acc, policy, updated) + } + None -> list.concat(acc, [Pair(policy, [Pair(name, qty)])]) + } + }, + ) +} + +/// Functional update: replace `key`'s value in the Pairs (key must exist +/// per caller's check). +fn insert_pair(p: Pairs, key: ByteArray, value: AssetEntries) -> Pairs { + list.map( + p, + fn(entry) { + let Pair(k, v) = entry + if k == key { + Pair(k, value) + } else { + Pair(k, v) + } + }, + ) +} + +/// Find the unique continuing output back to `script_addr`. Returns +/// (datum, value) when exactly one such output exists. +fn find_continuing_output( + outputs: List, + script_addr: Address, +) -> Option<(EscrowDatum, Value)> { + let candidates = list.filter(outputs, fn(o) { o.address == script_addr }) + when candidates is { + [single] -> { + when single.datum is { + InlineDatum(d) -> { + expect new_datum: EscrowDatum = d + Some((new_datum, single.value)) + } + _ -> None + } + } + _ -> None + } +} + +/// True iff a >= b in every (policy, asset) component using opaque Value +/// arithmetic. +fn value_geq_value(a: Value, b: Value) -> Bool { + list.all( + flatten(b), + fn(triple) { + let (policy, name, qty) = triple + quantity_of(a, policy, name) >= qty + }, + ) +} + +/// True iff `paid` (a Value) >= `flat` (a FlatValue) per-component. +fn value_geq_flat(paid: Value, flat: FlatValue) -> Bool { + list.all( + flat, + fn(policy_entry) { + let Pair(policy, assets) = policy_entry + list.all( + assets, + fn(asset_entry) { + let Pair(name, qty) = asset_entry + quantity_of(paid, policy, name) >= qty + }, + ) + }, + ) +} + +/// For each Deposit entry, an output to that contributor's base address +/// must pay at least the entry's value. +fn refund_outputs_satisfy( + tx_outputs: List, + deposits: List, +) -> Bool { + list.all( + deposits, + fn(d) { + let target = pkh_to_base_address(d.contributor) + let paid = + list.foldr( + tx_outputs, + zero, + fn(o, acc) { + if o.address == target { + merge(acc, o.value) + } else { + acc + } + }, + ) + value_geq_flat(paid, d.value) + }, + ) +} + +fn tx_lower_ms(tx: Transaction) -> Option { + when tx.validity_range.lower_bound.bound_type is { + Finite(t) -> Some(t) + _ -> None + } +} + +fn tx_upper_ms(tx: Transaction) -> Option { + when tx.validity_range.upper_bound.bound_type is { + Finite(t) -> Some(t) + _ -> None + } +} + +/// Compute new deposits list = old with `net_added` (a FlatValue) +/// attributed to `contributor` (merge into existing entry if present, +/// else append). +fn expected_deposits_after( + old: List, + contributor: VerificationKeyHash, + net_added: FlatValue, +) -> List { + let has_entry = list.any(old, fn(d) { d.contributor == contributor }) + if has_entry { + list.map( + old, + fn(d) { + if d.contributor == contributor { + DepositEntry { contributor: d.contributor, value: flat_merge(d.value, net_added) } + } else { + d + } + }, + ) + } else { + list.concat(old, [DepositEntry { contributor, value: net_added }]) + } +} + +/// Component-wise add of two FlatValues, preserving first-seen ordering. +fn flat_merge(a: FlatValue, b: FlatValue) -> FlatValue { + list.foldr( + b, + a, + fn(b_entry, acc) { + let Pair(b_policy, b_assets) = b_entry + when pairs.get_first(acc, b_policy) is { + Some(a_assets) -> { + let merged = merge_assets(a_assets, b_assets) + insert_pair(acc, b_policy, merged) + } + None -> list.concat(acc, [Pair(b_policy, b_assets)]) + } + }, + ) +} + +fn merge_assets(a: AssetEntries, b: AssetEntries) -> AssetEntries { + list.foldr( + b, + a, + fn(b_entry, acc) { + let Pair(b_name, b_qty) = b_entry + when pairs.get_first(acc, b_name) is { + Some(a_qty) -> insert_asset(acc, b_name, a_qty + b_qty) + None -> list.concat(acc, [Pair(b_name, b_qty)]) + } + }, + ) +} + +fn insert_asset(p: AssetEntries, key: ByteArray, value: Int) -> AssetEntries { + list.map( + p, + fn(entry) { + let Pair(k, v) = entry + if k == key { + Pair(k, value) + } else { + Pair(k, v) + } + }, + ) +} + +// ----- validator ----- + +validator escrow { + spend( + datum: Option, + redeemer: EscrowRedeemer, + own_ref: OutputReference, + self: Transaction, + ) { + expect Some(d) = datum + expect Some(in_) = find_input(self.inputs, own_ref) + let in_value = in_.output.value + let script_addr = in_.output.address + + when redeemer is { + Deposit { contributor } -> { + expect d.state == Open + expect contributor == d.party_a || contributor == d.party_b + expect signed_by(self, contributor) + expect Some((new_d, new_value)) = + find_continuing_output(self.outputs, script_addr) + // Datum unchanged except `deposits` + expect new_d.party_a == d.party_a + expect new_d.party_b == d.party_b + expect new_d.recipient == d.recipient + expect new_d.open_deadline_ms == d.open_deadline_ms + expect new_d.lock_period_ms == d.lock_period_ms + expect new_d.state == Open + // Net added = new_value - in_value (in Value space) + let net_added = value_to_flat(merge(new_value, negate(in_value))) + // Deposits list updated correctly + let expected = expected_deposits_after(d.deposits, contributor, net_added) + cbor.serialise(expected) == cbor.serialise(new_d.deposits) + } + + Agree -> { + expect d.state == Open + expect signed_by(self, d.party_a) + expect signed_by(self, d.party_b) + expect Some(upper) = tx_upper_ms(self) + expect upper <= d.open_deadline_ms + expect Some((new_d, new_value)) = + find_continuing_output(self.outputs, script_addr) + expect value_geq_value(new_value, in_value) && value_geq_value(in_value, new_value) + expect new_d.party_a == d.party_a + expect new_d.party_b == d.party_b + expect new_d.recipient == d.recipient + expect new_d.open_deadline_ms == d.open_deadline_ms + expect new_d.lock_period_ms == d.lock_period_ms + expect cbor.serialise(new_d.deposits) == cbor.serialise(d.deposits) + new_d.state == Agreed { agreed_at_ms: upper } + } + + Veto -> { + expect Agreed { .. } = d.state + expect signed_by(self, d.party_a) || signed_by(self, d.party_b) + refund_outputs_satisfy(self.outputs, d.deposits) + } + + Settle -> { + expect Agreed { agreed_at_ms } = d.state + expect Some(lower) = tx_lower_ms(self) + expect lower > agreed_at_ms + d.lock_period_ms + let recipient_addr = pkh_to_base_address(d.recipient) + let paid = + list.foldr( + self.outputs, + zero, + fn(o, acc) { + if o.address == recipient_addr { + merge(acc, o.value) + } else { + acc + } + }, + ) + value_geq_value(paid, in_value) + } + + Refund -> { + expect d.state == Open + expect Some(lower) = tx_lower_ms(self) + expect lower > d.open_deadline_ms + refund_outputs_satisfy(self.outputs, d.deposits) + } + } + } + + else(_) { + fail + } +} + +// ----- tests ----- + +test minimal_smoke() { + // Smoke test: type-checks. Real e2e tests run on preprod_test2 from + // aldabra-escrow's MCP integration tests. + True +}