fix(escrow_wip): apply 2026-05-09 internal audit findings
Two HIGH validator-side bugs + several MED/LOW off-chain issues found in the subagent-driven audit on this branch. New validator hash: a8081acef26935d9b5f44b92052178e17301b6d6e6808c91c5b56f5d. ## HIGH-1: Deposit redeemer let depositors drain tokens aiken-escrow/validators/escrow.ak Deposit branch now requires `value_geq_value(new_value, in_value)` before computing net_added. Previously net_added could carry negative quantities (when new_value < in_value component-wise), letting a depositor write a matching new_d.deposits with reduced values and pocket the difference as wallet change. Latent under v1 ADA-only MCP usage but the validator must hold against all callers. ## HIGH-2: Empty/partial deposits enabled funds drain via Veto/Refund Veto and Refund branches now require `value_eq(deposits_to_value(d.deposits), in_value)` — the tracked deposits must account for the full locked value. Previously `refund_outputs_satisfy(_, [])` was vacuously true on empty deposits, so a driver could fire Veto/Refund on an escrow opened with `initial_contributor=None` (deposits=[], in_value>0) and pocket the input's lovelace as change. Defense in depth: escrow_open builder now refuses `initial_contributor=None`. New helper `deposits_to_value` folds deposit FlatValues into a Value via `assets.add` for the equality check. ## MED: off-chain fixes - escrow_open min-utxo bumped 1M → 2M (Conway-era inline-datum + script-address outputs need ~1.4-1.7 ADA, NOT the 1 ADA default). - escrow_settle_unsigned + escrow_refund_timeout_unsigned now derive `validity_lower_ms` via slot_to_posix_ms(network, slot) instead of Koios's `block_time*1000` — the chain reconstructs `lower` from the slot, so Koios's ~1s drift could pass off-chain preflight while the chain rejects at the strict-`>` boundary. - escrow_open_unsigned MCP tool no longer accepts (and silently discards) `fee_lovelace` — the unsigned-tx builder auto-estimates. ## LOW: defensive depth - escrow_veto + escrow_refund_timeout: `qty as u64` → `u64::try_from` so a corrupt or adversarial datum with negative i128 qty can't slip through with a wraparound. ## Tests - 36 escrow builder tests pass (added rejects_no_initial_contributor) - 132 dao tests pass under --features escrow_wip - aldabra-mcp release build clean ## Infra - Validator artifact files (plutus.json, validator.cbor.hex) regenerated. Dockerfile already wired to bake them at /etc/aldabra/escrow/ for MCP tools' validator_script_path arg. - Internal audit findings written up at aiken-escrow/README.md including the v2-deferred items (multi-asset spend-input, lovelace-not-cross-checked, etc.) Third-party audit still required before any mainnet deployment.
This commit is contained in:
parent
ef38ff0e57
commit
7daa62b5e5
8 changed files with 192 additions and 65 deletions
|
|
@ -3584,11 +3584,8 @@ impl WalletService {
|
|||
lock_period_ms,
|
||||
initial_contributor_pkh_hex,
|
||||
initial_lovelace,
|
||||
fee_lovelace,
|
||||
}: EscrowOpenUnsignedArgs,
|
||||
) -> Result<String, String> {
|
||||
let _ = fee_lovelace; // accepted for forward compat; payment builder is fee-estimating
|
||||
|
||||
let party_a = decode_pkh28(&party_a_pkh_hex, "party_a_pkh_hex")?;
|
||||
let party_b = decode_pkh28(&party_b_pkh_hex, "party_b_pkh_hex")?;
|
||||
let recipient = decode_pkh28(&recipient_pkh_hex, "recipient_pkh_hex")?;
|
||||
|
|
@ -3906,13 +3903,17 @@ impl WalletService {
|
|||
|
||||
let cfg = self.dao_cfg_for_escrow()?;
|
||||
let wallet_utxos = pull_wallet_utxos(&self.inner.chain, &self.inner.address).await?;
|
||||
let (tip_slot, tip_ms) = fetch_tip_slot_ms(self).await?;
|
||||
let (tip_slot, _tip_ms) = fetch_tip_slot_ms(self).await?;
|
||||
|
||||
// lower bound: caller-driven from tip; we sanity-check it satisfies
|
||||
// the lock-window elapsed gate. Validator extracts `lower` from
|
||||
// valid_from_slot anyway, so we anchor lower at tip_slot.
|
||||
// MED-2/3 fix (2026-05-09 audit): derive `validity_lower_ms`
|
||||
// from the slot via the Shelley constants, NOT from Koios's
|
||||
// `block_time*1000`. The chain reconstructs `lower` from
|
||||
// `valid_from_slot(slot)` via slot↔ms math; using Koios's
|
||||
// block_time can drift up to ~1s, which at the strict-`>`
|
||||
// validator boundary makes the off-chain preflight pass while
|
||||
// the chain rejects → fees + collateral burned.
|
||||
let validity_lower_slot = tip_slot;
|
||||
let validity_lower_ms = tip_ms;
|
||||
let validity_lower_ms = slot_to_posix_ms(cfg.network, validity_lower_slot)?;
|
||||
|
||||
let validity_upper_slot = tip_slot + validity_window_seconds.unwrap_or(1800);
|
||||
|
||||
|
|
@ -3977,9 +3978,10 @@ impl WalletService {
|
|||
|
||||
let cfg = self.dao_cfg_for_escrow()?;
|
||||
let wallet_utxos = pull_wallet_utxos(&self.inner.chain, &self.inner.address).await?;
|
||||
let (tip_slot, tip_ms) = fetch_tip_slot_ms(self).await?;
|
||||
let (tip_slot, _tip_ms) = fetch_tip_slot_ms(self).await?;
|
||||
// MED-2/3 fix: same slot-derived ms as Settle (see escrow_settle_unsigned).
|
||||
let validity_lower_slot = tip_slot;
|
||||
let validity_lower_ms = tip_ms;
|
||||
let validity_lower_ms = slot_to_posix_ms(cfg.network, validity_lower_slot)?;
|
||||
let validity_upper_slot = tip_slot + validity_window_seconds.unwrap_or(1800);
|
||||
|
||||
let unsigned = build_unsigned_escrow_refund_timeout(EscrowRefundTimeoutArgs {
|
||||
|
|
@ -4286,13 +4288,16 @@ pub struct EscrowOpenUnsignedArgs {
|
|||
/// Veto-window length after Agree, in ms.
|
||||
pub lock_period_ms: i64,
|
||||
/// Optional pkh of the initial contributor (must equal party_a or party_b).
|
||||
/// If unset, opens with empty deposits — both parties top up later.
|
||||
/// **HIGH-2 fix 2026-05-09:** The builder now REJECTS `None` here — every
|
||||
/// escrow must have at least one initial contributor at open time.
|
||||
/// Sending `null` returns an error. Field stays `Option<String>` for
|
||||
/// schema-stability.
|
||||
#[serde(default)]
|
||||
pub initial_contributor_pkh_hex: Option<String>,
|
||||
/// Lovelace to lock at the escrow output. Must clear min-utxo floor (~1 ADA).
|
||||
/// Lovelace to lock at the escrow output. **MED-5 fix:** must clear
|
||||
/// 2_000_000 (≥2 ADA) — Conway-era inline-datum + script-address
|
||||
/// outputs need ~1.4-1.7 ADA min-utxo, with headroom.
|
||||
pub initial_lovelace: u64,
|
||||
/// Estimated total fee in lovelace. ~2_500_000 reasonable for v1.
|
||||
pub fee_lovelace: u64,
|
||||
}
|
||||
|
||||
// ─── escrow_wip spend-tool args (deposit / agree / veto / settle / refund) ──
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue