From b7074fd81bbf4de86738a381806d9e09c3a3d3e8 Mon Sep 17 00:00:00 2001 From: Kayos Date: Thu, 7 May 2026 19:43:10 -0700 Subject: [PATCH] fix(dao): prepend (cons) Created lock instead of append in proposal_create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agora's stake validator (ppermitVote) enforces output_locks = pcons NEW_LOCK old_locks (head-cons, not append). proposal_create was using Vec::push which appends, so when the stake had any pre-existing locks the output lock order didn't match what the validator expected and the chain rejected with CekError on the stake validator (5178-byte script, hash 57d6b17f...). The bug went undetected on 2026-05-07 because that day's first proposal_create ran on a stake with locked_by = [], where push and prepend produce the same single-element vector. Today's proposal #1 attempt — stake already holding a Created lock for #0 — flushed it out. Mirror the prepend pattern that proposal_cosign and proposal_vote already use: build new_locks with the new lock at index 0, then extend with the old locks. --- crates/aldabra-dao/src/builder/proposal_create.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/aldabra-dao/src/builder/proposal_create.rs b/crates/aldabra-dao/src/builder/proposal_create.rs index dd962dd..d569e8c 100644 --- a/crates/aldabra-dao/src/builder/proposal_create.rs +++ b/crates/aldabra-dao/src/builder/proposal_create.rs @@ -352,12 +352,21 @@ pub fn build_unsigned_proposal_create( starting_time: args.starting_time_ms, }; - // New stake datum: copy old, append Created lock for the new proposal. - let mut new_stake = args.stake_in.datum.clone(); - new_stake.locked_by.push(ProposalLock { + // New stake datum: copy old, PREPEND a Created lock for the new + // proposal. Order matters — the stake validator's ppermitVote uses + // `pcons NEW_LOCK old_locks` (head-cons, NOT append). If we append + // and the input had pre-existing locks, the chain rejects with + // CekError on the stake validator. Caught 2026-05-08 trying to + // create proposal #1 while the stake still held a Created lock + // from proposal #0; cosign + vote builders already prepend. + let mut new_locks = Vec::with_capacity(args.stake_in.datum.locked_by.len() + 1); + new_locks.push(ProposalLock { proposal_id: new_proposal_id, action: ProposalAction::Created, }); + new_locks.extend(args.stake_in.datum.locked_by.iter().cloned()); + let mut new_stake = args.stake_in.datum.clone(); + new_stake.locked_by = new_locks; let new_governor_datum_pd = new_governor.to_plutus_data()?; let new_proposal_datum_pd = new_proposal.to_plutus_data()?;