fix(dao): use PermitVote (not DepositWithdraw) for stake spend on proposal_create

Stake validator's DepositWithdraw branch requires locked_by to stay
EMPTY. proposal_create wants to ADD a Created lock for the new
proposal — that's PermitVote's job, not DepositWithdraw's.

Caught by base64-decoding the CekError's failing-script header on
preprod_test today: 0x59 0x14 0x37 = bytes(5175) ⇒ 5178-byte script
= stake validator (57d6b17f), not the governor we'd been suspecting.

This is the audit C-2b fix that landed late.
This commit is contained in:
Kayos 2026-05-07 18:09:20 -07:00
parent f44a4f209c
commit 1bc4e949ab

View file

@ -374,12 +374,13 @@ pub fn build_unsigned_proposal_create(
// Governor spend: GovernorRedeemer::CreateProposal = Integer 0 (per
// EnumIsData encoding fix 2026-05-05).
//
// Stake spend: AUDIT-C2 — the reference tx (7c8db1432a07...) shows the
// stake input being spent with the stake validator invoked. Per Agora's
// design the stake validator's DepositWithdraw branch handles "modify
// stake AND register a Created lock" when the same tx has the governor's
// CreateProposal redeemer. For an InfoOnly proposal with no deposit:
// DepositWithdraw(0). The reference tx had DepositWithdraw(200) (50→250).
// Stake spend: redeemer is PermitVote (Constr 2 []). DepositWithdraw
// requires locked_by to STAY empty — which conflicts with adding a
// Created lock for the new proposal. PermitVote is the redeemer that
// grants new locks (for create/vote/cosign) on a stake. Caught
// 2026-05-07 PM via base64-decoded failing-script header (5178 bytes
// = stake validator); the bare CekError under traces-stripped Agora
// pointed at the stake's lock-state invariant.
//
// Mint redeemer: per `Agora/Proposal/Scripts.hs:118` the policy is
// `\_gst _redeemer ctx -> ...` — redeemer is unused. Constr 0 [] is fine.
@ -388,7 +389,7 @@ pub fn build_unsigned_proposal_create(
minicbor::to_vec(&crate::agora::plutus_data::int(0)?)
.map_err(|e| DaoError::Cbor(format!("governor spend redeemer encode: {e}")))?;
let stake_spend_redeemer_cbor =
minicbor::to_vec(&StakeRedeemer::DepositWithdraw(0).to_plutus_data()?)
minicbor::to_vec(&StakeRedeemer::PermitVote.to_plutus_data()?)
.map_err(|e| DaoError::Cbor(format!("stake spend redeemer encode: {e}")))?;
let mint_redeemer_cbor =
minicbor::to_vec(&crate::agora::plutus_data::constr(0, vec![]))