use crate::*;
use ::u128::mul_div_u64;
use locked_voter::{Escrow, Locker};
use num_traits::ToPrimitive;
#[derive(Accounts)]
pub struct Sync<'info> {
pub locker: Account<'info, Locker>,
pub escrow: Account<'info, Escrow>,
#[account(mut)]
pub locker_history: AccountLoader<'info, LockerHistory>,
#[account(mut)]
pub escrow_history: AccountLoader<'info, EscrowHistory>,
}
impl<'info> Sync<'info> {
fn sync(&self) -> Result<()> {
let locker_history = &mut self.locker_history.load_mut()?;
let escrow_history = &mut self.escrow_history.load_mut()?;
assert_keys_eq!(locker_history.locker, self.locker);
assert_keys_eq!(escrow_history.escrow, self.escrow);
invariant!(locker_history.era == escrow_history.era, EraMismatch);
let start_ts = unwrap_int!(calculate_era_start_ts(locker_history.era));
let now = unwrap_int!(Clock::get()?.unix_timestamp.to_u64());
let power_if_max_lockup = unwrap_int!(self
.escrow
.amount
.checked_mul(self.locker.params.max_stake_vote_multiplier.into()));
let escrow_started_at = unwrap_int!(self.escrow.escrow_started_at.to_u64());
let escrow_ends_at = unwrap_int!(self.escrow.escrow_ends_at.to_u64());
if escrow_started_at == 0 {
return Ok(());
}
let mut period_start_ts = start_ts;
for period in 0..ERA_NUM_PERIODS {
if period > 0 {
period_start_ts = unwrap_int!(period_start_ts.checked_add(PERIOD_SECONDS.into()));
}
if now >= period_start_ts {
continue;
}
let prev_period_ve_balance = escrow_history.ve_balances[period];
let ve_balance: u64 = unwrap_int!(calculate_voter_power_for_period(
power_if_max_lockup,
period_start_ts,
escrow_started_at,
escrow_ends_at,
self.locker.params.max_stake_duration,
));
if ve_balance == 0 {
invariant!(prev_period_ve_balance == 0);
continue;
}
locker_history.ve_balances[period] = unwrap_checked!({
locker_history.ve_balances[period]
.checked_sub(prev_period_ve_balance)?
.checked_add(ve_balance)
});
escrow_history.ve_balances[period] = ve_balance;
invariant!(ve_balance >= prev_period_ve_balance, EscrowBalanceDecreased);
if prev_period_ve_balance == 0 && ve_balance != 0 {
locker_history.ve_counts[period] =
unwrap_int!(locker_history.ve_counts[period].checked_add(1));
}
}
Ok(())
}
}
fn calculate_voter_power_for_period(
power_if_max_lockup: u64,
period_start_ts: u64,
escrow_started_at: u64,
escrow_ends_at: u64,
max_stake_duration: u64,
) -> Option<u64> {
if period_start_ts == 0 {
return None;
}
if escrow_started_at == 0 {
return Some(0);
}
if period_start_ts < escrow_started_at || period_start_ts >= escrow_ends_at {
return Some(0);
}
let seconds_until_lockup_expiry = escrow_ends_at.checked_sub(period_start_ts)?.to_u64()?;
let relevant_seconds_until_lockup_expiry = seconds_until_lockup_expiry.min(max_stake_duration);
let power = mul_div_u64(
power_if_max_lockup,
relevant_seconds_until_lockup_expiry,
max_stake_duration,
)?;
Some(power)
}
pub fn handler(ctx: Context<Sync>) -> Result<()> {
ctx.accounts.sync()
}
impl<'info> Validate<'info> for Sync<'info> {
fn validate(&self) -> Result<()> {
assert_keys_eq!(self.locker, self.escrow.locker);
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use super::*;
use locked_voter::LockerParams;
use proptest::prelude::*;
fn run_test_identical_to_locked_voter(
escrow_amount: u64,
max_stake_vote_multiplier: u8,
period_start_ts: i64,
escrow_started_at: i64,
escrow_ends_at: i64,
max_stake_duration: u64,
) {
let reference_locker = LockerParams {
max_stake_duration,
max_stake_vote_multiplier,
..Default::default()
};
let reference_escrow = Escrow {
amount: escrow_amount,
escrow_started_at,
escrow_ends_at,
..Default::default()
};
let power_if_max_lockup = escrow_amount * (max_stake_vote_multiplier as u64);
let reference_power =
reference_locker.calculate_voter_power(&reference_escrow, period_start_ts);
let new_power = calculate_voter_power_for_period(
power_if_max_lockup,
period_start_ts as u64,
escrow_started_at as u64,
escrow_ends_at as u64,
max_stake_duration,
);
assert_eq!(reference_power, new_power);
}
proptest! {
#[test]
fn test_identical_to_locked_voter(
escrow_amount in 0..=(u64::MAX >> 8),
max_stake_vote_multiplier: u8,
period_start_ts in 0..=i64::MAX,
escrow_started_at in 0..=i64::MAX,
escrow_ends_at in 0..=i64::MAX,
max_stake_duration: u64,
) {
run_test_identical_to_locked_voter(
escrow_amount,
max_stake_vote_multiplier,
period_start_ts,
escrow_started_at,
escrow_ends_at,
max_stake_duration
);
}
}
}