Add constant-maturity lockup
Adds LockupKind::Constant, extends the reset_lockup instruction and adds the internal_transfer instruction to allow working with constant maturity lockups.
This commit is contained in:
parent
b41dfae916
commit
49e137eb51
36
README.md
36
README.md
|
@ -68,6 +68,34 @@ realm authority to:
|
|||
3. If necessary, later make a proposal to call `Clawback` on their deposit to
|
||||
retrieve all remaining locked tokens.
|
||||
|
||||
## Manage Constant Maturity Deposits
|
||||
|
||||
Constant maturity deposits are useful when there's a vote weight bonus for
|
||||
locking up tokens: With cliff or daily/monthly vested deposits the remaining
|
||||
lockup period decreases as the time of maturity approaches and thus the vote
|
||||
weight decreases over time as well.
|
||||
|
||||
Constant maturity lockup keeps tokens at a fixed maturity. That guarantees a
|
||||
fixed vote weight, but also means they need to be manually transitioned to a
|
||||
different lockup type before they can eventually be withdrawn.
|
||||
|
||||
Setting up a constant maturity lockup is easy:
|
||||
|
||||
1. Create a deposit entry of `Constant` lockup type with the chosen number of
|
||||
days.
|
||||
2. `Deposit` tokens into it.
|
||||
3. Use it to vote.
|
||||
|
||||
If you want access to the tokens again, you need to start the unlocking process
|
||||
by either
|
||||
- changing the whole deposit entry to `Cliff` with `ResetLockup`, or
|
||||
- creating a new `Cliff` deposit entry and transfering some locked tokens from
|
||||
your `Constant` deposit entry over with `InternalTransfer`.
|
||||
|
||||
In both cases you'll need to wait for the cliff to be reached before being able
|
||||
to access the tokens again.
|
||||
|
||||
|
||||
# Instruction Overview
|
||||
|
||||
## Setup
|
||||
|
@ -103,7 +131,13 @@ realm authority to:
|
|||
|
||||
- [`ResetLockup`](programs/voter-stake-registry/src/instructions/reset_lockup.rs)
|
||||
|
||||
Re-lock tokens where the lockup has expired, or increase the duration of the lockup.
|
||||
Re-lock tokens where the lockup has expired, or increase the duration of the lockup or
|
||||
change the lockup kind.
|
||||
|
||||
- [`InternalTransfer`](programs/voter-stake-registry/src/instructions/internal_transfer.rs)
|
||||
|
||||
Transfer locked tokens from one deposit entry to another. Useful for splitting off a
|
||||
chunk of a "constant" lockup deposit entry that you want to start the unlock process on.
|
||||
|
||||
- [`UpdateVoterWeightRecord`](programs/voter-stake-registry/src/instructions/update_voter_weight_record.rs)
|
||||
|
||||
|
|
|
@ -62,4 +62,12 @@ pub enum ErrorCode {
|
|||
VotingMintConfiguredWithDifferentIndex,
|
||||
#[msg("")]
|
||||
InternalProgramError,
|
||||
#[msg("")]
|
||||
InsufficientLockedTokens,
|
||||
#[msg("")]
|
||||
MustKeepTokensLocked,
|
||||
#[msg("")]
|
||||
InvalidLockupKind,
|
||||
#[msg("")]
|
||||
InvalidChangeToClawbackDepositEntry,
|
||||
}
|
||||
|
|
|
@ -79,18 +79,15 @@ pub fn deposit(ctx: Context<Deposit>, deposit_entry_index: u8, amount: u64) -> R
|
|||
// - add the new funds to the locked up token count, so they will vest over
|
||||
// the remaining periods.
|
||||
let curr_ts = registrar.clock_unix_timestamp();
|
||||
let vested_amount = d_entry.vested(curr_ts)?;
|
||||
require!(
|
||||
vested_amount <= d_entry.amount_initially_locked_native,
|
||||
InternalProgramError
|
||||
);
|
||||
d_entry.amount_initially_locked_native -= vested_amount;
|
||||
d_entry.lockup.remove_past_periods(curr_ts)?;
|
||||
d_entry.resolve_vesting(curr_ts)?;
|
||||
|
||||
// Deposit tokens into the registrar and increase the lockup amount too.
|
||||
token::transfer(ctx.accounts.transfer_ctx(), amount)?;
|
||||
d_entry.amount_deposited_native += amount;
|
||||
d_entry.amount_initially_locked_native += amount;
|
||||
d_entry.amount_deposited_native = d_entry.amount_deposited_native.checked_add(amount).unwrap();
|
||||
d_entry.amount_initially_locked_native = d_entry
|
||||
.amount_initially_locked_native
|
||||
.checked_add(amount)
|
||||
.unwrap();
|
||||
|
||||
msg!(
|
||||
"Deposited amount {} at deposit index {} with lockup kind {:?} and {} seconds left",
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
use crate::error::*;
|
||||
use crate::state::*;
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct InternalTransfer<'info> {
|
||||
pub registrar: AccountLoader<'info, Registrar>,
|
||||
|
||||
// checking the PDA address it just an extra precaution,
|
||||
// the other constraints must be exhaustive
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [registrar.key().as_ref(), b"voter".as_ref(), voter_authority.key().as_ref()],
|
||||
bump = voter.load()?.voter_bump,
|
||||
has_one = voter_authority,
|
||||
has_one = registrar)]
|
||||
pub voter: AccountLoader<'info, Voter>,
|
||||
pub voter_authority: Signer<'info>,
|
||||
}
|
||||
|
||||
/// Transfers locked tokens from the source deposit entry to the target deposit entry.
|
||||
///
|
||||
/// The target deposit entry must have equal or longer lockup period, and be of a kind
|
||||
/// that is at least equally strict.
|
||||
///
|
||||
/// Note that this never transfers withdrawable tokens, only tokens that are still
|
||||
/// locked up.
|
||||
///
|
||||
/// The primary usecases are:
|
||||
/// - consolidating multiple small deposit entries into a single big one for cleanup
|
||||
/// - transfering a small part of a big "constant" lockup deposit entry into a "cliff"
|
||||
/// locked deposit entry to start the unlocking process (reset_lockup could only
|
||||
/// change the whole deposit entry to "cliff")
|
||||
pub fn internal_transfer(
|
||||
ctx: Context<InternalTransfer>,
|
||||
source_deposit_entry_index: u8,
|
||||
target_deposit_entry_index: u8,
|
||||
amount: u64,
|
||||
) -> Result<()> {
|
||||
let registrar = &ctx.accounts.registrar.load()?;
|
||||
let voter = &mut ctx.accounts.voter.load_mut()?;
|
||||
let curr_ts = registrar.clock_unix_timestamp();
|
||||
|
||||
let source = voter.active_deposit_mut(source_deposit_entry_index)?;
|
||||
source.resolve_vesting(curr_ts)?;
|
||||
let source_seconds_left = source.lockup.seconds_left(curr_ts);
|
||||
let source_strictness = source.lockup.kind.strictness();
|
||||
let source_mint_idx = source.voting_mint_config_idx;
|
||||
|
||||
// Allowing transfers from clawback-enabled deposits could be used to avoid
|
||||
// clawback by making proposal instructions target the wrong entry index.
|
||||
require!(!source.allow_clawback, InvalidChangeToClawbackDepositEntry);
|
||||
|
||||
// Reduce source amounts
|
||||
require!(
|
||||
amount <= source.amount_initially_locked_native,
|
||||
InsufficientLockedTokens
|
||||
);
|
||||
source.amount_deposited_native = source.amount_deposited_native.checked_sub(amount).unwrap();
|
||||
source.amount_initially_locked_native =
|
||||
source.amount_initially_locked_native.saturating_sub(amount);
|
||||
|
||||
// Check target compatibility
|
||||
let target = voter.active_deposit_mut(target_deposit_entry_index)?;
|
||||
target.resolve_vesting(curr_ts)?;
|
||||
require!(
|
||||
target.voting_mint_config_idx == source_mint_idx,
|
||||
InvalidMint
|
||||
);
|
||||
require!(
|
||||
target.lockup.seconds_left(curr_ts) >= source_seconds_left,
|
||||
InvalidLockupPeriod
|
||||
);
|
||||
require!(
|
||||
target.lockup.kind.strictness() >= source_strictness,
|
||||
InvalidLockupKind
|
||||
);
|
||||
|
||||
// Add target amounts
|
||||
target.amount_deposited_native = target.amount_deposited_native.checked_add(amount).unwrap();
|
||||
target.amount_initially_locked_native = target
|
||||
.amount_initially_locked_native
|
||||
.checked_add(amount)
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -7,6 +7,7 @@ pub use create_registrar::*;
|
|||
pub use create_voter::*;
|
||||
pub use deposit::*;
|
||||
pub use grant::*;
|
||||
pub use internal_transfer::*;
|
||||
pub use reset_lockup::*;
|
||||
pub use set_time_offset::*;
|
||||
pub use update_max_vote_weight::*;
|
||||
|
@ -22,6 +23,7 @@ mod create_registrar;
|
|||
mod create_voter;
|
||||
mod deposit;
|
||||
mod grant;
|
||||
mod internal_transfer;
|
||||
mod reset_lockup;
|
||||
mod set_time_offset;
|
||||
mod update_max_vote_weight;
|
||||
|
|
|
@ -4,9 +4,10 @@ use anchor_lang::prelude::*;
|
|||
|
||||
#[derive(Accounts)]
|
||||
pub struct ResetLockup<'info> {
|
||||
pub registrar: AccountLoader<'info, Registrar>,
|
||||
|
||||
// checking the PDA address it just an extra precaution,
|
||||
// the other constraints must be exhaustive
|
||||
pub registrar: AccountLoader<'info, Registrar>,
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [registrar.key().as_ref(), b"voter".as_ref(), voter_authority.key().as_ref()],
|
||||
|
@ -23,23 +24,33 @@ pub struct ResetLockup<'info> {
|
|||
pub fn reset_lockup(
|
||||
ctx: Context<ResetLockup>,
|
||||
deposit_entry_index: u8,
|
||||
kind: LockupKind,
|
||||
periods: u32,
|
||||
) -> Result<()> {
|
||||
let registrar = &ctx.accounts.registrar.load()?;
|
||||
let voter = &mut ctx.accounts.voter.load_mut()?;
|
||||
let d = voter.active_deposit_mut(deposit_entry_index)?;
|
||||
|
||||
// The lockup period can only be increased.
|
||||
let curr_ts = registrar.clock_unix_timestamp();
|
||||
require!(
|
||||
periods as u64 >= d.lockup.periods_left(curr_ts)?,
|
||||
InvalidDays
|
||||
);
|
||||
require!(periods > 0, InvalidDays);
|
||||
|
||||
// Lock up every deposited token again
|
||||
d.amount_initially_locked_native = d.amount_deposited_native;
|
||||
d.lockup = Lockup::new_from_periods(d.lockup.kind, curr_ts, periods)?;
|
||||
let source = voter.active_deposit_mut(deposit_entry_index)?;
|
||||
|
||||
// Must not decrease duration or strictness
|
||||
require!(
|
||||
(periods as u64).checked_mul(kind.period_secs()).unwrap()
|
||||
>= source.lockup.seconds_left(curr_ts),
|
||||
InvalidLockupPeriod
|
||||
);
|
||||
require!(
|
||||
kind.strictness() >= source.lockup.kind.strictness(),
|
||||
InvalidLockupKind
|
||||
);
|
||||
|
||||
// Don't re-lock clawback deposits. Users must withdraw and create a new one.
|
||||
require!(!source.allow_clawback, InvalidChangeToClawbackDepositEntry);
|
||||
|
||||
// Change the deposit entry.
|
||||
let d_entry = voter.active_deposit_mut(deposit_entry_index)?;
|
||||
d_entry.amount_initially_locked_native = d_entry.amount_deposited_native;
|
||||
d_entry.lockup = Lockup::new_from_periods(kind, curr_ts, periods)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -104,7 +104,10 @@ pub fn withdraw(ctx: Context<Withdraw>, deposit_entry_index: u8, amount: u64) ->
|
|||
amount <= deposit_entry.amount_deposited_native,
|
||||
InternalProgramError
|
||||
);
|
||||
deposit_entry.amount_deposited_native -= amount;
|
||||
deposit_entry.amount_deposited_native = deposit_entry
|
||||
.amount_deposited_native
|
||||
.checked_sub(amount)
|
||||
.unwrap();
|
||||
|
||||
// Transfer the tokens to withdraw.
|
||||
let registrar_seeds = registrar_seeds!(registrar);
|
||||
|
|
|
@ -145,9 +145,24 @@ pub mod voter_stake_registry {
|
|||
pub fn reset_lockup(
|
||||
ctx: Context<ResetLockup>,
|
||||
deposit_entry_index: u8,
|
||||
kind: LockupKind,
|
||||
periods: u32,
|
||||
) -> Result<()> {
|
||||
instructions::reset_lockup(ctx, deposit_entry_index, periods)
|
||||
instructions::reset_lockup(ctx, deposit_entry_index, kind, periods)
|
||||
}
|
||||
|
||||
pub fn internal_transfer(
|
||||
ctx: Context<InternalTransfer>,
|
||||
source_deposit_entry_index: u8,
|
||||
target_deposit_entry_index: u8,
|
||||
amount: u64,
|
||||
) -> Result<()> {
|
||||
instructions::internal_transfer(
|
||||
ctx,
|
||||
source_deposit_entry_index,
|
||||
target_deposit_entry_index,
|
||||
amount,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update_voter_weight_record(ctx: Context<UpdateVoterWeightRecord>) -> Result<()> {
|
||||
|
|
|
@ -127,6 +127,9 @@ impl DepositEntry {
|
|||
LockupKind::Cliff => {
|
||||
self.voting_power_cliff(curr_ts, max_locked_vote_weight, lockup_saturation_secs)
|
||||
}
|
||||
LockupKind::Constant => {
|
||||
self.voting_power_cliff(curr_ts, max_locked_vote_weight, lockup_saturation_secs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,6 +258,7 @@ impl DepositEntry {
|
|||
LockupKind::Daily => self.vested_linearly(curr_ts),
|
||||
LockupKind::Monthly => self.vested_linearly(curr_ts),
|
||||
LockupKind::Cliff => Ok(0),
|
||||
LockupKind::Constant => Ok(0),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -292,4 +296,106 @@ impl DepositEntry {
|
|||
.checked_sub(self.amount_locked(curr_ts))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Adjusts the deposit and remaining lockup periods such that
|
||||
/// no parts of amount_initially_locked_native have vested.
|
||||
///
|
||||
/// That makes it easier to deal with changes to the locked
|
||||
/// amount because amount_initially_locked_native represents
|
||||
/// exactly the amount that is locked.
|
||||
///
|
||||
/// Example:
|
||||
/// If 30 tokens are locked up over 3 months, vesting each month,
|
||||
/// then after month 2:
|
||||
/// amount_initially_locked_native = 30
|
||||
/// amount_deposited_native = 30
|
||||
/// vested() = 20
|
||||
/// period_current() = 2
|
||||
/// periods_total() = 3
|
||||
/// And after this function was called:
|
||||
/// amount_initially_locked_native = 10
|
||||
/// amount_deposited_native = 30
|
||||
/// vested() = 0
|
||||
/// period_current() = 0
|
||||
/// periods_total() = 1
|
||||
pub fn resolve_vesting(&mut self, curr_ts: i64) -> Result<()> {
|
||||
let vested_amount = self.vested(curr_ts)?;
|
||||
require!(
|
||||
vested_amount <= self.amount_initially_locked_native,
|
||||
InternalProgramError
|
||||
);
|
||||
self.amount_initially_locked_native -= vested_amount;
|
||||
self.lockup.remove_past_periods(curr_ts)?;
|
||||
require!(self.vested(curr_ts)? == 0, InternalProgramError);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
pub fn resolve_vesting() -> Result<()> {
|
||||
let mut deposit = DepositEntry {
|
||||
amount_deposited_native: 35,
|
||||
amount_initially_locked_native: 30,
|
||||
lockup: Lockup::new_from_periods(LockupKind::Monthly, 1000, 3).unwrap(),
|
||||
is_used: true,
|
||||
allow_clawback: false,
|
||||
voting_mint_config_idx: 0,
|
||||
padding: [0; 13],
|
||||
};
|
||||
let initial_deposit = deposit.clone();
|
||||
let month = deposit.lockup.kind.period_secs() as i64;
|
||||
// function to avoid unaligned references when used with assert!()
|
||||
let amount_initially_locked =
|
||||
|deposit: &DepositEntry| deposit.amount_initially_locked_native;
|
||||
|
||||
let mut time = 1001;
|
||||
assert_eq!(deposit.vested(time).unwrap(), 0);
|
||||
assert_eq!(deposit.amount_withdrawable(time), 5);
|
||||
deposit.resolve_vesting(time).unwrap(); // no effect
|
||||
assert_eq!(deposit.vested(time).unwrap(), 0);
|
||||
assert_eq!(deposit.amount_withdrawable(time), 5);
|
||||
assert_eq!(
|
||||
deposit.lockup.seconds_left(time),
|
||||
initial_deposit.lockup.seconds_left(time)
|
||||
);
|
||||
assert_eq!(deposit.lockup.period_current(time).unwrap(), 0);
|
||||
assert_eq!(deposit.lockup.periods_total().unwrap(), 3);
|
||||
assert_eq!(amount_initially_locked(&deposit), 30);
|
||||
|
||||
time = 1001 + month;
|
||||
assert_eq!(deposit.vested(time).unwrap(), 10);
|
||||
assert_eq!(deposit.lockup.period_current(time).unwrap(), 1);
|
||||
assert_eq!(deposit.lockup.periods_total().unwrap(), 3);
|
||||
deposit.resolve_vesting(time).unwrap();
|
||||
assert_eq!(deposit.vested(time).unwrap(), 0);
|
||||
assert_eq!(deposit.amount_withdrawable(time), 15);
|
||||
assert_eq!(
|
||||
deposit.lockup.seconds_left(time),
|
||||
initial_deposit.lockup.seconds_left(time)
|
||||
);
|
||||
assert_eq!(deposit.lockup.period_current(time).unwrap(), 0);
|
||||
assert_eq!(deposit.lockup.periods_total().unwrap(), 2);
|
||||
assert_eq!(amount_initially_locked(&deposit), 20);
|
||||
|
||||
time = 1001 + 3 * month;
|
||||
assert_eq!(deposit.vested(time).unwrap(), 20);
|
||||
assert_eq!(deposit.lockup.period_current(time).unwrap(), 2);
|
||||
assert_eq!(deposit.lockup.periods_total().unwrap(), 2);
|
||||
deposit.resolve_vesting(time).unwrap();
|
||||
assert_eq!(deposit.vested(time).unwrap(), 0);
|
||||
assert_eq!(deposit.amount_withdrawable(time), 35);
|
||||
assert_eq!(
|
||||
deposit.lockup.seconds_left(time),
|
||||
initial_deposit.lockup.seconds_left(time)
|
||||
);
|
||||
assert_eq!(deposit.lockup.period_current(time).unwrap(), 0);
|
||||
assert_eq!(deposit.lockup.periods_total().unwrap(), 0);
|
||||
assert_eq!(amount_initially_locked(&deposit), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,7 +76,10 @@ impl Lockup {
|
|||
|
||||
/// Number of seconds left in the lockup.
|
||||
/// May be more than end_ts-start_ts if curr_ts < start_ts.
|
||||
pub fn seconds_left(&self, curr_ts: i64) -> u64 {
|
||||
pub fn seconds_left(&self, mut curr_ts: i64) -> u64 {
|
||||
if self.kind == LockupKind::Constant {
|
||||
curr_ts = self.start_ts;
|
||||
}
|
||||
if curr_ts >= self.end_ts {
|
||||
0
|
||||
} else {
|
||||
|
@ -135,10 +138,21 @@ impl Lockup {
|
|||
#[repr(u8)]
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy, PartialEq)]
|
||||
pub enum LockupKind {
|
||||
/// No lockup, tokens can be withdrawn as long as not engaged in a proposal.
|
||||
None,
|
||||
|
||||
/// Lock up for a number of days, where a linear fraction vests each day.
|
||||
Daily,
|
||||
|
||||
/// Lock up for a number of months, where a linear fraction vests each month.
|
||||
Monthly,
|
||||
|
||||
/// Lock up for a number of days, no vesting.
|
||||
Cliff,
|
||||
|
||||
/// Lock up permanently. The number of days specified becomes the minimum
|
||||
/// unlock period when the deposit (or a part of it) is changed to Cliff.
|
||||
Constant,
|
||||
}
|
||||
|
||||
impl LockupKind {
|
||||
|
@ -152,6 +166,18 @@ impl LockupKind {
|
|||
LockupKind::Daily => SECS_PER_DAY,
|
||||
LockupKind::Monthly => SECS_PER_MONTH,
|
||||
LockupKind::Cliff => SECS_PER_DAY, // arbitrary choice
|
||||
LockupKind::Constant => SECS_PER_DAY, // arbitrary choice
|
||||
}
|
||||
}
|
||||
|
||||
/// Lockups cannot decrease in strictness
|
||||
pub fn strictness(&self) -> u8 {
|
||||
match self {
|
||||
LockupKind::None => 0,
|
||||
LockupKind::Daily => 1,
|
||||
LockupKind::Monthly => 2,
|
||||
LockupKind::Cliff => 3, // can freely move between Cliff and Constant
|
||||
LockupKind::Constant => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -566,11 +566,13 @@ impl AddinCookie {
|
|||
voter: &VoterCookie,
|
||||
authority: &Keypair,
|
||||
deposit_entry_index: u8,
|
||||
kind: voter_stake_registry::state::LockupKind,
|
||||
periods: u32,
|
||||
) -> Result<(), TransportError> {
|
||||
let data =
|
||||
anchor_lang::InstructionData::data(&voter_stake_registry::instruction::ResetLockup {
|
||||
deposit_entry_index,
|
||||
kind,
|
||||
periods,
|
||||
});
|
||||
|
||||
|
@ -597,6 +599,47 @@ impl AddinCookie {
|
|||
.await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn internal_transfer(
|
||||
&self,
|
||||
registrar: &RegistrarCookie,
|
||||
voter: &VoterCookie,
|
||||
authority: &Keypair,
|
||||
source_deposit_entry_index: u8,
|
||||
target_deposit_entry_index: u8,
|
||||
amount: u64,
|
||||
) -> Result<(), TransportError> {
|
||||
let data = anchor_lang::InstructionData::data(
|
||||
&voter_stake_registry::instruction::InternalTransfer {
|
||||
source_deposit_entry_index,
|
||||
target_deposit_entry_index,
|
||||
amount,
|
||||
},
|
||||
);
|
||||
|
||||
let accounts = anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&voter_stake_registry::accounts::InternalTransfer {
|
||||
registrar: registrar.address,
|
||||
voter: voter.address,
|
||||
voter_authority: authority.pubkey(),
|
||||
},
|
||||
None,
|
||||
);
|
||||
|
||||
let instructions = vec![Instruction {
|
||||
program_id: self.program_id,
|
||||
accounts,
|
||||
data,
|
||||
}];
|
||||
|
||||
// clone the secrets
|
||||
let signer = Keypair::from_base58_string(&authority.to_base58_string());
|
||||
|
||||
self.solana
|
||||
.process_transaction(&instructions, Some(&[&signer]))
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn set_time_offset(
|
||||
&self,
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
use anchor_spl::token::TokenAccount;
|
||||
use program_test::*;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transport::TransportError};
|
||||
|
||||
mod program_test;
|
||||
|
||||
struct Balances {
|
||||
token: u64,
|
||||
vault: u64,
|
||||
deposit: u64,
|
||||
voter_weight: u64,
|
||||
}
|
||||
|
||||
async fn balances(
|
||||
context: &TestContext,
|
||||
registrar: &RegistrarCookie,
|
||||
address: Pubkey,
|
||||
voter: &VoterCookie,
|
||||
voting_mint: &VotingMintConfigCookie,
|
||||
deposit_id: u8,
|
||||
) -> Balances {
|
||||
// Advance slots to avoid caching of the UpdateVoterWeightRecord call
|
||||
// TODO: Is this something that could be an issue on a live node?
|
||||
context.solana.advance_clock_by_slots(2).await;
|
||||
|
||||
let token = context.solana.token_account_balance(address).await;
|
||||
let vault = voting_mint.vault_balance(&context.solana).await;
|
||||
let deposit = voter.deposit_amount(&context.solana, deposit_id).await;
|
||||
let vwr = context
|
||||
.addin
|
||||
.update_voter_weight_record(®istrar, &voter)
|
||||
.await
|
||||
.unwrap();
|
||||
Balances {
|
||||
token,
|
||||
vault,
|
||||
deposit,
|
||||
voter_weight: vwr.voter_weight,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unaligned_references)]
|
||||
#[tokio::test]
|
||||
async fn test_deposit_constant() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
let addin = &context.addin;
|
||||
|
||||
let payer = &context.users[0].key;
|
||||
let realm_authority = Keypair::new();
|
||||
let realm = context
|
||||
.governance
|
||||
.create_realm(
|
||||
"testrealm",
|
||||
realm_authority.pubkey(),
|
||||
&context.mints[0],
|
||||
&payer,
|
||||
&context.addin.program_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
let voter_authority = &context.users[1].key;
|
||||
let token_owner_record = realm
|
||||
.create_token_owner_record(voter_authority.pubkey(), &payer)
|
||||
.await;
|
||||
|
||||
let registrar = addin
|
||||
.create_registrar(&realm, &realm_authority, payer)
|
||||
.await;
|
||||
let mngo_voting_mint = addin
|
||||
.configure_voting_mint(
|
||||
®istrar,
|
||||
&realm_authority,
|
||||
payer,
|
||||
0,
|
||||
&context.mints[0],
|
||||
0,
|
||||
1.0,
|
||||
1.0,
|
||||
2 * 24 * 60 * 60,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let voter = addin
|
||||
.create_voter(®istrar, &token_owner_record, &voter_authority, &payer)
|
||||
.await;
|
||||
|
||||
let reference_account = context.users[1].token_accounts[0];
|
||||
let get_balances = |depot_id| {
|
||||
balances(
|
||||
&context,
|
||||
®istrar,
|
||||
reference_account,
|
||||
&voter,
|
||||
&mngo_voting_mint,
|
||||
depot_id,
|
||||
)
|
||||
};
|
||||
let withdraw = |amount: u64| {
|
||||
addin.withdraw(
|
||||
®istrar,
|
||||
&voter,
|
||||
&mngo_voting_mint,
|
||||
&voter_authority,
|
||||
reference_account,
|
||||
0,
|
||||
amount,
|
||||
)
|
||||
};
|
||||
let deposit = |amount: u64| {
|
||||
addin.deposit(
|
||||
®istrar,
|
||||
&voter,
|
||||
&mngo_voting_mint,
|
||||
&voter_authority,
|
||||
reference_account,
|
||||
0,
|
||||
amount,
|
||||
)
|
||||
};
|
||||
|
||||
// test deposit and withdraw
|
||||
|
||||
let initial = get_balances(0).await;
|
||||
assert_eq!(initial.vault, 0);
|
||||
assert_eq!(initial.deposit, 0);
|
||||
|
||||
addin
|
||||
.create_deposit_entry(
|
||||
®istrar,
|
||||
&voter,
|
||||
&voter_authority,
|
||||
&mngo_voting_mint,
|
||||
0,
|
||||
voter_stake_registry::state::LockupKind::Constant,
|
||||
2, // days
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
deposit(9000).await.unwrap();
|
||||
|
||||
let after_deposit = get_balances(0).await;
|
||||
assert_eq!(initial.token, after_deposit.token + 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.deposit, 9000);
|
||||
|
||||
withdraw(1).await.expect_err("all locked up");
|
||||
|
||||
// advance three days
|
||||
addin
|
||||
.set_time_offset(®istrar, &realm_authority, 3 * 24 * 60 * 60)
|
||||
.await;
|
||||
let after_day3 = get_balances(0).await;
|
||||
assert_eq!(after_day3.voter_weight, after_deposit.voter_weight); // unchanged
|
||||
|
||||
withdraw(1).await.expect_err("all locked up");
|
||||
|
||||
deposit(1000).await.unwrap();
|
||||
|
||||
let after_deposit = get_balances(0).await;
|
||||
assert_eq!(initial.token, after_deposit.token + after_deposit.vault);
|
||||
assert_eq!(after_deposit.voter_weight, 2 * after_deposit.vault); // saturated locking bonus
|
||||
assert_eq!(after_deposit.vault, 10000);
|
||||
assert_eq!(after_deposit.deposit, 10000);
|
||||
|
||||
withdraw(1).await.expect_err("all locked up");
|
||||
|
||||
// Change the whole thing to cliff lockup
|
||||
addin
|
||||
.reset_lockup(
|
||||
®istrar,
|
||||
&voter,
|
||||
&voter_authority,
|
||||
0,
|
||||
voter_stake_registry::state::LockupKind::Cliff,
|
||||
1,
|
||||
)
|
||||
.await
|
||||
.expect_err("can't reduce period");
|
||||
addin
|
||||
.reset_lockup(
|
||||
®istrar,
|
||||
&voter,
|
||||
&voter_authority,
|
||||
0,
|
||||
voter_stake_registry::state::LockupKind::Cliff,
|
||||
2,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let after_reset = get_balances(0).await;
|
||||
assert_eq!(initial.token, after_reset.token + after_reset.vault);
|
||||
assert_eq!(after_reset.voter_weight, 2 * after_reset.vault); // saturated locking bonus
|
||||
assert_eq!(after_reset.vault, 10000);
|
||||
assert_eq!(after_reset.deposit, 10000);
|
||||
|
||||
withdraw(1).await.expect_err("all locked up");
|
||||
|
||||
// advance to six days
|
||||
addin
|
||||
.set_time_offset(®istrar, &realm_authority, 6 * 24 * 60 * 60)
|
||||
.await;
|
||||
|
||||
withdraw(10000).await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
use anchor_spl::token::TokenAccount;
|
||||
use program_test::*;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transport::TransportError};
|
||||
use std::cell::RefCell;
|
||||
use std::sync::Arc;
|
||||
use voter_stake_registry::state::LockupKind;
|
||||
|
||||
mod program_test;
|
||||
|
||||
async fn get_lockup_data(
|
||||
solana: &SolanaCookie,
|
||||
voter: Pubkey,
|
||||
index: u8,
|
||||
time_offset: i64,
|
||||
) -> (u64, u64, u64, u64, u64) {
|
||||
let now = solana.get_clock().await.unix_timestamp + time_offset;
|
||||
let voter = solana
|
||||
.get_account::<voter_stake_registry::state::Voter>(voter)
|
||||
.await;
|
||||
let d = voter.deposits[index as usize];
|
||||
let duration = d.lockup.periods_total().unwrap() * d.lockup.kind.period_secs();
|
||||
(
|
||||
// time since lockup start (saturating at "duration")
|
||||
(duration - d.lockup.seconds_left(now)) as u64,
|
||||
// duration of lockup
|
||||
duration,
|
||||
d.amount_initially_locked_native,
|
||||
d.amount_deposited_native,
|
||||
d.amount_withdrawable(now),
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(unaligned_references)]
|
||||
#[tokio::test]
|
||||
async fn test_internal_transfer() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
let addin = &context.addin;
|
||||
|
||||
let payer = &context.users[0].key;
|
||||
let realm_authority = Keypair::new();
|
||||
let realm = context
|
||||
.governance
|
||||
.create_realm(
|
||||
"testrealm",
|
||||
realm_authority.pubkey(),
|
||||
&context.mints[0],
|
||||
&payer,
|
||||
&context.addin.program_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
let voter_authority = &context.users[1].key;
|
||||
let token_owner_record = realm
|
||||
.create_token_owner_record(voter_authority.pubkey(), &payer)
|
||||
.await;
|
||||
|
||||
let registrar = addin
|
||||
.create_registrar(&realm, &realm_authority, payer)
|
||||
.await;
|
||||
let mngo_voting_mint = addin
|
||||
.configure_voting_mint(
|
||||
®istrar,
|
||||
&realm_authority,
|
||||
payer,
|
||||
0,
|
||||
&context.mints[0],
|
||||
0,
|
||||
1.0,
|
||||
0.0,
|
||||
5 * 365 * 24 * 60 * 60,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let voter = addin
|
||||
.create_voter(®istrar, &token_owner_record, &voter_authority, &payer)
|
||||
.await;
|
||||
|
||||
let reference_account = context.users[1].token_accounts[0];
|
||||
let deposit = |index: u8, amount: u64| {
|
||||
addin.deposit(
|
||||
®istrar,
|
||||
&voter,
|
||||
&mngo_voting_mint,
|
||||
&voter_authority,
|
||||
reference_account,
|
||||
index,
|
||||
amount,
|
||||
)
|
||||
};
|
||||
let internal_transfer = |source: u8, target: u8, amount: u64| {
|
||||
addin.internal_transfer(®istrar, &voter, &voter_authority, source, target, amount)
|
||||
};
|
||||
let time_offset = Arc::new(RefCell::new(0i64));
|
||||
let advance_time = |extra: u64| {
|
||||
*time_offset.borrow_mut() += extra as i64;
|
||||
addin.set_time_offset(®istrar, &realm_authority, *time_offset.borrow())
|
||||
};
|
||||
let lockup_status =
|
||||
|index: u8| get_lockup_data(&context.solana, voter.address, index, *time_offset.borrow());
|
||||
|
||||
let month = LockupKind::Monthly.period_secs();
|
||||
let day = 24 * 60 * 60;
|
||||
let hour = 60 * 60;
|
||||
|
||||
//
|
||||
// test transfering locked funds from a partially vested deposit to another one
|
||||
//
|
||||
addin
|
||||
.create_deposit_entry(
|
||||
®istrar,
|
||||
&voter,
|
||||
&voter_authority,
|
||||
&mngo_voting_mint,
|
||||
0,
|
||||
LockupKind::Monthly,
|
||||
3,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
deposit(0, 300).await.unwrap();
|
||||
|
||||
advance_time(month + hour).await;
|
||||
addin
|
||||
.create_deposit_entry(
|
||||
®istrar,
|
||||
&voter,
|
||||
&voter_authority,
|
||||
&mngo_voting_mint,
|
||||
1,
|
||||
LockupKind::Daily,
|
||||
3,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
deposit(1, 30).await.unwrap();
|
||||
|
||||
// both deposits have vested one period
|
||||
advance_time(day + hour).await;
|
||||
assert_eq!(
|
||||
lockup_status(0).await,
|
||||
(month + day + 2 * hour, 3 * month, 300, 300, 100)
|
||||
);
|
||||
assert_eq!(lockup_status(1).await, (day + hour, 3 * day, 30, 30, 10));
|
||||
|
||||
internal_transfer(0, 1, 1)
|
||||
.await
|
||||
.expect_err("can't make less strict/period");
|
||||
internal_transfer(1, 0, 21)
|
||||
.await
|
||||
.expect_err("can only transfer locked");
|
||||
internal_transfer(1, 0, 10).await.unwrap();
|
||||
|
||||
context.solana.advance_clock_by_slots(2).await;
|
||||
assert_eq!(
|
||||
lockup_status(0).await,
|
||||
(day + 2 * hour, 2 * month, 210, 310, 100)
|
||||
);
|
||||
assert_eq!(lockup_status(1).await, (hour, 2 * day, 10, 20, 10));
|
||||
|
||||
//
|
||||
// test partially moving tokens from constant deposit to cliff
|
||||
//
|
||||
addin
|
||||
.create_deposit_entry(
|
||||
®istrar,
|
||||
&voter,
|
||||
&voter_authority,
|
||||
&mngo_voting_mint,
|
||||
2,
|
||||
LockupKind::Constant,
|
||||
5,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
deposit(2, 1000).await.unwrap();
|
||||
addin
|
||||
.create_deposit_entry(
|
||||
®istrar,
|
||||
&voter,
|
||||
&voter_authority,
|
||||
&mngo_voting_mint,
|
||||
3,
|
||||
LockupKind::Cliff,
|
||||
5,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(lockup_status(2).await, (0, 5 * day, 1000, 1000, 0));
|
||||
assert_eq!(lockup_status(3).await, (0, 5 * day, 0, 0, 0));
|
||||
|
||||
internal_transfer(2, 3, 100).await.unwrap();
|
||||
|
||||
context.solana.advance_clock_by_slots(2).await;
|
||||
assert_eq!(lockup_status(2).await, (0, 5 * day, 900, 900, 0));
|
||||
assert_eq!(lockup_status(3).await, (0, 5 * day, 100, 100, 0));
|
||||
|
||||
advance_time(2 * day + hour).await;
|
||||
|
||||
internal_transfer(2, 3, 100)
|
||||
.await
|
||||
.expect_err("target deposit has not enough period left");
|
||||
|
||||
addin
|
||||
.create_deposit_entry(
|
||||
®istrar,
|
||||
&voter,
|
||||
&voter_authority,
|
||||
&mngo_voting_mint,
|
||||
4,
|
||||
LockupKind::Cliff,
|
||||
8,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
internal_transfer(2, 4, 100).await.unwrap();
|
||||
|
||||
assert_eq!(lockup_status(2).await, (0, 5 * day, 800, 800, 0));
|
||||
assert_eq!(
|
||||
lockup_status(3).await,
|
||||
(2 * day + hour, 5 * day, 100, 100, 0)
|
||||
);
|
||||
assert_eq!(lockup_status(4).await, (0, 8 * day, 100, 100, 0));
|
||||
|
||||
advance_time(day + hour).await;
|
||||
context.solana.advance_clock_by_slots(2).await;
|
||||
|
||||
// still ok, cliff deposit 4 still has 7 days of lockup left, which is >= 5
|
||||
internal_transfer(2, 4, 800).await.unwrap();
|
||||
|
||||
assert_eq!(lockup_status(2).await, (0, 5 * day, 0, 0, 0));
|
||||
assert_eq!(lockup_status(4).await, (hour, 7 * day, 900, 900, 0));
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -4,6 +4,7 @@ use solana_program_test::*;
|
|||
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transport::TransportError};
|
||||
use std::cell::RefCell;
|
||||
use std::sync::Arc;
|
||||
use voter_stake_registry::state::LockupKind;
|
||||
|
||||
mod program_test;
|
||||
|
||||
|
@ -99,8 +100,8 @@ async fn test_reset_lockup() -> Result<(), TransportError> {
|
|||
amount,
|
||||
)
|
||||
};
|
||||
let reset_lockup = |index: u8, periods: u32| {
|
||||
addin.reset_lockup(®istrar, &voter, &voter_authority, index, periods)
|
||||
let reset_lockup = |index: u8, periods: u32, kind: LockupKind| {
|
||||
addin.reset_lockup(®istrar, &voter, &voter_authority, index, kind, periods)
|
||||
};
|
||||
let time_offset = Arc::new(RefCell::new(0i64));
|
||||
let advance_time = |extra: u64| {
|
||||
|
@ -110,6 +111,7 @@ async fn test_reset_lockup() -> Result<(), TransportError> {
|
|||
let lockup_status =
|
||||
|index: u8| get_lockup_data(&context.solana, voter.address, index, *time_offset.borrow());
|
||||
|
||||
let month = LockupKind::Monthly.period_secs();
|
||||
let day = 24 * 60 * 60;
|
||||
let hour = 60 * 60;
|
||||
|
||||
|
@ -121,7 +123,7 @@ async fn test_reset_lockup() -> Result<(), TransportError> {
|
|||
&voter_authority,
|
||||
&mngo_voting_mint,
|
||||
7,
|
||||
voter_stake_registry::state::LockupKind::Daily,
|
||||
LockupKind::Daily,
|
||||
3,
|
||||
false,
|
||||
)
|
||||
|
@ -131,10 +133,10 @@ async fn test_reset_lockup() -> Result<(), TransportError> {
|
|||
assert_eq!(lockup_status(7).await, (0, 3 * day, 80, 80, 0));
|
||||
deposit(7, 10).await.unwrap();
|
||||
assert_eq!(lockup_status(7).await, (0, 3 * day, 90, 90, 0));
|
||||
reset_lockup(7, 2)
|
||||
reset_lockup(7, 2, LockupKind::Daily)
|
||||
.await
|
||||
.expect_err("can't relock for less periods");
|
||||
reset_lockup(7, 3).await.unwrap(); // just resets start to current timestamp
|
||||
reset_lockup(7, 3, LockupKind::Daily).await.unwrap(); // just resets start to current timestamp
|
||||
assert_eq!(lockup_status(7).await, (0, 3 * day, 90, 90, 0));
|
||||
|
||||
// advance more than a day
|
||||
|
@ -144,7 +146,7 @@ async fn test_reset_lockup() -> Result<(), TransportError> {
|
|||
assert_eq!(lockup_status(7).await, (day + hour, 3 * day, 90, 90, 30));
|
||||
deposit(7, 10).await.unwrap();
|
||||
assert_eq!(lockup_status(7).await, (hour, 2 * day, 70, 100, 30));
|
||||
reset_lockup(7, 10).await.unwrap();
|
||||
reset_lockup(7, 10, LockupKind::Daily).await.unwrap();
|
||||
assert_eq!(lockup_status(7).await, (0, 10 * day, 100, 100, 0));
|
||||
|
||||
// advance four more days
|
||||
|
@ -160,12 +162,12 @@ async fn test_reset_lockup() -> Result<(), TransportError> {
|
|||
lockup_status(7).await,
|
||||
(4 * day + hour, 10 * day, 100, 80, 20)
|
||||
);
|
||||
reset_lockup(7, 5)
|
||||
reset_lockup(7, 5, LockupKind::Daily)
|
||||
.await
|
||||
.expect_err("can't relock for less periods");
|
||||
reset_lockup(7, 6).await.unwrap();
|
||||
reset_lockup(7, 6, LockupKind::Daily).await.unwrap();
|
||||
assert_eq!(lockup_status(7).await, (0, 6 * day, 80, 80, 0));
|
||||
reset_lockup(7, 8).await.unwrap();
|
||||
reset_lockup(7, 8, LockupKind::Daily).await.unwrap();
|
||||
assert_eq!(lockup_status(7).await, (0, 8 * day, 80, 80, 0));
|
||||
|
||||
// advance three more days
|
||||
|
@ -183,9 +185,24 @@ async fn test_reset_lockup() -> Result<(), TransportError> {
|
|||
|
||||
withdraw(7, 20).await.unwrap(); // partially withdraw vested
|
||||
assert_eq!(lockup_status(7).await, (hour, 5 * day, 60, 70, 10));
|
||||
reset_lockup(7, 10).await.unwrap();
|
||||
reset_lockup(7, 10, LockupKind::Daily).await.unwrap();
|
||||
assert_eq!(lockup_status(7).await, (0, 10 * day, 70, 70, 0));
|
||||
|
||||
reset_lockup(7, 1, LockupKind::Monthly).await.unwrap();
|
||||
assert_eq!(lockup_status(7).await, (0, 1 * month, 70, 70, 0));
|
||||
|
||||
reset_lockup(7, 31, LockupKind::Daily)
|
||||
.await
|
||||
.expect_err("decreasing strictness");
|
||||
reset_lockup(7, 31, LockupKind::None)
|
||||
.await
|
||||
.expect_err("decreasing strictness");
|
||||
reset_lockup(7, 30, LockupKind::Cliff)
|
||||
.await
|
||||
.expect_err("period shortnend");
|
||||
reset_lockup(7, 31, LockupKind::Cliff).await.unwrap();
|
||||
assert_eq!(lockup_status(7).await, (0, 31 * day, 70, 70, 0));
|
||||
|
||||
// tests for cliff vesting
|
||||
addin
|
||||
.create_deposit_entry(
|
||||
|
@ -194,7 +211,7 @@ async fn test_reset_lockup() -> Result<(), TransportError> {
|
|||
&voter_authority,
|
||||
&mngo_voting_mint,
|
||||
5,
|
||||
voter_stake_registry::state::LockupKind::Cliff,
|
||||
LockupKind::Cliff,
|
||||
3,
|
||||
false,
|
||||
)
|
||||
|
@ -202,12 +219,12 @@ async fn test_reset_lockup() -> Result<(), TransportError> {
|
|||
.unwrap();
|
||||
deposit(5, 80).await.unwrap();
|
||||
assert_eq!(lockup_status(5).await, (0, 3 * day, 80, 80, 0));
|
||||
reset_lockup(5, 2)
|
||||
reset_lockup(5, 2, LockupKind::Cliff)
|
||||
.await
|
||||
.expect_err("can't relock for less periods");
|
||||
reset_lockup(5, 3).await.unwrap(); // just resets start to current timestamp
|
||||
reset_lockup(5, 3, LockupKind::Cliff).await.unwrap(); // just resets start to current timestamp
|
||||
assert_eq!(lockup_status(5).await, (0, 3 * day, 80, 80, 0));
|
||||
reset_lockup(5, 4).await.unwrap();
|
||||
reset_lockup(5, 4, LockupKind::Cliff).await.unwrap();
|
||||
assert_eq!(lockup_status(5).await, (0, 4 * day, 80, 80, 0));
|
||||
|
||||
// advance to end of cliff
|
||||
|
@ -215,7 +232,7 @@ async fn test_reset_lockup() -> Result<(), TransportError> {
|
|||
context.solana.advance_clock_by_slots(2).await;
|
||||
|
||||
assert_eq!(lockup_status(5).await, (4 * day, 4 * day, 80, 80, 80));
|
||||
reset_lockup(5, 1).await.unwrap();
|
||||
reset_lockup(5, 1, LockupKind::Cliff).await.unwrap();
|
||||
assert_eq!(lockup_status(5).await, (0, 1 * day, 80, 80, 0));
|
||||
withdraw(5, 10).await.expect_err("nothing unlocked");
|
||||
|
||||
|
@ -228,7 +245,7 @@ async fn test_reset_lockup() -> Result<(), TransportError> {
|
|||
assert_eq!(lockup_status(5).await, (day, day, 80, 70, 70));
|
||||
deposit(5, 5).await.unwrap();
|
||||
assert_eq!(lockup_status(5).await, (0, 0, 5, 75, 75));
|
||||
reset_lockup(5, 1).await.unwrap();
|
||||
reset_lockup(5, 1, LockupKind::Cliff).await.unwrap();
|
||||
assert_eq!(lockup_status(5).await, (0, 1 * day, 75, 75, 0));
|
||||
deposit(5, 15).await.unwrap();
|
||||
assert_eq!(lockup_status(5).await, (0, 1 * day, 90, 90, 0));
|
||||
|
|
Loading…
Reference in New Issue