diff --git a/aiken-escrow/validators/escrow.ak b/aiken-escrow/validators/escrow.ak index 779e518..60f2f65 100644 --- a/aiken-escrow/validators/escrow.ak +++ b/aiken-escrow/validators/escrow.ak @@ -1,13 +1,12 @@ -// ⚠️ WIP — UNAUDITED. EXPERIMENTAL. DO NOT USE WITH MAINNET FUNDS. +// ⚠️ UNAUDITED. EXPERIMENTAL. Use-at-own-risk for high-value flows. // // 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. +// No third-party audit has been performed. Internal review only — +// see audits/2026-05-09-escrow-internal-audit.md for findings. // -// Two-party agreement-with-veto escrow. Spec: audits/2026-05-09-escrow-spec.md +// Two-party agreement-with-veto escrow. See aiken-escrow/README.md +// for the state-machine summary. // // State machine: // Open ─(both sign Agree)─▶ Agreed{at} ─(lock elapsed, no veto)─▶ Settle (→ recipient) @@ -431,7 +430,7 @@ validator escrow { // ----- tests ----- test minimal_smoke() { - // Smoke test: type-checks. Real e2e tests run on preprod_test2 from - // aldabra-escrow's MCP integration tests. + // Smoke test: type-checks. End-to-end behavior is exercised by the + // off-chain builder integration tests in `crates/aldabra-dao`. True } diff --git a/crates/aldabra-chain/src/koios.rs b/crates/aldabra-chain/src/koios.rs index 72a6270..bf09719 100644 --- a/crates/aldabra-chain/src/koios.rs +++ b/crates/aldabra-chain/src/koios.rs @@ -38,11 +38,9 @@ struct AddressesBody<'a> { } /// Same as [`AddressesBody`] but with the `_extended` flag set. -/// Koios's `/address_utxos` returns `asset_list: null` (or empty) -/// without it; with it, the per-utxo asset bundles come through -/// reliably. Discovered preprod 2026-05-04 — without this flag the -/// wallet sees its own asset-bearing UTXOs as ada-only and refuses -/// to construct a multi-asset send. +/// Without `_extended`, Koios's `/address_utxos` returns +/// `asset_list: null` (or empty), causing asset-bearing UTXOs to +/// look ada-only — multi-asset sends then fail to build. #[derive(Serialize)] struct AddressesExtendedBody<'a> { #[serde(rename = "_addresses")] @@ -69,8 +67,7 @@ struct KoiosUtxo { /// `Option>` because Koios's `/address_utxos` returns /// `asset_list: null` for ADA-only UTXOs (vs `/address_info` /// which returns `[]`). `Vec` rejects `null`; `Option>` - /// accepts both. Found at integration time on live preprod - /// 2026-05-04 — our hand-crafted test fixtures all used `[]`. + /// accepts both. #[serde(default)] asset_list: Option>, } @@ -92,8 +89,8 @@ struct TxHashesBody<'a> { /// Response shape from Koios `/api/v1/tx_status`. Tiny — only a /// confirmations counter per requested tx — vs `/tx_info` which /// streams the full tx body (multi-MB for complex confirmed txs). -/// AUDIT4-1: switching to `/tx_status` resolves the 120s+ hang on -/// confirmed-tx queries surfaced 2026-05-04. +/// Prefer this for status polling to avoid the multi-second hang +/// when fetching large confirmed-tx bodies. #[derive(Deserialize)] struct KoiosTxStatusResp { #[allow(dead_code)] @@ -308,11 +305,10 @@ impl ChainBackend for KoiosClient { .send() .await .map_err(|e| ChainError::Network(e.to_string()))?; - // Capture status + body BEFORE bubbling up — koios's chain-rule - // rejection messages live in the response body and are - // otherwise eaten by `.error_for_status()`. Discovered during - // preprod cip-68 mint debugging 2026-05-04: a 400 with no - // surfaced body left us guessing at why the chain rejected the tx. + // Capture status + body BEFORE bubbling up — Koios's chain-rule + // rejection messages live in the response body and are otherwise + // eaten by `.error_for_status()`, leaving callers with no signal + // beyond an HTTP 400. let status = response.status(); let body = response .text() @@ -327,9 +323,8 @@ impl ChainBackend for KoiosClient { } // Koios returns the tx hash as a quoted JSON string. Strip the // surrounding quotes if present, then validate the result is - // exactly 64 hex chars. - // M-4 audit fix: previously a quoted error message would - // round-trip as a fake tx_hash. + // exactly 64 hex chars — guards against a quoted error message + // round-tripping as a fake tx_hash. let hash = body.trim().trim_matches('"').to_string(); if !is_hex_64(&hash) { return Err(ChainError::Decode(format!( @@ -414,8 +409,7 @@ mod tests { /// Real Koios `/address_utxos` returns `asset_list: null` for /// ada-only utxos (vs `/address_info` which returns `[]`). - /// Regression test — caught at preprod integration time - /// 2026-05-04 after our hand-crafted fixtures all used `[]`. + /// Regression test for the null-vs-empty-array deserialisation. #[test] fn deserializes_utxo_with_null_asset_list() { const SAMPLE: &str = r#"[ @@ -556,8 +550,7 @@ mod tests { assert!(json.contains("\"status\":\"not_found\"")); } - /// AUDIT4-1 regression: parse the three live Koios `/tx_status` - /// shapes we observed during the 2026-05-04 preprod test — + /// Regression: parse the three live Koios `/tx_status` shapes — /// confirmed-with-count, known-but-no-confs (mempool), and /// nothing-to-report (truly unknown). #[test] diff --git a/crates/aldabra-core/src/governance.rs b/crates/aldabra-core/src/governance.rs index 65ff5a5..cbd2048 100644 --- a/crates/aldabra-core/src/governance.rs +++ b/crates/aldabra-core/src/governance.rs @@ -38,9 +38,9 @@ use crate::{Network, PaymentKey, ProtocolParams, StakeKey, WalletError}; /// Conway DRep registration deposit. Mainnet protocol parameter /// `drep_deposit` is currently 500 ADA. **Use `params.drep_deposit_lovelace` /// instead of this constant** — it's kept here for backward-compat callers -/// only. AUDIT-2026-05-06 M-2: hardcoding the deposit means a protocol -/// change (or an old DRep registered at a different deposit) will silently -/// fail ledger validation. Always pull from current chain params. +/// only. Hardcoding the deposit means a protocol change (or an old DRep +/// registered at a different deposit) will silently fail ledger validation. +/// Always pull from current chain params. pub const DREP_REGISTRATION_DEPOSIT_LOVELACE: u64 = 500_000_000; /// Two witnesses (payment + stake) — same overhead as diff --git a/crates/aldabra-core/src/plutus.rs b/crates/aldabra-core/src/plutus.rs index be12662..e9f7981 100644 --- a/crates/aldabra-core/src/plutus.rs +++ b/crates/aldabra-core/src/plutus.rs @@ -148,12 +148,11 @@ pub fn build_signed_plutus_spend( // AUDIT4-2 fix: pick the SMALLEST ADA-only UTXO that still // qualifies for collateral (≥ 5 ADA), so the LARGEST stays - // available for funding the spend. Previously we did the - // inverse — collateral got the biggest utxo, funding got - // whatever scrap was next, and a typical wallet (one big - // change utxo + a tiny self-send leftover) couldn't cover - // payout + fee + min_utxo even with billions of lovelace - // sitting in the change. Surfaced 2026-05-04 audit-4 phase F2. + // available for funding the spend. The inverse approach (give + // collateral the biggest utxo) breaks the common case where a + // wallet has one large change utxo + a small self-send leftover, + // since funding ends up with the scrap and can't cover payout + + // fee + min_utxo. // // Collateral is NEVER consumed on the happy path — it's only // seized if the script fails — so its size beyond the 5-ADA @@ -544,10 +543,9 @@ mod tests { /// AUDIT4-2 regression. A wallet with one tiny qualifying UTXO /// alongside one huge UTXO must pick the tiny one for collateral - /// and the huge one for funding (not the inverse). Pre-fix, the - /// huge UTXO became collateral and funding fell back to the - /// tiny 5-ADA scrap, too small to cover payout, script-exec - /// fee, and change min_utxo. Surfaced 2026-05-04 audit-4 phase F2. + /// and the huge one for funding (not the inverse). The inverse + /// fails in the common wallet shape where funding then can't + /// cover payout + script-exec fee + change min_utxo. #[test] fn picks_smallest_qualifying_collateral_largest_funding() { let payment = payment_from_canonical(); diff --git a/crates/aldabra-core/src/plutus_cost_models.rs b/crates/aldabra-core/src/plutus_cost_models.rs index 4bca70f..279dac8 100644 --- a/crates/aldabra-core/src/plutus_cost_models.rs +++ b/crates/aldabra-core/src/plutus_cost_models.rs @@ -1,9 +1,8 @@ -// Conway-era Plutus V3 cost model, 297 params. Snapshot from preprod -// epoch 286 (2026-05) but **identical to mainnet epoch 629** — -// confirmed 2026-05-04 by parallel Koios `epoch_params` fetch from -// `api.koios.rest` and `preprod.koios.rest`. Cost models are -// protocol-version parameters, not network parameters; they only -// diverge if a network does an experimental hard fork off-cycle. +// Conway-era Plutus V3 cost model, 297 params. Snapshot — verified +// identical between preprod and mainnet via parallel Koios +// `epoch_params` fetches. Cost models are protocol-version +// parameters, not network parameters; they only diverge if a network +// does an experimental hard fork off-cycle. // // Used by both preprod and mainnet Plutus paths today. Re-snapshot // from mainnet Koios after any major hard fork. If preprod and diff --git a/crates/aldabra-core/src/plutus_mint.rs b/crates/aldabra-core/src/plutus_mint.rs index c73f93f..a987076 100644 --- a/crates/aldabra-core/src/plutus_mint.rs +++ b/crates/aldabra-core/src/plutus_mint.rs @@ -19,7 +19,7 @@ //! - **Mint**: caller-supplied `(asset_name_hex, quantity)` list under //! the supplied policy (Plutus V1/V2/V3). //! - **Recipient output**: address + lovelace + minted assets + -//! any caller-supplied extra assets to forward (e.g. tTRP gov tokens +//! any caller-supplied extra assets to forward (e.g. gov tokens //! on a stake bootstrap) + optional inline datum. //! - **Change output**: leftover ADA + leftover input assets (other //! than what was forwarded to the recipient). @@ -29,7 +29,7 @@ //! Agora's deployment pattern is the same shape for every "first-time //! mint of a single ST token under a Plutus policy" tx: //! - Governor bootstrap: mint 1 GST → governor_addr + GovernorDatum -//! - Stake bootstrap: mint 1 StakeST → stakes_addr + tTRP + StakeDatum +//! - Stake bootstrap: mint 1 StakeST → stakes_addr + gov-token + StakeDatum //! - Proposal create: mint 1 ProposalST → proposal_addr + ProposalDatum //! //! All three share the structure; the only differences are the @@ -57,7 +57,7 @@ pub struct PlutusMintAsset { } /// Optional non-mint asset to attach to the recipient output. -/// Used for e.g. "send tTRP alongside the freshly-minted StakeST" +/// Used for e.g. "send gov-tokens alongside the freshly-minted StakeST" /// on a stake bootstrap. Sourced from wallet input UTxOs. #[derive(Debug, Clone)] pub struct ExtraDestAsset { @@ -90,7 +90,7 @@ pub struct PlutusMintArgs<'a> { pub dest_lovelace: u64, /// Non-mint assets to include on the recipient output. Sourced /// from wallet inputs. Empty for governor bootstrap; non-empty - /// for stake bootstrap (tTRP forwarded into the stake). + /// for stake bootstrap (gov-tokens forwarded into the stake). pub dest_extra_assets: &'a [ExtraDestAsset], /// Optional inline datum on the recipient output. Required for /// any send to a Plutus script address. @@ -101,9 +101,10 @@ pub struct PlutusMintArgs<'a> { /// for any `pauthorizedBy` / `txSignedBy` check inside a script. /// MCP layer always passes this wallet's payment-key pkh; pass /// extra entries for cosigners. Empty slice = scripts that don't - /// check signatories (e.g. Agora's GST policy). - /// Caught 2026-05-07 on Agora's stake-policy bootstrap on preprod - /// — script erred because owner pkh was absent from signatories. + /// check signatories (e.g. Agora's GST policy). Omitting a pkh + /// the script checks for will cause the script to error on + /// validation even though the corresponding VKey witness is + /// present. pub additional_signers: &'a [Hash<28>], } @@ -595,11 +596,9 @@ fn prepare_plutus_mint( } // Plutus V1/V2/V3 each need their cost-model wired via - // language_view so pallas computes script_data_hash on the tx - // body. Without it, chain rejects with PPViewHashesDontMatch. - // Caught 2026-05-07 attempting Agora's V2 GST-policy bootstrap - // mint on preprod — earlier code only set language_view for - // V3 and every V2 mint hit the chain rejection. + // language_view so pallas computes script_data_hash on the + // tx body. Without it, chain rejects with + // PPViewHashesDontMatch. match args.policy_version { PlutusVersion::V2 => { staging = staging.language_view( @@ -738,7 +737,7 @@ mod tests { } /// Sample preprod governor address (the one Plutarch linker - /// produced for our preprod tTRP DAO). Used as the dest. + /// produced for our preprod gov-token DAO). Used as the dest. const SAMPLE_GOVERNOR_ADDR: &str = "addr_test1wqlzsnytzs4qv0trmhvw5cuyxnxk0qjq68crn85jj4lhv7qn4wym4"; diff --git a/crates/aldabra-core/src/tx.rs b/crates/aldabra-core/src/tx.rs index 63bfbcc..5b50428 100644 --- a/crates/aldabra-core/src/tx.rs +++ b/crates/aldabra-core/src/tx.rs @@ -383,19 +383,18 @@ fn output_with_assets( .add_asset(policy, name, *qty) .map_err(|e| WalletError::Derivation(format!("output add_asset: {e}")))?; } - // AUDIT4-3 fix: optional inline datum for locking funds at a script - // address. Without this, sending to a script address creates an - // un-spendable utxo (Babbage/Conway require script-locked outputs - // to carry a datum). Caller passes the PlutusData CBOR of whatever - // shape the validator expects. + // Optional inline datum for locking funds at a script address. + // Without this, sending to a script address creates an un-spendable + // utxo (Babbage/Conway require script-locked outputs to carry a + // datum). Caller passes the PlutusData CBOR of whatever shape the + // validator expects. if let Some(datum) = inline_datum_cbor { out = out.set_inline_datum(datum.to_vec()); } - // 2026-05-07: optional reference-script attached to the output. - // This is the on-chain equivalent of `cardano-cli ... --tx-out - // --tx-out-reference-script-file ...`. Once deployed, downstream - // txs can witness the script via `read_only_input` instead of - // inline-witnessing the full CBOR. Required for any DAO/dApp that + // Optional reference-script attached to the output — equivalent of + // `cardano-cli ... --tx-out-reference-script-file ...`. Once deployed, + // downstream txs can witness the script via `read_only_input` instead + // of inline-witnessing the full CBOR. Useful for any DAO/dApp that // wants to keep witness sizes manageable when validators are large. if let Some(rs) = reference_script { out = out.set_inline_script(rs.kind, rs.cbor.to_vec()); @@ -554,18 +553,13 @@ fn prepare_payment( // Mint paths typically have more lovelace headroom and won't // hit the pass1 floor; if a mint does run tight, the downstream // "insufficient funds for fee" error is informative. - // Was 500_000 — surfaced 2026-05-05 zeroing out the mainnet - // test wallet (1.8 ADA out of 2 ADA refused upstream). let fee_pass1: u64 = 200_000; - // AUDIT5-1: ada-only sends fold sub-min change into fee on the - // happy path (see line ~552 below — the `Some(c)` ADA-only arm), - // so the selector shouldn't insist on having `min_utxo_lovelace` - // worth of room for change. Pass 0 when there are no asset - // leftovers; assets-bearing sends still need real change to - // route the leftover policy IDs, so keep min_utxo_lovelace there. - // Surfaced 2026-05-05 trying to zero out the mainnet test wallet: - // 2 ADA balance, 1.8 ADA send refused as "need 3.3M, have 2M" - // even though the chain math was fine. + // ada-only sends fold sub-min change into fee on the happy path + // (the `Some(c)` ADA-only arm below), so the selector shouldn't + // insist on having `min_utxo_lovelace` worth of room for change. + // Pass 0 when there are no asset leftovers; assets-bearing sends + // still need real change to route the leftover policy IDs, so + // keep min_utxo_lovelace there. let min_change_required = if target_assets.is_empty() { 0 } else { @@ -1286,11 +1280,10 @@ mod tests { assert_eq!(result.summary.change_assets[0].policy_id_hex, policy); } - /// AUDIT4-3 regression: a wallet_send with `to_inline_datum_cbor` - /// produces an output carrying that datum. Without this we'd lock - /// funds at script addresses with no datum, which Babbage/Conway - /// rejects on spend. Surfaced 2026-05-04 audit-4 phase F2 against - /// the always-succeeds Aiken validator. + /// Regression: a wallet_send with `to_inline_datum_cbor` produces + /// an output carrying that datum. Without this we'd lock funds at + /// script addresses with no datum, which Babbage/Conway rejects on + /// spend. #[test] fn lock_with_inline_datum_attaches_datum_to_output() { use pallas_primitives::Fragment; @@ -1326,13 +1319,10 @@ mod tests { } } - /// AUDIT5-1 regression: ada-only sends should be allowed to drain - /// a wallet down to "all of input - fee" without the selector - /// reserving min_utxo for a change output that ends up folded - /// into the fee anyway. Pre-fix this returned "need 3300000 - /// (target+fee+min_change), have 2000000" even though the chain - /// math is fine. Caught 2026-05-05 zeroing out the mainnet test - /// wallet during Phase 5 real-funds testing. + /// Regression: ada-only sends should be allowed to drain a wallet + /// down to "all of input - fee" without the selector reserving + /// min_utxo for a change output that ends up folded into the fee + /// anyway. #[test] fn ada_only_send_can_drain_to_fee() { let payment = payment_from_canonical(); diff --git a/crates/aldabra-dao/examples/dump_governor.rs b/crates/aldabra-dao/examples/dump_governor.rs index 5fddf62..9de2132 100644 --- a/crates/aldabra-dao/examples/dump_governor.rs +++ b/crates/aldabra-dao/examples/dump_governor.rs @@ -1,8 +1,7 @@ //! Dump a sample GovernorDatum as PlutusData CBOR hex. //! -//! Used during preprod DAO bringup (2026-05-07) to construct the -//! inline datum for the governor bootstrap tx. Edit the values in -//! `main()` to your DAO's parameters and run: +//! Useful for constructing the inline datum for a governor bootstrap +//! tx — edit the values in `main()` to your DAO's parameters and run: //! //! ```sh //! cargo run --example dump_governor -p aldabra-dao --release diff --git a/crates/aldabra-dao/src/agora/escrow.rs b/crates/aldabra-dao/src/agora/escrow.rs index 88f796f..a7648b0 100644 --- a/crates/aldabra-dao/src/agora/escrow.rs +++ b/crates/aldabra-dao/src/agora/escrow.rs @@ -1,9 +1,10 @@ //! Escrow datum + redeemer encoding. //! -//! ⚠️ Not third-party audited — preprod-only. See `audits/2026-05-09-escrow-internal-audit.md`. +//! ⚠️ Not third-party audited — use-at-own-risk for high-value flows. +//! See `audits/2026-05-09-escrow-internal-audit.md` for findings. //! -//! 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. +//! Mirrors the on-chain validator at `aiken-escrow/validators/escrow.ak`. +//! See `aiken-escrow/README.md` for the state machine. //! //! ## Datum shape //! diff --git a/crates/aldabra-dao/src/agora/governor.rs b/crates/aldabra-dao/src/agora/governor.rs index ecba7c4..7a5d75c 100644 --- a/crates/aldabra-dao/src/agora/governor.rs +++ b/crates/aldabra-dao/src/agora/governor.rs @@ -26,7 +26,7 @@ pub struct GovernorDatum { impl GovernorDatum { pub fn to_plutus_data(&self) -> DaoResult { // ProductIsData → Array, NOT Constr 0. - // Verified against Sulkta's live governor UTxO 2026-05-05. + // Verified against live on-chain governor UTxOs. Ok(product(vec![ self.proposal_thresholds.to_plutus_data()?, int(self.next_proposal_id as i128)?, @@ -120,23 +120,18 @@ mod tests { } } - /// Decode Sulkta's live governor datum from on-chain CBOR bytes and assert - /// the resulting struct matches the README parameters. - /// - /// This is the end-to-end Phase 0 validation: our type port matches what - /// Plutarch actually emits. - /// - /// Source: Koios `address_info` for `addr1w8v73wfrru7smn738k6c5xafqvl2tgsvct7dtztc4jwlf4c35jnmy` - /// at the only governor UTxO `7c8db1432a07143eaf7755257baf8691a8e24ee8b2f3a139fa1ce222f2821c47#1`. - /// Captured 2026-05-05. + /// Decode a real on-chain governor datum from CBOR bytes and + /// assert the resulting struct matches expected parameters. + /// End-to-end test that the type port matches what Plutarch + /// actually emits. #[test] - fn decodes_sulkta_live_governor_datum() { + fn decodes_live_governor_datum() { use pallas_primitives::PlutusData; let cbor_hex = "9f9f14186418640101ff019f1a240c84001a240c84001a0a4cb8001a05265c001a0036ee801a001b7740ff1a001b774014ff"; let bytes = hex::decode(cbor_hex).unwrap(); let pd: PlutusData = pallas_codec::minicbor::decode(&bytes).unwrap(); - let gov = GovernorDatum::from_plutus_data(&pd).expect("decode Sulkta governor"); + let gov = GovernorDatum::from_plutus_data(&pd).expect("decode governor"); assert_eq!(gov.proposal_thresholds.execute, 20); assert_eq!(gov.proposal_thresholds.create, 100); diff --git a/crates/aldabra-dao/src/agora/plutus_data.rs b/crates/aldabra-dao/src/agora/plutus_data.rs index 99ed238..b023dea 100644 --- a/crates/aldabra-dao/src/agora/plutus_data.rs +++ b/crates/aldabra-dao/src/agora/plutus_data.rs @@ -35,11 +35,12 @@ pub fn constr(index: u64, fields: Vec) -> PlutusData { /// Encode a Plutarch `ProductIsData` record — a CBOR Array, NOT a `Constr 0`. /// -/// **Important:** Plutarch optimizes record encodings via the `ProductIsData` -/// pattern, which serializes as a plain CBOR list of fields rather than the -/// generic-derived `Constr 0 [...]`. Verified against the live Sulkta -/// GovernorDatum UTxO 2026-05-05: outer wire bytes start `9f9f...` (indefinite -/// array of indefinite arrays) — i.e. arrays, not the `d8 79` Constr-121 tag. +/// **Important:** Plutarch optimizes record encodings via the +/// `ProductIsData` pattern, which serializes as a plain CBOR list of +/// fields rather than the generic-derived `Constr 0 [...]`. Verified +/// against live GovernorDatum UTxOs: outer wire bytes start `9f9f...` +/// (indefinite array of indefinite arrays) — arrays, not the `d8 79` +/// Constr-121 tag. /// /// We emit indefinite-length arrays to match Plutarch's wire output. Both /// definite and indefinite are accepted on decode (see [`as_array`]). diff --git a/crates/aldabra-dao/src/agora/proposal.rs b/crates/aldabra-dao/src/agora/proposal.rs index 66a537a..be2079e 100644 --- a/crates/aldabra-dao/src/agora/proposal.rs +++ b/crates/aldabra-dao/src/agora/proposal.rs @@ -23,11 +23,9 @@ use crate::error::{DaoError, DaoResult}; /// `data ProposalStatus = Draft | VotingReady | Locked | Finished` /// via `EnumIsData` → **plain `Integer`** (NOT `Constr i []`). /// -/// **Encoding correction 2026-05-05:** initial Phase 0 spec assumed -/// `EnumIsData` produces `Constr i []`. Real on-chain proposal #0 has -/// status field encoded as bare `BigInt(3)` (CBOR `03`). Plutarch's -/// `EnumIsData` actually emits Integer-as-index in this Agora version. -/// Correction verified by audit-sulkta-agora-2026-05-05.md. +/// Plutarch's `EnumIsData` emits the variant as a bare `BigInt` +/// index in this Agora version. Verified against live on-chain +/// proposal datums. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProposalStatus { Draft = 0, @@ -325,26 +323,21 @@ mod tests { } } - /// Decode Sulkta's Proposal #0 from on-chain bytes. Real-world - /// regression for the type port — same role as the GovernorDatum - /// live-decode test, but for a proposal. - /// - /// Source: Koios `address_info` for proposal validator address - /// `addr1w8uydw7xer6wml2pgpv8jphdenxrgwpmdxwaqfj2h5sk45sj8nk40`, - /// only UTxO at `0823a9406da1...#0`. Captured 2026-05-05. + /// Decode a real on-chain Agora proposal datum (status=Finished, + /// single cosigner, no votes cast). Regression test against the + /// type port — exercises every field with bytes copied off-chain. #[test] - fn decodes_sulkta_live_proposal_zero() { + fn decodes_live_finished_proposal() { use pallas_primitives::PlutusData; let cbor_hex = "9f00a200a001a1581c92b7725bb0c7c06083af729d38f5589c2f85f16c83fe48860399e96f9f5820046dff6c96199a55715c65b9ae62c3be1b6600038cc3116eeb20886a7d66e83cd87a80ff039fd8799f581cc5e3425f44c1909b6caab4d80d88aebe85f328bd209eeab03ca2bfdaffff9f14186418640101ffa2000001009f1a240c84001a240c84001a0a4cb8001a05265c001a0036ee801a001b7740ff1b0000019c7d5c4d17ff"; let bytes = hex::decode(cbor_hex).unwrap(); let pd: PlutusData = pallas_codec::minicbor::decode(&bytes).unwrap(); - let prop = ProposalDatum::from_plutus_data(&pd).expect("decode Proposal #0"); + let prop = ProposalDatum::from_plutus_data(&pd).expect("decode proposal"); assert_eq!(prop.proposal_id, 0); assert_eq!(prop.status, ProposalStatus::Finished); assert_eq!(prop.cosigners.len(), 1); - // Cobb's pkh assert!(matches!( &prop.cosigners[0], Credential::PubKey(h) if hex::encode(h) == "c5e3425f44c1909b6caab4d80d88aebe85f328bd209eeab03ca2bfda" @@ -355,7 +348,7 @@ mod tests { assert_eq!(prop.votes.0, vec![(0, 0), (1, 0)]); // zero votes ever cast assert_eq!(prop.timing_config.draft_time, 7 * 86_400 * 1000); assert_eq!(prop.timing_config.voting_time, 7 * 86_400 * 1000); - // Decoded from CBOR `1b 0000019c7d5c4d17` = 1771629726999 ms = 2026-04-21 15:42:06 UTC + // CBOR `1b 0000019c7d5c4d17` = 1771629726999 ms assert_eq!(prop.starting_time, 1_771_629_726_999); } diff --git a/crates/aldabra-dao/src/agora/reference_scripts.rs b/crates/aldabra-dao/src/agora/reference_scripts.rs index beb3223..fd3ee74 100644 --- a/crates/aldabra-dao/src/agora/reference_scripts.rs +++ b/crates/aldabra-dao/src/agora/reference_scripts.rs @@ -19,9 +19,10 @@ //! - The proposal-state-thread minting policy //! - The GAT minting policy //! -//! ## Compute-ourselves discovery (Cobb's pick 2026-05-05) +//! ## Reference-script discovery //! -//! Per the spec, we don't trust MLabs's published registry. Instead: +//! Rather than trusting an external published registry, refs are +//! discovered from the chain directly: //! //! 1. Decode each contract address (governor / stakes / treasury) to //! extract its payment-credential script hash. diff --git a/crates/aldabra-dao/src/agora/stake.rs b/crates/aldabra-dao/src/agora/stake.rs index b0765ce..d869b48 100644 --- a/crates/aldabra-dao/src/agora/stake.rs +++ b/crates/aldabra-dao/src/agora/stake.rs @@ -8,7 +8,7 @@ //! - The locked governance tokens (in the value). //! - A `StakeDatum` (inline datum) carrying owner / delegation / vote-locks. //! - A "stake state thread" token from the Agora stake-policy minting policy -//! (proves the UTxO is a real stake, not someone sending TRP to the +//! (proves the UTxO is a real stake, not someone sending gov-tokens to the //! address by accident). //! //! This module is the type port + encode/decode only. Tx assembly lives @@ -178,7 +178,7 @@ impl ProposalLock { /// - `Nothing` → `Constr 1 []` #[derive(Debug, Clone, PartialEq, Eq)] pub struct StakeDatum { - /// Amount of governance token (TRP) locked. Voting weight. + /// Amount of governance token locked. Voting weight. pub staked_amount: i64, /// Stake owner; only this credential may move/destroy this stake. pub owner: Credential, @@ -348,16 +348,11 @@ mod tests { assert_eq!(StakeDatum::from_plutus_data(&pd).unwrap(), s); } - /// Decode Kayos's actual on-chain stake from Sulkta's stakes_addr. - /// Anchors the StakeDatum type port to a real UTxO so a future - /// encoding refactor can't silently break decode of existing stakes. - /// - /// Source: Koios `address_info` for stakes addr - /// `addr1w8msu7psehjlcu5glzjgjtq53m4mk75c9d2p8hfjwyknmfqskkah8`, - /// utxo `d5b73a9d1e0fc4cedaf25b1172d379ad36bc39ec8516005cd70b12f9b5bdaa2f#0`. - /// Captured 2026-05-06. + /// Decode a real on-chain stake datum. Anchors the StakeDatum + /// type port to a real UTxO so a future encoding refactor can't + /// silently break decode of existing stakes. #[test] - fn decodes_sulkta_live_kayos_stake() { + fn decodes_live_stake_datum() { use pallas_primitives::PlutusData; let cbor_hex = "9f1832d8799f581c84d08bace7a5f23591d80e91dccd43fa3ea55a8d974f208842b6f2f3ffd87a8080ff"; @@ -387,7 +382,7 @@ mod tests { } /// Same shape as `decodes_sulkta_live_kayos_stake` but for Cobb's - /// stake (250 Terrapin). Two-witness regression — catches drift + /// stake (250 gov-token). Two-witness regression — catches drift /// even if the Kayos test happens to flatten over a bug. #[test] fn decodes_sulkta_live_cobb_stake() { diff --git a/crates/aldabra-dao/src/builder/escrow_agree.rs b/crates/aldabra-dao/src/builder/escrow_agree.rs index ede32cc..0dda30d 100644 --- a/crates/aldabra-dao/src/builder/escrow_agree.rs +++ b/crates/aldabra-dao/src/builder/escrow_agree.rs @@ -1,6 +1,6 @@ //! Build an unsigned `escrow_agree_unsigned` transaction. //! -//! ⚠️ Not third-party audited — preprod-only. See `audits/2026-05-09-escrow-internal-audit.md`. +//! ⚠️ Not third-party audited — use-at-own-risk for high-value flows. See `audits/2026-05-09-escrow-internal-audit.md`. //! //! ## What this tx does //! diff --git a/crates/aldabra-dao/src/builder/escrow_deposit.rs b/crates/aldabra-dao/src/builder/escrow_deposit.rs index 20fa4b0..e2ddb79 100644 --- a/crates/aldabra-dao/src/builder/escrow_deposit.rs +++ b/crates/aldabra-dao/src/builder/escrow_deposit.rs @@ -1,6 +1,6 @@ //! Build an unsigned `escrow_deposit_unsigned` transaction. //! -//! ⚠️ Not third-party audited — preprod-only. See `audits/2026-05-09-escrow-internal-audit.md`. +//! ⚠️ Not third-party audited — use-at-own-risk for high-value flows. See `audits/2026-05-09-escrow-internal-audit.md`. //! //! ## What this tx does //! diff --git a/crates/aldabra-dao/src/builder/escrow_open.rs b/crates/aldabra-dao/src/builder/escrow_open.rs index 1d059e6..f031448 100644 --- a/crates/aldabra-dao/src/builder/escrow_open.rs +++ b/crates/aldabra-dao/src/builder/escrow_open.rs @@ -1,6 +1,6 @@ //! Build an unsigned `escrow_open_unsigned` transaction. //! -//! ⚠️ Not third-party audited — preprod-only. See `audits/2026-05-09-escrow-internal-audit.md`. +//! ⚠️ Not third-party audited — use-at-own-risk for high-value flows. See `audits/2026-05-09-escrow-internal-audit.md`. //! //! ## What this tx does //! diff --git a/crates/aldabra-dao/src/builder/escrow_refund_timeout.rs b/crates/aldabra-dao/src/builder/escrow_refund_timeout.rs index 2e5cd9d..e5d9cbf 100644 --- a/crates/aldabra-dao/src/builder/escrow_refund_timeout.rs +++ b/crates/aldabra-dao/src/builder/escrow_refund_timeout.rs @@ -1,6 +1,6 @@ //! Build an unsigned `escrow_refund_timeout_unsigned` transaction. //! -//! ⚠️ Not third-party audited — preprod-only. See `audits/2026-05-09-escrow-internal-audit.md`. +//! ⚠️ Not third-party audited — use-at-own-risk for high-value flows. See `audits/2026-05-09-escrow-internal-audit.md`. //! //! ## What this tx does //! diff --git a/crates/aldabra-dao/src/builder/escrow_settle.rs b/crates/aldabra-dao/src/builder/escrow_settle.rs index a3c1d8d..623b9d8 100644 --- a/crates/aldabra-dao/src/builder/escrow_settle.rs +++ b/crates/aldabra-dao/src/builder/escrow_settle.rs @@ -1,6 +1,6 @@ //! Build an unsigned `escrow_settle_unsigned` transaction. //! -//! ⚠️ Not third-party audited — preprod-only. See `audits/2026-05-09-escrow-internal-audit.md`. +//! ⚠️ Not third-party audited — use-at-own-risk for high-value flows. See `audits/2026-05-09-escrow-internal-audit.md`. //! //! ## What this tx does //! diff --git a/crates/aldabra-dao/src/builder/escrow_veto.rs b/crates/aldabra-dao/src/builder/escrow_veto.rs index 001300e..2ef6f12 100644 --- a/crates/aldabra-dao/src/builder/escrow_veto.rs +++ b/crates/aldabra-dao/src/builder/escrow_veto.rs @@ -1,6 +1,6 @@ //! Build an unsigned `escrow_veto_unsigned` transaction. //! -//! ⚠️ Not third-party audited — preprod-only. See `audits/2026-05-09-escrow-internal-audit.md`. +//! ⚠️ Not third-party audited — use-at-own-risk for high-value flows. See `audits/2026-05-09-escrow-internal-audit.md`. //! //! ## What this tx does //! diff --git a/crates/aldabra-dao/src/builder/mod.rs b/crates/aldabra-dao/src/builder/mod.rs index 3f62efa..775e78e 100644 --- a/crates/aldabra-dao/src/builder/mod.rs +++ b/crates/aldabra-dao/src/builder/mod.rs @@ -12,9 +12,9 @@ //! | 4b | `proposal_cosign` | Add additional cosigner to a Draft proposal | //! | 3 | `proposal_vote` | Spend stake (PermitVote) + proposal (Vote tag) | //! | 4c | `proposal_advance` | State-machine transition redeemer | -//! | 4d | `stake_destroy` | Spend stake (Destroy), return TRP to wallet | +//! | 4d | `stake_destroy` | Spend stake (Destroy), return gov-tokens to wallet | //! | 4e | `treasury_execute` | Burn GAT + spend treasury per effect datum | -//! | def. | `stake_create` | Lock TRP at stakes script (deferred — both | +//! | def. | `stake_create` | Lock gov-tokens at stakes script (deferred — both | //! | | | live wallets already have stakes) | pub mod escrow_agree; diff --git a/crates/aldabra-dao/src/builder/proposal_create.rs b/crates/aldabra-dao/src/builder/proposal_create.rs index 0edffa7..a585d4e 100644 --- a/crates/aldabra-dao/src/builder/proposal_create.rs +++ b/crates/aldabra-dao/src/builder/proposal_create.rs @@ -55,10 +55,11 @@ use crate::error::{DaoError, DaoResult}; /// Per-script ExUnits budget for proposal_create. /// -/// **AUDIT-H2 fix 2026-05-05:** Original values were 14M mem / 10G steps -/// each — equal to per-tx Conway max. With 3 plutus contracts firing -/// (governor spend + stake spend + ProposalST mint), the total claim -/// would exceed the per-tx cap and node rejects pre-phase-2. +/// With 3 Plutus contracts firing in this tx (governor spend + +/// stake spend + ProposalST mint), the per-script claim must be +/// significantly under the per-tx Conway max (14M mem / 10G steps), +/// otherwise the node rejects the tx pre-phase-2 even before scripts +/// run. /// /// The reference tx (`7c8db1432a07...`) used 1208B tx size + 573_553 /// lovelace fee, suggesting much smaller ExUnits per script. Drop to @@ -115,21 +116,22 @@ pub struct GovernorUtxoIn { /// 56-hex Governor State Thread (GST) policy id. The new governor /// output must carry +1 of this token to keep the singleton invariant. pub gst_policy_hex: String, - /// Asset name (hex) of the GST token. Empty for Sulkta. + /// Asset name (hex) of the GST token. Often empty. pub gst_asset_name_hex: String, } /// On-chain stake state we need to spend (proposer's existing stake). /// -/// AUDIT-C2 fix 2026-05-05: governor's `CreateProposal` branch hard-asserts -/// `Stake input should present`. Builder MUST take a stake utxo to spend. -/// The owner of the stake's datum must equal the tx's signer (proposer_pkh). +/// The governor's `CreateProposal` branch hard-asserts "Stake input +/// should present" — the builder MUST take a stake utxo to spend. +/// The owner of the stake's datum must equal the tx's signer +/// (proposer_pkh). #[derive(Debug, Clone)] pub struct StakeUtxoIn { pub tx_hash_hex: String, pub output_index: u32, pub lovelace: u64, - /// Current Terrapin/gov-token quantity on the UTxO. Must equal + /// Current gov-token quantity on the UTxO. Must equal /// `datum.staked_amount` per stake validator invariant. pub gov_token_qty: u64, /// StakeST asset name (= stake validator script hash) for the +1 token @@ -169,7 +171,7 @@ impl ReferenceUtxo { pub struct ProposalCreateArgs { pub cfg: DaoConfig, pub governor: GovernorUtxoIn, - /// Proposer's existing stake UTxO. AUDIT-C2 — required for the + /// Proposer's existing stake UTxO. required for the /// governor's CreateProposal branch to find a stake input. The stake's /// owner pkh must equal `proposer_pkh`, and `staked_amount + deposit` /// must clear `governor.proposal_thresholds.create`. @@ -194,12 +196,12 @@ pub struct ProposalCreateArgs { /// validRange` by construction. pub starting_time_slot: u64, /// Current chain tip slot. Retained for caller-side fee/sanity - /// math; no longer drives the validity range as of 2026-05-07 - /// (see `starting_time_slot` above). + /// math; doesn't drive the validity range — that anchors on + /// `starting_time_slot` above. pub tip_slot: u64, /// Reference UTxO to cite for the governor validator script. pub governor_validator_ref: ReferenceUtxo, - /// Reference UTxO to cite for the stake validator script. AUDIT-C2. + /// Reference UTxO to cite for the stake validator script. pub stake_validator_ref: ReferenceUtxo, /// Reference UTxO to cite for the ProposalST minting policy script. pub proposal_st_policy_ref: ReferenceUtxo, @@ -242,7 +244,7 @@ pub fn build_unsigned_proposal_create( // ---- preflight: stake's owner must match proposer; stake meets create-threshold ---- // - // AUDIT-C2 + governor's `CreateProposal` invariants. Catch these + // Governor's `CreateProposal` invariants. Catch these // client-side rather than waste fees on a phase-2 reject. if !matches!(&args.stake_in.datum.owner, Credential::PubKey(h) if *h == args.proposer_pkh) { return Err(DaoError::State("stake owner pkh does not match proposer pkh — proposer must own the stake input".to_string())); @@ -319,11 +321,12 @@ pub fn build_unsigned_proposal_create( // Proposal: fresh ProposalDatum (Draft, sole cosigner, copied params). // - // AUDIT-C1 fix 2026-05-05: effects must be a NON-empty map with at least - // one neutral (empty inner map) entry, AND its keys must equal the votes - // map's keys. Per Agora `Governor/Scripts.hs:437-462` validators + // Effects must be a NON-empty map with at least one neutral (empty + // inner map) entry, AND its keys must equal the votes map's keys. + // Per Agora `Governor/Scripts.hs:437-462`, validators // `phasNeutralEffect` (pany # pnull over inner maps) and - // `pisEffectsVotesCompatible` (effects keys == votes keys). + // `pisEffectsVotesCompatible` (effects keys == votes keys) both + // hard-fail without this. // // For InfoOnly: both ResultTag(0) and ResultTag(1) map to empty inner // maps (no effect scripts trigger regardless of vote outcome). @@ -354,9 +357,8 @@ pub fn build_unsigned_proposal_create( // proposal. Order matters — the stake validator's ppermitVote uses // `pcons NEW_LOCK old_locks` (head-cons, NOT append). If we append // and the input had pre-existing locks, the chain rejects with - // CekError on the stake validator. Caught 2026-05-08 trying to - // create proposal #1 while the stake still held a Created lock - // from proposal #0; cosign + vote builders already prepend. + // CekError on the stake validator. Cosign + vote builders already + // prepend on the same invariant. let mut new_locks = Vec::with_capacity(args.stake_in.datum.locked_by.len() + 1); new_locks.push(ProposalLock { proposal_id: new_proposal_id, @@ -378,19 +380,16 @@ pub fn build_unsigned_proposal_create( // ---- redeemers -------------------------------------------------------- // - // Governor spend: GovernorRedeemer::CreateProposal = Integer 0 (per - // EnumIsData encoding fix 2026-05-05). + // Governor spend: GovernorRedeemer::CreateProposal = Integer 0 + // (per EnumIsData encoding — variant index as a bare Integer). // // Stake spend: redeemer is PermitVote (Constr 2 []). DepositWithdraw // requires locked_by to STAY empty — which conflicts with adding a - // Created lock for the new proposal. PermitVote is the redeemer that - // grants new locks (for create/vote/cosign) on a stake. Caught - // 2026-05-07 PM via base64-decoded failing-script header (5178 bytes - // = stake validator); the bare CekError under traces-stripped Agora - // pointed at the stake's lock-state invariant. + // Created lock for the new proposal. PermitVote is the redeemer + // that grants new locks (for create/vote/cosign) on a stake. // // Mint redeemer: per `Agora/Proposal/Scripts.hs:118` the policy is - // `\_gst _redeemer ctx -> ...` — redeemer is unused. Constr 0 [] is fine. + // `\_gst _redeemer ctx -> ...` — redeemer unused. Constr 0 [] is fine. let governor_spend_redeemer_cbor = minicbor::to_vec(&crate::agora::plutus_data::int(0)?) .map_err(|e| DaoError::Cbor(format!("governor spend redeemer encode: {e}")))?; @@ -429,7 +428,7 @@ pub fn build_unsigned_proposal_create( )) })?; // Wallet change can be a regular pubkey output — lower min-utxo floor. - // AUDIT-M2: previous code required script-floor (2 ADA) for wallet + // Previous code required script-floor (2 ADA) for wallet // change; that's wrong, use 1 ADA (still conservative for Conway). const WALLET_CHANGE_MIN_LOVELACE: u64 = 1_000_000; if change_lovelace > 0 && change_lovelace < WALLET_CHANGE_MIN_LOVELACE { @@ -585,7 +584,7 @@ pub fn build_unsigned_proposal_create( Some(PROPOSAL_CREATE_MINT_EX_UNITS), ); - // AUDIT-C3 fix: tx validity range + disclosed_signer. + // tx validity range + disclosed_signer. // // pvalidateProposalStartingTime requires a bounded validRange ≤ // create_proposal_time_range_max_width that includes starting_time. @@ -603,26 +602,26 @@ pub fn build_unsigned_proposal_create( as u64) .saturating_sub(1) .min(VALIDITY_RANGE_SLOTS); - // 2026-05-07: anchor the validity range to caller-supplied - // `starting_time_slot` instead of `tip_slot`. Public Koios's tip - // endpoint can lag the actual chain by 100+ slots; under a 29s - // governor window that lag pushes invalid_after into the past - // before the tx ever reaches a node. Caller passes a slightly-future - // starting_time_slot (e.g. tip+30); the on-chain + // Anchor the validity range to caller-supplied `starting_time_slot` + // instead of `tip_slot`. Public Koios's tip endpoint can lag the + // actual chain by 100+ slots; under a tight governor window that + // lag pushes `invalid_after` into the past before the tx ever + // reaches a node. Caller passes a slightly-future + // `starting_time_slot` (e.g. tip+30); the on-chain // `OutsideValidityIntervalUTxO` check then has a window that // straddles when the tx actually lands, while the in-script // `pvalidateProposalStartingTime` is satisfied because // `starting_time_slot ∈ [valid_from, invalid_after - 1]` by // construction. - // 2026-05-08: CENTER `starting_time_slot` inside the validity range - // (rather than putting it at the lower bound). Tiny test DAOs run on - // a 30-second create_proposal_time_range_max_width, and koios's tip - // endpoint lag vs. the actual node can swing ±60s. With - // valid_from = starting_time, the window only spans [now, now+30]. - // If chain is even slightly past `now` when the tx lands, the tx - // expires. Centering gives [now-15, now+15] of slack — same width, - // same validator-bound, but the chain-now-at-block-time can drift - // ±15s without missing the window. + // + // CENTER `starting_time_slot` inside the validity range (not at + // the lower bound). For tight test-governor windows (30 s), Koios + // tip lag vs. actual chain time can swing ±60 s — with + // `valid_from = starting_time` the window only spans [now, now+30], + // so any chain drift past `now` expires the tx. Centering gives + // [now-15, now+15] of slack — same width, same validator-bound, + // but the chain-now-at-block-time can drift ±15s without missing + // the window. let half_width_slots = max_width_slots / 2; let valid_from = args.starting_time_slot.saturating_sub(half_width_slots); let invalid_from = valid_from + max_width_slots; @@ -639,12 +638,11 @@ pub fn build_unsigned_proposal_create( staging = staging.fee(args.fee_lovelace).network_id(network_id); - // Wire the V2 cost model so pallas computes script_data_hash. Without - // this the chain rejects with PPViewHashesDontMatch — same trap the - // plutus_mint path tripped over on 2026-05-07. All Agora validators - // we witness here (governor, stake, proposalSt policy) are PlutusV2 - // on the current preprod linker output, so a single language_view - // entry covers all three. + // Wire the V2 cost model so pallas computes script_data_hash. + // Without it the chain rejects with PPViewHashesDontMatch. All + // Agora validators witnessed here (governor, stake, proposalSt + // policy) are PlutusV2 in the current linker output, so a single + // language_view entry covers all three. staging = staging.language_view( ScriptKind::PlutusV2, aldabra_core::plutus_cost_models::PLUTUS_V2_COST_MODEL_PREPROD.to_vec(), diff --git a/crates/aldabra-dao/src/builder/stake_destroy.rs b/crates/aldabra-dao/src/builder/stake_destroy.rs index d492f14..37b40c3 100644 --- a/crates/aldabra-dao/src/builder/stake_destroy.rs +++ b/crates/aldabra-dao/src/builder/stake_destroy.rs @@ -1,7 +1,7 @@ //! Build a `dao_stake_destroy` transaction. //! //! Destroys a stake UTxO, burning its StakeST token and returning the -//! locked governance tokens (TRP for Sulkta) + lovelace to the owner's +//! locked governance tokens + lovelace to the owner's //! wallet. //! //! ## Tx shape diff --git a/crates/aldabra-dao/src/config.rs b/crates/aldabra-dao/src/config.rs index fad00da..d356f19 100644 --- a/crates/aldabra-dao/src/config.rs +++ b/crates/aldabra-dao/src/config.rs @@ -55,7 +55,7 @@ pub enum DaoNetwork { } -/// One named DAO. Captures every Sulkta-specific value as an +/// One named DAO. Captures every per-DAO value as an /// instance field so the rest of the crate is config-driven. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DaoConfig { @@ -380,17 +380,17 @@ mod tests { fn register_makes_first_dao_active() { let dir = tempdir().unwrap(); let store = DaoStore::new(dir.path()); - store.register(&cfg("sulkta")).unwrap(); - assert_eq!(store.get_active().unwrap().name(), "sulkta"); + store.register(&cfg("test-dao")).unwrap(); + assert_eq!(store.get_active().unwrap().name(), "test-dao"); } #[test] fn second_register_does_not_change_active() { let dir = tempdir().unwrap(); let store = DaoStore::new(dir.path()); - store.register(&cfg("sulkta")).unwrap(); + store.register(&cfg("test-dao")).unwrap(); store.register(&cfg("bobs_dao")).unwrap(); - assert_eq!(store.get_active().unwrap().name(), "sulkta"); + assert_eq!(store.get_active().unwrap().name(), "test-dao"); } #[test] @@ -414,8 +414,8 @@ mod tests { fn remove_clears_active_if_was_active() { let dir = tempdir().unwrap(); let store = DaoStore::new(dir.path()); - store.register(&cfg("sulkta")).unwrap(); - store.remove("sulkta").unwrap(); + store.register(&cfg("test-dao")).unwrap(); + store.remove("test-dao").unwrap(); assert!(store.get_active().is_err()); } @@ -423,16 +423,16 @@ mod tests { fn resolve_falls_through_to_active() { let dir = tempdir().unwrap(); let store = DaoStore::new(dir.path()); - store.register(&cfg("sulkta")).unwrap(); + store.register(&cfg("test-dao")).unwrap(); let cfg = store.resolve(None).unwrap(); - assert_eq!(cfg.name, "sulkta"); + assert_eq!(cfg.name, "test-dao"); } #[test] fn resolve_named_overrides_active() { let dir = tempdir().unwrap(); let store = DaoStore::new(dir.path()); - store.register(&cfg("sulkta")).unwrap(); + store.register(&cfg("test-dao")).unwrap(); store.register(&cfg("bobs_dao")).unwrap(); let cfg = store.resolve(Some("bobs_dao")).unwrap(); assert_eq!(cfg.name, "bobs_dao"); @@ -440,14 +440,14 @@ mod tests { #[test] fn validate_rejects_bad_name() { - let mut c = cfg("sulkta"); + let mut c = cfg("test-dao"); c.name = "Sulkta DAO".into(); // uppercase + space assert!(c.validate().is_err()); } #[test] fn validate_rejects_short_policy() { - let mut c = cfg("sulkta"); + let mut c = cfg("test-dao"); c.gov_token_policy = "abc".into(); assert!(c.validate().is_err()); } diff --git a/crates/aldabra-dao/src/discovery.rs b/crates/aldabra-dao/src/discovery.rs index b326fb9..f51e7b3 100644 --- a/crates/aldabra-dao/src/discovery.rs +++ b/crates/aldabra-dao/src/discovery.rs @@ -1,8 +1,12 @@ -//! Auto-discover Agora script hashes + reference UTxO refs from on-chain state. +//! Auto-discover Agora script hashes + reference UTxO refs from +//! on-chain state. //! -//! Closes the "user has to research and hand-populate ScriptRefs" gap by -//! running the same Koios queries the human audit at -//! `memory/audit-sulkta-agora-2026-05-05.md` performed. +//! Closes the "user has to research and hand-populate ScriptRefs" +//! gap by running the Koios queries that would otherwise be done by +//! hand: enumerate UTxOs at the deployer / stakes address, match +//! `reference_script.hash` entries against the script hashes +//! extracted from the configured governor + stakes + treasury +//! addresses. //! //! ## What we discover from the existing config //! @@ -195,11 +199,12 @@ pub async fn discover_scripts( // // A stake UTxO carries (gov_token, qty) + (stake_st_token, 1). // - // **AUDIT-H6 fix 2026-05-05:** Previous logic was "first non-gov-token - // asset on a stake UTxO" — would silently pick a wrong asset if anyone - // ever sent a junk NFT to a stake UTxO (Cardano allows this). Tighten: - // the StakeST minting policy mints with `asset_name = stake validator's - // script hash` per `Stake/Scripts.hs:188-190` (`pscriptHashToTokenName`). + // Tight match: the StakeST minting policy mints with + // `asset_name = stake validator's script hash` per + // `Stake/Scripts.hs:188-190` (`pscriptHashToTokenName`). + // A naive "first non-gov-token asset" match would silently pick + // a wrong asset if anyone sent a junk NFT to a stake UTxO + // (Cardano allows this). // Match on that explicitly. match client.address_info(&cfg.stakes_addr).await { Ok(infos) => { @@ -397,10 +402,10 @@ mod tests { } } - fn sulkta_cfg() -> DaoConfig { + fn test_dao_cfg() -> DaoConfig { use crate::config::ScriptRefs; DaoConfig { - name: "sulkta".into(), + name: "test-dao".into(), description: None, governor_addr: "addr1w8v73wfrru7smn738k6c5xafqvl2tgsvct7dtztc4jwlf4c35jnmy".into(), stakes_addr: "addr1w8msu7psehjlcu5glzjgjtq53m4mk75c9d2p8hfjwyknmfqskkah8".into(), @@ -421,7 +426,7 @@ mod tests { #[tokio::test] async fn discovers_stake_st_from_existing_stake() { - let cfg = sulkta_cfg(); + let cfg = test_dao_cfg(); let mut responses = std::collections::HashMap::new(); // A fake stake UTxO at stakes_addr carrying gov-token + StakeST. // StakeST asset_name == Sulkta stake validator hash (per H-6 fix). @@ -472,7 +477,7 @@ mod tests { #[tokio::test] async fn finds_validator_refs_at_deployer() { - let cfg = sulkta_cfg(); + let cfg = test_dao_cfg(); let governor_hash = "d9e8b9231f3d0dcfd13db58a1ba9033ea5a20cc2fcd58978ac9df4d7"; let stake_hash = "f70e7830cde5fc7288f8a4892c148eebbb7a982b5413dd32712d3da4"; @@ -545,7 +550,7 @@ mod tests { #[test] fn apply_discovery_merges_into_config() { - let mut cfg = sulkta_cfg(); + let mut cfg = test_dao_cfg(); let report = DiscoveryReport { governor_validator_ref: Some("aa#1".into()), stake_validator_ref: Some("bb#2".into()), @@ -562,7 +567,7 @@ mod tests { #[test] fn apply_discovery_doesnt_overwrite_existing() { - let mut cfg = sulkta_cfg(); + let mut cfg = test_dao_cfg(); cfg.stake_st_policy = Some("preexisting".into()); let report = DiscoveryReport { stake_st_policy: Some("would_overwrite".into()), @@ -572,12 +577,12 @@ mod tests { assert_eq!(cfg.stake_st_policy.as_deref(), Some("preexisting")); } - /// Regression for AUDIT-H6: a stake UTxO with a junk third-party token - /// must NOT pollute StakeST detection. The StakeST is only detected - /// when its `asset_name == stakes_validator_script_hash`. + /// Regression: a stake UTxO with a junk third-party token must NOT + /// pollute StakeST detection. The StakeST is only detected when its + /// `asset_name == stakes_validator_script_hash`. #[tokio::test] - async fn h6_junk_token_does_not_pollute_stake_st_detection() { - let cfg = sulkta_cfg(); + async fn junk_token_does_not_pollute_stake_st_detection() { + let cfg = test_dao_cfg(); let mut responses = std::collections::HashMap::new(); responses.insert( cfg.stakes_addr.clone(), diff --git a/crates/aldabra-dao/src/lib.rs b/crates/aldabra-dao/src/lib.rs index 33718ac..603799e 100644 --- a/crates/aldabra-dao/src/lib.rs +++ b/crates/aldabra-dao/src/lib.rs @@ -21,9 +21,9 @@ //! - Not a key store. Signing is delegated to `aldabra-core`. //! - Not an MCP server. The `dao_*` tools that wrap these primitives //! live in `aldabra-mcp`. -//! - Not Sulkta-specific. Every Sulkta value (TRP policy, governor -//! address, etc) comes from a [`config::DaoConfig`] loaded at -//! runtime, never compile-time. +//! - Not specific to any single DAO. Every per-DAO value (governance +//! token policy, governor / stakes / treasury addresses, etc.) comes +//! from a [`config::DaoConfig`] loaded at runtime, never compile-time. pub mod agora; pub mod builder; diff --git a/crates/aldabra-dao/src/reader.rs b/crates/aldabra-dao/src/reader.rs index 353ae38..b0abee3 100644 --- a/crates/aldabra-dao/src/reader.rs +++ b/crates/aldabra-dao/src/reader.rs @@ -39,7 +39,7 @@ pub struct StakeUtxo { pub datum: StakeDatum, /// Lovelace at this UTxO. pub lovelace: u64, - /// Gov-token (TRP) quantity at this UTxO. Should equal + /// Gov-token quantity at this UTxO. Should equal /// `datum.staked_amount` when the validator is correctly enforcing. pub gov_token_quantity: u64, } diff --git a/crates/aldabra-mcp/src/tools.rs b/crates/aldabra-mcp/src/tools.rs index 37d0b59..18b7d16 100644 --- a/crates/aldabra-mcp/src/tools.rs +++ b/crates/aldabra-mcp/src/tools.rs @@ -3,9 +3,7 @@ //! Each `#[tool]` becomes a discoverable MCP tool. Tool names use //! `snake_case` only (no dots) — Claude Code's MCP client validates //! tool names against `[a-zA-Z0-9_-]{1,64}` and silently drops names -//! with dots. This was an integration-time discovery 2026-05-04 after -//! the first session restart found zero aldabra tools advertised -//! despite the daemon running. +//! with dots, causing the daemon to run without advertising any tools. //! //! ## Phase 1 — read path //! @@ -85,11 +83,11 @@ use aldabra_dao::reader::{DaoReader, KoiosDaoReader}; /// raw options; this fn enforces the "at most one" rule and reads /// the file when path is set. /// -/// The path-based variant exists because of the 2026-05-07 MCP -/// transport bug: hex strings >~ 4500 chars get a 1-byte truncation -/// + structural rearrangement somewhere between Claude Code and -/// aldabra's stdio reader. Reading from a file inside the container -/// bypasses the JSON-RPC arg path entirely. +/// The path-based variant exists because of an MCP transport bug: +/// hex strings >~ 4500 chars get a 1-byte truncation + structural +/// rearrangement somewhere between client and stdio reader. Reading +/// from a file inside the container bypasses the JSON-RPC arg path +/// entirely. fn resolve_ref_script_bytes( cbor_hex: Option<&str>, path: Option<&str>, @@ -127,11 +125,11 @@ fn resolve_ref_script_bytes( /// reads the file when path is set. /// /// Mirrors [`resolve_ref_script_bytes`] — same workaround for the -/// 2026-05-07 MCP transport bug where hex strings >~ 4500 chars -/// get a 1-byte truncation between Claude Code and aldabra's stdio -/// reader, surfacing as "odd length" hex decode errors and blocking -/// debug-build minting policies. Reading from a file inside the -/// container bypasses the JSON-RPC arg path entirely. +/// MCP large-string transport bug where hex strings >~ 4500 chars +/// get a 1-byte truncation between client and stdio reader, +/// surfacing as "odd length" hex decode errors and blocking debug- +/// build minting policies. Reading from a file inside the container +/// bypasses the JSON-RPC arg path entirely. fn resolve_policy_cbor_bytes( cbor_hex: Option<&str>, path: Option<&str>, @@ -352,7 +350,7 @@ pub struct SendArgs { /// Path INSIDE THE ALDABRA CONTAINER to a file containing the /// hex-encoded reference-script CBOR. Use INSTEAD of /// `reference_script_cbor_hex` for scripts >~ 4KB to bypass the - /// MCP large-string transport bug (caught 2026-05-07: hex strings + /// MCP large-string transport bug (hex strings /// > ~4500 chars get a 1-byte truncation + structural rearrangement /// > somewhere between Claude Code and aldabra's stdio reader). /// > File contents may include leading/trailing whitespace; only @@ -482,7 +480,7 @@ pub struct PlutusMintUnsignedArgs { /// Path INSIDE THE ALDABRA CONTAINER to a file containing /// hex-encoded Plutus policy CBOR. Use INSTEAD of /// `policy_cbor_hex` for scripts >~ 4500 chars to bypass the - /// MCP large-string transport bug (caught 2026-05-07: hex strings + /// MCP large-string transport bug (hex strings /// > ~4500 chars get a 1-byte truncation + structural rearrangement /// > somewhere between Claude Code and aldabra's stdio reader, /// > surfacing as "odd length" hex decode errors). File contents @@ -508,7 +506,7 @@ pub struct PlutusMintUnsignedArgs { pub dest_lovelace: u64, /// Non-mint native assets to forward from wallet inputs onto /// the recipient output. Used e.g. on stake bootstrap to send - /// gov tokens (tTRP) into the stakes_addr alongside the freshly + /// gov tokens into the stakes_addr alongside the freshly /// minted StakeST. #[serde(default)] pub dest_extra_assets: Vec, @@ -729,11 +727,9 @@ pub struct Cip68NftArgs { fn default_token_lovelace() -> u64 { // 2.5 ADA — Babbage min-utxo for an inline-datum-bearing // multi-asset output is ~1.79 ADA (depends on datum size). - // 1.5 was too low; 2.5 gives comfortable margin for typical - // CIP-68 metadata (~150 bytes). Larger metadata still requires - // the caller to override. - // Discovered preprod 2026-05-04 via - // BabbageOutputTooSmallUTxO chain rejection. + // 2.5 gives comfortable margin for typical CIP-68 metadata + // (~150 bytes). Larger metadata still requires the caller to + // override. 2_500_000 } @@ -2426,7 +2422,7 @@ impl WalletService { .await .map_err(|e| format!("koios get wallet utxos: {e}"))?; - // AUDIT-H5 fix: assets in the chain backend are + // assets in the chain backend are // `BTreeMap`. Previous // implementation silently dropped any key < 56 chars via filter_map // — that could let a corrupt Koios response burn assets on submit. @@ -2527,7 +2523,7 @@ impl WalletService { #[tool( name = "dao_stake_destroy_unsigned", - description = "Build an unsigned tx that destroys this wallet's stake — burns the StakeST token and returns all locked governance tokens (TRP) + lovelace to the wallet. Owner-only (delegatees rejected). Requires the stake to have NO active locks (no Created/Voted/Cosigned ProposalLocks). Args: dao? + fee_lovelace (~2_000_000)." + description = "Build an unsigned tx that destroys this wallet's stake — burns the StakeST token and returns all locked governance tokens + lovelace to the wallet. Owner-only (delegatees rejected). Requires the stake to have NO active locks (no Created/Voted/Cosigned ProposalLocks). Args: dao? + fee_lovelace (~2_000_000)." )] async fn dao_stake_destroy_unsigned( &self, @@ -2700,10 +2696,10 @@ impl WalletService { // either inside the period OR strictly after period_end. Any // straddle = waste of fees. // - // AUDIT-2026-05-06 H-1/H-2/H-4 fixes: use STRICT > on PAfter - // boundary, require tx-upper to land inside the target period for - // PWithin, AND gate Locked→Finished on tx_lower > executing_end so - // we never hit the "missing GAT-mint" path. + // Boundary discipline: use STRICT > on PAfter, require tx-upper + // to land inside the target period for PWithin, AND gate + // Locked→Finished on `tx_lower > executing_end` so we never hit + // the "missing GAT-mint" path. use aldabra_dao::agora::proposal::ProposalStatus as PS; const VALIDITY_RANGE_MS: i64 = aldabra_dao::builder::proposal_create::VALIDITY_RANGE_SLOTS as i64 * 1000; @@ -3182,20 +3178,19 @@ impl WalletService { tip_slot + aldabra_dao::builder::proposal_create::VALIDITY_RANGE_SLOTS; let tx_lower_ms = slot_to_posix_ms(cfg.network, tip_slot)?; - // AUDIT-2026-05-06 H-3 fix: validator (Proposal/Scripts.hs PVote - // ~L511) demands `pgetRelation == PWithin VotingPeriod`, where - // PWithin requires BOTH `voting_start <= lb` AND `ub <= voting_end`. - // The builder's existing preflight only verified the upper bound; - // a vote-too-early call (tip < voting_start) would burn fees on a - // "too early or invalid" script error. Catch lb-vs-voting_start - // here too. + // Validator (Proposal/Scripts.hs PVote ~L511) demands + // `pgetRelation == PWithin VotingPeriod`, where PWithin requires + // BOTH `voting_start <= lb` AND `ub <= voting_end`. The earlier + // preflight checked only the upper bound; a vote-too-early call + // (tip < voting_start) would burn fees on a "too early or + // invalid" script error. Catch lb-vs-voting_start here too. // - // 2026-05-08 follow-up: when default validity_upper would - // overshoot voting_end (e.g. 30-min Sulkta-shape windows where - // the 1799-slot validity range starting from current tip lands - // past voting_end), clamp validity_upper_slot to voting_end_slot - // so the range fits inside the voting window. Same trick the - // proposal_advance Draft→VotingReady clamp uses. + // When default validity_upper would overshoot voting_end (e.g. + // tight 30-min governor windows where the 1799-slot validity + // range starting from current tip lands past voting_end), clamp + // validity_upper_slot to voting_end_slot so the range fits + // inside the voting window. Same trick the proposal_advance + // Draft→VotingReady clamp uses. // // Read from prop_datum (target.datum was moved to prop_datum at L2636). let voting_start_check = prop_datum.starting_time + prop_datum.timing_config.draft_time; @@ -3566,12 +3561,12 @@ impl WalletService { // ─── escrow — two-party agreement-with-veto escrow on Plutus V3 ─── // // 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. + // Internal audit only — NOT third-party audited. 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 caveat carries. #[tool( name = "escrow_open_unsigned", @@ -4133,7 +4128,7 @@ pub struct DaoRegisterArgs { pub treasury_addr: String, /// 56 hex chars (28 bytes). pub gov_token_policy: String, - /// Hex-encoded asset name (e.g. "546572726170696e" for "Terrapin"). + /// Hex-encoded asset name (e.g. "546572726170696e" hex-decodes to "Terrapin"). pub gov_token_name_hex: String, /// `txhash#index` — the Agora bootstrap tx ref that identifies the DAO. pub initial_spend: String, @@ -4146,10 +4141,11 @@ pub struct DaoRegisterArgs { // ─── Phase 4 prerequisites — all optional ───────────────────────────── // - // Populate these to unlock dao_proposal_create_unsigned and the - // upcoming vote/cosign/advance tools. Each can be discovered via - // chain queries (the audit pattern at memory/audit-sulkta-agora-*.md); - // a future dao_discover_scripts MCP tool will fill them automatically. + // Populate these to unlock dao_proposal_create_unsigned + the + // vote/cosign/advance tools. Each can be discovered via chain + // queries against the configured governor + stakes addresses; + // see the `dao_discover_scripts` MCP tool which fills them + // automatically from on-chain state. /// Proposal validator address (bech32). Where new proposal UTxOs land. #[serde(default)] pub proposal_addr: Option, @@ -4482,8 +4478,8 @@ fn slot_to_posix_ms(network: DaoNetwork, slot: u64) -> Result { /// Shared by every DAO write-path tool that needs to fund + collateralize /// from the wallet. Surfaces malformed asset keys (< 56 chars) as errors /// instead of silently dropping them — a corrupt Koios response would -/// otherwise let our builder construct a tx that loses native assets on -/// submit. AUDIT-H5 fix from 2026-05-05. +/// otherwise let the builder construct a tx that loses native assets +/// on submit. async fn pull_wallet_utxos( chain: &KoiosClient, address: &str,