split accounts.rs into state/*
Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
parent
7fa584e77a
commit
9823f282d4
|
@ -1,5 +1,6 @@
|
|||
use crate::account::*;
|
||||
use crate::error::*;
|
||||
use crate::state::lockup::*;
|
||||
use crate::state::registrar::registrar_seeds;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token;
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::account::*;
|
||||
use crate::error::*;
|
||||
use crate::state::lockup::*;
|
||||
use crate::state::voter::Voter;
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::account::*;
|
||||
use crate::error::*;
|
||||
use crate::state::lockup::*;
|
||||
use crate::state::voter::Voter;
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use crate::account::*;
|
||||
use crate::error::*;
|
||||
use crate::state::lockup::*;
|
||||
use crate::state::registrar::Registrar;
|
||||
use crate::state::voter::Voter;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::Mint;
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::account::*;
|
||||
use crate::error::*;
|
||||
use crate::state::lockup::*;
|
||||
use crate::state::registrar::Registrar;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::associated_token::AssociatedToken;
|
||||
use anchor_spl::token::{Mint, Token, TokenAccount};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::account::*;
|
||||
use crate::error::*;
|
||||
use crate::state::lockup::*;
|
||||
use crate::state::registrar::Registrar;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::{Mint, Token};
|
||||
use spl_governance::state::realm;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use crate::account::*;
|
||||
use crate::error::*;
|
||||
use crate::state::lockup::*;
|
||||
use crate::state::registrar::Registrar;
|
||||
use crate::state::voter::Voter;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
|
||||
use spl_governance::addins::voter_weight::VoterWeightAccountType;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use crate::account::*;
|
||||
use crate::error::*;
|
||||
use crate::state::lockup::*;
|
||||
use crate::state::registrar::Registrar;
|
||||
use crate::state::voter::Voter;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::{self, Mint, Token, TokenAccount};
|
||||
use std::convert::TryFrom;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use crate::account::*;
|
||||
use crate::error::*;
|
||||
use crate::state::lockup::*;
|
||||
use crate::state::registrar::Registrar;
|
||||
use crate::state::voter::Voter;
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::account::*;
|
||||
use crate::error::*;
|
||||
use crate::state::lockup::*;
|
||||
use crate::state::registrar::Registrar;
|
||||
use anchor_lang::prelude::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use crate::account::*;
|
||||
use crate::error::*;
|
||||
use crate::state::deposit_entry::{FIXED_VOTE_WEIGHT_FACTOR, LOCKING_VOTE_WEIGHT_FACTOR};
|
||||
use crate::state::lockup::*;
|
||||
use crate::state::registrar::Registrar;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::Mint;
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use crate::account::*;
|
||||
use crate::error::*;
|
||||
use crate::state::lockup::*;
|
||||
use crate::state::registrar::Registrar;
|
||||
use crate::state::voter::Voter;
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
pub const VOTER_WEIGHT_RECORD: [u8; 19] = *b"voter-weight-record";
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
use crate::account::*;
|
||||
use crate::error::*;
|
||||
use crate::state::lockup::*;
|
||||
use crate::state::registrar::registrar_seeds;
|
||||
use crate::state::registrar::Registrar;
|
||||
use crate::state::voter::Voter;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::{self, Mint, Token, TokenAccount};
|
||||
use spl_governance::state::token_owner_record;
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
use account::*;
|
||||
use anchor_lang::prelude::*;
|
||||
use error::*;
|
||||
use instructions::*;
|
||||
use state::lockup::*;
|
||||
|
||||
#[macro_use]
|
||||
pub mod account;
|
||||
mod error;
|
||||
mod instructions;
|
||||
pub mod state;
|
||||
|
||||
// The program address.
|
||||
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
||||
|
|
|
@ -0,0 +1,261 @@
|
|||
use crate::error::*;
|
||||
use crate::state::exchange_entry::ExchangeRateEntry;
|
||||
use crate::state::lockup::{Lockup, LockupKind};
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
/// Vote weight is amount * FIXED_VOTE_WEIGHT_FACTOR +
|
||||
/// LOCKING_VOTE_WEIGHT_FACTOR * amount * time / max time
|
||||
pub const FIXED_VOTE_WEIGHT_FACTOR: u64 = 1;
|
||||
pub const LOCKING_VOTE_WEIGHT_FACTOR: u64 = 0;
|
||||
|
||||
/// Bookkeeping for a single deposit for a given mint and lockup schedule.
|
||||
#[zero_copy]
|
||||
pub struct DepositEntry {
|
||||
// True if the deposit entry is being used.
|
||||
pub is_used: bool,
|
||||
|
||||
// Points to the ExchangeRate this deposit uses.
|
||||
pub rate_idx: u8,
|
||||
|
||||
/// Amount in deposited, in native currency. Withdraws of vested tokens
|
||||
/// directly reduce this amount.
|
||||
///
|
||||
/// This directly tracks the total amount added by the user. They may
|
||||
/// never withdraw more than this amount.
|
||||
pub amount_deposited_native: u64,
|
||||
|
||||
/// Amount in locked when the lockup began, in native currency.
|
||||
///
|
||||
/// Note that this is not adjusted for withdraws. It is possible for this
|
||||
/// value to be bigger than amount_deposited_native after some vesting
|
||||
/// and withdrawals.
|
||||
///
|
||||
/// This value is needed to compute the amount that vests each peroid,
|
||||
/// which should not change due to withdraws.
|
||||
pub amount_initially_locked_native: u64,
|
||||
|
||||
pub allow_clawback: bool,
|
||||
|
||||
// Locked state.
|
||||
pub lockup: Lockup,
|
||||
}
|
||||
|
||||
impl DepositEntry {
|
||||
/// # Voting Power Caclulation
|
||||
///
|
||||
/// Returns the voting power for the deposit, giving locked tokens boosted
|
||||
/// voting power that scales linearly with the lockup time.
|
||||
///
|
||||
/// For each cliff-locked token, the vote weight is:
|
||||
///
|
||||
/// ```
|
||||
/// voting_power = amount * (fixed_factor + locking_factor * time_factor)
|
||||
/// ```
|
||||
///
|
||||
/// with
|
||||
/// fixed_factor = FIXED_VOTE_WEIGHT_FACTOR
|
||||
/// locking_factor = LOCKING_VOTE_WEIGHT_FACTOR
|
||||
/// time_factor = lockup_time_remaining / max_lockup_time
|
||||
///
|
||||
/// Linear vesting schedules can be thought of as a sequence of cliff-
|
||||
/// locked tokens and have the matching voting weight.
|
||||
///
|
||||
/// To achieve this with the SPL governance program--which requires a "max
|
||||
/// vote weight"--we attach what amounts to a scalar multiplier between 0
|
||||
/// and 1 to normalize voting power. This multiplier is a function of
|
||||
/// the lockup schedule. Here we will describe two, a one time
|
||||
/// cliff and a linear vesting schedule unlocking daily.
|
||||
///
|
||||
/// ## Cliff Lockup
|
||||
///
|
||||
/// The cliff lockup allows one to lockup their tokens for a set period
|
||||
/// of time, unlocking all at once on a given date.
|
||||
///
|
||||
/// The calculation for this is straightforward and is detailed above.
|
||||
///
|
||||
/// ### Decay
|
||||
///
|
||||
/// As time passes, the voting power decays until it's back to just
|
||||
/// fixed_factor when the cliff has passed. This is important because at
|
||||
/// each point in time the lockup should be equivalent to a new lockup
|
||||
/// made for the remaining time period.
|
||||
///
|
||||
/// ## Daily Vesting Lockup
|
||||
///
|
||||
/// Daily vesting can be calculated with simple series sum.
|
||||
///
|
||||
/// For the sake of example, suppose we locked up 10 tokens for two days,
|
||||
/// vesting linearly once a day. In other words, we have 5 tokens locked for
|
||||
/// 1 day and 5 tokens locked for two days.
|
||||
///
|
||||
/// Visually, we can see this in a two year timeline
|
||||
///
|
||||
/// 0 5 10 amount unlocked
|
||||
/// | ---- | ---- |
|
||||
/// 0 1 2 days
|
||||
///
|
||||
/// Then, to calculate the voting power at any time in the first day, we
|
||||
/// have (for a max_lockup_time of 2555 days)
|
||||
///
|
||||
/// ```
|
||||
/// voting_power =
|
||||
/// 5 * (fixed_factor + locking_factor * 1/2555)
|
||||
/// + 5 * (fixed_factor + locking_factor * 2/2555)
|
||||
/// = 10 * fixed_factor
|
||||
/// + 5 * locking_factor * (1 + 2)/2555
|
||||
/// ```
|
||||
///
|
||||
/// Since 7 years is the maximum lock, and 1 day is the minimum, we have
|
||||
/// a time_factor of 1/2555 for a one day lock, 2/2555 for a two day lock,
|
||||
/// 2555/2555 for a 7 year lock, and 0 for no lock.
|
||||
///
|
||||
/// Let's now generalize this to a daily vesting schedule over N days.
|
||||
/// Let "amount" be the total amount for vesting. Then the total voting
|
||||
/// power to start is
|
||||
///
|
||||
/// ```
|
||||
/// voting_power =
|
||||
/// = amount * fixed_factor
|
||||
/// + amount/N * locking_factor * (1 + 2 + ... + N)/2555
|
||||
/// ```
|
||||
///
|
||||
/// ### Decay
|
||||
///
|
||||
/// With every vesting one of the summands in the time term disappears
|
||||
/// and the remaining locking time for others decreases. That means after
|
||||
/// m days, the remaining voting power is
|
||||
///
|
||||
/// ```
|
||||
/// voting_power =
|
||||
/// = amount * fixed_factor
|
||||
/// + amount/N * locking_factor * (1 + 2 + ... + (N - m))/2555
|
||||
/// ```
|
||||
///
|
||||
/// Example: After N-1 days, only a 1/Nth fraction of the initial amount
|
||||
/// is still locked up and the rest has vested. And that amount has
|
||||
/// a time factor of 1/2555.
|
||||
///
|
||||
/// The computation below uses 1 + 2 + ... + n = n * (n + 1) / 2.
|
||||
pub fn voting_power(&self, rate: &ExchangeRateEntry, curr_ts: i64) -> Result<u64> {
|
||||
let fixed_contribution = rate
|
||||
.convert(self.amount_deposited_native)
|
||||
.checked_mul(FIXED_VOTE_WEIGHT_FACTOR)
|
||||
.unwrap();
|
||||
if LOCKING_VOTE_WEIGHT_FACTOR == 0 {
|
||||
return Ok(fixed_contribution);
|
||||
}
|
||||
|
||||
let max_locked_contribution = rate.convert(self.amount_initially_locked_native);
|
||||
Ok(fixed_contribution
|
||||
+ self
|
||||
.voting_power_locked(curr_ts, max_locked_contribution)?
|
||||
.checked_mul(LOCKING_VOTE_WEIGHT_FACTOR)
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
/// Vote contribution from locked funds only, not scaled by
|
||||
/// LOCKING_VOTE_WEIGHT_FACTOR yet.
|
||||
pub fn voting_power_locked(&self, curr_ts: i64, max_contribution: u64) -> Result<u64> {
|
||||
if curr_ts < self.lockup.start_ts || curr_ts >= self.lockup.end_ts {
|
||||
return Ok(0);
|
||||
}
|
||||
match self.lockup.kind {
|
||||
LockupKind::None => Ok(0),
|
||||
LockupKind::Daily => self.voting_power_linear_vesting(curr_ts, max_contribution),
|
||||
LockupKind::Monthly => self.voting_power_linear_vesting(curr_ts, max_contribution),
|
||||
LockupKind::Cliff => self.voting_power_cliff(curr_ts, max_contribution),
|
||||
}
|
||||
}
|
||||
|
||||
fn voting_power_linear_vesting(&self, curr_ts: i64, max_contribution: u64) -> Result<u64> {
|
||||
let max_periods = self.lockup.kind.max_periods();
|
||||
let periods_left = self.lockup.periods_left(curr_ts)?;
|
||||
let periods_total = self.lockup.periods_total()?;
|
||||
|
||||
if periods_left == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// TODO: Switch the decay interval to be seconds, not days. That means each
|
||||
// of the period cliff-locked deposits here will decay in vote power over the
|
||||
// period. That complicates the computaton here, but makes it easier to do
|
||||
// the right thing if the period_secs() aren't a multiple of a day.
|
||||
//
|
||||
// This computes
|
||||
// amount / periods_total * (1 + 2 + ... + periods_left) / max_periods
|
||||
// See the comment on voting_power().
|
||||
let decayed_vote_weight = max_contribution
|
||||
.checked_mul(
|
||||
// Ok to divide by two here because, if n is zero, then the
|
||||
// voting power is zero. And if n is one or above, then the
|
||||
// numerator is 2 or above.
|
||||
periods_left
|
||||
.checked_mul(periods_left.checked_add(1).unwrap())
|
||||
.unwrap()
|
||||
.checked_div(2)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.checked_div(max_periods.checked_mul(periods_total).unwrap())
|
||||
.unwrap();
|
||||
|
||||
Ok(decayed_vote_weight)
|
||||
}
|
||||
|
||||
fn voting_power_cliff(&self, curr_ts: i64, max_contribution: u64) -> Result<u64> {
|
||||
// TODO: Decay by the second, not by the day.
|
||||
let decayed_voting_weight = self
|
||||
.lockup
|
||||
.periods_left(curr_ts)?
|
||||
.checked_mul(max_contribution)
|
||||
.unwrap()
|
||||
.checked_div(self.lockup.kind.max_periods())
|
||||
.unwrap();
|
||||
|
||||
Ok(decayed_voting_weight)
|
||||
}
|
||||
|
||||
/// Returns the amount of unlocked tokens for this deposit--in native units
|
||||
/// of the original token amount (not scaled by the exchange rate).
|
||||
pub fn vested(&self, curr_ts: i64) -> Result<u64> {
|
||||
if curr_ts < self.lockup.start_ts {
|
||||
return Ok(0);
|
||||
}
|
||||
if curr_ts >= self.lockup.end_ts {
|
||||
return Ok(self.amount_initially_locked_native);
|
||||
}
|
||||
match self.lockup.kind {
|
||||
LockupKind::None => Ok(self.amount_initially_locked_native),
|
||||
LockupKind::Daily => self.vested_linearly(curr_ts),
|
||||
LockupKind::Monthly => self.vested_linearly(curr_ts),
|
||||
LockupKind::Cliff => Ok(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn vested_linearly(&self, curr_ts: i64) -> Result<u64> {
|
||||
let period_current = self.lockup.period_current(curr_ts)?;
|
||||
let periods_total = self.lockup.periods_total()?;
|
||||
if period_current >= periods_total {
|
||||
return Ok(self.amount_initially_locked_native);
|
||||
}
|
||||
let vested = self
|
||||
.amount_initially_locked_native
|
||||
.checked_mul(period_current)
|
||||
.unwrap()
|
||||
.checked_div(periods_total)
|
||||
.unwrap();
|
||||
Ok(vested)
|
||||
}
|
||||
|
||||
/// Returns the amount that may be withdrawn given current vesting
|
||||
/// and previous withdraws.
|
||||
pub fn amount_withdrawable(&self, curr_ts: i64) -> u64 {
|
||||
let still_locked = self
|
||||
.amount_initially_locked_native
|
||||
.checked_sub(self.vested(curr_ts).unwrap())
|
||||
.unwrap();
|
||||
self.amount_deposited_native
|
||||
.checked_sub(still_locked)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
use anchor_lang::__private::bytemuck::Zeroable;
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
/// Exchange rate for an asset that can be used to mint voting rights.
|
||||
#[zero_copy]
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Default)]
|
||||
pub struct ExchangeRateEntry {
|
||||
/// Mint for this entry.
|
||||
pub mint: Pubkey,
|
||||
|
||||
/// Mint decimals.
|
||||
pub mint_decimals: u8,
|
||||
|
||||
/// Exchange rate for 1.0 decimal-respecting unit of mint currency
|
||||
/// into the common vote currency.
|
||||
///
|
||||
/// Example: If rate=2, then 1.000 of mint currency has a vote weight
|
||||
/// of 2.000000 in common vote currency. In the example mint decimals
|
||||
/// was 3 and common_decimals was 6.
|
||||
pub rate: u64,
|
||||
|
||||
/// Factor for converting mint native currency to common vote currency,
|
||||
/// including decimal handling.
|
||||
///
|
||||
/// Examples:
|
||||
/// - if common and mint have the same number of decimals, this is the same as 'rate'
|
||||
/// - common decimals = 6, mint decimals = 3, rate = 5 -> 500
|
||||
pub conversion_factor: u64,
|
||||
}
|
||||
|
||||
impl ExchangeRateEntry {
|
||||
/// Converts an amount in this ExchangeRateEntry's mint's native currency
|
||||
/// to the equivalent common registrar vote currency amount.
|
||||
pub fn convert(&self, amount_native: u64) -> u64 {
|
||||
amount_native.checked_mul(self.conversion_factor).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Zeroable for ExchangeRateEntry {}
|
|
@ -1,4 +1,6 @@
|
|||
use crate::error::*;
|
||||
use crate::state::exchange_entry::ExchangeRateEntry;
|
||||
use crate::state::registrar::Registrar;
|
||||
use anchor_lang::__private::bytemuck::Zeroable;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::vote_weight_record;
|
||||
|
@ -27,389 +29,6 @@ pub const MAX_DAYS_LOCKED: u64 = 7 * 365;
|
|||
/// Maximum number of months one can lock for.
|
||||
pub const MAX_MONTHS_LOCKED: u64 = 7 * 12;
|
||||
|
||||
/// Vote weight is amount * FIXED_VOTE_WEIGHT_FACTOR +
|
||||
/// LOCKING_VOTE_WEIGHT_FACTOR * amount * time / max time
|
||||
pub const FIXED_VOTE_WEIGHT_FACTOR: u64 = 1;
|
||||
pub const LOCKING_VOTE_WEIGHT_FACTOR: u64 = 0;
|
||||
|
||||
/// Instance of a voting rights distributor.
|
||||
#[account]
|
||||
#[derive(Default)]
|
||||
pub struct Registrar {
|
||||
pub governance_program_id: Pubkey,
|
||||
pub realm: Pubkey,
|
||||
pub realm_governing_token_mint: Pubkey,
|
||||
pub realm_authority: Pubkey,
|
||||
pub clawback_authority: Pubkey,
|
||||
pub bump: u8,
|
||||
// The length should be adjusted for one's use case.
|
||||
pub rates: [ExchangeRateEntry; 2],
|
||||
|
||||
/// The decimals to use when converting deposits into a common currency.
|
||||
///
|
||||
/// This must be larger or equal to the max of decimals over all accepted
|
||||
/// token mints.
|
||||
pub vote_weight_decimals: u8,
|
||||
|
||||
/// Debug only: time offset, to allow tests to move forward in time.
|
||||
pub time_offset: i64,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! registrar_seeds {
|
||||
( $registrar:expr ) => {
|
||||
&[
|
||||
$registrar.realm.as_ref(),
|
||||
b"registrar".as_ref(),
|
||||
$registrar.realm_governing_token_mint.as_ref(),
|
||||
&[$registrar.bump],
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
impl Registrar {
|
||||
pub fn new_rate(
|
||||
&self,
|
||||
mint: Pubkey,
|
||||
mint_decimals: u8,
|
||||
rate: u64,
|
||||
) -> Result<ExchangeRateEntry> {
|
||||
require!(self.vote_weight_decimals >= mint_decimals, InvalidDecimals);
|
||||
let decimal_diff = self
|
||||
.vote_weight_decimals
|
||||
.checked_sub(mint_decimals)
|
||||
.unwrap();
|
||||
Ok(ExchangeRateEntry {
|
||||
mint,
|
||||
rate,
|
||||
mint_decimals,
|
||||
conversion_factor: rate.checked_mul(10u64.pow(decimal_diff.into())).unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clock_unix_timestamp(&self) -> i64 {
|
||||
Clock::get().unwrap().unix_timestamp + self.time_offset
|
||||
}
|
||||
}
|
||||
|
||||
/// User account for minting voting rights.
|
||||
#[account(zero_copy)]
|
||||
pub struct Voter {
|
||||
pub voter_authority: Pubkey,
|
||||
pub registrar: Pubkey,
|
||||
pub voter_bump: u8,
|
||||
pub voter_weight_record_bump: u8,
|
||||
pub deposits: [DepositEntry; 32],
|
||||
|
||||
/// The most recent slot a deposit was made in.
|
||||
///
|
||||
/// Would like to use solana_program::clock::Slot here, but Anchor's IDL
|
||||
/// does not know the type.
|
||||
pub last_deposit_slot: u64,
|
||||
}
|
||||
|
||||
impl Voter {
|
||||
pub fn weight(&self, registrar: &Registrar) -> Result<u64> {
|
||||
let curr_ts = registrar.clock_unix_timestamp();
|
||||
self.deposits
|
||||
.iter()
|
||||
.filter(|d| d.is_used)
|
||||
.try_fold(0, |sum, d| {
|
||||
d.voting_power(®istrar.rates[d.rate_idx as usize], curr_ts)
|
||||
.map(|vp| sum + vp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Exchange rate for an asset that can be used to mint voting rights.
|
||||
#[zero_copy]
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Default)]
|
||||
pub struct ExchangeRateEntry {
|
||||
/// Mint for this entry.
|
||||
pub mint: Pubkey,
|
||||
|
||||
/// Mint decimals.
|
||||
pub mint_decimals: u8,
|
||||
|
||||
/// Exchange rate for 1.0 decimal-respecting unit of mint currency
|
||||
/// into the common vote currency.
|
||||
///
|
||||
/// Example: If rate=2, then 1.000 of mint currency has a vote weight
|
||||
/// of 2.000000 in common vote currency. In the example mint decimals
|
||||
/// was 3 and common_decimals was 6.
|
||||
pub rate: u64,
|
||||
|
||||
/// Factor for converting mint native currency to common vote currency,
|
||||
/// including decimal handling.
|
||||
///
|
||||
/// Examples:
|
||||
/// - if common and mint have the same number of decimals, this is the same as 'rate'
|
||||
/// - common decimals = 6, mint decimals = 3, rate = 5 -> 500
|
||||
pub conversion_factor: u64,
|
||||
}
|
||||
|
||||
impl ExchangeRateEntry {
|
||||
/// Converts an amount in this ExchangeRateEntry's mint's native currency
|
||||
/// to the equivalent common registrar vote currency amount.
|
||||
pub fn convert(&self, amount_native: u64) -> u64 {
|
||||
amount_native.checked_mul(self.conversion_factor).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Zeroable for ExchangeRateEntry {}
|
||||
|
||||
/// Bookkeeping for a single deposit for a given mint and lockup schedule.
|
||||
#[zero_copy]
|
||||
pub struct DepositEntry {
|
||||
// True if the deposit entry is being used.
|
||||
pub is_used: bool,
|
||||
|
||||
// Points to the ExchangeRate this deposit uses.
|
||||
pub rate_idx: u8,
|
||||
|
||||
/// Amount in deposited, in native currency. Withdraws of vested tokens
|
||||
/// directly reduce this amount.
|
||||
///
|
||||
/// This directly tracks the total amount added by the user. They may
|
||||
/// never withdraw more than this amount.
|
||||
pub amount_deposited_native: u64,
|
||||
|
||||
/// Amount in locked when the lockup began, in native currency.
|
||||
///
|
||||
/// Note that this is not adjusted for withdraws. It is possible for this
|
||||
/// value to be bigger than amount_deposited_native after some vesting
|
||||
/// and withdrawals.
|
||||
///
|
||||
/// This value is needed to compute the amount that vests each peroid,
|
||||
/// which should not change due to withdraws.
|
||||
pub amount_initially_locked_native: u64,
|
||||
|
||||
pub allow_clawback: bool,
|
||||
|
||||
// Locked state.
|
||||
pub lockup: Lockup,
|
||||
}
|
||||
|
||||
impl DepositEntry {
|
||||
/// # Voting Power Caclulation
|
||||
///
|
||||
/// Returns the voting power for the deposit, giving locked tokens boosted
|
||||
/// voting power that scales linearly with the lockup time.
|
||||
///
|
||||
/// For each cliff-locked token, the vote weight is:
|
||||
///
|
||||
/// ```
|
||||
/// voting_power = amount * (fixed_factor + locking_factor * time_factor)
|
||||
/// ```
|
||||
///
|
||||
/// with
|
||||
/// fixed_factor = FIXED_VOTE_WEIGHT_FACTOR
|
||||
/// locking_factor = LOCKING_VOTE_WEIGHT_FACTOR
|
||||
/// time_factor = lockup_time_remaining / max_lockup_time
|
||||
///
|
||||
/// Linear vesting schedules can be thought of as a sequence of cliff-
|
||||
/// locked tokens and have the matching voting weight.
|
||||
///
|
||||
/// To achieve this with the SPL governance program--which requires a "max
|
||||
/// vote weight"--we attach what amounts to a scalar multiplier between 0
|
||||
/// and 1 to normalize voting power. This multiplier is a function of
|
||||
/// the lockup schedule. Here we will describe two, a one time
|
||||
/// cliff and a linear vesting schedule unlocking daily.
|
||||
///
|
||||
/// ## Cliff Lockup
|
||||
///
|
||||
/// The cliff lockup allows one to lockup their tokens for a set period
|
||||
/// of time, unlocking all at once on a given date.
|
||||
///
|
||||
/// The calculation for this is straightforward and is detailed above.
|
||||
///
|
||||
/// ### Decay
|
||||
///
|
||||
/// As time passes, the voting power decays until it's back to just
|
||||
/// fixed_factor when the cliff has passed. This is important because at
|
||||
/// each point in time the lockup should be equivalent to a new lockup
|
||||
/// made for the remaining time period.
|
||||
///
|
||||
/// ## Daily Vesting Lockup
|
||||
///
|
||||
/// Daily vesting can be calculated with simple series sum.
|
||||
///
|
||||
/// For the sake of example, suppose we locked up 10 tokens for two days,
|
||||
/// vesting linearly once a day. In other words, we have 5 tokens locked for
|
||||
/// 1 day and 5 tokens locked for two days.
|
||||
///
|
||||
/// Visually, we can see this in a two year timeline
|
||||
///
|
||||
/// 0 5 10 amount unlocked
|
||||
/// | ---- | ---- |
|
||||
/// 0 1 2 days
|
||||
///
|
||||
/// Then, to calculate the voting power at any time in the first day, we
|
||||
/// have (for a max_lockup_time of 2555 days)
|
||||
///
|
||||
/// ```
|
||||
/// voting_power =
|
||||
/// 5 * (fixed_factor + locking_factor * 1/2555)
|
||||
/// + 5 * (fixed_factor + locking_factor * 2/2555)
|
||||
/// = 10 * fixed_factor
|
||||
/// + 5 * locking_factor * (1 + 2)/2555
|
||||
/// ```
|
||||
///
|
||||
/// Since 7 years is the maximum lock, and 1 day is the minimum, we have
|
||||
/// a time_factor of 1/2555 for a one day lock, 2/2555 for a two day lock,
|
||||
/// 2555/2555 for a 7 year lock, and 0 for no lock.
|
||||
///
|
||||
/// Let's now generalize this to a daily vesting schedule over N days.
|
||||
/// Let "amount" be the total amount for vesting. Then the total voting
|
||||
/// power to start is
|
||||
///
|
||||
/// ```
|
||||
/// voting_power =
|
||||
/// = amount * fixed_factor
|
||||
/// + amount/N * locking_factor * (1 + 2 + ... + N)/2555
|
||||
/// ```
|
||||
///
|
||||
/// ### Decay
|
||||
///
|
||||
/// With every vesting one of the summands in the time term disappears
|
||||
/// and the remaining locking time for others decreases. That means after
|
||||
/// m days, the remaining voting power is
|
||||
///
|
||||
/// ```
|
||||
/// voting_power =
|
||||
/// = amount * fixed_factor
|
||||
/// + amount/N * locking_factor * (1 + 2 + ... + (N - m))/2555
|
||||
/// ```
|
||||
///
|
||||
/// Example: After N-1 days, only a 1/Nth fraction of the initial amount
|
||||
/// is still locked up and the rest has vested. And that amount has
|
||||
/// a time factor of 1/2555.
|
||||
///
|
||||
/// The computation below uses 1 + 2 + ... + n = n * (n + 1) / 2.
|
||||
pub fn voting_power(&self, rate: &ExchangeRateEntry, curr_ts: i64) -> Result<u64> {
|
||||
let fixed_contribution = rate
|
||||
.convert(self.amount_deposited_native)
|
||||
.checked_mul(FIXED_VOTE_WEIGHT_FACTOR)
|
||||
.unwrap();
|
||||
if LOCKING_VOTE_WEIGHT_FACTOR == 0 {
|
||||
return Ok(fixed_contribution);
|
||||
}
|
||||
|
||||
let max_locked_contribution = rate.convert(self.amount_initially_locked_native);
|
||||
Ok(fixed_contribution
|
||||
+ self
|
||||
.voting_power_locked(curr_ts, max_locked_contribution)?
|
||||
.checked_mul(LOCKING_VOTE_WEIGHT_FACTOR)
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
/// Vote contribution from locked funds only, not scaled by
|
||||
/// LOCKING_VOTE_WEIGHT_FACTOR yet.
|
||||
fn voting_power_locked(&self, curr_ts: i64, max_contribution: u64) -> Result<u64> {
|
||||
if curr_ts < self.lockup.start_ts || curr_ts >= self.lockup.end_ts {
|
||||
return Ok(0);
|
||||
}
|
||||
match self.lockup.kind {
|
||||
LockupKind::None => Ok(0),
|
||||
LockupKind::Daily => self.voting_power_linear_vesting(curr_ts, max_contribution),
|
||||
LockupKind::Monthly => self.voting_power_linear_vesting(curr_ts, max_contribution),
|
||||
LockupKind::Cliff => self.voting_power_cliff(curr_ts, max_contribution),
|
||||
}
|
||||
}
|
||||
|
||||
fn voting_power_linear_vesting(&self, curr_ts: i64, max_contribution: u64) -> Result<u64> {
|
||||
let max_periods = self.lockup.kind.max_periods();
|
||||
let periods_left = self.lockup.periods_left(curr_ts)?;
|
||||
let periods_total = self.lockup.periods_total()?;
|
||||
|
||||
if periods_left == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// TODO: Switch the decay interval to be seconds, not days. That means each
|
||||
// of the period cliff-locked deposits here will decay in vote power over the
|
||||
// period. That complicates the computaton here, but makes it easier to do
|
||||
// the right thing if the period_secs() aren't a multiple of a day.
|
||||
//
|
||||
// This computes
|
||||
// amount / periods_total * (1 + 2 + ... + periods_left) / max_periods
|
||||
// See the comment on voting_power().
|
||||
let decayed_vote_weight = max_contribution
|
||||
.checked_mul(
|
||||
// Ok to divide by two here because, if n is zero, then the
|
||||
// voting power is zero. And if n is one or above, then the
|
||||
// numerator is 2 or above.
|
||||
periods_left
|
||||
.checked_mul(periods_left.checked_add(1).unwrap())
|
||||
.unwrap()
|
||||
.checked_div(2)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.checked_div(max_periods.checked_mul(periods_total).unwrap())
|
||||
.unwrap();
|
||||
|
||||
Ok(decayed_vote_weight)
|
||||
}
|
||||
|
||||
fn voting_power_cliff(&self, curr_ts: i64, max_contribution: u64) -> Result<u64> {
|
||||
// TODO: Decay by the second, not by the day.
|
||||
let decayed_voting_weight = self
|
||||
.lockup
|
||||
.periods_left(curr_ts)?
|
||||
.checked_mul(max_contribution)
|
||||
.unwrap()
|
||||
.checked_div(self.lockup.kind.max_periods())
|
||||
.unwrap();
|
||||
|
||||
Ok(decayed_voting_weight)
|
||||
}
|
||||
|
||||
/// Returns the amount of unlocked tokens for this deposit--in native units
|
||||
/// of the original token amount (not scaled by the exchange rate).
|
||||
pub fn vested(&self, curr_ts: i64) -> Result<u64> {
|
||||
if curr_ts < self.lockup.start_ts {
|
||||
return Ok(0);
|
||||
}
|
||||
if curr_ts >= self.lockup.end_ts {
|
||||
return Ok(self.amount_initially_locked_native);
|
||||
}
|
||||
match self.lockup.kind {
|
||||
LockupKind::None => Ok(self.amount_initially_locked_native),
|
||||
LockupKind::Daily => self.vested_linearly(curr_ts),
|
||||
LockupKind::Monthly => self.vested_linearly(curr_ts),
|
||||
LockupKind::Cliff => Ok(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn vested_linearly(&self, curr_ts: i64) -> Result<u64> {
|
||||
let period_current = self.lockup.period_current(curr_ts)?;
|
||||
let periods_total = self.lockup.periods_total()?;
|
||||
if period_current >= periods_total {
|
||||
return Ok(self.amount_initially_locked_native);
|
||||
}
|
||||
let vested = self
|
||||
.amount_initially_locked_native
|
||||
.checked_mul(period_current)
|
||||
.unwrap()
|
||||
.checked_div(periods_total)
|
||||
.unwrap();
|
||||
Ok(vested)
|
||||
}
|
||||
|
||||
/// Returns the amount that may be withdrawn given current vesting
|
||||
/// and previous withdraws.
|
||||
pub fn amount_withdrawable(&self, curr_ts: i64) -> u64 {
|
||||
let still_locked = self
|
||||
.amount_initially_locked_native
|
||||
.checked_sub(self.vested(curr_ts).unwrap())
|
||||
.unwrap();
|
||||
self.amount_deposited_native
|
||||
.checked_sub(still_locked)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[zero_copy]
|
||||
#[derive(AnchorSerialize, AnchorDeserialize)]
|
||||
pub struct Lockup {
|
||||
|
@ -493,6 +112,7 @@ impl LockupKind {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::state::deposit_entry::DepositEntry;
|
||||
|
||||
#[test]
|
||||
pub fn days_left_start() -> Result<()> {
|
|
@ -0,0 +1,5 @@
|
|||
pub mod deposit_entry;
|
||||
pub mod exchange_entry;
|
||||
pub mod lockup;
|
||||
pub mod registrar;
|
||||
pub mod voter;
|
|
@ -0,0 +1,65 @@
|
|||
use crate::error::*;
|
||||
use crate::state::exchange_entry::ExchangeRateEntry;
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
/// Instance of a voting rights distributor.
|
||||
#[account]
|
||||
#[derive(Default)]
|
||||
pub struct Registrar {
|
||||
pub governance_program_id: Pubkey,
|
||||
pub realm: Pubkey,
|
||||
pub realm_governing_token_mint: Pubkey,
|
||||
pub realm_authority: Pubkey,
|
||||
pub clawback_authority: Pubkey,
|
||||
pub bump: u8,
|
||||
// The length should be adjusted for one's use case.
|
||||
pub rates: [ExchangeRateEntry; 2],
|
||||
|
||||
/// The decimals to use when converting deposits into a common currency.
|
||||
///
|
||||
/// This must be larger or equal to the max of decimals over all accepted
|
||||
/// token mints.
|
||||
pub vote_weight_decimals: u8,
|
||||
|
||||
/// Debug only: time offset, to allow tests to move forward in time.
|
||||
pub time_offset: i64,
|
||||
}
|
||||
|
||||
impl Registrar {
|
||||
pub fn new_rate(
|
||||
&self,
|
||||
mint: Pubkey,
|
||||
mint_decimals: u8,
|
||||
rate: u64,
|
||||
) -> Result<ExchangeRateEntry> {
|
||||
require!(self.vote_weight_decimals >= mint_decimals, InvalidDecimals);
|
||||
let decimal_diff = self
|
||||
.vote_weight_decimals
|
||||
.checked_sub(mint_decimals)
|
||||
.unwrap();
|
||||
Ok(ExchangeRateEntry {
|
||||
mint,
|
||||
rate,
|
||||
mint_decimals,
|
||||
conversion_factor: rate.checked_mul(10u64.pow(decimal_diff.into())).unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clock_unix_timestamp(&self) -> i64 {
|
||||
Clock::get().unwrap().unix_timestamp + self.time_offset
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! registrar_seeds {
|
||||
( $registrar:expr ) => {
|
||||
&[
|
||||
$registrar.realm.as_ref(),
|
||||
b"registrar".as_ref(),
|
||||
$registrar.realm_governing_token_mint.as_ref(),
|
||||
&[$registrar.bump],
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
pub use registrar_seeds;
|
|
@ -0,0 +1,33 @@
|
|||
use crate::error::*;
|
||||
use crate::state::deposit_entry::DepositEntry;
|
||||
use crate::state::registrar::Registrar;
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
/// User account for minting voting rights.
|
||||
#[account(zero_copy)]
|
||||
pub struct Voter {
|
||||
pub voter_authority: Pubkey,
|
||||
pub registrar: Pubkey,
|
||||
pub voter_bump: u8,
|
||||
pub voter_weight_record_bump: u8,
|
||||
pub deposits: [DepositEntry; 32],
|
||||
|
||||
/// The most recent slot a deposit was made in.
|
||||
///
|
||||
/// Would like to use solana_program::clock::Slot here, but Anchor's IDL
|
||||
/// does not know the type.
|
||||
pub last_deposit_slot: u64,
|
||||
}
|
||||
|
||||
impl Voter {
|
||||
pub fn weight(&self, registrar: &Registrar) -> Result<u64> {
|
||||
let curr_ts = registrar.clock_unix_timestamp();
|
||||
self.deposits
|
||||
.iter()
|
||||
.filter(|d| d.is_used)
|
||||
.try_fold(0, |sum, d| {
|
||||
d.voting_power(®istrar.rates[d.rate_idx as usize], curr_ts)
|
||||
.map(|vp| sum + vp)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -227,7 +227,7 @@ impl AddinCookie {
|
|||
voter: &VoterCookie,
|
||||
voter_authority: &Keypair,
|
||||
exchange_rate: &ExchangeRateCookie,
|
||||
lockup_kind: voter_stake_registry::account::LockupKind,
|
||||
lockup_kind: voter_stake_registry::state::lockup::LockupKind,
|
||||
periods: i32,
|
||||
allow_clawback: bool,
|
||||
) -> std::result::Result<(), TransportError> {
|
||||
|
@ -402,7 +402,8 @@ impl AddinCookie {
|
|||
&self,
|
||||
registrar: &RegistrarCookie,
|
||||
voter: &VoterCookie,
|
||||
) -> std::result::Result<voter_stake_registry::account::VoterWeightRecord, TransportError> {
|
||||
) -> std::result::Result<voter_stake_registry::state::lockup::VoterWeightRecord, TransportError>
|
||||
{
|
||||
let data = anchor_lang::InstructionData::data(
|
||||
&voter_stake_registry::instruction::UpdateVoterWeightRecord {},
|
||||
);
|
||||
|
@ -427,7 +428,7 @@ impl AddinCookie {
|
|||
|
||||
Ok(self
|
||||
.solana
|
||||
.get_account::<voter_stake_registry::account::VoterWeightRecord>(
|
||||
.get_account::<voter_stake_registry::state::lockup::VoterWeightRecord>(
|
||||
voter.voter_weight_record,
|
||||
)
|
||||
.await)
|
||||
|
@ -511,7 +512,7 @@ impl ExchangeRateCookie {
|
|||
impl VoterCookie {
|
||||
pub async fn deposit_amount(&self, solana: &SolanaCookie, deposit_id: u8) -> u64 {
|
||||
solana
|
||||
.get_account::<voter_stake_registry::account::Voter>(self.address)
|
||||
.get_account::<voter_stake_registry::state::voter::Voter>(self.address)
|
||||
.await
|
||||
.deposits[deposit_id as usize]
|
||||
.amount_deposited_native
|
||||
|
|
|
@ -62,7 +62,7 @@ async fn test_basic() -> Result<(), TransportError> {
|
|||
&voter,
|
||||
voter_authority,
|
||||
&mngo_rate,
|
||||
voter_stake_registry::account::LockupKind::Cliff,
|
||||
voter_stake_registry::state::lockup::LockupKind::Cliff,
|
||||
0,
|
||||
false,
|
||||
)
|
||||
|
|
|
@ -80,7 +80,7 @@ async fn test_clawback() -> Result<(), TransportError> {
|
|||
&voter,
|
||||
voter_authority,
|
||||
&mngo_rate,
|
||||
voter_stake_registry::account::LockupKind::Daily,
|
||||
voter_stake_registry::state::lockup::LockupKind::Daily,
|
||||
10,
|
||||
true,
|
||||
)
|
||||
|
|
|
@ -122,7 +122,7 @@ async fn test_deposit_cliff() -> Result<(), TransportError> {
|
|||
&voter,
|
||||
&voter_authority,
|
||||
&mngo_rate,
|
||||
voter_stake_registry::account::LockupKind::Cliff,
|
||||
voter_stake_registry::state::lockup::LockupKind::Cliff,
|
||||
3, // days
|
||||
false,
|
||||
)
|
||||
|
|
|
@ -122,7 +122,7 @@ async fn test_deposit_daily_vesting() -> Result<(), TransportError> {
|
|||
&voter,
|
||||
&voter_authority,
|
||||
&mngo_rate,
|
||||
voter_stake_registry::account::LockupKind::Daily,
|
||||
voter_stake_registry::state::lockup::LockupKind::Daily,
|
||||
3,
|
||||
false,
|
||||
)
|
||||
|
|
|
@ -122,7 +122,7 @@ async fn test_deposit_monthly_vesting() -> Result<(), TransportError> {
|
|||
&voter,
|
||||
&voter_authority,
|
||||
&mngo_rate,
|
||||
voter_stake_registry::account::LockupKind::Monthly,
|
||||
voter_stake_registry::state::lockup::LockupKind::Monthly,
|
||||
3,
|
||||
false,
|
||||
)
|
||||
|
|
|
@ -3,6 +3,7 @@ use solana_program_test::*;
|
|||
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transport::TransportError};
|
||||
|
||||
use program_test::*;
|
||||
use voter_stake_registry::state::lockup::LockupKind;
|
||||
|
||||
mod program_test;
|
||||
|
||||
|
@ -127,7 +128,7 @@ async fn test_deposit_no_locking() -> Result<(), TransportError> {
|
|||
&voter,
|
||||
&voter_authority,
|
||||
&mngo_rate,
|
||||
voter_stake_registry::account::LockupKind::None,
|
||||
LockupKind::None,
|
||||
0,
|
||||
false,
|
||||
)
|
||||
|
@ -157,7 +158,7 @@ async fn test_deposit_no_locking() -> Result<(), TransportError> {
|
|||
&voter,
|
||||
&voter_authority,
|
||||
&mngo_rate,
|
||||
voter_stake_registry::account::LockupKind::None,
|
||||
LockupKind::None,
|
||||
0,
|
||||
false,
|
||||
)
|
||||
|
@ -234,7 +235,7 @@ async fn test_deposit_no_locking() -> Result<(), TransportError> {
|
|||
&voter2,
|
||||
&voter2_authority,
|
||||
&mngo_rate,
|
||||
voter_stake_registry::account::LockupKind::None,
|
||||
LockupKind::None,
|
||||
5, // shouldn't matter
|
||||
false,
|
||||
)
|
||||
|
@ -273,7 +274,7 @@ async fn test_deposit_no_locking() -> Result<(), TransportError> {
|
|||
&voter,
|
||||
&voter_authority,
|
||||
&mngo_rate,
|
||||
voter_stake_registry::account::LockupKind::None,
|
||||
LockupKind::None,
|
||||
1, // shouldn't matter
|
||||
false,
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue