feat(escrow): drop escrow_wip feature gate, ship as default surface

Per Cobb 2026-05-09 directive: after the audit + preprod E2E green-light
(6/6 builders, 9 successful txs, 0 failures), drop the compile-time gate
and integrate escrow as a default-on feature. The "not third-party
audited" framing becomes a runtime notice carried by escrow_open_unsigned
rather than a Cargo feature.

Changes:
- aldabra-dao/Cargo.toml: drop [features] block + escrow_wip = []
- aldabra-dao/src/agora/mod.rs: pub mod escrow (no cfg gate)
- aldabra-dao/src/builder/mod.rs: 6 escrow_* modules unconditional
- aldabra-mcp/Cargo.toml: drop features = ["escrow_wip"] from dao dep
- aldabra-mcp/src/tools.rs:
  - Drop "WIP — UNAUDITED:" prefix from all 6 escrow tool descriptions
  - Drop "wip_warning" JSON field from all 6 spend-tool responses
  - Add "audit_notice" field on escrow_open_unsigned response only
    (per Cobb's framing — once-per-escrow-conversation, not repeated
    on every subsequent tool)
  - Update section header comment to reflect post-WIP status
- 7 escrow source files (1 agora + 6 builder): replace
  "WIP / UNAUDITED. Feature-gated behind escrow_wip" docstring with
  "Not third-party audited — preprod-only" + audit doc reference

Verified: 133 dao tests pass (was 132 under --features escrow_wip;
+1 from the rejects_no_initial_contributor test that's now always
compiled). aldabra-mcp release build clean.

The runtime audit_notice on escrow_open_unsigned reads:
  "This escrow validator has had an internal review and a 9-tx preprod
   E2E pass, but has NOT been audited by an external third party. Use
   at your own risk. If the user is opening this with anything beyond
   test-net or low-value funds, pass this notice along and confirm they
   accept the risk. Validator hash: a8081acef26935d9b5f44b92052178e17301b6d6e6808c91c5b56f5d."

This carries the same caveat the WIP framing did, but in a form the
calling agent can surface inline to the user opening the escrow.
This commit is contained in:
Kayos 2026-05-09 23:08:30 -07:00
parent 0273fe29e4
commit 30a7137de5
12 changed files with 31 additions and 59 deletions

View file

@ -71,13 +71,6 @@ thiserror = { workspace = true }
# Logging.
tracing = { workspace = true }
[features]
default = []
# WIP / unaudited two-party escrow validator + builders + types. Compiled
# out of default builds until external audit lands. Enable with
# --features escrow_wip from the workspace root.
escrow_wip = []
[dev-dependencies]
# DaoStore tests use a temp dir as the data root.
tempfile = "3"

View file

@ -1,6 +1,6 @@
//! Escrow datum + redeemer encoding.
//!
//! ⚠️ WIP / UNAUDITED. Feature-gated behind `escrow_wip`. Preprod-only.
//! ⚠️ Not third-party audited — preprod-only. See `audits/2026-05-09-escrow-internal-audit.md`.
//!
//! Mirrors the on-chain validator at `aiken-escrow/escrow/validators/escrow.ak`.
//! See `audits/2026-05-09-escrow-spec.md` for the full state machine.

View file

