add monthly vesting

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2021-11-26 21:04:05 +01:00
parent 23ad57302d
commit bff99fc155
3 changed files with 215 additions and 4 deletions

View File

@ -10,16 +10,23 @@ use std::convert::TryFrom;
vote_weight_record!(crate::ID);
/// Seconds in one day.
/// for localnet, to make testing of vesting possible,
/// set a low value so tests can just sleep for 10s to simulate a day
#[cfg(feature = "localnet")]
pub const SECS_PER_DAY: i64 = 10;
#[cfg(not(feature = "localnet"))]
pub const SECS_PER_DAY: i64 = 86_400;
/// Seconds in one month.
#[cfg(feature = "localnet")]
pub const SECS_PER_MONTH: i64 = 10;
#[cfg(not(feature = "localnet"))]
pub const SECS_PER_MONTH: i64 = 86_400 * 30;
/// Maximum number of days one can lock for.
pub const MAX_DAYS_LOCKED: u64 = 2555;
/// Maximum number of months one can lock for.
pub const MAX_MONTHS_LOCKED: u64 = 2555;
/// Instance of a voting rights distributor.
#[account(zero_copy)]
pub struct Registrar {
@ -199,6 +206,7 @@ impl DepositEntry {
}
match self.lockup.kind {
LockupKind::Daily => self.voting_power_daily(curr_ts),
LockupKind::Monthly => self.voting_power_monthly(curr_ts),
LockupKind::Cliff => self.voting_power_cliff(curr_ts),
}
}
@ -229,6 +237,32 @@ impl DepositEntry {
Ok(decayed_vote_weight)
}
fn voting_power_monthly(&self, curr_ts: i64) -> Result<u64> {
let m = MAX_MONTHS_LOCKED;
let n = self.lockup.months_left(curr_ts)?;
if n == 0 {
return Ok(0);
}
let decayed_vote_weight = self
.amount_scaled
.checked_mul(
// Ok to divide by two here because, if n is zero, then the
// voting power is zero. And if n is one or above, then the
// numerator is 2 or above.
n.checked_mul(n.checked_add(1).unwrap())
.unwrap()
.checked_div(2)
.unwrap(),
)
.unwrap()
.checked_div(m.checked_mul(n).unwrap()) //.checked_mul(2).unwrap())
.unwrap();
Ok(decayed_vote_weight)
}
fn voting_power_cliff(&self, curr_ts: i64) -> Result<u64> {
let decayed_voting_weight = self
.lockup
@ -250,6 +284,7 @@ impl DepositEntry {
}
match self.lockup.kind {
LockupKind::Daily => self.vested_daily(curr_ts),
LockupKind::Monthly => self.vested_monthly(curr_ts),
LockupKind::Cliff => self.vested_cliff(),
}
}
@ -269,6 +304,21 @@ impl DepositEntry {
Ok(vested)
}
fn vested_monthly(&self, curr_ts: i64) -> Result<u64> {
let month_current = self.lockup.month_current(curr_ts)?;
let months_total = self.lockup.months_total()?;
if month_current >= months_total {
return Ok(self.amount_deposited);
}
let vested = self
.amount_deposited
.checked_mul(month_current)
.unwrap()
.checked_div(months_total)
.unwrap();
Ok(vested)
}
fn vested_cliff(&self) -> Result<u64> {
let curr_ts = Clock::get()?.unix_timestamp;
if curr_ts < self.lockup.end_ts {
@ -326,12 +376,43 @@ impl Lockup {
Ok(lockup_days)
}
/// Returns the number of months left on the lockup.
pub fn months_left(&self, curr_ts: i64) -> Result<u64> {
Ok(self
.months_total()?
.saturating_sub(self.month_current(curr_ts)?))
}
/// Returns the current month in the vesting schedule.
pub fn month_current(&self, curr_ts: i64) -> Result<u64> {
let d = u64::try_from({
let secs_elapsed = curr_ts.saturating_sub(self.start_ts);
secs_elapsed.checked_div(SECS_PER_MONTH).unwrap()
})
.map_err(|_| ErrorCode::UnableToConvert)?;
Ok(d)
}
/// Returns the total amount of months in the lockup period.
pub fn months_total(&self) -> Result<u64> {
// Number of seconds in the entire lockup.
let lockup_secs = self.end_ts.checked_sub(self.start_ts).unwrap();
require!(lockup_secs % SECS_PER_MONTH == 0, InvalidLockupPeriod);
// Total months in the entire lockup.
let lockup_months =
u64::try_from(lockup_secs.checked_div(SECS_PER_MONTH).unwrap()).unwrap();
Ok(lockup_months)
}
}
#[repr(u8)]
#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)]
pub enum LockupKind {
Daily,
Monthly,
Cliff,
}
@ -398,7 +479,7 @@ mod tests {
run_test_days_left(TestDaysLeft {
expected_days_left: 1,
days_total: 10.0,
curr_day: 9.1,
curr_day: 9.9,
})
}
@ -420,6 +501,51 @@ mod tests {
})
}
#[test]
pub fn months_left_start() -> Result<()> {
run_test_months_left(TestMonthsLeft {
expected_months_left: 10,
months_total: 10.0,
curr_month: 0.,
})
}
#[test]
pub fn months_left_one_half() -> Result<()> {
run_test_months_left(TestMonthsLeft {
expected_months_left: 10,
months_total: 10.0,
curr_month: 0.5,
})
}
#[test]
pub fn months_left_one_and_a_half() -> Result<()> {
run_test_months_left(TestMonthsLeft {
expected_months_left: 9,
months_total: 10.0,
curr_month: 1.5,
})
}
#[test]
pub fn months_left_ten() -> Result<()> {
run_test_months_left(TestMonthsLeft {
expected_months_left: 9,
months_total: 10.0,
curr_month: 1.5,
})
}
#[test]
pub fn months_left_eleven() -> Result<()> {
run_test_months_left(TestMonthsLeft {
expected_months_left: 0,
months_total: 10.0,
curr_month: 11.,
})
}
#[test]
pub fn voting_power_cliff_warmup() -> Result<()> {
// 10 tokens with 6 decimals.
@ -736,6 +862,12 @@ mod tests {
curr_day: f64,
}
struct TestMonthsLeft {
expected_months_left: u64,
months_total: f64,
curr_month: f64,
}
struct TestVotingPower {
amount_deposited: u64,
days_total: f64,
@ -759,6 +891,21 @@ mod tests {
Ok(())
}
fn run_test_months_left(t: TestMonthsLeft) -> Result<()> {
let start_ts = 1634929833;
let end_ts = start_ts + months_to_secs(t.months_total);
let curr_ts = start_ts + months_to_secs(t.curr_month);
let l = Lockup {
kind: LockupKind::Monthly,
start_ts,
end_ts,
padding: [0u8; 16],
};
let months_left = l.months_left(curr_ts)?;
assert_eq!(months_left, t.expected_months_left);
Ok(())
}
fn run_test_voting_power(t: TestVotingPower) -> Result<()> {
let start_ts = 1634929833;
let end_ts = start_ts + days_to_secs(t.days_total);
@ -782,7 +929,12 @@ mod tests {
}
fn days_to_secs(days: f64) -> i64 {
let d = 86_400.0 * days;
let d = 86_400. * days;
d.round() as i64
}
fn months_to_secs(months: f64) -> i64 {
let d = 86_400. * 30. * months;
d.round() as i64
}

View File

@ -249,6 +249,7 @@ pub mod governance_registry {
// Get the deposit being withdrawn from.
let deposit_entry = &mut voter.deposits[deposit_id as usize];
require!(deposit_entry.is_used, InvalidDepositId);
msg!("deposit_entry.vested() {:?}", deposit_entry.vested());
require!(deposit_entry.vested()? >= amount, InsufficientVestedTokens);
require!(
deposit_entry.amount_left() >= amount,

View File

@ -358,6 +358,64 @@ describe("voting-rights", () => {
assert.ok(vtAccount.amount.toNumber() === 0);
});
it("Deposits monthly locked A tokens", async () => {
const amount = new BN(10);
const kind = { monthly: {} };
const months = 1;
await program.rpc.createDeposit(kind, amount, months, {
accounts: {
deposit: {
voter,
exchangeVault: exchangeVaultA,
depositToken: godA,
votingToken,
authority: program.provider.wallet.publicKey,
registrar,
depositMint: mintA,
votingMint: votingMintA,
tokenProgram,
systemProgram,
associatedTokenProgram,
rent,
},
},
});
const voterAccount = await program.account.voter.fetch(voter);
const deposit = voterAccount.deposits[2];
assert.ok(deposit.isUsed);
assert.ok(deposit.amountDeposited.toNumber() === 10);
assert.ok(deposit.rateIdx === 0);
});
it("Withdraws monthly locked A tokens", async () => {
await sleep(10000);
const depositId = 2;
const amount = new BN(10);
await program.rpc.withdraw(depositId, amount, {
accounts: {
registrar,
voter,
exchangeVault: exchangeVaultA,
withdrawMint: mintA,
votingToken,
votingMint: votingMintA,
destination: godA,
authority: program.provider.wallet.publicKey,
tokenProgram,
},
});
const voterAccount = await program.account.voter.fetch(voter);
const deposit = voterAccount.deposits[0];
assert.ok(deposit.isUsed);
assert.ok(deposit.amountDeposited.toNumber() === 0);
assert.ok(deposit.rateIdx === 0);
const vtAccount = await votingTokenClientA.getAccountInfo(votingToken);
assert.ok(vtAccount.amount.toNumber() === 0);
});
it("Updates a vote weight record", async () => {
await program.rpc.updateVoterWeightRecord({
accounts: {