diff --git a/crates/aldabra-mcp/src/tools.rs b/crates/aldabra-mcp/src/tools.rs index 69cc65e..356225b 100644 --- a/crates/aldabra-mcp/src/tools.rs +++ b/crates/aldabra-mcp/src/tools.rs @@ -3083,9 +3083,8 @@ impl WalletService { .and_then(|t| t.get("abs_slot")) .and_then(|s| s.as_u64()) .ok_or_else(|| format!("tip response missing abs_slot: {tip_resp}"))?; - let validity_upper_slot = tip_slot + let default_validity_upper_slot = tip_slot + aldabra_dao::builder::proposal_create::VALIDITY_RANGE_SLOTS; - let validity_upper_ms = slot_to_posix_ms(cfg.network, validity_upper_slot)?; let tx_lower_ms = slot_to_posix_ms(cfg.network, tip_slot)?; // AUDIT-2026-05-06 H-3 fix: validator (Proposal/Scripts.hs PVote @@ -3096,6 +3095,13 @@ impl WalletService { // "too early or invalid" script error. Catch lb-vs-voting_start // here too. // + // 2026-05-08 follow-up: when default validity_upper would + // overshoot voting_end (e.g. 30-min Sulkta-shape windows where + // the 1799-slot validity range starting from current tip lands + // past voting_end), clamp validity_upper_slot to voting_end_slot + // so the range fits inside the voting window. Same trick the + // proposal_advance Draft→VotingReady clamp uses. + // // Read from prop_datum (target.datum was moved to prop_datum at L2636). let voting_start_check = prop_datum.starting_time + prop_datum.timing_config.draft_time; @@ -3108,12 +3114,24 @@ impl WalletService { voting_start_check.saturating_sub(tx_lower_ms) )); } - if validity_upper_ms > voting_end_check { - return Err(format!( - "tx upper bound {validity_upper_ms} ms is after voting window end {voting_end_check} ms \ - — voting closed for proposal #{proposal_id}" - )); - } + let voting_end_slot = posix_ms_to_slot(cfg.network, voting_end_check)?; + let validity_upper_slot = if voting_end_slot < default_validity_upper_slot { + // Clamp to voting_end. Reject if remaining slots are too narrow + // to include the tx (≤ 5 slots is the same threshold the + // advance clamp uses). + if voting_end_slot <= tip_slot + 5 { + return Err(format!( + "voting window has only {} slots remaining (voting_end_slot={voting_end_slot}, \ + tip_slot={tip_slot}) — too narrow to include the vote tx; voting period \ + effectively closed for proposal #{proposal_id}", + voting_end_slot.saturating_sub(tip_slot), + )); + } + voting_end_slot + } else { + default_validity_upper_slot + }; + let validity_upper_ms = slot_to_posix_ms(cfg.network, validity_upper_slot)?; // Wallet utxos with H-5-style asset propagation. let wallet_utxos: Vec = {