@ -40,12 +40,10 @@ pub mod stake;
pub mod treasury;
pub mod authority_token;
pub mod escrow;
pub mod plutus_data;
pub mod reference_scripts;
#[cfg(feature = "escrow_wip")]
pub mod escrow;
pub use governor::{GovernorDatum, GovernorRedeemer};
pub use proposal::{
ProposalDatum, ProposalRedeemer, ProposalStatus, ProposalThresholds, ProposalTimingConfig,

View file

@ -1,6 +1,6 @@
//! Build an unsigned `escrow_agree_unsigned` transaction.
//!
//! ⚠️ WIP / UNAUDITED. Feature-gated behind `escrow_wip`.
//! ⚠️ Not third-party audited — preprod-only. See `audits/2026-05-09-escrow-internal-audit.md`.
//!
//! ## What this tx does
//!

View file

@ -1,6 +1,6 @@
//! Build an unsigned `escrow_deposit_unsigned` transaction.
//!
//! ⚠️ WIP / UNAUDITED. Feature-gated behind `escrow_wip`.
//! ⚠️ Not third-party audited — preprod-only. See `audits/2026-05-09-escrow-internal-audit.md`.
//!
//! ## What this tx does
//!

View file

@ -1,6 +1,6 @@
//! Build an unsigned `escrow_open_unsigned` transaction.
//!
//! ⚠️ WIP / UNAUDITED. Feature-gated behind `escrow_wip`.
//! ⚠️ Not third-party audited — preprod-only. See `audits/2026-05-09-escrow-internal-audit.md`.
//!
//! ## What this tx does
//!

View file

@ -1,6 +1,6 @@
//! Build an unsigned `escrow_refund_timeout_unsigned` transaction.
//!
//! ⚠️ WIP / UNAUDITED. Feature-gated behind `escrow_wip`.
//! ⚠️ Not third-party audited — preprod-only. See `audits/2026-05-09-escrow-internal-audit.md`.
//!
//! ## What this tx does
//!

View file

@ -1,6 +1,6 @@
//! Build an unsigned `escrow_settle_unsigned` transaction.
//!
//! ⚠️ WIP / UNAUDITED. Feature-gated behind `escrow_wip`.
//! ⚠️ Not third-party audited — preprod-only. See `audits/2026-05-09-escrow-internal-audit.md`.
//!
//! ## What this tx does
//!

View file

@ -1,6 +1,6 @@
//! Build an unsigned `escrow_veto_unsigned` transaction.
//!
//! ⚠️ WIP / UNAUDITED. Feature-gated behind `escrow_wip`.
//! ⚠️ Not third-party audited — preprod-only. See `audits/2026-05-09-escrow-internal-audit.md`.
//!
//! ## What this tx does
//!

View file

@ -17,22 +17,15 @@
//! | def. | `stake_create` | Lock TRP at stakes script (deferred — both |
//! | | | live wallets already have stakes) |
pub mod escrow_agree;
pub mod escrow_deposit;
pub mod escrow_open;
pub mod escrow_refund_timeout;
pub mod escrow_settle;
pub mod escrow_veto;
pub mod proposal_advance;
pub mod proposal_cosign;
pub mod proposal_create;
pub mod proposal_retract_votes;
pub mod proposal_vote;
pub mod stake_destroy;
#[cfg(feature = "escrow_wip")]
pub mod escrow_agree;
#[cfg(feature = "escrow_wip")]
pub mod escrow_deposit;
#[cfg(feature = "escrow_wip")]
pub mod escrow_open;
#[cfg(feature = "escrow_wip")]
pub mod escrow_refund_timeout;
#[cfg(feature = "escrow_wip")]
pub mod escrow_settle;
#[cfg(feature = "escrow_wip")]
pub mod escrow_veto;

View file

@ -20,18 +20,7 @@ path = "src/main.rs"
[dependencies]
aldabra-core = { path = "../aldabra-core" }
aldabra-chain = { path = "../aldabra-chain" }
# Always pulls aldabra-dao/escrow_wip so the MCP binary can expose
# the escrow_* tool surface unconditionally. The "WIP — UNAUDITED:"
# prefix in every tool's description is the runtime gate; the dao
# crate's escrow_wip feature stays gated for downstream Rust consumers
# that want to opt out at the source level.
#
# Rationale: rmcp 0.1.5's #[tool(tool_box)] macro doesn't compose with
# #[cfg] on individual methods (it scans the impl AST and references
# every #[tool]-tagged method's generated wrapper, regardless of cfg
# eligibility). Force-pulling the feature at the dep level avoids the
# macro/cfg conflict.
aldabra-dao = { path = "../aldabra-dao", features = ["escrow_wip"] }
aldabra-dao = { path = "../aldabra-dao" }
# Used directly in tools.rs to decode the wallet's bech32 address into a
# payment-credential hash (so `dao_my_stake` can match against StakeDatum.owner).

View file

@ -3563,15 +3563,19 @@ impl WalletService {
.to_string())
}
// ─── escrow_wip — WIP/UNAUDITED two-party agreement-with-veto escrow ───
// ─── escrow — two-party agreement-with-veto escrow on Plutus V3 ───
//
// Feature-gated (`--features escrow_wip`) so the default release build
// doesn't surface tools whose validator hasn't been externally audited.
// Enable for preprod E2E only.
// Validator hash: a8081acef26935d9b5f44b92052178e17301b6d6e6808c91c5b56f5d.
// Internal audit pass + 9-tx preprod E2E shipped 2026-05-09. Has NOT been
// through external third-party audit; the `escrow_open_unsigned` response
// carries a runtime "use at own risk" notice so the calling agent has it
// in-context for the conversation that opens an escrow. Subsequent escrow
// tools (deposit / agree / veto / settle / refund_timeout) don't repeat
// the notice — once acknowledged at open, the same caveat carries.
#[tool(
name = "escrow_open_unsigned",
description = "WIP — UNAUDITED: Build (but DO NOT submit) an unsigned escrow_open tx. Locks lovelace at the escrow validator script address with an inline EscrowDatum (state=Open, optional initial deposit). Two-party agreement-with-veto: party_a + party_b must both sign Agree to flip the state; either can fire Veto from Agreed; recipient is settled-to after the lock period. ⚠️ v1 WIP feature — the validator has NOT been externally audited. Do not route mainnet value through it. Args: escrow_script_address (bech32), party_a/b/recipient_pkh_hex (28-byte hex), open_deadline_ms (after which Refund-timeout becomes valid), lock_period_ms (veto window after Agree), initial_contributor_pkh_hex (optional — party_a or party_b — funds initial deposit), initial_lovelace (lovelace to lock at the script), fee_lovelace (~2.5 ADA reasonable). Returns CBOR-hex of the unsigned tx body. Caller signs via wallet_sign_partial then submits via wallet_submit_signed_tx."
description = "Build (but DO NOT submit) an unsigned escrow_open tx. Locks lovelace at the escrow validator script address with an inline EscrowDatum (state=Open, initial deposit). Two-party agreement-with-veto: party_a + party_b must both sign Agree to flip the state; either can fire Veto from Agreed; recipient is settled-to after the lock period. The response carries an `audit_notice` field — read it before proceeding; pass that text on to the user opening the escrow. Args: escrow_script_address (bech32), party_a/b/recipient_pkh_hex (28-byte hex), open_deadline_ms (after which Refund-timeout becomes valid), lock_period_ms (veto window after Agree), initial_contributor_pkh_hex (party_a or party_b — funds initial deposit), initial_lovelace (lovelace to lock at the script). Returns CBOR-hex of the unsigned tx body. Caller signs via wallet_sign_partial then submits via wallet_submit_signed_tx."
)]
async fn escrow_open_unsigned(
&self,
@ -3651,14 +3655,14 @@ impl WalletService {
"escrow_datum_cbor_hex": unsigned.escrow_datum_cbor_hex,
"summary": unsigned.summary,
"next_step": "review tx_cbor_hex (decode + audit), sign via wallet_sign_partial, submit via wallet_submit_signed_tx. After submission, query the script address to discover the new escrow UTxO + its tx_hash#index for follow-up deposit/agree/veto/settle/refund tools.",
"wip_warning": "⚠️ UNAUDITED escrow validator. Use preprod testnet only.",
"audit_notice": "This escrow validator has had an internal review and a 9-tx preprod E2E pass, but has NOT been audited by an external third party. Use at your own risk. If the user is opening this with anything beyond test-net or low-value funds, pass this notice along and confirm they accept the risk. Validator hash: a8081acef26935d9b5f44b92052178e17301b6d6e6808c91c5b56f5d.",
})
.to_string())
}
#[tool(
name = "escrow_deposit_unsigned",
description = "WIP — UNAUDITED: Build (but DO NOT submit) an unsigned escrow_deposit tx. Plutus V3 spend with continuing-output state transition (only `deposits` field mutated). Validator runs `Deposit{contributor}` redeemer. v1 ADA-only deposits — multi-asset deferred until cbor-canonicality of Aiken Pairs is golden-tested. Args: escrow_script_address, validator_script_cbor_hex OR validator_script_path (V3 UPLC), escrow_in_tx_hash_hex + escrow_in_output_index (the existing escrow UTxO), escrow_in_lovelace, escrow_in_datum_cbor_hex (the current EscrowDatum encoded as Plutus Data CBOR — pull via chain_address_info), contributor_pkh_hex (must equal party_a or party_b), add_lovelace (>0), fee_lovelace, validity_window_seconds (optional, default 1800). Returns CBOR-hex of the unsigned tx body."
description = "Build (but DO NOT submit) an unsigned escrow_deposit tx. Plutus V3 spend with continuing-output state transition (only `deposits` field mutated). Validator runs `Deposit{contributor}` redeemer. v1 ADA-only deposits — multi-asset deferred until cbor-canonicality of Aiken Pairs is golden-tested. Args: escrow_script_address, validator_script_cbor_hex OR validator_script_path (V3 UPLC), escrow_in_tx_hash_hex + escrow_in_output_index (the existing escrow UTxO), escrow_in_lovelace, escrow_in_datum_cbor_hex (the current EscrowDatum encoded as Plutus Data CBOR — pull via chain_address_info), contributor_pkh_hex (must equal party_a or party_b), add_lovelace (>0), fee_lovelace, validity_window_seconds (optional, default 1800). Returns CBOR-hex of the unsigned tx body."
)]
async fn escrow_deposit_unsigned(
&self,
@ -3718,14 +3722,13 @@ impl WalletService {
"new_escrow_lovelace": unsigned.new_escrow_lovelace,
"summary": unsigned.summary,
"next_step": "sign via wallet_sign_partial (contributor must sign), submit via wallet_submit_signed_tx. After submission, the new escrow UTxO at script address has tx_hash = tx_hash_hex above, output_index = 0; pass these to subsequent escrow tools.",
"wip_warning": "⚠️ UNAUDITED escrow validator. Use preprod testnet only.",
})
.to_string())
}
#[tool(
name = "escrow_agree_unsigned",
description = "WIP — UNAUDITED: Build an unsigned escrow_agree tx — flips state Open → Agreed{at=upper}. BOTH party_a + party_b must sign (this tool is run by the driver, who signs first; co-signer adds witness via wallet_sign_partial). Validator enforces validity_upper ≤ open_deadline_ms. Args: escrow_script_address, validator_script_cbor_hex OR validator_script_path, escrow_in_tx_hash_hex + escrow_in_output_index + escrow_in_lovelace + escrow_in_datum_cbor_hex, driver_pkh_hex (party_a or party_b), fee_lovelace, validity_window_seconds (optional default 1800; clamps to open_deadline if needed). Returns CBOR-hex requiring co-signer's witness."
description = "Build an unsigned escrow_agree tx — flips state Open → Agreed{at=upper}. BOTH party_a + party_b must sign (this tool is run by the driver, who signs first; co-signer adds witness via wallet_sign_partial). Validator enforces validity_upper ≤ open_deadline_ms. Args: escrow_script_address, validator_script_cbor_hex OR validator_script_path, escrow_in_tx_hash_hex + escrow_in_output_index + escrow_in_lovelace + escrow_in_datum_cbor_hex, driver_pkh_hex (party_a or party_b), fee_lovelace, validity_window_seconds (optional default 1800; clamps to open_deadline if needed). Returns CBOR-hex requiring co-signer's witness."
)]
async fn escrow_agree_unsigned(
&self,
@ -3803,14 +3806,13 @@ impl WalletService {
"co_signer_pkh_hex": unsigned.co_signer_pkh_hex,
"summary": unsigned.summary,
"next_step": "this tx requires BOTH parties' signatures. Driver signs via wallet_sign_partial, then sends the partial-signed CBOR to the co-signer (pkh = co_signer_pkh_hex) who calls wallet_sign_partial again, then wallet_submit_signed_tx.",
"wip_warning": "⚠️ UNAUDITED escrow validator. Use preprod testnet only.",
})
.to_string())
}
#[tool(
name = "escrow_veto_unsigned",
description = "WIP — UNAUDITED: Build an unsigned escrow_veto tx — consumes an Agreed escrow and refunds every contributor to their enterprise (null-stake) address. Either party can fire (validator: signed_by(a) || signed_by(b)). Args: escrow_script_address, validator_script_cbor_hex OR validator_script_path, escrow_in_tx_hash_hex + escrow_in_output_index + escrow_in_lovelace + escrow_in_datum_cbor_hex, driver_pkh_hex (party_a or party_b), fee_lovelace, validity_window_seconds (optional)."
description = "Build an unsigned escrow_veto tx — consumes an Agreed escrow and refunds every contributor to their enterprise (null-stake) address. Either party can fire (validator: signed_by(a) || signed_by(b)). Args: escrow_script_address, validator_script_cbor_hex OR validator_script_path, escrow_in_tx_hash_hex + escrow_in_output_index + escrow_in_lovelace + escrow_in_datum_cbor_hex, driver_pkh_hex (party_a or party_b), fee_lovelace, validity_window_seconds (optional)."
)]
async fn escrow_veto_unsigned(
&self,
@ -3865,14 +3867,13 @@ impl WalletService {
"refunds": unsigned.refunds.iter().map(|(p,l)| serde_json::json!({"contributor_pkh_hex": p, "lovelace": l})).collect::<Vec<_>>(),
"summary": unsigned.summary,
"next_step": "driver signs via wallet_sign_partial then submits via wallet_submit_signed_tx.",
"wip_warning": "⚠️ UNAUDITED escrow validator. Use preprod testnet only.",
})
.to_string())
}
#[tool(
name = "escrow_settle_unsigned",
description = "WIP — UNAUDITED: Build an unsigned escrow_settle tx — consumes an Agreed escrow whose lock window has elapsed and pays the entire in_value to the recipient's enterprise address. Validator gate: state==Agreed AND lower > agreed_at_ms + lock_period_ms (strict gt). No party signer required by validator — anyone with funding + collateral can push it. Args: escrow_script_address, validator_script_cbor_hex OR validator_script_path, escrow_in_tx_hash_hex + escrow_in_output_index + escrow_in_lovelace + escrow_in_datum_cbor_hex, driver_pkh_hex (whoever's paying fees), fee_lovelace, validity_window_seconds (optional)."
description = "Build an unsigned escrow_settle tx — consumes an Agreed escrow whose lock window has elapsed and pays the entire in_value to the recipient's enterprise address. Validator gate: state==Agreed AND lower > agreed_at_ms + lock_period_ms (strict gt). No party signer required by validator — anyone with funding + collateral can push it. Args: escrow_script_address, validator_script_cbor_hex OR validator_script_path, escrow_in_tx_hash_hex + escrow_in_output_index + escrow_in_lovelace + escrow_in_datum_cbor_hex, driver_pkh_hex (whoever's paying fees), fee_lovelace, validity_window_seconds (optional)."
)]
async fn escrow_settle_unsigned(
&self,
@ -3940,14 +3941,13 @@ impl WalletService {
"recipient_lovelace_paid": unsigned.recipient_lovelace_paid,
"summary": unsigned.summary,
"next_step": "driver signs via wallet_sign_partial then submits via wallet_submit_signed_tx.",
"wip_warning": "⚠️ UNAUDITED escrow validator. Use preprod testnet only.",
})
.to_string())
}
#[tool(
name = "escrow_refund_timeout_unsigned",
description = "WIP — UNAUDITED: Build an unsigned escrow_refund_timeout tx — consumes an Open escrow whose open_deadline has elapsed and refunds every contributor. No party signer required. Validator gate: state==Open AND lower > open_deadline_ms (strict gt). Args: escrow_script_address, validator_script_cbor_hex OR validator_script_path, escrow_in_tx_hash_hex + escrow_in_output_index + escrow_in_lovelace + escrow_in_datum_cbor_hex, driver_pkh_hex, fee_lovelace, validity_window_seconds (optional)."
description = "Build an unsigned escrow_refund_timeout tx — consumes an Open escrow whose open_deadline has elapsed and refunds every contributor. No party signer required. Validator gate: state==Open AND lower > open_deadline_ms (strict gt). Args: escrow_script_address, validator_script_cbor_hex OR validator_script_path, escrow_in_tx_hash_hex + escrow_in_output_index + escrow_in_lovelace + escrow_in_datum_cbor_hex, driver_pkh_hex, fee_lovelace, validity_window_seconds (optional)."
)]
async fn escrow_refund_timeout_unsigned(
&self,
@ -4006,7 +4006,6 @@ impl WalletService {
"refunds": unsigned.refunds.iter().map(|(p,l)| serde_json::json!({"contributor_pkh_hex": p, "lovelace": l})).collect::<Vec<_>>(),
"summary": unsigned.summary,
"next_step": "driver signs via wallet_sign_partial then submits via wallet_submit_signed_tx.",
"wip_warning": "⚠️ UNAUDITED escrow validator. Use preprod testnet only.",
})
.to_string())
}
@ -4300,7 +4299,7 @@ pub struct EscrowOpenUnsignedArgs {
pub initial_lovelace: u64,
}
// ─── escrow_wip spend-tool args (deposit / agree / veto / settle / refund) ──
// ─── escrow spend-tool args (deposit / agree / veto / settle / refund) ──
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct EscrowDepositUnsignedArgs {