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