use soroban_sdk::{contracttype, panic_with_error, Address, Env};
use crate::votes::{
emit_delegate_changed, emit_delegate_votes_changed, VotesError, VOTES_EXTEND_AMOUNT,
VOTES_TTL_THRESHOLD,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum CheckpointOp {
Add,
Sub,
}
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Checkpoint {
pub ledger: u32,
pub votes: u128,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[contracttype]
pub enum CheckpointType {
TotalSupply,
Account(Address),
}
#[derive(Clone)]
#[contracttype]
pub enum VotesStorageKey {
Delegatee(Address),
NumCheckpoints(Address),
DelegateCheckpoint(Address, u32),
NumTotalSupplyCheckpoints,
TotalSupplyCheckpoint(u32),
VotingUnits(Address),
}
pub fn get_checkpoint(e: &Env, checkpoint_type: &CheckpointType, index: u32) -> Checkpoint {
let key = checkpoint_storage_key(checkpoint_type, index);
let Some(checkpoint) = e.storage().persistent().get::<_, Checkpoint>(&key) else {
panic_with_error!(e, VotesError::CheckpointNotFound);
};
e.storage().persistent().extend_ttl(&key, VOTES_TTL_THRESHOLD, VOTES_EXTEND_AMOUNT);
checkpoint
}
pub fn get_votes(e: &Env, account: &Address) -> u128 {
let cp_type = CheckpointType::Account(account.clone());
let num = get_num_checkpoints(e, &cp_type);
if num == 0 {
return 0;
}
get_checkpoint(e, &cp_type, num - 1).votes
}
pub fn get_votes_at_checkpoint(e: &Env, account: &Address, ledger: u32) -> u128 {
if ledger >= e.ledger().sequence() {
panic_with_error!(e, VotesError::FutureLookup);
}
let cp_type = CheckpointType::Account(account.clone());
let num = get_num_checkpoints(e, &cp_type);
lookup_checkpoint_at(e, ledger, num, &cp_type)
}
pub fn get_total_supply(e: &Env) -> u128 {
let cp_type = CheckpointType::TotalSupply;
let num = get_num_checkpoints(e, &cp_type);
if num == 0 {
return 0;
}
get_checkpoint(e, &cp_type, num - 1).votes
}
pub fn get_total_supply_at_checkpoint(e: &Env, ledger: u32) -> u128 {
if ledger >= e.ledger().sequence() {
panic_with_error!(e, VotesError::FutureLookup);
}
let cp_type = CheckpointType::TotalSupply;
let num = get_num_checkpoints(e, &cp_type);
lookup_checkpoint_at(e, ledger, num, &cp_type)
}
pub fn get_delegate(e: &Env, account: &Address) -> Option<Address> {
let key = VotesStorageKey::Delegatee(account.clone());
if let Some(delegatee) = e.storage().persistent().get::<_, Address>(&key) {
e.storage().persistent().extend_ttl(&key, VOTES_TTL_THRESHOLD, VOTES_EXTEND_AMOUNT);
Some(delegatee)
} else {
None
}
}
pub fn num_checkpoints(e: &Env, account: &Address) -> u32 {
get_num_checkpoints(e, &CheckpointType::Account(account.clone()))
}
pub fn get_voting_units(e: &Env, account: &Address) -> u128 {
let key = VotesStorageKey::VotingUnits(account.clone());
if let Some(units) = e.storage().persistent().get::<_, u128>(&key) {
e.storage().persistent().extend_ttl(&key, VOTES_TTL_THRESHOLD, VOTES_EXTEND_AMOUNT);
units
} else {
0
}
}
pub fn delegate(e: &Env, account: &Address, delegatee: &Address) {
account.require_auth();
let old_delegate = get_delegate(e, account);
if old_delegate.as_ref() == Some(delegatee) {
panic_with_error!(e, VotesError::SameDelegate);
}
e.storage().persistent().set(&VotesStorageKey::Delegatee(account.clone()), delegatee);
emit_delegate_changed(e, account, old_delegate.clone(), delegatee);
let voting_units = get_voting_units(e, account);
move_delegate_votes(e, old_delegate.as_ref(), Some(delegatee), voting_units);
}
pub fn transfer_voting_units(e: &Env, from: Option<&Address>, to: Option<&Address>, amount: u128) {
if amount == 0 {
return;
}
let from_delegate = from.and_then(|addr| get_delegate(e, addr));
let to_delegate = to.and_then(|addr| get_delegate(e, addr));
if let Some(from_addr) = from {
let from_units = get_voting_units(e, from_addr);
let Some(new_from_units) = from_units.checked_sub(amount) else {
panic_with_error!(e, VotesError::InsufficientVotingUnits);
};
set_voting_units(e, from_addr, new_from_units);
} else {
push_checkpoint(e, &CheckpointType::TotalSupply, CheckpointOp::Add, amount);
}
if let Some(to_addr) = to {
let to_units = get_voting_units(e, to_addr);
let Some(new_to_units) = to_units.checked_add(amount) else {
panic_with_error!(e, VotesError::MathOverflow);
};
set_voting_units(e, to_addr, new_to_units);
} else {
push_checkpoint(e, &CheckpointType::TotalSupply, CheckpointOp::Sub, amount);
}
move_delegate_votes(e, from_delegate.as_ref(), to_delegate.as_ref(), amount);
}
fn set_voting_units(e: &Env, account: &Address, units: u128) {
let key = VotesStorageKey::VotingUnits(account.clone());
if units == 0 {
e.storage().persistent().remove(&key);
} else {
e.storage().persistent().set(&key, &units);
}
}
fn move_delegate_votes(e: &Env, from: Option<&Address>, to: Option<&Address>, amount: u128) {
if amount == 0 {
return;
}
if from == to {
return;
}
if let Some(from_addr) = from {
let cp_type = CheckpointType::Account(from_addr.clone());
let (old_votes, new_votes) = push_checkpoint(e, &cp_type, CheckpointOp::Sub, amount);
emit_delegate_votes_changed(e, from_addr, old_votes, new_votes);
}
if let Some(to_addr) = to {
let cp_type = CheckpointType::Account(to_addr.clone());
let (old_votes, new_votes) = push_checkpoint(e, &cp_type, CheckpointOp::Add, amount);
emit_delegate_votes_changed(e, to_addr, old_votes, new_votes);
}
}
fn lookup_checkpoint_at(e: &Env, ledger: u32, num: u32, checkpoint_type: &CheckpointType) -> u128 {
if num == 0 {
return 0;
}
let latest = get_checkpoint(e, checkpoint_type, num - 1);
if latest.ledger <= ledger {
return latest.votes;
}
let first = get_checkpoint(e, checkpoint_type, 0);
if first.ledger > ledger {
return 0;
}
let mut low: u32 = 0;
let mut high: u32 = num - 1;
while low < high {
let mid = low + (high - low).div_ceil(2);
let checkpoint = get_checkpoint(e, checkpoint_type, mid);
if checkpoint.ledger <= ledger {
low = mid;
} else {
high = mid - 1;
}
}
get_checkpoint(e, checkpoint_type, low).votes
}
fn apply_checkpoint_op(e: &Env, previous: u128, op: CheckpointOp, delta: u128) -> u128 {
match op {
CheckpointOp::Add => previous
.checked_add(delta)
.unwrap_or_else(|| panic_with_error!(e, VotesError::MathOverflow)),
CheckpointOp::Sub => previous
.checked_sub(delta)
.unwrap_or_else(|| panic_with_error!(e, VotesError::MathOverflow)),
}
}
fn checkpoint_storage_key(checkpoint_type: &CheckpointType, index: u32) -> VotesStorageKey {
match checkpoint_type {
CheckpointType::TotalSupply => VotesStorageKey::TotalSupplyCheckpoint(index),
CheckpointType::Account(account) => {
VotesStorageKey::DelegateCheckpoint(account.clone(), index)
}
}
}
fn get_num_checkpoints(e: &Env, checkpoint_type: &CheckpointType) -> u32 {
match checkpoint_type {
CheckpointType::TotalSupply => {
let key = VotesStorageKey::NumTotalSupplyCheckpoints;
e.storage().instance().get(&key).unwrap_or(0)
}
CheckpointType::Account(account) => {
let key = VotesStorageKey::NumCheckpoints(account.clone());
if let Some(checkpoints) = e.storage().persistent().get::<_, u32>(&key) {
e.storage().persistent().extend_ttl(&key, VOTES_TTL_THRESHOLD, VOTES_EXTEND_AMOUNT);
checkpoints
} else {
0
}
}
}
}
fn push_checkpoint(
e: &Env,
checkpoint_type: &CheckpointType,
op: CheckpointOp,
delta: u128,
) -> (u128, u128) {
let num = get_num_checkpoints(e, checkpoint_type);
let ledger = e.ledger().sequence();
let last_checkpoint =
if num > 0 { Some(get_checkpoint(e, checkpoint_type, num - 1)) } else { None };
let previous_votes = last_checkpoint.as_ref().map_or(0, |cp| cp.votes);
let votes = apply_checkpoint_op(e, previous_votes, op, delta);
if let Some(cp) = &last_checkpoint {
if cp.ledger == ledger {
let key = checkpoint_storage_key(checkpoint_type, num - 1);
e.storage().persistent().set(&key, &Checkpoint { ledger, votes });
return (previous_votes, votes);
}
}
let key = checkpoint_storage_key(checkpoint_type, num);
e.storage().persistent().set(&key, &Checkpoint { ledger, votes });
match checkpoint_type {
CheckpointType::TotalSupply => {
let num_key = VotesStorageKey::NumTotalSupplyCheckpoints;
e.storage().instance().set(&num_key, &(num + 1));
}
CheckpointType::Account(account) => {
let num_key = VotesStorageKey::NumCheckpoints(account.clone());
e.storage().persistent().set(&num_key, &(num + 1));
}
}
(previous_votes, votes)
}