use soroban_sdk::{
contracttype, panic_with_error, xdr::ToXdr, Address, Bytes, BytesN, Env, String, Symbol, Val,
Vec,
};
use crate::{
governor::{
emit_proposal_cancelled, emit_proposal_created, emit_proposal_executed,
emit_proposal_queued, emit_quorum_changed, emit_vote_cast, GovernorError, ProposalState,
GOVERNOR_EXTEND_AMOUNT, GOVERNOR_TTL_THRESHOLD, MAX_DESCRIPTION_LENGTH,
},
votes::VotesClient,
};
#[derive(Clone)]
#[contracttype]
pub enum GovernorStorageKey {
Name,
Version,
VotingDelay,
VotingPeriod,
ProposalThreshold,
Proposal(BytesN<32>),
NumQuorumCheckpoints,
QuorumCheckpoint(u32),
ProposalVote(BytesN<32>),
HasVoted(BytesN<32>, Address),
TokenContract,
}
#[derive(Clone)]
#[contracttype]
pub struct ProposalCore {
pub proposer: Address,
pub vote_snapshot: u32,
pub vote_end: u32,
pub state: ProposalState,
}
#[derive(Clone)]
#[contracttype]
pub struct QuorumCheckpoint {
pub ledger: u32,
pub quorum: u128,
}
#[derive(Clone)]
#[contracttype]
pub struct ProposalVoteCounts {
pub against_votes: u128,
pub for_votes: u128,
pub abstain_votes: u128,
}
pub const VOTE_AGAINST: u32 = 0;
pub const VOTE_FOR: u32 = 1;
pub const VOTE_ABSTAIN: u32 = 2;
pub fn get_name(e: &Env) -> String {
e.storage()
.instance()
.get(&GovernorStorageKey::Name)
.unwrap_or_else(|| panic_with_error!(e, GovernorError::NameNotSet))
}
pub fn get_version(e: &Env) -> String {
e.storage()
.instance()
.get(&GovernorStorageKey::Version)
.unwrap_or_else(|| panic_with_error!(e, GovernorError::VersionNotSet))
}
pub fn get_proposal_threshold(e: &Env) -> u128 {
e.storage()
.instance()
.get(&GovernorStorageKey::ProposalThreshold)
.unwrap_or_else(|| panic_with_error!(e, GovernorError::ProposalThresholdNotSet))
}
pub fn get_voting_delay(e: &Env) -> u32 {
e.storage()
.instance()
.get(&GovernorStorageKey::VotingDelay)
.unwrap_or_else(|| panic_with_error!(e, GovernorError::VotingDelayNotSet))
}
pub fn get_voting_period(e: &Env) -> u32 {
e.storage()
.instance()
.get(&GovernorStorageKey::VotingPeriod)
.unwrap_or_else(|| panic_with_error!(e, GovernorError::VotingPeriodNotSet))
}
pub fn get_proposal_core(e: &Env, proposal_id: &BytesN<32>) -> ProposalCore {
let key = GovernorStorageKey::Proposal(proposal_id.clone());
let core: ProposalCore = e
.storage()
.persistent()
.get(&key)
.unwrap_or_else(|| panic_with_error!(e, GovernorError::ProposalNotFound));
e.storage().persistent().extend_ttl(&key, GOVERNOR_TTL_THRESHOLD, GOVERNOR_EXTEND_AMOUNT);
core
}
pub fn get_proposal_state(e: &Env, proposal_id: &BytesN<32>, quorum: u128) -> ProposalState {
let core = get_proposal_core(e, proposal_id);
derive_proposal_state(e, proposal_id, &core, quorum)
}
pub fn get_proposal_snapshot(e: &Env, proposal_id: &BytesN<32>) -> u32 {
let core = get_proposal_core(e, proposal_id);
core.vote_snapshot
}
pub fn get_proposal_deadline(e: &Env, proposal_id: &BytesN<32>) -> u32 {
let core = get_proposal_core(e, proposal_id);
core.vote_end
}
pub fn get_proposal_proposer(e: &Env, proposal_id: &BytesN<32>) -> Address {
let core = get_proposal_core(e, proposal_id);
core.proposer
}
pub fn get_token_contract(e: &Env) -> Address {
e.storage()
.instance()
.get(&GovernorStorageKey::TokenContract)
.unwrap_or_else(|| panic_with_error!(e, GovernorError::TokenContractNotSet))
}
pub fn set_name(e: &Env, name: String) {
e.storage().instance().set(&GovernorStorageKey::Name, &name);
}
pub fn set_version(e: &Env, version: String) {
e.storage().instance().set(&GovernorStorageKey::Version, &version);
}
pub fn set_proposal_threshold(e: &Env, threshold: u128) {
e.storage().instance().set(&GovernorStorageKey::ProposalThreshold, &threshold);
}
pub fn set_voting_delay(e: &Env, delay: u32) {
e.storage().instance().set(&GovernorStorageKey::VotingDelay, &delay);
}
pub fn set_voting_period(e: &Env, period: u32) {
e.storage().instance().set(&GovernorStorageKey::VotingPeriod, &period);
}
pub fn set_token_contract(e: &Env, token_contract: &Address) {
let key = GovernorStorageKey::TokenContract;
if e.storage().instance().has(&key) {
panic_with_error!(e, GovernorError::TokenContractAlreadySet);
}
e.storage().instance().set(&key, token_contract);
}
pub fn propose(
e: &Env,
targets: Vec<Address>,
functions: Vec<Symbol>,
args: Vec<Vec<Val>>,
description: String,
proposer: &Address,
) -> BytesN<32> {
let targets_len = targets.len();
if targets_len == 0 {
panic_with_error!(e, GovernorError::EmptyProposal);
}
if targets_len != functions.len() || targets_len != args.len() {
panic_with_error!(e, GovernorError::InvalidProposalLength);
}
if description.len() > MAX_DESCRIPTION_LENGTH {
panic_with_error!(e, GovernorError::DescriptionTooLong);
}
let snapshot = e.ledger().sequence() - 1;
let proposer_votes = get_voting_power(e, proposer, snapshot);
let threshold = get_proposal_threshold(e);
if proposer_votes < threshold {
panic_with_error!(e, GovernorError::InsufficientProposerVotes);
}
let current_ledger = e.ledger().sequence();
let description_hash = e.crypto().keccak256(&description.to_bytes()).to_bytes();
let proposal_id = hash_proposal(e, &targets, &functions, &args, &description_hash);
if e.storage().persistent().has(&GovernorStorageKey::Proposal(proposal_id.clone())) {
panic_with_error!(e, GovernorError::ProposalAlreadyExists);
}
let voting_delay = get_voting_delay(e);
let voting_period = get_voting_period(e);
let Some(vote_snapshot) = current_ledger.checked_add(voting_delay) else {
panic_with_error!(e, GovernorError::MathOverflow);
};
let Some(vote_end) = vote_snapshot.checked_add(voting_period) else {
panic_with_error!(e, GovernorError::MathOverflow);
};
let proposal = ProposalCore {
proposer: proposer.clone(),
vote_snapshot,
vote_end,
state: ProposalState::Pending,
};
e.storage().persistent().set(&GovernorStorageKey::Proposal(proposal_id.clone()), &proposal);
emit_proposal_created(
e,
&proposal_id,
proposer,
&targets,
&functions,
&args,
vote_snapshot,
vote_end,
&description,
);
proposal_id
}
pub fn execute(
e: &Env,
targets: Vec<Address>,
functions: Vec<Symbol>,
args: Vec<Vec<Val>>,
description_hash: &BytesN<32>,
queue_enabled: bool,
quorum: u128,
) -> BytesN<32> {
let proposal_id = hash_proposal(e, &targets, &functions, &args, description_hash);
let mut proposal = get_proposal_core(e, &proposal_id);
let state = derive_proposal_state(e, &proposal_id, &proposal, quorum);
if state == ProposalState::Executed {
panic_with_error!(e, GovernorError::ProposalAlreadyExecuted);
}
if queue_enabled {
if state != ProposalState::Queued {
panic_with_error!(e, GovernorError::ProposalNotQueued);
}
} else if state != ProposalState::Succeeded {
panic_with_error!(e, GovernorError::ProposalNotSuccessful);
}
for i in 0..targets.len() {
let target = targets.get_unchecked(i);
let function = functions.get_unchecked(i);
let func_args = args.get_unchecked(i);
e.invoke_contract::<Val>(&target, &function, func_args);
}
proposal.state = ProposalState::Executed;
e.storage().persistent().set(&GovernorStorageKey::Proposal(proposal_id.clone()), &proposal);
emit_proposal_executed(e, &proposal_id);
proposal_id
}
pub fn queue(
e: &Env,
targets: Vec<Address>,
functions: Vec<Symbol>,
args: Vec<Vec<Val>>,
description_hash: &BytesN<32>,
eta: u32,
quorum: u128,
) -> BytesN<32> {
let proposal_id = hash_proposal(e, &targets, &functions, &args, description_hash);
let mut proposal = get_proposal_core(e, &proposal_id);
let state = derive_proposal_state(e, &proposal_id, &proposal, quorum);
if state != ProposalState::Succeeded {
panic_with_error!(e, GovernorError::ProposalNotSuccessful);
}
proposal.state = ProposalState::Queued;
e.storage().persistent().set(&GovernorStorageKey::Proposal(proposal_id.clone()), &proposal);
emit_proposal_queued(e, &proposal_id, eta);
proposal_id
}
pub fn cancel(
e: &Env,
targets: Vec<Address>,
functions: Vec<Symbol>,
args: Vec<Vec<Val>>,
description_hash: &BytesN<32>,
) -> BytesN<32> {
let proposal_id = hash_proposal(e, &targets, &functions, &args, description_hash);
let mut proposal = get_proposal_core(e, &proposal_id);
match proposal.state {
ProposalState::Canceled | ProposalState::Expired | ProposalState::Executed => {
panic_with_error!(e, GovernorError::ProposalNotCancellable)
}
_ => {}
}
proposal.state = ProposalState::Canceled;
e.storage().persistent().set(&GovernorStorageKey::Proposal(proposal_id.clone()), &proposal);
emit_proposal_cancelled(e, &proposal_id);
proposal_id
}
pub fn hash_proposal(
e: &Env,
targets: &Vec<Address>,
functions: &Vec<Symbol>,
args: &Vec<Vec<Val>>,
description_hash: &BytesN<32>,
) -> BytesN<32> {
let mut data = Bytes::new(e);
data.append(&targets.to_xdr(e));
data.append(&functions.to_xdr(e));
data.append(&args.to_xdr(e));
data.append(&Bytes::from_slice(e, description_hash.to_array().as_slice()));
e.crypto().keccak256(&data).to_bytes()
}
pub fn check_proposal_state(e: &Env, proposal_id: &BytesN<32>, quorum: u128) -> u32 {
let core = get_proposal_core(e, proposal_id);
let state = derive_proposal_state(e, proposal_id, &core, quorum);
if state != ProposalState::Active {
panic_with_error!(e, GovernorError::ProposalNotActive);
}
core.vote_snapshot
}
fn derive_proposal_state(
e: &Env,
proposal_id: &BytesN<32>,
core: &ProposalCore,
quorum: u128,
) -> ProposalState {
match core.state {
ProposalState::Canceled | ProposalState::Executed | ProposalState::Queued => {
return core.state;
}
ProposalState::Expired => {
return core.state;
}
_ => {}
}
let current_ledger = e.ledger().sequence();
if current_ledger <= core.vote_snapshot {
return ProposalState::Pending;
}
if current_ledger <= core.vote_end {
return ProposalState::Active;
}
let counts = get_proposal_vote_counts(e, proposal_id);
let Some(participation) = counts.for_votes.checked_add(counts.abstain_votes) else {
panic_with_error!(e, GovernorError::MathOverflow);
};
if participation >= quorum && counts.for_votes > counts.against_votes {
return ProposalState::Succeeded;
}
ProposalState::Defeated
}
pub fn counting_mode(e: &Env) -> Symbol {
Symbol::new(e, "simple")
}
pub fn has_voted(e: &Env, proposal_id: &BytesN<32>, account: &Address) -> bool {
let key = GovernorStorageKey::HasVoted(proposal_id.clone(), account.clone());
if e.storage().persistent().has(&key) {
e.storage().persistent().extend_ttl(&key, GOVERNOR_TTL_THRESHOLD, GOVERNOR_EXTEND_AMOUNT);
true
} else {
false
}
}
pub fn get_quorum(e: &Env, ledger: u32) -> u128 {
let num: u32 =
e.storage().instance().get(&GovernorStorageKey::NumQuorumCheckpoints).unwrap_or(0);
if num == 0 {
panic_with_error!(e, GovernorError::QuorumNotSet);
}
let latest = get_quorum_checkpoint(e, num - 1);
if latest.ledger <= ledger {
return latest.quorum;
}
let first = get_quorum_checkpoint(e, 0);
if first.ledger > ledger {
panic_with_error!(e, GovernorError::QuorumNotSet);
}
let mut low: u32 = 0;
let mut high: u32 = num - 1;
while low < high {
let mid = low + (high - low).div_ceil(2);
let cp = get_quorum_checkpoint(e, mid);
if cp.ledger <= ledger {
low = mid;
} else {
high = mid - 1;
}
}
get_quorum_checkpoint(e, low).quorum
}
fn get_quorum_checkpoint(e: &Env, index: u32) -> QuorumCheckpoint {
let key = GovernorStorageKey::QuorumCheckpoint(index);
let cp: QuorumCheckpoint = e
.storage()
.persistent()
.get(&key)
.unwrap_or_else(|| panic_with_error!(e, GovernorError::QuorumNotSet));
e.storage().persistent().extend_ttl(&key, GOVERNOR_TTL_THRESHOLD, GOVERNOR_EXTEND_AMOUNT);
cp
}
pub fn quorum_reached(e: &Env, proposal_id: &BytesN<32>, quorum: u128) -> bool {
let counts = get_proposal_vote_counts(e, proposal_id);
let Some(participation) = counts.for_votes.checked_add(counts.abstain_votes) else {
panic_with_error!(e, GovernorError::MathOverflow);
};
participation >= quorum
}
pub fn tally_succeeded(e: &Env, proposal_id: &BytesN<32>) -> bool {
let counts = get_proposal_vote_counts(e, proposal_id);
counts.for_votes > counts.against_votes
}
pub fn get_proposal_vote_counts(e: &Env, proposal_id: &BytesN<32>) -> ProposalVoteCounts {
let key = GovernorStorageKey::ProposalVote(proposal_id.clone());
e.storage()
.persistent()
.get::<_, ProposalVoteCounts>(&key)
.inspect(|_| {
e.storage().persistent().extend_ttl(
&key,
GOVERNOR_TTL_THRESHOLD,
GOVERNOR_EXTEND_AMOUNT,
);
})
.unwrap_or(ProposalVoteCounts { against_votes: 0, for_votes: 0, abstain_votes: 0 })
}
pub fn set_quorum(e: &Env, quorum: u128) {
let num: u32 =
e.storage().instance().get(&GovernorStorageKey::NumQuorumCheckpoints).unwrap_or(0);
let ledger = e.ledger().sequence();
let old_quorum = if num > 0 {
let last = get_quorum_checkpoint(e, num - 1);
if last.ledger == ledger {
e.storage().persistent().set(
&GovernorStorageKey::QuorumCheckpoint(num - 1),
&QuorumCheckpoint { ledger, quorum },
);
emit_quorum_changed(e, last.quorum, quorum);
return;
}
last.quorum
} else {
0u128
};
e.storage()
.persistent()
.set(&GovernorStorageKey::QuorumCheckpoint(num), &QuorumCheckpoint { ledger, quorum });
e.storage().instance().set(&GovernorStorageKey::NumQuorumCheckpoints, &(num + 1));
emit_quorum_changed(e, old_quorum, quorum);
}
pub fn count_vote(
e: &Env,
proposal_id: &BytesN<32>,
account: &Address,
vote_type: u32,
weight: u128,
) {
let voted_key = GovernorStorageKey::HasVoted(proposal_id.clone(), account.clone());
if e.storage().persistent().has(&voted_key) {
panic_with_error!(e, GovernorError::AlreadyVoted);
}
let mut counts = get_proposal_vote_counts(e, proposal_id);
match vote_type {
VOTE_AGAINST => {
let Some(new_against) = counts.against_votes.checked_add(weight) else {
panic_with_error!(e, GovernorError::MathOverflow);
};
counts.against_votes = new_against;
}
VOTE_FOR => {
let Some(new_for) = counts.for_votes.checked_add(weight) else {
panic_with_error!(e, GovernorError::MathOverflow);
};
counts.for_votes = new_for;
}
VOTE_ABSTAIN => {
let Some(new_abstain) = counts.abstain_votes.checked_add(weight) else {
panic_with_error!(e, GovernorError::MathOverflow);
};
counts.abstain_votes = new_abstain;
}
_ => panic_with_error!(e, GovernorError::InvalidVoteType),
}
let vote_key = GovernorStorageKey::ProposalVote(proposal_id.clone());
e.storage().persistent().set(&vote_key, &counts);
e.storage().persistent().set(&voted_key, &true);
}
pub fn cast_vote(
e: &Env,
proposal_id: &BytesN<32>,
vote_type: u32,
reason: &String,
voter: &Address,
quorum: u128,
) -> u128 {
let snapshot = check_proposal_state(e, proposal_id, quorum);
let voter_weight = get_voting_power(e, voter, snapshot);
count_vote(e, proposal_id, voter, vote_type, voter_weight);
emit_vote_cast(e, voter, proposal_id, vote_type, voter_weight, reason);
voter_weight
}
fn get_voting_power(e: &Env, account: &Address, ledger: u32) -> u128 {
let token = get_token_contract(e);
VotesClient::new(e, &token).get_votes_at_checkpoint(&account.clone(), &ledger)
}