Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions stellar-swipe/contracts/governance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ mod test_pause_propagation;
#[cfg(test)]
#[allow(non_snake_case)]
mod test_portableDD;
#[cfg(test)]
mod test_simulation;

use committees::{
list_committees as list_registered_committees, CommitteeAction, CommitteeElection,
Expand Down Expand Up @@ -61,8 +63,9 @@ use proposals::{
calculate_proposal_statistics, cancel_proposal, configure_governance, create_proposal,
default_governance_config, effective_status, execute_proposal, finalize_proposal,
get_active_proposals, get_all_proposals, get_category_threshold, get_governance_config,
get_proposal, reclaim_expired_proposal, set_category_thresholds, withdraw_proposal, Proposal,
ProposalStatistics, ProposalStatus, ProposalType, Vote, VoteDelegation,
get_proposal, reclaim_expired_proposal, set_category_thresholds, simulate_proposal,
withdraw_proposal, Proposal, ProposalStatistics, ProposalStatus, ProposalType,
SimulationEffect, SimulationResult, Vote, VoteDelegation,
VoteType as GovernanceVoteType,
};
pub use proposals::{CategoryThreshold, GovernanceConfig, ProposalCategory};
Expand Down Expand Up @@ -742,6 +745,35 @@ impl GovernanceContract {
proposals::execute_proposal(&env, proposal_id, executor)
}

/// # Summary
/// Simulate execution of a governance proposal **without mutating state**.
///
/// Runs the same logic as `execute_proposal` but returns a [`SimulationResult`]
/// describing every storage effect the proposal would cause, allowing
/// maintainers to validate proposal effects before executing on-chain.
///
/// No authentication is required - the simulation is read-only and safe
/// to call via `simulateTransaction` RPC.
///
/// # Parameters
/// - `env`: Soroban environment.
/// - `proposal_id`: ID of the proposal to simulate.
///
/// # Returns
/// `Ok(SimulationResult)` describing whether the execution would succeed,
/// an error message if it would fail, and the list of effects.
///
/// # Errors
/// - [`GovernanceError::NotInitialized`] - contract not initialized.
/// - [`GovernanceError::ProposalNotFound`] - `proposal_id` does not exist.
pub fn simulate_proposal(
env: Env,
proposal_id: u64,
) -> Result<SimulationResult, GovernanceError> {
require_initialized(&env)?;
proposals::simulate_proposal(&env, proposal_id)
}

pub fn cancel_proposal(
env: Env,
proposal_id: u64,
Expand Down
137 changes: 137 additions & 0 deletions stellar-swipe/contracts/governance/src/proposals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,36 @@ use crate::{
require_admin, GovernanceError, StorageKey,
};

// -- #917: Dry-run simulation result types ---------------------------------

/// Outcome of a simulated proposal execution.
///
/// Returned by `simulate_proposal_action` -- surfaces every effect the proposal
/// *would* have on storage without actually writing anything, so maintainers
/// can verify correctness before executing on-chain.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SimulationEffect {
/// Human-readable label for the affected storage slot, e.g.
/// `"parameter:max_fee"`, `"treasury:USDC"`, `"feature:flash_loans"`.
pub key: String,
/// Current (pre-simulation) value as a debug string.
pub current: String,
/// Value the proposal would set as a debug string.
pub proposed: String,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SimulationResult {
/// Whether the simulation completed without errors.
pub success: bool,
/// The error that would have been returned (empty when `success` is true).
pub error: String,
/// Ordered list of individual storage mutations the execution would cause.
pub effects: Vec<SimulationEffect>,
}

// ── #692: Integer square root (Newton's method, no floating-point) ───────────
//
// #837: the previous version seeded Newton's method with `(x + 1) / 2`. When
Expand Down Expand Up @@ -500,6 +530,113 @@ pub fn cast_vote(
}
put_proposal(env, &proposal)
}
pub fn simulate_proposal_action(
env: &Env,
proposal: &Proposal,
) -> Result<SimulationResult, GovernanceError> {
let mut effects: Vec<SimulationEffect> = Vec::new(env);

// Simulation reads current state and records effects without writing.
// TreasurySpend uses `return Ok(SimulationResult{...})` for early-exit
// when the treasury balance is insufficient.
let _ = match &proposal.proposal_type {
ProposalType::ParameterChange(parameter, _current, proposed) => {
let params: Map<String, i128> = env
.storage()
.instance()
.get(&StorageKey::GovernanceParameters)
.unwrap_or(Map::new(env));
let cur = params.get(parameter.clone()).unwrap_or(0);
effects.push_back(SimulationEffect {
key: String::from_str(env, &(String::from_str(env, "parameter:") + parameter)),
current: cur.to_val().to_string(),
proposed: (*proposed).to_val().to_string(),
});
Ok(())
}
ProposalType::TreasurySpend(recipient, amount, asset, _purpose) => {
let treasury = get_treasury(env);
let bal = treasury.assets.get(asset.clone()).unwrap_or(0);
if bal < *amount {
return Ok(SimulationResult {
success: false,
error: String::from_str(env, "Insufficient treasury balance"),
effects: Vec::new(env),
});
}
effects.push_back(SimulationEffect {
key: String::from_str(env, "treasury:balance"),
current: bal.to_val().to_string(),
proposed: (bal - *amount).to_val().to_string(),
});
effects.push_back(SimulationEffect {
key: String::from_str(env, "treasury:recipient"),
current: String::from_str(env, "(unchanged)"),
proposed: recipient.to_string(),
});
Ok(())
}
ProposalType::FeatureToggle(feature, enabled) => {
let flags: Map<String, bool> = env
.storage()
.instance()
.get(&StorageKey::GovernanceFeatures)
.unwrap_or(Map::new(env));
let cur = flags.get(feature.clone()).unwrap_or(false);
effects.push_back(SimulationEffect {
key: String::from_str(env, &(String::from_str(env, "feature:") + feature)),
current: cur.to_val().to_string(),
proposed: (*enabled).to_val().to_string(),
});
Ok(())
}
ProposalType::ContractUpgrade(contract_name, new_hash) => {
let upgrades: Map<String, Bytes> = env
.storage()
.instance()
.get(&StorageKey::GovernanceUpgrades)
.unwrap_or(Map::new(env));
let cur = upgrades.get(contract_name.clone());
effects.push_back(SimulationEffect {
key: String::from_str(env, &(String::from_str(env, "upgrade:") + contract_name)),
current: match cur { Some(h) => h.to_string(), None => String::from_str(env, "(none)") },
proposed: new_hash.to_string(),
});
Ok(())
}
ProposalType::SignalProposal(message) => {
effects.push_back(SimulationEffect {
key: String::from_str(env, "signal"),
current: String::from_str(env, "(no state change)"),
proposed: message.clone(),
});
Ok(())
}
ProposalType::Custom(executor) => {
effects.push_back(SimulationEffect {
key: String::from_str(env, "custom"),
current: String::from_str(env, "(external call)"),
proposed: executor.to_string(),
});
Ok(())
}
};

Ok(SimulationResult {
success: true,
error: String::new(env),
effects,
})
}

pub fn simulate_proposal(
env: &Env,
proposal_id: u64,
) -> Result<SimulationResult, GovernanceError> {
let proposal = get_proposal(env, proposal_id)?;
simulate_proposal_action(env, &proposal)
}


pub fn finalize_proposal(env: &Env, proposal_id: u64) -> Result<ProposalStatus, GovernanceError> {
let mut proposal = get_proposal(env, proposal_id)?;
Expand Down
Loading
Loading