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:
Christian Kamm 2021-12-09 11:58:15 +01:00 committed by GitHub
parent 317b7168eb
commit ddf37c4de0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 418 additions and 208 deletions

View File

@ -54,4 +54,8 @@ pub enum ErrorCode {
InvalidTokenOwnerRecord, InvalidTokenOwnerRecord,
#[msg("")] #[msg("")]
InvalidRealmAuthority, InvalidRealmAuthority,
#[msg("")]
VoterWeightOverflow,
#[msg("")]
LockupSaturationMustBePositive,
} }

View File

@ -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(())
} }

View File

@ -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"

View File

@ -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.

View File

@ -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(

View File

@ -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)

View File

@ -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 {

View File

@ -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]

View File

@ -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
} }
} }

View File

@ -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,

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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(&registrar, &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(&registrar, &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(&registrar, &realm_authority, 71 * 60 * 60) .set_time_offset(&registrar, &realm_authority, 71 * 60 * 60)
.await; .await;

View File

@ -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(&registrar, &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(&registrar, &realm_authority, 25 * 60 * 60) .set_time_offset(&registrar, &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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;