Configurable options for vote weight scaling factors (#7)
Configurable options for vote weight scaling and lockup saturation Co-authored-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
parent
317b7168eb
commit
ddf37c4de0
|
@ -54,4 +54,8 @@ pub enum ErrorCode {
|
||||||
InvalidTokenOwnerRecord,
|
InvalidTokenOwnerRecord,
|
||||||
#[msg("")]
|
#[msg("")]
|
||||||
InvalidRealmAuthority,
|
InvalidRealmAuthority,
|
||||||
|
#[msg("")]
|
||||||
|
VoterWeightOverflow,
|
||||||
|
#[msg("")]
|
||||||
|
LockupSaturationMustBePositive,
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,9 @@ use anchor_lang::prelude::*;
|
||||||
use anchor_spl::associated_token::AssociatedToken;
|
use anchor_spl::associated_token::AssociatedToken;
|
||||||
use anchor_spl::token::{Mint, Token, TokenAccount};
|
use anchor_spl::token::{Mint, Token, TokenAccount};
|
||||||
|
|
||||||
|
// Remaining accounts must be all the token mints that have registered
|
||||||
|
// as voting mints, including the newly registered one.
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
#[instruction(idx: u16, rate: u64, decimals: u8)]
|
|
||||||
pub struct ConfigureVotingMint<'info> {
|
pub struct ConfigureVotingMint<'info> {
|
||||||
#[account(mut, has_one = realm_authority)]
|
#[account(mut, has_one = realm_authority)]
|
||||||
pub registrar: Box<Account<'info, Registrar>>,
|
pub registrar: Box<Account<'info, Registrar>>,
|
||||||
|
@ -35,36 +36,81 @@ pub struct ConfigureVotingMint<'info> {
|
||||||
/// deposit the mint in exchange for vote weight. There can only be a single
|
/// deposit the mint in exchange for vote weight. There can only be a single
|
||||||
/// exchange rate per mint.
|
/// exchange rate per mint.
|
||||||
///
|
///
|
||||||
/// `idx`: index of the rate to be set
|
/// * `idx`: index of the rate to be set
|
||||||
/// `rate`: multiplier to apply for converting tokens to vote weight
|
/// * `digit_shift`: how many digits to shift the native token amount, see below
|
||||||
/// `decimals`: number of decimals of mint that make one unit of token
|
/// * `deposit_scaled_factor`: vote weight factor for deposits, in 1/1e9 units
|
||||||
|
/// * `lockup_scaled_factor`: max extra weight for lockups, in 1/1e9 units
|
||||||
|
/// * `lockup_saturation_secs`: lockup duration at which the full vote weight
|
||||||
|
/// bonus is given to locked up deposits
|
||||||
///
|
///
|
||||||
/// The vote weight for one native token will be:
|
/// The vote weight for `amount` of native tokens will be
|
||||||
/// ```
|
/// ```
|
||||||
/// rate * 10^vote_weight_decimals / 10^decimals
|
/// vote_weight =
|
||||||
|
/// amount * 10^(digit_shift)
|
||||||
|
/// * (deposit_scaled_factor/1e9
|
||||||
|
/// + lockup_duration_factor * lockup_scaled_factor/1e9)
|
||||||
/// ```
|
/// ```
|
||||||
|
/// where lockup_duration_factor is a value between 0 and 1, depending on how long
|
||||||
|
/// the amount is locked up. It is 1 when the lockup duration is greater or equal
|
||||||
|
/// lockup_saturation_secs.
|
||||||
|
///
|
||||||
|
/// Warning: Choose values that ensure that the vote weight will not overflow the
|
||||||
|
/// u64 limit! There is a check based on the supply of all configured mints, but
|
||||||
|
/// do your own checking too.
|
||||||
|
///
|
||||||
|
/// If you use a single mint, prefer digit_shift=0 and deposit_scaled_factor +
|
||||||
|
/// lockup_scaled_factor <= 1e9. That way you won't have issues with overflow no
|
||||||
|
/// matter the size of the mint's supply.
|
||||||
|
///
|
||||||
|
/// Digit shifting is particularly useful when using several voting token mints
|
||||||
|
/// that have a different number of decimals. It can be used to align them to
|
||||||
|
/// a common number of decimals.
|
||||||
|
///
|
||||||
|
/// Example: If you have token A with 6 decimals and token B with 9 decimals, you
|
||||||
|
/// could set up:
|
||||||
|
/// * A with digit_shift=0, deposit_scaled_factor=2e9, lockup_scaled_factor=0
|
||||||
|
/// * B with digit_shift=-3, deposit_scaled_factor=1e9, lockup_scaled_factor=1e9
|
||||||
|
///
|
||||||
|
/// That would make 1.0 decimaled tokens of A as valuable as 2.0 decimaled tokens
|
||||||
|
/// of B when unlocked. B tokens could be locked up to double their vote weight. As
|
||||||
|
/// long as A's and B's supplies are below 2^63, there could be no overflow.
|
||||||
|
///
|
||||||
|
/// Note that in this example, you need 1000 native B tokens before receiving 1
|
||||||
|
/// unit of vote weight. If the supplies were significantly lower, you could use
|
||||||
|
/// * A with digit_shift=3, deposit_scaled_factor=2e9, lockup_scaled_factor=0
|
||||||
|
/// * B with digit_shift=0, deposit_scaled_factor=1e9, lockup_scaled_factor=1e9
|
||||||
|
/// to not lose precision on B tokens.
|
||||||
|
///
|
||||||
pub fn configure_voting_mint(
|
pub fn configure_voting_mint(
|
||||||
ctx: Context<ConfigureVotingMint>,
|
ctx: Context<ConfigureVotingMint>,
|
||||||
idx: u16,
|
idx: u16,
|
||||||
rate: u64,
|
digit_shift: i8,
|
||||||
decimals: u8,
|
deposit_scaled_factor: u64,
|
||||||
|
lockup_scaled_factor: u64,
|
||||||
|
lockup_saturation_secs: u64,
|
||||||
grant_authority: Option<Pubkey>,
|
grant_authority: Option<Pubkey>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
require!(rate > 0, InvalidRate);
|
require!(lockup_saturation_secs > 0, LockupSaturationMustBePositive);
|
||||||
let registrar = &mut ctx.accounts.registrar;
|
let registrar = &mut ctx.accounts.registrar;
|
||||||
require!(
|
require!(
|
||||||
(idx as usize) < registrar.voting_mints.len(),
|
(idx as usize) < registrar.voting_mints.len(),
|
||||||
OutOfBoundsVotingMintConfigIndex
|
OutOfBoundsVotingMintConfigIndex
|
||||||
);
|
);
|
||||||
require!(
|
require!(
|
||||||
registrar.voting_mints[idx as usize].rate == 0,
|
!registrar.voting_mints[idx as usize].in_use(),
|
||||||
VotingMintConfigIndexAlreadyInUse
|
VotingMintConfigIndexAlreadyInUse
|
||||||
);
|
);
|
||||||
registrar.voting_mints[idx as usize] = registrar.new_voting_mint_config(
|
registrar.voting_mints[idx as usize] = VotingMintConfig {
|
||||||
ctx.accounts.mint.key(),
|
mint: ctx.accounts.mint.key(),
|
||||||
decimals,
|
digit_shift,
|
||||||
rate,
|
deposit_scaled_factor,
|
||||||
grant_authority,
|
lockup_scaled_factor,
|
||||||
)?;
|
lockup_saturation_secs,
|
||||||
|
grant_authority: grant_authority.unwrap_or(Pubkey::new_from_array([0; 32])),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for overflow in vote weight
|
||||||
|
registrar.max_vote_weight(ctx.remaining_accounts)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use spl_governance::state::realm;
|
||||||
use std::mem::size_of;
|
use std::mem::size_of;
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
#[instruction(vote_weight_decimals: u8, registrar_bump: u8)]
|
#[instruction(registrar_bump: u8)]
|
||||||
pub struct CreateRegistrar<'info> {
|
pub struct CreateRegistrar<'info> {
|
||||||
/// The voting registrar. There can only be a single registrar
|
/// The voting registrar. There can only be a single registrar
|
||||||
/// per governance realm and governing mint.
|
/// per governance realm and governing mint.
|
||||||
|
@ -51,11 +51,7 @@ pub struct CreateRegistrar<'info> {
|
||||||
///
|
///
|
||||||
/// To use the registrar, call ConfigVotingMint to register token mints that may be
|
/// To use the registrar, call ConfigVotingMint to register token mints that may be
|
||||||
/// used for voting.
|
/// used for voting.
|
||||||
pub fn create_registrar(
|
pub fn create_registrar(ctx: Context<CreateRegistrar>, registrar_bump: u8) -> Result<()> {
|
||||||
ctx: Context<CreateRegistrar>,
|
|
||||||
vote_weight_decimals: u8,
|
|
||||||
registrar_bump: u8,
|
|
||||||
) -> Result<()> {
|
|
||||||
let registrar = &mut ctx.accounts.registrar;
|
let registrar = &mut ctx.accounts.registrar;
|
||||||
registrar.bump = registrar_bump;
|
registrar.bump = registrar_bump;
|
||||||
registrar.governance_program_id = ctx.accounts.governance_program_id.key();
|
registrar.governance_program_id = ctx.accounts.governance_program_id.key();
|
||||||
|
@ -63,7 +59,6 @@ pub fn create_registrar(
|
||||||
registrar.realm_governing_token_mint = ctx.accounts.realm_governing_token_mint.key();
|
registrar.realm_governing_token_mint = ctx.accounts.realm_governing_token_mint.key();
|
||||||
registrar.realm_authority = ctx.accounts.realm_authority.key();
|
registrar.realm_authority = ctx.accounts.realm_authority.key();
|
||||||
registrar.clawback_authority = ctx.accounts.clawback_authority.key();
|
registrar.clawback_authority = ctx.accounts.clawback_authority.key();
|
||||||
registrar.vote_weight_decimals = vote_weight_decimals;
|
|
||||||
registrar.time_offset = 0;
|
registrar.time_offset = 0;
|
||||||
|
|
||||||
// Verify that "realm_authority" is the expected authority on "realm"
|
// Verify that "realm_authority" is the expected authority on "realm"
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use anchor_spl::token::Mint;
|
|
||||||
|
|
||||||
// Remaining accounts should all the token mints that have registered
|
// Remaining accounts should all the token mints that have registered
|
||||||
// exchange rates.
|
// exchange rates.
|
||||||
|
@ -21,24 +20,7 @@ pub struct UpdateMaxVoteWeight<'info> {
|
||||||
/// defined by the registrar's `rate_decimal` field.
|
/// defined by the registrar's `rate_decimal` field.
|
||||||
pub fn update_max_vote_weight<'info>(ctx: Context<UpdateMaxVoteWeight>) -> Result<()> {
|
pub fn update_max_vote_weight<'info>(ctx: Context<UpdateMaxVoteWeight>) -> Result<()> {
|
||||||
let registrar = &ctx.accounts.registrar;
|
let registrar = &ctx.accounts.registrar;
|
||||||
let _max_vote_weight = {
|
let _max_vote_weight = registrar.max_vote_weight(ctx.remaining_accounts)?;
|
||||||
let total: Result<u64> = ctx
|
|
||||||
.remaining_accounts
|
|
||||||
.iter()
|
|
||||||
.map(|acc| Account::<Mint>::try_from(acc))
|
|
||||||
.collect::<std::result::Result<Vec<Account<Mint>>, ProgramError>>()?
|
|
||||||
.iter()
|
|
||||||
.try_fold(0u64, |sum, m| {
|
|
||||||
let mint_idx = registrar.voting_mint_config_index(m.key())?;
|
|
||||||
let mint_config = registrar.voting_mints[mint_idx];
|
|
||||||
let amount = mint_config.convert(m.supply);
|
|
||||||
let total = sum.checked_add(amount).unwrap();
|
|
||||||
Ok(total)
|
|
||||||
});
|
|
||||||
total?
|
|
||||||
.checked_mul(FIXED_VOTE_WEIGHT_FACTOR + LOCKING_VOTE_WEIGHT_FACTOR)
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
// TODO: SPL governance has not yet implemented this feature.
|
// TODO: SPL governance has not yet implemented this feature.
|
||||||
// When it has, probably need to write the result into an account,
|
// When it has, probably need to write the result into an account,
|
||||||
// similar to VoterWeightRecord.
|
// similar to VoterWeightRecord.
|
||||||
|
|
|
@ -58,22 +58,28 @@ declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
||||||
pub mod voter_stake_registry {
|
pub mod voter_stake_registry {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn create_registrar(
|
pub fn create_registrar(ctx: Context<CreateRegistrar>, registrar_bump: u8) -> Result<()> {
|
||||||
ctx: Context<CreateRegistrar>,
|
instructions::create_registrar(ctx, registrar_bump)
|
||||||
vote_weight_decimals: u8,
|
|
||||||
registrar_bump: u8,
|
|
||||||
) -> Result<()> {
|
|
||||||
instructions::create_registrar(ctx, vote_weight_decimals, registrar_bump)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn configure_voting_mint(
|
pub fn configure_voting_mint(
|
||||||
ctx: Context<ConfigureVotingMint>,
|
ctx: Context<ConfigureVotingMint>,
|
||||||
idx: u16,
|
idx: u16,
|
||||||
rate: u64,
|
digit_shift: i8,
|
||||||
decimals: u8,
|
deposit_scaled_factor: u64,
|
||||||
|
lockup_scaled_factor: u64,
|
||||||
|
lockup_saturation_secs: u64,
|
||||||
grant_authority: Option<Pubkey>,
|
grant_authority: Option<Pubkey>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
instructions::configure_voting_mint(ctx, idx, rate, decimals, grant_authority)
|
instructions::configure_voting_mint(
|
||||||
|
ctx,
|
||||||
|
idx,
|
||||||
|
digit_shift,
|
||||||
|
deposit_scaled_factor,
|
||||||
|
lockup_scaled_factor,
|
||||||
|
lockup_saturation_secs,
|
||||||
|
grant_authority,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_voter(
|
pub fn create_voter(
|
||||||
|
|
|
@ -2,15 +2,9 @@ use crate::error::*;
|
||||||
use crate::state::lockup::{Lockup, LockupKind};
|
use crate::state::lockup::{Lockup, LockupKind};
|
||||||
use crate::state::voting_mint_config::VotingMintConfig;
|
use crate::state::voting_mint_config::VotingMintConfig;
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
|
use std::cmp::min;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
/// 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;
|
|
||||||
|
|
||||||
pub const MAX_SECS_LOCKED: u64 = 7 * 365 * 24 * 60 * 60;
|
|
||||||
|
|
||||||
/// Bookkeeping for a single deposit for a given mint and lockup schedule.
|
/// Bookkeeping for a single deposit for a given mint and lockup schedule.
|
||||||
#[zero_copy]
|
#[zero_copy]
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -53,13 +47,14 @@ impl DepositEntry {
|
||||||
/// For each cliff-locked token, the vote weight is:
|
/// For each cliff-locked token, the vote weight is:
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// voting_power = amount * (fixed_factor + locking_factor * time_factor)
|
/// voting_power = deposit_vote_weight
|
||||||
|
/// + lockup_duration_factor * max_lockup_vote_weight
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// with
|
/// with
|
||||||
/// fixed_factor = FIXED_VOTE_WEIGHT_FACTOR
|
/// deposit_vote_weight and max_lockup_vote_weight from the
|
||||||
/// locking_factor = LOCKING_VOTE_WEIGHT_FACTOR
|
/// VotingMintConfig
|
||||||
/// time_factor = lockup_time_remaining / max_lockup_time
|
/// lockup_duration_factor = lockup_time_remaining / max_lockup_time
|
||||||
///
|
///
|
||||||
/// Linear vesting schedules can be thought of as a sequence of cliff-
|
/// Linear vesting schedules can be thought of as a sequence of cliff-
|
||||||
/// locked tokens and have the matching voting weight.
|
/// locked tokens and have the matching voting weight.
|
||||||
|
@ -90,59 +85,71 @@ impl DepositEntry {
|
||||||
/// voting_power_linear_vesting() below.
|
/// voting_power_linear_vesting() below.
|
||||||
///
|
///
|
||||||
pub fn voting_power(&self, voting_mint_config: &VotingMintConfig, curr_ts: i64) -> Result<u64> {
|
pub fn voting_power(&self, voting_mint_config: &VotingMintConfig, curr_ts: i64) -> Result<u64> {
|
||||||
let fixed_contribution = voting_mint_config
|
let deposit_vote_weight =
|
||||||
.convert(self.amount_deposited_native)
|
voting_mint_config.deposit_vote_weight(self.amount_deposited_native)?;
|
||||||
.checked_mul(FIXED_VOTE_WEIGHT_FACTOR)
|
let max_locked_vote_weight =
|
||||||
.unwrap();
|
voting_mint_config.max_lockup_vote_weight(self.amount_initially_locked_native)?;
|
||||||
if LOCKING_VOTE_WEIGHT_FACTOR == 0 {
|
deposit_vote_weight
|
||||||
return Ok(fixed_contribution);
|
.checked_add(self.voting_power_locked(
|
||||||
}
|
curr_ts,
|
||||||
|
max_locked_vote_weight,
|
||||||
let max_locked_contribution =
|
voting_mint_config.lockup_saturation_secs,
|
||||||
voting_mint_config.convert(self.amount_initially_locked_native);
|
)?)
|
||||||
Ok(fixed_contribution
|
.ok_or(Error::ErrorCode(ErrorCode::VoterWeightOverflow))
|
||||||
+ self
|
|
||||||
.voting_power_locked(curr_ts, max_locked_contribution)?
|
|
||||||
.checked_mul(LOCKING_VOTE_WEIGHT_FACTOR)
|
|
||||||
.unwrap())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vote power contribution from locked funds only.
|
/// Vote power contribution from locked funds only.
|
||||||
/// Not scaled by LOCKING_VOTE_WEIGHT_FACTOR yet.
|
pub fn voting_power_locked(
|
||||||
pub fn voting_power_locked(&self, curr_ts: i64, max_locked_contribution: u64) -> Result<u64> {
|
&self,
|
||||||
if curr_ts >= self.lockup.end_ts {
|
curr_ts: i64,
|
||||||
|
max_locked_vote_weight: u64,
|
||||||
|
lockup_saturation_secs: u64,
|
||||||
|
) -> Result<u64> {
|
||||||
|
if curr_ts >= self.lockup.end_ts || max_locked_vote_weight == 0 {
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
match self.lockup.kind {
|
match self.lockup.kind {
|
||||||
LockupKind::None => Ok(0),
|
LockupKind::None => Ok(0),
|
||||||
LockupKind::Daily => self.voting_power_linear_vesting(curr_ts, max_locked_contribution),
|
LockupKind::Daily => self.voting_power_linear_vesting(
|
||||||
LockupKind::Monthly => {
|
curr_ts,
|
||||||
self.voting_power_linear_vesting(curr_ts, max_locked_contribution)
|
max_locked_vote_weight,
|
||||||
|
lockup_saturation_secs,
|
||||||
|
),
|
||||||
|
LockupKind::Monthly => self.voting_power_linear_vesting(
|
||||||
|
curr_ts,
|
||||||
|
max_locked_vote_weight,
|
||||||
|
lockup_saturation_secs,
|
||||||
|
),
|
||||||
|
LockupKind::Cliff => {
|
||||||
|
self.voting_power_cliff(curr_ts, max_locked_vote_weight, lockup_saturation_secs)
|
||||||
}
|
}
|
||||||
LockupKind::Cliff => self.voting_power_cliff(curr_ts, max_locked_contribution),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vote power contribution from funds with linear vesting.
|
/// Vote power contribution from funds with linear vesting.
|
||||||
/// Not scaled by LOCKING_VOTE_WEIGHT_FACTOR yet.
|
fn voting_power_cliff(
|
||||||
fn voting_power_cliff(&self, curr_ts: i64, max_locked_contribution: u64) -> Result<u64> {
|
&self,
|
||||||
let remaining = self.lockup.seconds_left(curr_ts);
|
curr_ts: i64,
|
||||||
|
max_locked_vote_weight: u64,
|
||||||
|
lockup_saturation_secs: u64,
|
||||||
|
) -> Result<u64> {
|
||||||
|
let remaining = min(self.lockup.seconds_left(curr_ts), lockup_saturation_secs);
|
||||||
Ok(u64::try_from(
|
Ok(u64::try_from(
|
||||||
(max_locked_contribution as u128)
|
(max_locked_vote_weight as u128)
|
||||||
.checked_mul(remaining as u128)
|
.checked_mul(remaining as u128)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.checked_div(MAX_SECS_LOCKED as u128)
|
.checked_div(lockup_saturation_secs as u128)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vote power contribution from cliff-locked funds.
|
/// Vote power contribution from cliff-locked funds.
|
||||||
/// Not scaled by LOCKING_VOTE_WEIGHT_FACTOR yet.
|
|
||||||
fn voting_power_linear_vesting(
|
fn voting_power_linear_vesting(
|
||||||
&self,
|
&self,
|
||||||
curr_ts: i64,
|
curr_ts: i64,
|
||||||
max_locked_contribution: u64,
|
max_locked_vote_weight: u64,
|
||||||
|
lockup_saturation_secs: u64,
|
||||||
) -> Result<u64> {
|
) -> Result<u64> {
|
||||||
let periods_left = self.lockup.periods_left(curr_ts)?;
|
let periods_left = self.lockup.periods_left(curr_ts)?;
|
||||||
let periods_total = self.lockup.periods_total()?;
|
let periods_total = self.lockup.periods_total()?;
|
||||||
|
@ -157,32 +164,54 @@ impl DepositEntry {
|
||||||
//
|
//
|
||||||
// For example, if there were 5 vesting periods, with 3 of them left
|
// For example, if there were 5 vesting periods, with 3 of them left
|
||||||
// (i.e. two have already vested and their tokens are no longer locked)
|
// (i.e. two have already vested and their tokens are no longer locked)
|
||||||
// we'd have (max_contribution / 5) weight in each of them, and the
|
// we'd have (max_locked_vote_weight / 5) weight in each of them, and the
|
||||||
// voting weight would be:
|
// voting power would be:
|
||||||
// (max_locked_contribution/5) * secs_left_for_cliff_1 / MAX_SECS_LOCKED
|
// (max_locked_vote_weight/5) * secs_left_for_cliff_1 / lockup_saturation_secs
|
||||||
// + (max_locked_contribution/5) * secs_left_for_cliff_2 / MAX_SECS_LOCKED
|
// + (max_locked_vote_weight/5) * secs_left_for_cliff_2 / lockup_saturation_secs
|
||||||
// + (max_locked_contribution/5) * secs_left_for_cliff_3 / MAX_SECS_LOCKED
|
// + (max_locked_vote_weight/5) * secs_left_for_cliff_3 / lockup_saturation_secs
|
||||||
//
|
//
|
||||||
// Or more simply:
|
// Or more simply:
|
||||||
// max_locked_contribution * (\sum_p secs_left_for_cliff_p) / (5 * MAX_SECS_LOCKED)
|
// max_locked_vote_weight * (\sum_p secs_left_for_cliff_p) / (5 * lockup_saturation_secs)
|
||||||
// = max_locked_contribution * lockup_secs / denominator
|
// = max_locked_vote_weight * lockup_secs / denominator
|
||||||
//
|
//
|
||||||
// The value secs_left_for_cliff_p splits up as
|
// The value secs_left_for_cliff_p splits up as
|
||||||
// secs_left_for_cliff_p = secs_to_closest_cliff + (p-1) * period_secs
|
// secs_left_for_cliff_p = min(
|
||||||
|
// secs_to_closest_cliff + (p-1) * period_secs,
|
||||||
|
// lockup_saturation_secs)
|
||||||
//
|
//
|
||||||
// So
|
// We can split the sum into the part before saturation and the part after:
|
||||||
|
// Let q be the largest integer <= periods_left where
|
||||||
|
// secs_to_closest_cliff + (q-1) * period_secs < lockup_saturation_secs
|
||||||
|
// => q < (lockup_saturation_secs + period_secs - secs_to_closest_cliff) / period_secs
|
||||||
|
// and r be the integer where q + r = periods_left, then:
|
||||||
// lockup_secs := \sum_p secs_left_for_cliff_p
|
// lockup_secs := \sum_p secs_left_for_cliff_p
|
||||||
// = periods_left * secs_to_closest_cliff
|
// = \sum_{p<=q} secs_left_for_cliff_p
|
||||||
// + period_secs * \sum_0^periods_left (p-1)
|
// + r * lockup_saturation_secs
|
||||||
|
// = q * secs_to_closest_cliff
|
||||||
|
// + period_secs * \sum_0^q (p-1)
|
||||||
|
// + r * lockup_saturation_secs
|
||||||
//
|
//
|
||||||
// Where the sum of full periods has a formula:
|
// Where the sum can be expanded to:
|
||||||
//
|
//
|
||||||
// sum_full_periods := \sum_0^periods_left (p-1)
|
// sum_full_periods := \sum_0^q (p-1)
|
||||||
// = periods_left * (periods_left - 1) / 2
|
// = q * (q - 1) / 2
|
||||||
//
|
//
|
||||||
|
|
||||||
// In the example above, periods_total was 5.
|
// In the example above, periods_total was 5.
|
||||||
let denominator = periods_total * MAX_SECS_LOCKED;
|
let denominator = periods_total * lockup_saturation_secs;
|
||||||
|
|
||||||
|
let secs_to_closest_cliff =
|
||||||
|
u64::try_from(self.lockup.end_ts - (period_secs * (periods_left - 1)) as i64 - curr_ts)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let lockup_saturation_periods =
|
||||||
|
(lockup_saturation_secs + period_secs - secs_to_closest_cliff) / period_secs;
|
||||||
|
let q = min(lockup_saturation_periods, periods_left);
|
||||||
|
let r = if q < periods_left {
|
||||||
|
periods_left - q
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
// Sum of the full periods left for all remaining vesting cliffs.
|
// Sum of the full periods left for all remaining vesting cliffs.
|
||||||
//
|
//
|
||||||
|
@ -193,17 +222,14 @@ impl DepositEntry {
|
||||||
// and the next has two full periods left
|
// and the next has two full periods left
|
||||||
// so sums to 3 = 3 * 2 / 2
|
// so sums to 3 = 3 * 2 / 2
|
||||||
// - if there's only one period left, the sum is 0
|
// - if there's only one period left, the sum is 0
|
||||||
let sum_full_periods = periods_left * (periods_left - 1) / 2;
|
let sum_full_periods = q * (q - 1) / 2;
|
||||||
|
|
||||||
let secs_to_closest_cliff =
|
|
||||||
u64::try_from(self.lockup.end_ts - (period_secs * (periods_left - 1)) as i64 - curr_ts)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Total number of seconds left over all periods_left remaining vesting cliffs
|
// Total number of seconds left over all periods_left remaining vesting cliffs
|
||||||
let lockup_secs = periods_left * secs_to_closest_cliff + sum_full_periods * period_secs;
|
let lockup_secs =
|
||||||
|
q * secs_to_closest_cliff + sum_full_periods * period_secs + r * lockup_saturation_secs;
|
||||||
|
|
||||||
Ok(u64::try_from(
|
Ok(u64::try_from(
|
||||||
(max_locked_contribution as u128)
|
(max_locked_vote_weight as u128)
|
||||||
.checked_mul(lockup_secs as u128)
|
.checked_mul(lockup_secs as u128)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.checked_div(denominator as u128)
|
.checked_div(denominator as u128)
|
||||||
|
|
|
@ -20,14 +20,6 @@ pub const SECS_PER_MONTH: i64 = 10;
|
||||||
#[cfg(not(feature = "localnet"))]
|
#[cfg(not(feature = "localnet"))]
|
||||||
pub const SECS_PER_MONTH: i64 = 365 * SECS_PER_DAY / 12;
|
pub const SECS_PER_MONTH: i64 = 365 * SECS_PER_DAY / 12;
|
||||||
|
|
||||||
/// Maximum number of days one can lock for.
|
|
||||||
/// TODO: Must match up with MAX_SECS_LOCKED etc
|
|
||||||
pub const MAX_DAYS_LOCKED: u64 = 7 * 365;
|
|
||||||
|
|
||||||
/// Maximum number of months one can lock for.
|
|
||||||
/// TODO: Must match up with MAX_SECS_LOCKED etc
|
|
||||||
pub const MAX_MONTHS_LOCKED: u64 = 7 * 12;
|
|
||||||
|
|
||||||
#[zero_copy]
|
#[zero_copy]
|
||||||
#[derive(AnchorSerialize, AnchorDeserialize)]
|
#[derive(AnchorSerialize, AnchorDeserialize)]
|
||||||
pub struct Lockup {
|
pub struct Lockup {
|
||||||
|
@ -61,7 +53,6 @@ impl Default for Lockup {
|
||||||
impl Lockup {
|
impl Lockup {
|
||||||
/// Create lockup for a given period
|
/// Create lockup for a given period
|
||||||
pub fn new_from_periods(kind: LockupKind, start_ts: i64, periods: u32) -> Result<Self> {
|
pub fn new_from_periods(kind: LockupKind, start_ts: i64, periods: u32) -> Result<Self> {
|
||||||
require!(periods as u64 <= kind.max_periods(), InvalidDays);
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
kind,
|
kind,
|
||||||
start_ts,
|
start_ts,
|
||||||
|
@ -130,6 +121,10 @@ pub enum LockupKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LockupKind {
|
impl LockupKind {
|
||||||
|
/// The lockup length is specified by passing the number of lockup periods
|
||||||
|
/// to create_deposit_entry. This describes a period's length.
|
||||||
|
///
|
||||||
|
/// For vesting lockups, the period length is also the vesting period.
|
||||||
pub fn period_secs(&self) -> i64 {
|
pub fn period_secs(&self) -> i64 {
|
||||||
match self {
|
match self {
|
||||||
LockupKind::None => 0,
|
LockupKind::None => 0,
|
||||||
|
@ -138,15 +133,6 @@ impl LockupKind {
|
||||||
LockupKind::Cliff => SECS_PER_DAY, // arbitrary choice
|
LockupKind::Cliff => SECS_PER_DAY, // arbitrary choice
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn max_periods(&self) -> u64 {
|
|
||||||
match self {
|
|
||||||
LockupKind::None => 0,
|
|
||||||
LockupKind::Daily => MAX_DAYS_LOCKED,
|
|
||||||
LockupKind::Monthly => MAX_MONTHS_LOCKED,
|
|
||||||
LockupKind::Cliff => MAX_DAYS_LOCKED,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -154,6 +140,10 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::state::deposit_entry::DepositEntry;
|
use crate::state::deposit_entry::DepositEntry;
|
||||||
|
|
||||||
|
// intentionally not a multiple of a day
|
||||||
|
const MAX_SECS_LOCKED: u64 = 365 * 24 * 60 * 60 + 7 * 60 * 60;
|
||||||
|
const MAX_DAYS_LOCKED: f64 = MAX_SECS_LOCKED as f64 / (24.0 * 60.0 * 60.0);
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn days_left_start() -> Result<()> {
|
pub fn days_left_start() -> Result<()> {
|
||||||
run_test_days_left(TestDaysLeft {
|
run_test_days_left(TestDaysLeft {
|
||||||
|
@ -297,7 +287,7 @@ mod tests {
|
||||||
pub fn voting_power_cliff_start() -> Result<()> {
|
pub fn voting_power_cliff_start() -> Result<()> {
|
||||||
// 10 tokens with 6 decimals.
|
// 10 tokens with 6 decimals.
|
||||||
let amount_deposited = 10 * 1_000_000;
|
let amount_deposited = 10 * 1_000_000;
|
||||||
let expected_voting_power = (10 * amount_deposited) / MAX_DAYS_LOCKED;
|
let expected_voting_power = locked_cliff_power(amount_deposited, 10.0);
|
||||||
run_test_voting_power(TestVotingPower {
|
run_test_voting_power(TestVotingPower {
|
||||||
expected_voting_power,
|
expected_voting_power,
|
||||||
amount_deposited,
|
amount_deposited,
|
||||||
|
@ -353,7 +343,7 @@ mod tests {
|
||||||
pub fn voting_power_cliff_one_day() -> Result<()> {
|
pub fn voting_power_cliff_one_day() -> Result<()> {
|
||||||
// 10 tokens with 6 decimals.
|
// 10 tokens with 6 decimals.
|
||||||
let amount_deposited = 10 * 1_000_000;
|
let amount_deposited = 10 * 1_000_000;
|
||||||
let expected_voting_power = (9 * amount_deposited) / MAX_DAYS_LOCKED;
|
let expected_voting_power = locked_cliff_power(amount_deposited, 9.0);
|
||||||
run_test_voting_power(TestVotingPower {
|
run_test_voting_power(TestVotingPower {
|
||||||
expected_voting_power,
|
expected_voting_power,
|
||||||
amount_deposited,
|
amount_deposited,
|
||||||
|
@ -382,7 +372,7 @@ mod tests {
|
||||||
// 10 tokens with 6 decimals.
|
// 10 tokens with 6 decimals.
|
||||||
let amount_deposited = 10 * 1_000_000;
|
let amount_deposited = 10 * 1_000_000;
|
||||||
// (8/2555) * deposit w/ 6 decimals.
|
// (8/2555) * deposit w/ 6 decimals.
|
||||||
let expected_voting_power = (8 * amount_deposited) / MAX_DAYS_LOCKED;
|
let expected_voting_power = locked_cliff_power(amount_deposited, 8.0);
|
||||||
run_test_voting_power(TestVotingPower {
|
run_test_voting_power(TestVotingPower {
|
||||||
expected_voting_power,
|
expected_voting_power,
|
||||||
amount_deposited,
|
amount_deposited,
|
||||||
|
@ -591,6 +581,48 @@ mod tests {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn voting_power_daily_saturation() -> Result<()> {
|
||||||
|
let days = MAX_DAYS_LOCKED.floor() as u64;
|
||||||
|
let amount_deposited = days * 1_000_000;
|
||||||
|
let expected_voting_power = locked_daily_power(amount_deposited, 0.0, days);
|
||||||
|
run_test_voting_power(TestVotingPower {
|
||||||
|
expected_voting_power,
|
||||||
|
amount_deposited,
|
||||||
|
days_total: MAX_DAYS_LOCKED.floor(),
|
||||||
|
curr_day: 0.0,
|
||||||
|
kind: LockupKind::Daily,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn voting_power_daily_above_saturation1() -> Result<()> {
|
||||||
|
let days = (MAX_DAYS_LOCKED + 10.0).floor() as u64;
|
||||||
|
let amount_deposited = days * 1_000_000;
|
||||||
|
let expected_voting_power = locked_daily_power(amount_deposited, 0.0, days);
|
||||||
|
run_test_voting_power(TestVotingPower {
|
||||||
|
expected_voting_power,
|
||||||
|
amount_deposited,
|
||||||
|
days_total: (MAX_DAYS_LOCKED + 10.0).floor(),
|
||||||
|
curr_day: 0.0,
|
||||||
|
kind: LockupKind::Daily,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn voting_power_daily_above_saturation2() -> Result<()> {
|
||||||
|
let days = (MAX_DAYS_LOCKED + 10.0).floor() as u64;
|
||||||
|
let amount_deposited = days * 1_000_000;
|
||||||
|
let expected_voting_power = locked_daily_power(amount_deposited, 0.5, days);
|
||||||
|
run_test_voting_power(TestVotingPower {
|
||||||
|
expected_voting_power,
|
||||||
|
amount_deposited,
|
||||||
|
days_total: (MAX_DAYS_LOCKED + 10.0).floor(),
|
||||||
|
curr_day: 0.5,
|
||||||
|
kind: LockupKind::Daily,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
struct TestDaysLeft {
|
struct TestDaysLeft {
|
||||||
expected_days_left: u64,
|
expected_days_left: u64,
|
||||||
days_total: f64,
|
days_total: f64,
|
||||||
|
@ -658,7 +690,7 @@ mod tests {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let curr_ts = start_ts + days_to_secs(t.curr_day);
|
let curr_ts = start_ts + days_to_secs(t.curr_day);
|
||||||
let power = d.voting_power_locked(curr_ts, t.amount_deposited)?;
|
let power = d.voting_power_locked(curr_ts, t.amount_deposited, MAX_SECS_LOCKED)?;
|
||||||
assert_eq!(power, t.expected_voting_power);
|
assert_eq!(power, t.expected_voting_power);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -691,11 +723,18 @@ mod tests {
|
||||||
let remaining_days = total_days as f64 - day - k as f64;
|
let remaining_days = total_days as f64 - day - k as f64;
|
||||||
total += locked_cliff_power_float(amount / total_days, remaining_days);
|
total += locked_cliff_power_float(amount / total_days, remaining_days);
|
||||||
}
|
}
|
||||||
total.floor() as u64
|
// the test code uses floats to compute the voting power; avoid
|
||||||
|
// getting incurrect expected results due to floating point rounding
|
||||||
|
(total + 0.0001).floor() as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
fn locked_cliff_power_float(amount: u64, remaining_days: f64) -> f64 {
|
fn locked_cliff_power_float(amount: u64, remaining_days: f64) -> f64 {
|
||||||
(amount as f64) * remaining_days / (MAX_DAYS_LOCKED as f64)
|
let relevant_days = if remaining_days < MAX_DAYS_LOCKED as f64 {
|
||||||
|
remaining_days
|
||||||
|
} else {
|
||||||
|
MAX_DAYS_LOCKED as f64
|
||||||
|
};
|
||||||
|
(amount as f64) * relevant_days / (MAX_DAYS_LOCKED as f64)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn locked_cliff_power(amount: u64, remaining_days: f64) -> u64 {
|
fn locked_cliff_power(amount: u64, remaining_days: f64) -> u64 {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::state::voting_mint_config::VotingMintConfig;
|
use crate::state::voting_mint_config::VotingMintConfig;
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
|
use anchor_spl::token::Mint;
|
||||||
|
|
||||||
/// Instance of a voting rights distributor.
|
/// Instance of a voting rights distributor.
|
||||||
#[account]
|
#[account]
|
||||||
|
@ -15,38 +16,11 @@ pub struct Registrar {
|
||||||
// The length should be adjusted for one's use case.
|
// The length should be adjusted for one's use case.
|
||||||
pub voting_mints: [VotingMintConfig; 2],
|
pub voting_mints: [VotingMintConfig; 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.
|
/// Debug only: time offset, to allow tests to move forward in time.
|
||||||
pub time_offset: i64,
|
pub time_offset: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Registrar {
|
impl Registrar {
|
||||||
pub fn new_voting_mint_config(
|
|
||||||
&self,
|
|
||||||
mint: Pubkey,
|
|
||||||
mint_decimals: u8,
|
|
||||||
rate: u64,
|
|
||||||
grant_authority: Option<Pubkey>,
|
|
||||||
) -> Result<VotingMintConfig> {
|
|
||||||
require!(self.vote_weight_decimals >= mint_decimals, InvalidDecimals);
|
|
||||||
let decimal_diff = self
|
|
||||||
.vote_weight_decimals
|
|
||||||
.checked_sub(mint_decimals)
|
|
||||||
.unwrap();
|
|
||||||
Ok(VotingMintConfig {
|
|
||||||
mint,
|
|
||||||
rate,
|
|
||||||
mint_decimals,
|
|
||||||
conversion_factor: rate.checked_mul(10u64.pow(decimal_diff.into())).unwrap(),
|
|
||||||
grant_authority: grant_authority.unwrap_or(Pubkey::new_from_array([0; 32])),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clock_unix_timestamp(&self) -> i64 {
|
pub fn clock_unix_timestamp(&self) -> i64 {
|
||||||
Clock::get().unwrap().unix_timestamp + self.time_offset
|
Clock::get().unwrap().unix_timestamp + self.time_offset
|
||||||
}
|
}
|
||||||
|
@ -57,6 +31,28 @@ impl Registrar {
|
||||||
.position(|r| r.mint == mint)
|
.position(|r| r.mint == mint)
|
||||||
.ok_or(Error::ErrorCode(ErrorCode::VotingMintNotFound))
|
.ok_or(Error::ErrorCode(ErrorCode::VotingMintNotFound))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn max_vote_weight(&self, mint_accounts: &[AccountInfo]) -> Result<u64> {
|
||||||
|
self.voting_mints
|
||||||
|
.iter()
|
||||||
|
.try_fold(0u64, |mut sum, voting_mint_config| -> Result<u64> {
|
||||||
|
if !voting_mint_config.in_use() {
|
||||||
|
return Ok(sum);
|
||||||
|
}
|
||||||
|
let mint_account = mint_accounts
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.key() == voting_mint_config.mint)
|
||||||
|
.ok_or(Error::ErrorCode(ErrorCode::VotingMintNotFound))?;
|
||||||
|
let mint = Account::<Mint>::try_from(mint_account)?;
|
||||||
|
sum = sum
|
||||||
|
.checked_add(voting_mint_config.deposit_vote_weight(mint.supply)?)
|
||||||
|
.ok_or(Error::ErrorCode(ErrorCode::VoterWeightOverflow))?;
|
||||||
|
sum = sum
|
||||||
|
.checked_add(voting_mint_config.max_lockup_vote_weight(mint.supply)?)
|
||||||
|
.ok_or(Error::ErrorCode(ErrorCode::VoterWeightOverflow))?;
|
||||||
|
Ok(sum)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
|
use crate::error::*;
|
||||||
use anchor_lang::__private::bytemuck::Zeroable;
|
use anchor_lang::__private::bytemuck::Zeroable;
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
const SCALED_FACTOR_BASE: u64 = 1_000_000_000;
|
||||||
|
|
||||||
/// Exchange rate for an asset that can be used to mint voting rights.
|
/// Exchange rate for an asset that can be used to mint voting rights.
|
||||||
|
///
|
||||||
|
/// See documentation of configure_voting_mint for details on how
|
||||||
|
/// native token amounts convert to vote weight.
|
||||||
#[zero_copy]
|
#[zero_copy]
|
||||||
#[derive(AnchorSerialize, AnchorDeserialize, Default)]
|
#[derive(AnchorSerialize, AnchorDeserialize, Default)]
|
||||||
pub struct VotingMintConfig {
|
pub struct VotingMintConfig {
|
||||||
/// Mint for this entry.
|
/// Mint for this entry.
|
||||||
pub mint: Pubkey,
|
pub mint: Pubkey,
|
||||||
|
|
||||||
/// Mint decimals.
|
/// Number of digits to shift native amounts, applying a 10^digit_shift factor.
|
||||||
pub mint_decimals: u8,
|
pub digit_shift: i8,
|
||||||
|
|
||||||
/// Exchange rate for 1.0 decimal-respecting unit of mint currency
|
/// Vote weight factor for deposits, in 1/SCALED_FACTOR_BASE units.
|
||||||
/// into the common vote currency.
|
pub deposit_scaled_factor: u64,
|
||||||
///
|
|
||||||
/// 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,
|
/// Maximum vote weight factor for lockups, in 1/SCALED_FACTOR_BASE units.
|
||||||
/// including decimal handling.
|
pub lockup_scaled_factor: u64,
|
||||||
///
|
|
||||||
/// Examples:
|
/// Number of seconds of lockup needed to reach the maximum lockup bonus.
|
||||||
/// - if common and mint have the same number of decimals, this is the same as 'rate'
|
pub lockup_saturation_secs: u64,
|
||||||
/// - common decimals = 6, mint decimals = 3, rate = 5 -> 5000.
|
|
||||||
pub conversion_factor: u64,
|
|
||||||
|
|
||||||
/// The authority that is allowed to push grants into voters
|
/// The authority that is allowed to push grants into voters
|
||||||
pub grant_authority: Pubkey,
|
pub grant_authority: Pubkey,
|
||||||
|
@ -33,9 +33,53 @@ pub struct VotingMintConfig {
|
||||||
|
|
||||||
impl VotingMintConfig {
|
impl VotingMintConfig {
|
||||||
/// Converts an amount in this voting mints's native currency
|
/// Converts an amount in this voting mints's native currency
|
||||||
/// to the equivalent common registrar vote currency amount.
|
/// to the base vote weight (without the deposit or lockup scalings)
|
||||||
pub fn convert(&self, amount_native: u64) -> u64 {
|
/// by applying the digit_shift factor.
|
||||||
amount_native.checked_mul(self.conversion_factor).unwrap()
|
pub fn base_vote_weight(&self, amount_native: u64) -> Result<u64> {
|
||||||
|
let compute = || -> Option<u64> {
|
||||||
|
let val = if self.digit_shift < 0 {
|
||||||
|
(amount_native as u128).checked_div(10u128.pow((-self.digit_shift) as u32))?
|
||||||
|
} else {
|
||||||
|
(amount_native as u128).checked_mul(10u128.pow(self.digit_shift as u32))?
|
||||||
|
};
|
||||||
|
u64::try_from(val).ok()
|
||||||
|
};
|
||||||
|
compute().ok_or(Error::ErrorCode(ErrorCode::VoterWeightOverflow))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a factor in SCALED_FACTOR_BASE units.
|
||||||
|
fn apply_factor(base_vote_weight: u64, factor: u64) -> Result<u64> {
|
||||||
|
let compute = || -> Option<u64> {
|
||||||
|
u64::try_from(
|
||||||
|
(base_vote_weight as u128)
|
||||||
|
.checked_mul(factor as u128)?
|
||||||
|
.checked_div(SCALED_FACTOR_BASE as u128)?,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
};
|
||||||
|
compute().ok_or(Error::ErrorCode(ErrorCode::VoterWeightOverflow))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The vote weight a deposit of a number of native tokens should have.
|
||||||
|
pub fn deposit_vote_weight(&self, amount_native: u64) -> Result<u64> {
|
||||||
|
Self::apply_factor(
|
||||||
|
self.base_vote_weight(amount_native)?,
|
||||||
|
self.deposit_scaled_factor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The maximum vote weight a number of locked up native tokens can have.
|
||||||
|
/// Will be multiplied with a factor between 0 and 1 for the lockup duration.
|
||||||
|
pub fn max_lockup_vote_weight(&self, amount_native: u64) -> Result<u64> {
|
||||||
|
Self::apply_factor(
|
||||||
|
self.base_vote_weight(amount_native)?,
|
||||||
|
self.lockup_scaled_factor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this voting mint is configured.
|
||||||
|
pub fn in_use(&self) -> bool {
|
||||||
|
self.lockup_saturation_secs > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,12 +52,8 @@ impl AddinCookie {
|
||||||
&self.program_id,
|
&self.program_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
let vote_weight_decimals = 6;
|
|
||||||
let data = anchor_lang::InstructionData::data(
|
let data = anchor_lang::InstructionData::data(
|
||||||
&voter_stake_registry::instruction::CreateRegistrar {
|
&voter_stake_registry::instruction::CreateRegistrar { registrar_bump },
|
||||||
vote_weight_decimals,
|
|
||||||
registrar_bump,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let accounts = anchor_lang::ToAccountMetas::to_account_metas(
|
let accounts = anchor_lang::ToAccountMetas::to_account_metas(
|
||||||
|
@ -104,7 +100,10 @@ impl AddinCookie {
|
||||||
payer: &Keypair,
|
payer: &Keypair,
|
||||||
index: u16,
|
index: u16,
|
||||||
mint: &MintCookie,
|
mint: &MintCookie,
|
||||||
rate: u64,
|
digit_shift: i8,
|
||||||
|
deposit_scaled_factor: f64,
|
||||||
|
lockup_scaled_factor: f64,
|
||||||
|
lockup_saturation_secs: u64,
|
||||||
grant_authority: Option<Pubkey>,
|
grant_authority: Option<Pubkey>,
|
||||||
) -> VotingMintConfigCookie {
|
) -> VotingMintConfigCookie {
|
||||||
let deposit_mint = mint.pubkey.unwrap();
|
let deposit_mint = mint.pubkey.unwrap();
|
||||||
|
@ -116,13 +115,15 @@ impl AddinCookie {
|
||||||
let data = anchor_lang::InstructionData::data(
|
let data = anchor_lang::InstructionData::data(
|
||||||
&voter_stake_registry::instruction::ConfigureVotingMint {
|
&voter_stake_registry::instruction::ConfigureVotingMint {
|
||||||
idx: index,
|
idx: index,
|
||||||
rate,
|
digit_shift,
|
||||||
decimals: mint.decimals,
|
deposit_scaled_factor: (deposit_scaled_factor * 1e9) as u64,
|
||||||
|
lockup_scaled_factor: (lockup_scaled_factor * 1e9) as u64,
|
||||||
|
lockup_saturation_secs,
|
||||||
grant_authority,
|
grant_authority,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let accounts = anchor_lang::ToAccountMetas::to_account_metas(
|
let mut accounts = anchor_lang::ToAccountMetas::to_account_metas(
|
||||||
&voter_stake_registry::accounts::ConfigureVotingMint {
|
&voter_stake_registry::accounts::ConfigureVotingMint {
|
||||||
vault,
|
vault,
|
||||||
mint: deposit_mint,
|
mint: deposit_mint,
|
||||||
|
@ -136,6 +137,10 @@ impl AddinCookie {
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
accounts.push(anchor_lang::prelude::AccountMeta::new_readonly(
|
||||||
|
deposit_mint,
|
||||||
|
false,
|
||||||
|
));
|
||||||
|
|
||||||
let instructions = vec![Instruction {
|
let instructions = vec![Instruction {
|
||||||
program_id: self.program_id,
|
program_id: self.program_id,
|
||||||
|
|
|
@ -41,7 +41,10 @@ async fn test_all_deposits() -> Result<(), TransportError> {
|
||||||
payer,
|
payer,
|
||||||
0,
|
0,
|
||||||
&context.mints[0],
|
&context.mints[0],
|
||||||
1,
|
0,
|
||||||
|
1.0,
|
||||||
|
0.0,
|
||||||
|
5 * 365 * 24 * 60 * 60,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -41,7 +41,10 @@ async fn test_basic() -> Result<(), TransportError> {
|
||||||
payer,
|
payer,
|
||||||
0,
|
0,
|
||||||
&context.mints[0],
|
&context.mints[0],
|
||||||
1,
|
0,
|
||||||
|
1.0,
|
||||||
|
0.0,
|
||||||
|
5 * 365 * 24 * 60 * 60,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -49,7 +49,10 @@ async fn test_clawback() -> Result<(), TransportError> {
|
||||||
realm_authority,
|
realm_authority,
|
||||||
0,
|
0,
|
||||||
community_token_mint,
|
community_token_mint,
|
||||||
1,
|
0,
|
||||||
|
1.0,
|
||||||
|
0.0,
|
||||||
|
5 * 365 * 24 * 60 * 60,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -74,7 +74,10 @@ async fn test_deposit_cliff() -> Result<(), TransportError> {
|
||||||
payer,
|
payer,
|
||||||
0,
|
0,
|
||||||
&context.mints[0],
|
&context.mints[0],
|
||||||
1,
|
0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
2 * 24 * 60 * 60,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
@ -140,14 +143,28 @@ async fn test_deposit_cliff() -> Result<(), TransportError> {
|
||||||
|
|
||||||
let after_deposit = get_balances(0).await;
|
let after_deposit = get_balances(0).await;
|
||||||
assert_eq!(initial.token, after_deposit.token + after_deposit.vault);
|
assert_eq!(initial.token, after_deposit.token + after_deposit.vault);
|
||||||
assert_eq!(after_deposit.voter_weight, after_deposit.vault);
|
assert_eq!(after_deposit.voter_weight, 2 * after_deposit.vault); // saturated locking bonus
|
||||||
assert_eq!(after_deposit.vault, 9000);
|
assert_eq!(after_deposit.vault, 9000);
|
||||||
assert_eq!(after_deposit.deposit, 9000);
|
assert_eq!(after_deposit.deposit, 9000);
|
||||||
|
|
||||||
// cannot withdraw yet, nothing is vested
|
// cannot withdraw yet, nothing is vested
|
||||||
withdraw(1).await.expect_err("nothing vested yet");
|
withdraw(1).await.expect_err("nothing vested yet");
|
||||||
|
|
||||||
// advance almost three days
|
// advance a day
|
||||||
|
addin
|
||||||
|
.set_time_offset(®istrar, &realm_authority, 24 * 60 * 60)
|
||||||
|
.await;
|
||||||
|
let after_day1 = get_balances(0).await;
|
||||||
|
assert_eq!(after_day1.voter_weight, 2 * after_day1.vault); // still saturated
|
||||||
|
|
||||||
|
// advance a second day
|
||||||
|
addin
|
||||||
|
.set_time_offset(®istrar, &realm_authority, 48 * 60 * 60)
|
||||||
|
.await;
|
||||||
|
let after_day2 = get_balances(0).await;
|
||||||
|
assert_eq!(after_day2.voter_weight, 3 * after_day2.vault / 2); // locking half done
|
||||||
|
|
||||||
|
// advance to almost three days
|
||||||
addin
|
addin
|
||||||
.set_time_offset(®istrar, &realm_authority, 71 * 60 * 60)
|
.set_time_offset(®istrar, &realm_authority, 71 * 60 * 60)
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -75,7 +75,10 @@ async fn test_deposit_daily_vesting() -> Result<(), TransportError> {
|
||||||
payer,
|
payer,
|
||||||
0,
|
0,
|
||||||
&context.mints[0],
|
&context.mints[0],
|
||||||
1,
|
0,
|
||||||
|
1.0,
|
||||||
|
0.5,
|
||||||
|
60 * 60 * 60, // 60h / 2.5d
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
@ -141,13 +144,27 @@ async fn test_deposit_daily_vesting() -> Result<(), TransportError> {
|
||||||
|
|
||||||
let after_deposit = get_balances(0).await;
|
let after_deposit = get_balances(0).await;
|
||||||
assert_eq!(initial.token, after_deposit.token + after_deposit.vault);
|
assert_eq!(initial.token, after_deposit.token + after_deposit.vault);
|
||||||
assert_eq!(after_deposit.voter_weight, after_deposit.vault);
|
// The vesting parts are locked for 72, 48 and 24h. Lockup saturates at 60h.
|
||||||
|
assert_eq!(
|
||||||
|
after_deposit.voter_weight,
|
||||||
|
((after_deposit.vault as f64) * (1.0 + 0.5 * (60.0 + 48.0 + 24.0) / 60.0 / 3.0)) as u64
|
||||||
|
);
|
||||||
assert_eq!(after_deposit.vault, 9000);
|
assert_eq!(after_deposit.vault, 9000);
|
||||||
assert_eq!(after_deposit.deposit, 9000);
|
assert_eq!(after_deposit.deposit, 9000);
|
||||||
|
|
||||||
// cannot withdraw yet, nothing is vested
|
// cannot withdraw yet, nothing is vested
|
||||||
withdraw(1).await.expect_err("nothing vested yet");
|
withdraw(1).await.expect_err("nothing vested yet");
|
||||||
|
|
||||||
|
// check vote weight reduction after an hour
|
||||||
|
addin
|
||||||
|
.set_time_offset(®istrar, &realm_authority, 60 * 60)
|
||||||
|
.await;
|
||||||
|
let after_hour = get_balances(0).await;
|
||||||
|
assert_eq!(
|
||||||
|
after_hour.voter_weight,
|
||||||
|
((after_hour.vault as f64) * (1.0 + 0.5 * (60.0 + 47.0 + 23.0) / 60.0 / 3.0)) as u64
|
||||||
|
);
|
||||||
|
|
||||||
// advance a day
|
// advance a day
|
||||||
addin
|
addin
|
||||||
.set_time_offset(®istrar, &realm_authority, 25 * 60 * 60)
|
.set_time_offset(®istrar, &realm_authority, 25 * 60 * 60)
|
||||||
|
@ -159,7 +176,10 @@ async fn test_deposit_daily_vesting() -> Result<(), TransportError> {
|
||||||
|
|
||||||
let after_withdraw = get_balances(0).await;
|
let after_withdraw = get_balances(0).await;
|
||||||
assert_eq!(initial.token, after_withdraw.token + after_withdraw.vault);
|
assert_eq!(initial.token, after_withdraw.token + after_withdraw.vault);
|
||||||
assert_eq!(after_withdraw.voter_weight, after_withdraw.vault);
|
assert_eq!(
|
||||||
|
after_withdraw.voter_weight,
|
||||||
|
((after_withdraw.vault as f64) * (1.0 + 0.5 * (47.0 + 23.0) / 60.0 / 2.0)) as u64
|
||||||
|
);
|
||||||
assert_eq!(after_withdraw.vault, 6000);
|
assert_eq!(after_withdraw.vault, 6000);
|
||||||
assert_eq!(after_withdraw.deposit, 6000);
|
assert_eq!(after_withdraw.deposit, 6000);
|
||||||
|
|
||||||
|
@ -169,7 +189,10 @@ async fn test_deposit_daily_vesting() -> Result<(), TransportError> {
|
||||||
|
|
||||||
let after_deposit = get_balances(0).await;
|
let after_deposit = get_balances(0).await;
|
||||||
assert_eq!(initial.token, after_deposit.token + after_deposit.vault);
|
assert_eq!(initial.token, after_deposit.token + after_deposit.vault);
|
||||||
assert_eq!(after_deposit.voter_weight, after_deposit.vault);
|
assert_eq!(
|
||||||
|
after_deposit.voter_weight,
|
||||||
|
((after_deposit.vault as f64) * (1.0 + 0.5 * (47.0 + 23.0) / 60.0 / 2.0)) as u64
|
||||||
|
);
|
||||||
assert_eq!(after_deposit.vault, 11000);
|
assert_eq!(after_deposit.vault, 11000);
|
||||||
assert_eq!(after_deposit.deposit, 11000);
|
assert_eq!(after_deposit.deposit, 11000);
|
||||||
|
|
||||||
|
@ -193,7 +216,10 @@ async fn test_deposit_daily_vesting() -> Result<(), TransportError> {
|
||||||
|
|
||||||
let after_withdraw = get_balances(0).await;
|
let after_withdraw = get_balances(0).await;
|
||||||
assert_eq!(initial.token, after_withdraw.token + after_withdraw.vault);
|
assert_eq!(initial.token, after_withdraw.token + after_withdraw.vault);
|
||||||
assert_eq!(after_withdraw.voter_weight, after_withdraw.vault);
|
assert_eq!(
|
||||||
|
after_withdraw.voter_weight,
|
||||||
|
((after_withdraw.vault as f64) * (1.0 + 0.5 * 23.0 / 60.0)) as u64
|
||||||
|
);
|
||||||
assert_eq!(after_withdraw.vault, 6500);
|
assert_eq!(after_withdraw.vault, 6500);
|
||||||
assert_eq!(after_withdraw.deposit, 6500);
|
assert_eq!(after_withdraw.deposit, 6500);
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,10 @@ async fn test_deposit_monthly_vesting() -> Result<(), TransportError> {
|
||||||
payer,
|
payer,
|
||||||
0,
|
0,
|
||||||
&context.mints[0],
|
&context.mints[0],
|
||||||
1,
|
0,
|
||||||
|
1.0,
|
||||||
|
0.0,
|
||||||
|
5 * 365 * 24 * 60 * 60,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -80,7 +80,10 @@ async fn test_deposit_no_locking() -> Result<(), TransportError> {
|
||||||
payer,
|
payer,
|
||||||
0,
|
0,
|
||||||
&context.mints[0],
|
&context.mints[0],
|
||||||
1,
|
0,
|
||||||
|
1.0,
|
||||||
|
10.0, // no locking, so has no effect
|
||||||
|
5 * 365 * 24 * 60 * 60,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -45,7 +45,10 @@ async fn test_grants() -> Result<(), TransportError> {
|
||||||
payer,
|
payer,
|
||||||
0,
|
0,
|
||||||
&context.mints[0],
|
&context.mints[0],
|
||||||
2,
|
0,
|
||||||
|
2.0,
|
||||||
|
0.0,
|
||||||
|
5 * 365 * 24 * 60 * 60,
|
||||||
Some(grant_authority.pubkey()),
|
Some(grant_authority.pubkey()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -63,7 +63,10 @@ async fn test_reset_lockup() -> Result<(), TransportError> {
|
||||||
payer,
|
payer,
|
||||||
0,
|
0,
|
||||||
&context.mints[0],
|
&context.mints[0],
|
||||||
1,
|
0,
|
||||||
|
1.0,
|
||||||
|
0.0,
|
||||||
|
5 * 365 * 24 * 60 * 60,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -45,7 +45,10 @@ async fn test_voting() -> Result<(), TransportError> {
|
||||||
payer,
|
payer,
|
||||||
0,
|
0,
|
||||||
&context.mints[0],
|
&context.mints[0],
|
||||||
2,
|
0,
|
||||||
|
2.0,
|
||||||
|
0.0,
|
||||||
|
5 * 365 * 24 * 60 * 60,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
Loading…
Reference in New Issue