fix(dao): EnumIsData → plain Integer, not Constr i []
Verified against Sulkta's live Proposal #0 datum 2026-05-05: status field is bare BigInt(3), not Constr 3 []. Plutarch's EnumIsData derive emits Integer-as-index in this Agora version. Affected: - ProposalStatus.{to,from}_plutus_data - GovernorRedeemer.to_plutus_data (consistency; no on-chain governor-redeemer evidence yet, but same EnumIsData derive) ProposalDatum.to_plutus_data signature updated for the new fallible ProposalStatus encoding (now returns DaoResult). Added regression test `decodes_sulkta_live_proposal_zero` that decodes Proposal #0's actual on-chain datum hex and asserts: proposal_id=0, status=Finished, cosigners=[Cobb's pkh], thresholds=20/100/100/1/1, votes={0:0, 1:0} (zero votes ever cast), starting_time=1772666551575ms. Closes audit findings 1 + 2 from memory/audit-sulkta-agora-2026-05-05.md.
This commit is contained in:
parent
5fb616c6c5
commit
a8ecdfa45d
2 changed files with 76 additions and 35 deletions
|
|
@ -56,16 +56,23 @@ impl GovernorDatum {
|
|||
|
||||
/// `GovernorRedeemer` — EnumIsData (declaration order):
|
||||
/// 0=CreateProposal, 1=MintGATs, 2=MutateGovernor.
|
||||
///
|
||||
/// **Encoded as plain Integer**, not `Constr i []` — same Plutarch
|
||||
/// `EnumIsData` pattern as `ProposalStatus` (verified against Sulkta's
|
||||
/// on-chain Proposal #0 status field). No on-chain governor-redeemer
|
||||
/// has been used yet to corroborate, but consistency with ProposalStatus
|
||||
/// is overwhelming evidence. If Phase 4a's first dao_proposal_create
|
||||
/// surfaces a mismatch, swap back to constr(self as u64, vec![]).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GovernorRedeemer {
|
||||
CreateProposal,
|
||||
MintGATs,
|
||||
MutateGovernor,
|
||||
CreateProposal = 0,
|
||||
MintGATs = 1,
|
||||
MutateGovernor = 2,
|
||||
}
|
||||
|
||||
impl GovernorRedeemer {
|
||||
pub fn to_plutus_data(self) -> PlutusData {
|
||||
constr(self as u64, vec![])
|
||||
pub fn to_plutus_data(self) -> DaoResult<PlutusData> {
|
||||
int(self as i128)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,16 +107,16 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn governor_redeemer_indices() {
|
||||
use crate::agora::plutus_data::as_constr;
|
||||
for (r, i) in [
|
||||
(GovernorRedeemer::CreateProposal, 0u64),
|
||||
fn governor_redeemer_encodes_as_integer() {
|
||||
// Per Plutarch EnumIsData (matches ProposalStatus shape verified
|
||||
// on chain). Plain Integer, not Constr i [].
|
||||
for (r, expected) in [
|
||||
(GovernorRedeemer::CreateProposal, 0i128),
|
||||
(GovernorRedeemer::MintGATs, 1),
|
||||
(GovernorRedeemer::MutateGovernor, 2),
|
||||
] {
|
||||
let pd = r.to_plutus_data();
|
||||
let (idx, _) = as_constr(&pd).unwrap();
|
||||
assert_eq!(idx, i);
|
||||
let pd = r.to_plutus_data().unwrap();
|
||||
assert_eq!(as_int(&pd).unwrap(), expected, "{:?}", r);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,36 +23,36 @@ use crate::agora::stake::Credential;
|
|||
use crate::error::{DaoError, DaoResult};
|
||||
|
||||
/// `data ProposalStatus = Draft | VotingReady | Locked | Finished`
|
||||
/// via `EnumIsData` → `Constr i []` for `i` ∈ `[0,3]`.
|
||||
/// via `EnumIsData` → **plain `Integer`** (NOT `Constr i []`).
|
||||
///
|
||||
/// **Encoding correction 2026-05-05:** initial Phase 0 spec assumed
|
||||
/// `EnumIsData` produces `Constr i []`. Real on-chain proposal #0 has
|
||||
/// status field encoded as bare `BigInt(3)` (CBOR `03`). Plutarch's
|
||||
/// `EnumIsData` actually emits Integer-as-index in this Agora version.
|
||||
/// Correction verified by audit-sulkta-agora-2026-05-05.md.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ProposalStatus {
|
||||
Draft,
|
||||
VotingReady,
|
||||
Locked,
|
||||
Finished,
|
||||
Draft = 0,
|
||||
VotingReady = 1,
|
||||
Locked = 2,
|
||||
Finished = 3,
|
||||
}
|
||||
|
||||
impl ProposalStatus {
|
||||
pub fn to_plutus_data(self) -> PlutusData {
|
||||
constr(self as u64, vec![])
|
||||
pub fn to_plutus_data(self) -> DaoResult<PlutusData> {
|
||||
int(self as i128)
|
||||
}
|
||||
|
||||
pub fn from_plutus_data(pd: &PlutusData) -> DaoResult<Self> {
|
||||
let (idx, fields) = as_constr(pd)?;
|
||||
if !fields.is_empty() {
|
||||
return Err(DaoError::Datum(format!(
|
||||
"ProposalStatus expects 0 fields, got {}",
|
||||
fields.len()
|
||||
)));
|
||||
}
|
||||
Ok(match idx {
|
||||
let n = as_int(pd)?;
|
||||
Ok(match n {
|
||||
0 => ProposalStatus::Draft,
|
||||
1 => ProposalStatus::VotingReady,
|
||||
2 => ProposalStatus::Locked,
|
||||
3 => ProposalStatus::Finished,
|
||||
other => {
|
||||
return Err(DaoError::Datum(format!(
|
||||
"ProposalStatus expects 0..=3, got {other}"
|
||||
"ProposalStatus expects integer 0..=3, got {other}"
|
||||
)))
|
||||
}
|
||||
})
|
||||
|
|
@ -201,7 +201,7 @@ impl ProposalDatum {
|
|||
Ok(product(vec![
|
||||
int(self.proposal_id as i128)?,
|
||||
self.effects_raw.clone(),
|
||||
self.status.to_plutus_data(),
|
||||
self.status.to_plutus_data()?,
|
||||
PlutusData::Array(MaybeIndefArray::Indef(cosigners_pd)),
|
||||
self.thresholds.to_plutus_data()?,
|
||||
self.votes.to_plutus_data()?,
|
||||
|
|
@ -266,16 +266,17 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn proposal_status_indices() {
|
||||
for (s, i) in [
|
||||
(ProposalStatus::Draft, 0u64),
|
||||
fn proposal_status_encodes_as_integer() {
|
||||
// Per Plutarch EnumIsData in this Agora version (verified against
|
||||
// Sulkta's Proposal #0 on chain): plain Integer, NOT Constr i [].
|
||||
for (s, expected) in [
|
||||
(ProposalStatus::Draft, 0i128),
|
||||
(ProposalStatus::VotingReady, 1),
|
||||
(ProposalStatus::Locked, 2),
|
||||
(ProposalStatus::Finished, 3),
|
||||
] {
|
||||
let pd = s.to_plutus_data();
|
||||
let (idx, _) = as_constr(&pd).unwrap();
|
||||
assert_eq!(idx, i);
|
||||
let pd = s.to_plutus_data().unwrap();
|
||||
assert_eq!(as_int(&pd).unwrap(), expected, "{:?}", s);
|
||||
assert_eq!(ProposalStatus::from_plutus_data(&pd).unwrap(), s);
|
||||
}
|
||||
}
|
||||
|
|
@ -328,6 +329,39 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// Decode Sulkta's Proposal #0 from on-chain bytes. Real-world
|
||||
/// regression for the type port — same role as the GovernorDatum
|
||||
/// live-decode test, but for a proposal.
|
||||
///
|
||||
/// Source: Koios `address_info` for proposal validator address
|
||||
/// `addr1w8uydw7xer6wml2pgpv8jphdenxrgwpmdxwaqfj2h5sk45sj8nk40`,
|
||||
/// only UTxO at `0823a9406da1...#0`. Captured 2026-05-05.
|
||||
#[test]
|
||||
fn decodes_sulkta_live_proposal_zero() {
|
||||
use pallas_primitives::PlutusData;
|
||||
|
||||
let cbor_hex = "9f00a200a001a1581c92b7725bb0c7c06083af729d38f5589c2f85f16c83fe48860399e96f9f5820046dff6c96199a55715c65b9ae62c3be1b6600038cc3116eeb20886a7d66e83cd87a80ff039fd8799f581cc5e3425f44c1909b6caab4d80d88aebe85f328bd209eeab03ca2bfdaffff9f14186418640101ffa2000001009f1a240c84001a240c84001a0a4cb8001a05265c001a0036ee801a001b7740ff1b0000019c7d5c4d17ff";
|
||||
let bytes = hex::decode(cbor_hex).unwrap();
|
||||
let pd: PlutusData = pallas_codec::minicbor::decode(&bytes).unwrap();
|
||||
let prop = ProposalDatum::from_plutus_data(&pd).expect("decode Proposal #0");
|
||||
|
||||
assert_eq!(prop.proposal_id, 0);
|
||||
assert_eq!(prop.status, ProposalStatus::Finished);
|
||||
assert_eq!(prop.cosigners.len(), 1);
|
||||
// Cobb's pkh
|
||||
assert!(matches!(
|
||||
&prop.cosigners[0],
|
||||
Credential::PubKey(h) if hex::encode(h) == "c5e3425f44c1909b6caab4d80d88aebe85f328bd209eeab03ca2bfda"
|
||||
));
|
||||
assert_eq!(prop.thresholds.execute, 20);
|
||||
assert_eq!(prop.thresholds.create, 100);
|
||||
assert_eq!(prop.thresholds.vote, 1);
|
||||
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.voting_time, 7 * 86_400 * 1000);
|
||||
assert_eq!(prop.starting_time, 1_772_666_551_575);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proposal_datum_round_trip_minimal() {
|
||||
let pd_unit = constr(0, vec![]); // opaque effects placeholder
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue