fix(dao): audit punch list — H-1 to H-4 + M-2 + pallas bump

Lands the high-priority fixes from the 2026-05-06 audit before any
mainnet submit of the new vote/cosign/advance/destroy txs.

## H-1: Locked→Finished gate (MCP tool)

`dao_proposal_advance_unsigned` now refuses LockedToFinished unless
`tx_lower_ms > executing_end`. During the executing period the
validator demands gstMoved=true (governor input present); builder
doesn't include the governor input, so a tx in that window would
fee-burn. The proper Locked→Finished + GAT-mint flow is Phase 4c-bis;
this gate keeps us out of the broken middle.

## H-2 + H-4: strict-boundary + tx-upper-inside-period (MCP tool)

Validator's pgetRelation is strict on PAfter (`period_end < lb`)
and demands `ub <= period_end` on PWithin. Tool now picks PWithin
only when `tx_lower_ms >= period_start && tx_upper_ms <= period_end`,
PAfter only when `tx_lower_ms > period_end` strictly, and explicit-
errors on the boundary-straddling case (when tx validity range
crosses out of the target period). Same logic mirrored for the
VotingReady→Locked + VotingReady→Finished branches.

## H-3: vote builder lower-bound preflight (MCP tool)

`dao_proposal_vote_unsigned` previously checked only validity_upper
vs voting_end_ms. Validator demands BOTH `voting_start <= lb` AND
`ub <= voting_end`. Vote-too-early would hit "too early or invalid"
script error. New preflight on tx_lower_ms vs voting_start.

## M-2: DRep deposit pulled from ProtocolParams

Hardcoded constant (500 ADA) was wrong if the protocol changes
drep_deposit OR if the DRep was originally registered at a different
deposit amount (deregistration must match registration). Added
`drep_deposit_lovelace: u64` to ProtocolParams (default 500 ADA),
governance.rs build_signed_drep_registration / deregistration now
read from params instead of the constant. Constant kept for
backward compat with a doc note pointing at the params field.

## Pallas fork bump 507fd9da → 8091abd1

M-4 from the audit landed on the fork: voting_procedures builder
debug_assert_ne!s against empty CBOR map (0xa0) and docs the
upstream NonEmptyKeyValuePairs::decode footgun.

L-1 from the audit was a false finding — the audit subagent
misread the constants. PROPOSAL_CREATE_*_EX_UNITS are already at
the post-2026-05-05-H-2 values (5M mem / 2G steps per spend, 2M / 1G
per mint). The new builders alias these correctly. No change needed.
This commit is contained in:
Sulkta 2026-05-06 08:06:44 -07:00
parent 86a2787b7c
commit 8b48e7224e
4 changed files with 110 additions and 26 deletions

View file

@ -36,8 +36,11 @@ use crate::tx::InputUtxo;
use crate::{Network, PaymentKey, ProtocolParams, StakeKey, WalletError};
/// Conway DRep registration deposit. Mainnet protocol parameter
/// `drep_deposit` is currently 500 ADA. Caller can pass an override
/// via `params` if a hardfork changes it; default constant here.
/// `drep_deposit` is currently 500 ADA. **Use `params.drep_deposit_lovelace`
/// instead of this constant** — it's kept here for backward-compat callers
/// only. AUDIT-2026-05-06 M-2: hardcoding the deposit means a protocol
/// change (or an old DRep registered at a different deposit) will silently
/// fail ledger validation. Always pull from current chain params.
pub const DREP_REGISTRATION_DEPOSIT_LOVELACE: u64 = 500_000_000;
/// Two witnesses (payment + stake) — same overhead as
@ -263,7 +266,7 @@ pub fn build_signed_drep_registration(
}
};
let cert = Certificate::RegDRepCert(drep_credential, DREP_REGISTRATION_DEPOSIT_LOVELACE, anchor);
let cert = Certificate::RegDRepCert(drep_credential, params.drep_deposit_lovelace, anchor);
let cert_bytes = minicbor::to_vec(&cert)
.map_err(|e| WalletError::Derivation(format!("encode RegDRep cert: {e}")))?;
@ -274,7 +277,7 @@ pub fn build_signed_drep_registration(
available_utxos,
change_address_bech32,
vec![cert_bytes],
DREP_REGISTRATION_DEPOSIT_LOVELACE,
params.drep_deposit_lovelace,
params,
)
}
@ -483,16 +486,15 @@ pub fn build_signed_drep_deregistration(
) -> Result<Vec<u8>, WalletError> {
let stake_pkh = stake_key.public_key_hash();
let drep_credential = StakeCredential::AddrKeyhash(stake_pkh);
let cert = Certificate::UnRegDRepCert(drep_credential, DREP_REGISTRATION_DEPOSIT_LOVELACE);
let cert = Certificate::UnRegDRepCert(drep_credential, params.drep_deposit_lovelace);
let cert_bytes = minicbor::to_vec(&cert)
.map_err(|e| WalletError::Derivation(format!("encode UnRegDRep cert: {e}")))?;
// Negative deposit — we get it back. Two-pass fee accounts for it
// by leaving `deposit` at 0 here and letting the wallet output absorb
// the refund. Note: pallas-txbuilder writes the deposit as a negative
// contribution implicitly via the cert; the change calc here just
// needs to know we DON'T owe the protocol anything. Caller should
// expect their wallet output to grow by 500 ADA - fee.
// Refund equals the deposit originally paid. Critical: this MUST match
// what the DRep was originally registered with, not "current chain
// drep_deposit." If the protocol changed deposit between registration
// and deregistration, caller needs to override `params.drep_deposit_lovelace`
// to the original-registration value. Otherwise ledger silently fails.
sign_cert_tx_with_refund(
payment_key,
stake_key,
@ -500,7 +502,7 @@ pub fn build_signed_drep_deregistration(
available_utxos,
change_address_bech32,
vec![cert_bytes],
DREP_REGISTRATION_DEPOSIT_LOVELACE,
params.drep_deposit_lovelace,
params,
)
}

View file

@ -79,6 +79,11 @@ pub struct ProtocolParams {
/// `None`, Plutus paths skip script_data_hash and the chain will
/// reject with `PPViewHashesDontMatch`.
pub plutus_v3_cost_model: Option<Vec<i64>>,
/// Conway DRep registration deposit (ledger param `drep_deposit`).
/// Mainnet default: 500 ADA. Used by `governance::build_signed_drep_*`.
/// Pass the chain's current value when constructing — registering
/// with the wrong amount fails ledger validation silently.
pub drep_deposit_lovelace: u64,
}
impl Default for ProtocolParams {
@ -95,6 +100,7 @@ impl Default for ProtocolParams {
// or fetched from `epoch_params`. None by default keeps
// the ada-only / mint paths zero-cost.
plutus_v3_cost_model: None,
drep_deposit_lovelace: 500_000_000,
}
}
}