From f5ea3180d1c33511757a5402ca9f9573360bd6a5 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 2 Dec 2021 16:28:12 +0100 Subject: [PATCH] Move all instructions to separate files --- .../src/access_control.rs | 10 - programs/voter-stake-registry/src/context.rs | 261 ----------- .../src/instructions/clawback.rs | 54 +++ .../src/instructions/close_deposit_entry.rs | 34 ++ .../src/instructions/close_voter.rs | 23 + .../src/instructions/create_deposit_entry.rs | 63 +++ .../src/instructions/create_exchange_rate.rs | 52 +++ .../src/instructions/create_registrar.rs | 73 +++ .../src/instructions/create_voter.rs | 87 ++++ .../src/instructions/deposit.rs | 93 ++++ .../src/instructions/mod.rs | 27 ++ .../src/instructions/reset_lockup.rs | 42 ++ .../src/instructions/set_time_offset.rs | 24 + .../instructions/update_max_vote_weight.rs | 51 +++ .../update_voter_weight_record.rs | 42 ++ .../src/instructions/withdraw.rs | 127 ++++++ programs/voter-stake-registry/src/lib.rs | 424 +----------------- 17 files changed, 812 insertions(+), 675 deletions(-) delete mode 100644 programs/voter-stake-registry/src/access_control.rs delete mode 100644 programs/voter-stake-registry/src/context.rs create mode 100644 programs/voter-stake-registry/src/instructions/clawback.rs create mode 100644 programs/voter-stake-registry/src/instructions/close_deposit_entry.rs create mode 100644 programs/voter-stake-registry/src/instructions/close_voter.rs create mode 100644 programs/voter-stake-registry/src/instructions/create_deposit_entry.rs create mode 100644 programs/voter-stake-registry/src/instructions/create_exchange_rate.rs create mode 100644 programs/voter-stake-registry/src/instructions/create_registrar.rs create mode 100644 programs/voter-stake-registry/src/instructions/create_voter.rs create mode 100644 programs/voter-stake-registry/src/instructions/deposit.rs create mode 100644 programs/voter-stake-registry/src/instructions/mod.rs create mode 100644 programs/voter-stake-registry/src/instructions/reset_lockup.rs create mode 100644 programs/voter-stake-registry/src/instructions/set_time_offset.rs create mode 100644 programs/voter-stake-registry/src/instructions/update_max_vote_weight.rs create mode 100644 programs/voter-stake-registry/src/instructions/update_voter_weight_record.rs create mode 100644 programs/voter-stake-registry/src/instructions/withdraw.rs diff --git a/programs/voter-stake-registry/src/access_control.rs b/programs/voter-stake-registry/src/access_control.rs deleted file mode 100644 index ae0620b..0000000 --- a/programs/voter-stake-registry/src/access_control.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::context::*; -use crate::error::*; -use anchor_lang::prelude::*; - -pub fn rate_is_empty(ctx: &Context, idx: u16) -> Result<()> { - let r = &ctx.accounts.registrar; - require!((idx as usize) < r.rates.len(), InvalidIndex); - require!(r.rates[idx as usize].rate == 0, RateNotZero); - Ok(()) -} diff --git a/programs/voter-stake-registry/src/context.rs b/programs/voter-stake-registry/src/context.rs deleted file mode 100644 index d9cb02e..0000000 --- a/programs/voter-stake-registry/src/context.rs +++ /dev/null @@ -1,261 +0,0 @@ -use crate::account::*; -use anchor_lang::prelude::*; -use anchor_lang::solana_program::sysvar::instructions as tx_instructions; -use anchor_spl::associated_token::AssociatedToken; -use anchor_spl::token::{self, Mint, Token, TokenAccount}; -use std::mem::size_of; - -pub const VOTER_WEIGHT_RECORD: [u8; 19] = *b"voter-weight-record"; - -#[derive(Accounts)] -#[instruction(vote_weight_decimals: u8, registrar_bump: u8)] -pub struct CreateRegistrar<'info> { - /// a voting registrar. There can only be a single registrar - /// per governance realm and governing mint. - #[account( - init, - seeds = [realm.key().as_ref(), b"registrar".as_ref(), realm_governing_token_mint.key().as_ref()], - bump = registrar_bump, - payer = payer, - space = 8 + size_of::() - )] - pub registrar: Box>, - - // realm is validated in the instruction: - // - realm is owned by the governance_program_id - // - realm_governing_token_mint must be the community or council mint - // - realm_authority is realm.authority - pub realm: UncheckedAccount<'info>, - - pub governance_program_id: UncheckedAccount<'info>, - pub realm_governing_token_mint: Account<'info, Mint>, - pub realm_authority: Signer<'info>, - - pub clawback_authority: UncheckedAccount<'info>, - - #[account(mut)] - pub payer: Signer<'info>, - - pub system_program: Program<'info, System>, - pub token_program: Program<'info, Token>, - pub rent: Sysvar<'info, Rent>, -} - -#[derive(Accounts)] -#[instruction(idx: u16, mint: Pubkey, rate: u64, decimals: u8)] -pub struct CreateExchangeRate<'info> { - #[account(mut, has_one = realm_authority)] - pub registrar: Box>, - pub realm_authority: Signer<'info>, - - #[account( - init, - payer = payer, - associated_token::authority = registrar, - associated_token::mint = deposit_mint, - )] - pub exchange_vault: Account<'info, TokenAccount>, - pub deposit_mint: Account<'info, Mint>, - - #[account(mut)] - pub payer: Signer<'info>, - - pub rent: Sysvar<'info, Rent>, - pub token_program: Program<'info, Token>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -#[instruction(voter_bump: u8, voter_weight_record_bump: u8)] -pub struct CreateVoter<'info> { - pub registrar: Box>, - - #[account( - init, - seeds = [registrar.key().as_ref(), b"voter".as_ref(), voter_authority.key().as_ref()], - bump = voter_bump, - payer = payer, - space = 8 + size_of::(), - )] - pub voter: AccountLoader<'info, Voter>, - pub voter_authority: Signer<'info>, - - #[account( - init, - seeds = [VOTER_WEIGHT_RECORD.as_ref(), registrar.key().as_ref(), voter_authority.key().as_ref()], - bump = voter_weight_record_bump, - payer = payer, - space = 150, - )] - pub voter_weight_record: Account<'info, VoterWeightRecord>, - - #[account(mut)] - pub payer: Signer<'info>, - - pub token_program: Program<'info, Token>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, - pub rent: Sysvar<'info, Rent>, - - #[account(address = tx_instructions::ID)] - pub instructions: UncheckedAccount<'info>, -} - -#[derive(Accounts)] -pub struct CreateDepositEntry<'info> { - pub registrar: Box>, - - #[account(mut, has_one = registrar, has_one = voter_authority)] - pub voter: AccountLoader<'info, Voter>, - pub voter_authority: Signer<'info>, - - pub deposit_mint: Box>, -} - -#[derive(Accounts)] -pub struct Deposit<'info> { - pub registrar: Box>, - - #[account(mut, has_one = registrar)] - pub voter: AccountLoader<'info, Voter>, - - #[account( - mut, - associated_token::authority = registrar, - associated_token::mint = deposit_mint, - )] - pub exchange_vault: Box>, - pub deposit_mint: Box>, - #[account(mut)] - pub deposit_authority: Signer<'info>, - #[account( - mut, - constraint = deposit_token.mint == deposit_mint.key(), - constraint = deposit_token.owner == deposit_authority.key(), - )] - pub deposit_token: Box>, - - pub token_program: Program<'info, Token>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, - pub rent: Sysvar<'info, Rent>, -} - -impl<'info> Deposit<'info> { - pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> { - let program = self.token_program.to_account_info(); - let accounts = token::Transfer { - from: self.deposit_token.to_account_info(), - to: self.exchange_vault.to_account_info(), - authority: self.deposit_authority.to_account_info(), - }; - CpiContext::new(program, accounts) - } -} - -#[derive(Accounts)] -pub struct WithdrawOrClawback<'info> { - pub registrar: Box>, - - #[account(mut, has_one = registrar)] - pub voter: AccountLoader<'info, Voter>, - - // token_owner_record is validated in the instruction: - // - owned by registrar.governance_program_id - // - for the registrar.realm - // - for the registrar.realm_governing_token_mint - // - governing_token_owner is voter_authority - pub token_owner_record: UncheckedAccount<'info>, - - // The address is verified in the instructions. - // For withdraw: must be voter_authority - // For clawback: must be registrar.clawback_authority - pub authority: Signer<'info>, - - #[account( - mut, - associated_token::authority = registrar, - associated_token::mint = withdraw_mint, - )] - pub exchange_vault: Box>, - pub withdraw_mint: Box>, - - #[account(mut)] - pub destination: Box>, - - pub token_program: Program<'info, Token>, -} - -impl<'info> WithdrawOrClawback<'info> { - pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> { - let program = self.token_program.to_account_info(); - let accounts = token::Transfer { - from: self.exchange_vault.to_account_info(), - to: self.destination.to_account_info(), - authority: self.registrar.to_account_info(), - }; - CpiContext::new(program, accounts) - } -} - -#[derive(Accounts)] -pub struct CloseDepositEntry<'info> { - #[account(mut, has_one = voter_authority)] - pub voter: AccountLoader<'info, Voter>, - pub voter_authority: Signer<'info>, -} - -#[derive(Accounts)] -pub struct UpdateSchedule<'info> { - pub registrar: Box>, - - #[account(mut, has_one = voter_authority, has_one = registrar)] - pub voter: AccountLoader<'info, Voter>, - pub voter_authority: Signer<'info>, -} - -#[derive(Accounts)] -pub struct UpdateVoterWeightRecord<'info> { - pub registrar: Box>, - - #[account(has_one = registrar)] - pub voter: AccountLoader<'info, Voter>, - - #[account( - mut, - seeds = [VOTER_WEIGHT_RECORD.as_ref(), registrar.key().as_ref(), voter.load()?.voter_authority.key().as_ref()], - bump = voter.load()?.voter_weight_record_bump, - constraint = voter_weight_record.realm == registrar.realm, - constraint = voter_weight_record.governing_token_owner == voter.load()?.voter_authority, - constraint = voter_weight_record.governing_token_mint == registrar.realm_governing_token_mint, - )] - pub voter_weight_record: Account<'info, VoterWeightRecord>, - - pub system_program: Program<'info, System>, -} - -// Remaining accounts should all the token mints that have registered -// exchange rates. -#[derive(Accounts)] -pub struct UpdateMaxVoteWeight<'info> { - pub registrar: Box>, - // TODO: SPL governance has not yet implemented this. - pub max_vote_weight_record: UncheckedAccount<'info>, -} - -#[derive(Accounts)] -pub struct CloseVoter<'info> { - #[account(mut, has_one = voter_authority, close = sol_destination)] - pub voter: AccountLoader<'info, Voter>, - pub voter_authority: Signer<'info>, - pub sol_destination: UncheckedAccount<'info>, -} - -#[derive(Accounts)] -#[instruction(time_offset: i64)] -pub struct SetTimeOffset<'info> { - #[account(mut, has_one = realm_authority)] - pub registrar: Box>, - pub realm_authority: Signer<'info>, -} diff --git a/programs/voter-stake-registry/src/instructions/clawback.rs b/programs/voter-stake-registry/src/instructions/clawback.rs new file mode 100644 index 0000000..5797263 --- /dev/null +++ b/programs/voter-stake-registry/src/instructions/clawback.rs @@ -0,0 +1,54 @@ +use crate::account::*; +use crate::error::*; +use anchor_lang::prelude::*; +use anchor_spl::token; + +use super::withdraw::WithdrawOrClawback; + +pub fn clawback(ctx: Context, deposit_id: u8) -> Result<()> { + msg!("--------clawback--------"); + // Load the accounts. + let registrar = &ctx.accounts.registrar; + let voter = &mut ctx.accounts.voter.load_mut()?; + require!(voter.deposits.len() > deposit_id.into(), InvalidDepositId); + require!( + ctx.accounts.authority.key() == registrar.clawback_authority, + InvalidAuthority + ); + + // Get the deposit being withdrawn from. + let curr_ts = registrar.clock_unix_timestamp(); + let deposit_entry = &mut voter.deposits[deposit_id as usize]; + require!(deposit_entry.is_used, InvalidDepositId); + require!( + deposit_entry.allow_clawback, + ErrorCode::ClawbackNotAllowedOnDeposit + ); + let unvested_amount = + deposit_entry.amount_initially_locked_native - deposit_entry.vested(curr_ts).unwrap(); + + // sanity check only + require!( + deposit_entry.amount_deposited_native >= unvested_amount, + InsufficientVestedTokens + ); + + // Transfer the tokens to withdraw. + let registrar_seeds = registrar_seeds!(registrar); + token::transfer( + ctx.accounts.transfer_ctx().with_signer(&[registrar_seeds]), + unvested_amount, + )?; + + // Update deposit book keeping. + deposit_entry.amount_deposited_native -= unvested_amount; + + // Now that all locked funds are withdrawn, end the lockup + deposit_entry.amount_initially_locked_native = 0; + deposit_entry.lockup.kind = LockupKind::None; + deposit_entry.lockup.start_ts = registrar.clock_unix_timestamp(); + deposit_entry.lockup.end_ts = deposit_entry.lockup.start_ts; + deposit_entry.allow_clawback = false; + + Ok(()) +} diff --git a/programs/voter-stake-registry/src/instructions/close_deposit_entry.rs b/programs/voter-stake-registry/src/instructions/close_deposit_entry.rs new file mode 100644 index 0000000..db497f5 --- /dev/null +++ b/programs/voter-stake-registry/src/instructions/close_deposit_entry.rs @@ -0,0 +1,34 @@ +use crate::account::*; +use crate::error::*; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct CloseDepositEntry<'info> { + #[account(mut, has_one = voter_authority)] + pub voter: AccountLoader<'info, Voter>, + pub voter_authority: Signer<'info>, +} + +/// Close an empty deposit, allowing it to be reused in the future +pub fn close_deposit_entry(ctx: Context, deposit_id: u8) -> Result<()> { + msg!("--------close_deposit--------"); + let voter = &mut ctx.accounts.voter.load_mut()?; + + require!(voter.deposits.len() > deposit_id as usize, InvalidDepositId); + let d = &mut voter.deposits[deposit_id as usize]; + require!(d.is_used, InvalidDepositId); + require!(d.amount_deposited_native == 0, VotingTokenNonZero); + + // Deposits that have clawback enabled are guaranteed to live until the end + // of their locking period. That ensures a deposit can't be closed and reopenend + // with a different locking kind or locking end time before funds are deposited. + if d.allow_clawback { + require!( + d.lockup.end_ts < Clock::get()?.unix_timestamp, + DepositStillLocked + ); + } + + d.is_used = false; + Ok(()) +} diff --git a/programs/voter-stake-registry/src/instructions/close_voter.rs b/programs/voter-stake-registry/src/instructions/close_voter.rs new file mode 100644 index 0000000..c249c9b --- /dev/null +++ b/programs/voter-stake-registry/src/instructions/close_voter.rs @@ -0,0 +1,23 @@ +use crate::account::*; +use crate::error::*; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct CloseVoter<'info> { + #[account(mut, has_one = voter_authority, close = sol_destination)] + pub voter: AccountLoader<'info, Voter>, + pub voter_authority: Signer<'info>, + pub sol_destination: UncheckedAccount<'info>, +} + +/// Closes the voter account, allowing one to retrieve rent exemption SOL. +/// Only accounts with no remaining deposits can be closed. +pub fn close_voter(ctx: Context) -> Result<()> { + msg!("--------close_voter--------"); + let voter = &ctx.accounts.voter.load()?; + let amount = voter.deposits.iter().fold(0u64, |sum, d| { + sum.checked_add(d.amount_deposited_native).unwrap() + }); + require!(amount == 0, VotingTokenNonZero); + Ok(()) +} diff --git a/programs/voter-stake-registry/src/instructions/create_deposit_entry.rs b/programs/voter-stake-registry/src/instructions/create_deposit_entry.rs new file mode 100644 index 0000000..cddd847 --- /dev/null +++ b/programs/voter-stake-registry/src/instructions/create_deposit_entry.rs @@ -0,0 +1,63 @@ +use crate::account::*; +use crate::error::*; +use anchor_lang::prelude::*; +use anchor_spl::token::Mint; + +#[derive(Accounts)] +pub struct CreateDepositEntry<'info> { + pub registrar: Box>, + + #[account(mut, has_one = registrar, has_one = voter_authority)] + pub voter: AccountLoader<'info, Voter>, + pub voter_authority: Signer<'info>, + + pub deposit_mint: Box>, +} + +/// Creates a new deposit entry. +pub fn create_deposit_entry( + ctx: Context, + kind: LockupKind, + periods: i32, + allow_clawback: bool, +) -> Result<()> { + msg!("--------create_deposit--------"); + + // Load accounts. + let registrar = &ctx.accounts.registrar; + let voter = &mut ctx.accounts.voter.load_mut()?; + + // Set the lockup start timestamp. + let start_ts = registrar.clock_unix_timestamp(); + + // Get the exchange rate entry associated with this deposit. + let er_idx = registrar + .rates + .iter() + .position(|r| r.mint == ctx.accounts.deposit_mint.key()) + .ok_or(ErrorCode::ExchangeRateEntryNotFound)?; + + // Get and set up the first free deposit entry. + let free_entry_idx = voter + .deposits + .iter() + .position(|d_entry| !d_entry.is_used) + .ok_or(ErrorCode::DepositEntryFull)?; + let d_entry = &mut voter.deposits[free_entry_idx]; + d_entry.is_used = true; + d_entry.rate_idx = free_entry_idx as u8; + d_entry.rate_idx = er_idx as u8; + d_entry.amount_deposited_native = 0; + d_entry.amount_initially_locked_native = 0; + d_entry.allow_clawback = allow_clawback; + d_entry.lockup = Lockup { + kind, + start_ts, + end_ts: start_ts + .checked_add(i64::from(periods).checked_mul(kind.period_secs()).unwrap()) + .unwrap(), + padding: [0u8; 16], + }; + + Ok(()) +} diff --git a/programs/voter-stake-registry/src/instructions/create_exchange_rate.rs b/programs/voter-stake-registry/src/instructions/create_exchange_rate.rs new file mode 100644 index 0000000..57dd9fe --- /dev/null +++ b/programs/voter-stake-registry/src/instructions/create_exchange_rate.rs @@ -0,0 +1,52 @@ +use crate::account::*; +use crate::error::*; +use anchor_lang::prelude::*; +use anchor_spl::associated_token::AssociatedToken; +use anchor_spl::token::{Mint, Token, TokenAccount}; + +#[derive(Accounts)] +#[instruction(idx: u16, mint: Pubkey, rate: u64, decimals: u8)] +pub struct CreateExchangeRate<'info> { + #[account(mut, has_one = realm_authority)] + pub registrar: Box>, + pub realm_authority: Signer<'info>, + + #[account( + init, + payer = payer, + associated_token::authority = registrar, + associated_token::mint = deposit_mint, + )] + pub exchange_vault: Account<'info, TokenAccount>, + pub deposit_mint: Account<'info, Mint>, + + #[account(mut)] + pub payer: Signer<'info>, + + pub rent: Sysvar<'info, Rent>, + pub token_program: Program<'info, Token>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub system_program: Program<'info, System>, +} + +/// Creates a new exchange rate for a given mint. This allows a voter to +/// deposit the mint in exchange for vTokens. There can only be a single +/// exchange rate per mint. +/// +/// WARNING: This can be freely called when any of the rates are empty. +/// This should be called immediately upon creation of a Registrar. +pub fn create_exchange_rate( + ctx: Context, + idx: u16, + mint: Pubkey, + rate: u64, + decimals: u8, +) -> Result<()> { + msg!("--------create_exchange_rate--------"); + require!(rate > 0, InvalidRate); + let registrar = &mut ctx.accounts.registrar; + require!((idx as usize) < registrar.rates.len(), InvalidIndex); + require!(registrar.rates[idx as usize].rate == 0, RateNotZero); + registrar.rates[idx as usize] = registrar.new_rate(mint, decimals, rate)?; + Ok(()) +} diff --git a/programs/voter-stake-registry/src/instructions/create_registrar.rs b/programs/voter-stake-registry/src/instructions/create_registrar.rs new file mode 100644 index 0000000..2ebf756 --- /dev/null +++ b/programs/voter-stake-registry/src/instructions/create_registrar.rs @@ -0,0 +1,73 @@ +use crate::account::*; +use crate::error::*; +use anchor_lang::prelude::*; +use anchor_spl::token::{Mint, Token}; +use spl_governance::state::realm; +use std::mem::size_of; + +#[derive(Accounts)] +#[instruction(vote_weight_decimals: u8, registrar_bump: u8)] +pub struct CreateRegistrar<'info> { + /// a voting registrar. There can only be a single registrar + /// per governance realm and governing mint. + #[account( + init, + seeds = [realm.key().as_ref(), b"registrar".as_ref(), realm_governing_token_mint.key().as_ref()], + bump = registrar_bump, + payer = payer, + space = 8 + size_of::() + )] + pub registrar: Box>, + + // realm is validated in the instruction: + // - realm is owned by the governance_program_id + // - realm_governing_token_mint must be the community or council mint + // - realm_authority is realm.authority + pub realm: UncheckedAccount<'info>, + + pub governance_program_id: UncheckedAccount<'info>, + pub realm_governing_token_mint: Account<'info, Mint>, + pub realm_authority: Signer<'info>, + + pub clawback_authority: UncheckedAccount<'info>, + + #[account(mut)] + pub payer: Signer<'info>, + + pub system_program: Program<'info, System>, + pub token_program: Program<'info, Token>, + pub rent: Sysvar<'info, Rent>, +} + +/// Creates a new voting registrar. There can only be a single registrar +/// per governance realm. +pub fn create_registrar( + ctx: Context, + vote_weight_decimals: u8, + registrar_bump: u8, +) -> Result<()> { + msg!("--------create_registrar--------"); + let registrar = &mut ctx.accounts.registrar; + registrar.bump = registrar_bump; + registrar.governance_program_id = ctx.accounts.governance_program_id.key(); + registrar.realm = ctx.accounts.realm.key(); + registrar.realm_governing_token_mint = ctx.accounts.realm_governing_token_mint.key(); + registrar.realm_authority = ctx.accounts.realm_authority.key(); + registrar.clawback_authority = ctx.accounts.clawback_authority.key(); + registrar.vote_weight_decimals = vote_weight_decimals; + registrar.time_offset = 0; + + // Verify that "realm_authority" is the expected authority on "realm" + // and that the mint matches one of the realm mints too. + let realm = realm::get_realm_data_for_governing_token_mint( + ®istrar.governance_program_id, + &ctx.accounts.realm.to_account_info(), + ®istrar.realm_governing_token_mint, + )?; + require!( + realm.authority.unwrap() == ctx.accounts.realm_authority.key(), + ErrorCode::InvalidRealmAuthority + ); + + Ok(()) +} diff --git a/programs/voter-stake-registry/src/instructions/create_voter.rs b/programs/voter-stake-registry/src/instructions/create_voter.rs new file mode 100644 index 0000000..d74628a --- /dev/null +++ b/programs/voter-stake-registry/src/instructions/create_voter.rs @@ -0,0 +1,87 @@ +use crate::account::*; +use crate::error::*; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::sysvar::instructions as tx_instructions; +use anchor_spl::associated_token::AssociatedToken; +use anchor_spl::token::Token; +use spl_governance::addins::voter_weight::VoterWeightAccountType; +use std::mem::size_of; + +pub const VOTER_WEIGHT_RECORD: [u8; 19] = *b"voter-weight-record"; + +#[derive(Accounts)] +#[instruction(voter_bump: u8, voter_weight_record_bump: u8)] +pub struct CreateVoter<'info> { + pub registrar: Box>, + + #[account( + init, + seeds = [registrar.key().as_ref(), b"voter".as_ref(), voter_authority.key().as_ref()], + bump = voter_bump, + payer = payer, + space = 8 + size_of::(), + )] + pub voter: AccountLoader<'info, Voter>, + pub voter_authority: Signer<'info>, + + #[account( + init, + seeds = [VOTER_WEIGHT_RECORD.as_ref(), registrar.key().as_ref(), voter_authority.key().as_ref()], + bump = voter_weight_record_bump, + payer = payer, + space = 150, + )] + pub voter_weight_record: Account<'info, VoterWeightRecord>, + + #[account(mut)] + pub payer: Signer<'info>, + + pub token_program: Program<'info, Token>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub system_program: Program<'info, System>, + pub rent: Sysvar<'info, Rent>, + + #[account(address = tx_instructions::ID)] + pub instructions: UncheckedAccount<'info>, +} + +/// Creates a new voter account. There can only be a single voter per +/// user wallet. +pub fn create_voter( + ctx: Context, + voter_bump: u8, + voter_weight_record_bump: u8, +) -> Result<()> { + msg!("--------create_voter--------"); + // Forbid creating voter accounts from CPI. The goal is to make automation + // impossible that weakens some of the limitations intentionally imposed on + // locked tokens. + { + let ixns = ctx.accounts.instructions.to_account_info(); + let current_index = tx_instructions::load_current_index_checked(&ixns)? as usize; + let current_ixn = tx_instructions::load_instruction_at_checked(current_index, &ixns)?; + require!( + current_ixn.program_id == *ctx.program_id, + ErrorCode::ForbiddenCpi + ); + } + + // Load accounts. + let registrar = &ctx.accounts.registrar; + let voter = &mut ctx.accounts.voter.load_init()?; + let voter_weight_record = &mut ctx.accounts.voter_weight_record; + + // Init the voter. + voter.voter_bump = voter_bump; + voter.voter_weight_record_bump = voter_weight_record_bump; + voter.voter_authority = ctx.accounts.voter_authority.key(); + voter.registrar = ctx.accounts.registrar.key(); + + // Init the voter weight record. + voter_weight_record.account_type = VoterWeightAccountType::VoterWeightRecord; + voter_weight_record.realm = registrar.realm; + voter_weight_record.governing_token_mint = registrar.realm_governing_token_mint; + voter_weight_record.governing_token_owner = ctx.accounts.voter_authority.key(); + + Ok(()) +} diff --git a/programs/voter-stake-registry/src/instructions/deposit.rs b/programs/voter-stake-registry/src/instructions/deposit.rs new file mode 100644 index 0000000..d0c4fc6 --- /dev/null +++ b/programs/voter-stake-registry/src/instructions/deposit.rs @@ -0,0 +1,93 @@ +use crate::account::*; +use crate::error::*; +use anchor_lang::prelude::*; +use anchor_spl::associated_token::AssociatedToken; +use anchor_spl::token::{self, Mint, Token, TokenAccount}; +use std::convert::TryFrom; + +#[derive(Accounts)] +pub struct Deposit<'info> { + pub registrar: Box>, + + #[account(mut, has_one = registrar)] + pub voter: AccountLoader<'info, Voter>, + + #[account( + mut, + associated_token::authority = registrar, + associated_token::mint = deposit_mint, + )] + pub exchange_vault: Box>, + pub deposit_mint: Box>, + #[account(mut)] + pub deposit_authority: Signer<'info>, + #[account( + mut, + constraint = deposit_token.mint == deposit_mint.key(), + constraint = deposit_token.owner == deposit_authority.key(), + )] + pub deposit_token: Box>, + + pub token_program: Program<'info, Token>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub system_program: Program<'info, System>, + pub rent: Sysvar<'info, Rent>, +} + +impl<'info> Deposit<'info> { + pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> { + let program = self.token_program.to_account_info(); + let accounts = token::Transfer { + from: self.deposit_token.to_account_info(), + to: self.exchange_vault.to_account_info(), + authority: self.deposit_authority.to_account_info(), + }; + CpiContext::new(program, accounts) + } +} + +/// Adds tokens to a deposit entry by depositing tokens into the registrar. +pub fn deposit(ctx: Context, id: u8, amount: u64) -> Result<()> { + msg!("--------update_deposit--------"); + let registrar = &ctx.accounts.registrar; + let voter = &mut ctx.accounts.voter.load_mut()?; + + voter.last_deposit_slot = Clock::get()?.slot; + + // Get the exchange rate entry associated with this deposit. + let er_idx = registrar + .rates + .iter() + .position(|r| r.mint == ctx.accounts.deposit_mint.key()) + .ok_or(ErrorCode::ExchangeRateEntryNotFound)?; + let _er_entry = registrar.rates[er_idx]; + + require!(voter.deposits.len() > id as usize, InvalidDepositId); + let d_entry = &mut voter.deposits[id as usize]; + require!(d_entry.is_used, InvalidDepositId); + + // Deposit tokens into the registrar. + token::transfer(ctx.accounts.transfer_ctx(), amount)?; + d_entry.amount_deposited_native += amount; + + // Adding funds to a lockup that is already in progress can be complicated + // for linear vesting schedules because all added funds should be paid out + // gradually over the remaining lockup duration. + // The logic used is to wrap up the current lockup, and create a new one + // for the expected remainder duration. + let curr_ts = registrar.clock_unix_timestamp(); + d_entry.amount_initially_locked_native -= d_entry.vested(curr_ts)?; + d_entry.amount_initially_locked_native += amount; + d_entry.lockup.start_ts = d_entry + .lockup + .start_ts + .checked_add( + i64::try_from(d_entry.lockup.period_current(curr_ts)?) + .unwrap() + .checked_mul(d_entry.lockup.kind.period_secs()) + .unwrap(), + ) + .unwrap(); + + Ok(()) +} diff --git a/programs/voter-stake-registry/src/instructions/mod.rs b/programs/voter-stake-registry/src/instructions/mod.rs new file mode 100644 index 0000000..7ca145b --- /dev/null +++ b/programs/voter-stake-registry/src/instructions/mod.rs @@ -0,0 +1,27 @@ +pub use clawback::*; +pub use close_deposit_entry::*; +pub use close_voter::*; +pub use create_deposit_entry::*; +pub use create_exchange_rate::*; +pub use create_registrar::*; +pub use create_voter::*; +pub use deposit::*; +pub use reset_lockup::*; +pub use set_time_offset::*; +pub use update_max_vote_weight::*; +pub use update_voter_weight_record::*; +pub use withdraw::*; + +mod clawback; +mod close_deposit_entry; +mod close_voter; +mod create_deposit_entry; +mod create_exchange_rate; +mod create_registrar; +mod create_voter; +mod deposit; +mod reset_lockup; +mod set_time_offset; +mod update_max_vote_weight; +mod update_voter_weight_record; +mod withdraw; diff --git a/programs/voter-stake-registry/src/instructions/reset_lockup.rs b/programs/voter-stake-registry/src/instructions/reset_lockup.rs new file mode 100644 index 0000000..259496f --- /dev/null +++ b/programs/voter-stake-registry/src/instructions/reset_lockup.rs @@ -0,0 +1,42 @@ +use crate::account::*; +use crate::error::*; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct ResetLockup<'info> { + pub registrar: Box>, + + #[account(mut, has_one = voter_authority, has_one = registrar)] + pub voter: AccountLoader<'info, Voter>, + pub voter_authority: Signer<'info>, +} + +/// Resets a lockup to start at the current slot timestamp and to last for +/// `periods`, which must be >= the number of periods left on the lockup. +/// This will re-lock any non-withdrawn vested funds. +pub fn reset_lockup(ctx: Context, deposit_id: u8, periods: i64) -> Result<()> { + msg!("--------reset_lockup--------"); + let registrar = &ctx.accounts.registrar; + let voter = &mut ctx.accounts.voter.load_mut()?; + require!(voter.deposits.len() > deposit_id as usize, InvalidDepositId); + + let d = &mut voter.deposits[deposit_id as usize]; + require!(d.is_used, InvalidDepositId); + + // 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 + ); + + // TODO: Check for correctness + d.amount_initially_locked_native = d.amount_deposited_native; + + d.lockup.start_ts = curr_ts; + d.lockup.end_ts = curr_ts + .checked_add(periods.checked_mul(d.lockup.kind.period_secs()).unwrap()) + .unwrap(); + + Ok(()) +} diff --git a/programs/voter-stake-registry/src/instructions/set_time_offset.rs b/programs/voter-stake-registry/src/instructions/set_time_offset.rs new file mode 100644 index 0000000..b61d7b6 --- /dev/null +++ b/programs/voter-stake-registry/src/instructions/set_time_offset.rs @@ -0,0 +1,24 @@ +use crate::account::*; +use crate::error::*; +use anchor_lang::prelude::*; +use std::str::FromStr; + +#[derive(Accounts)] +#[instruction(time_offset: i64)] +pub struct SetTimeOffset<'info> { + #[account(mut, has_one = realm_authority)] + pub registrar: Box>, + pub realm_authority: Signer<'info>, +} + +pub fn set_time_offset(ctx: Context, time_offset: i64) -> Result<()> { + msg!("--------set_time_offset--------"); + let allowed_program = Pubkey::from_str("GovernanceProgram11111111111111111111111111").unwrap(); + let registrar = &mut ctx.accounts.registrar; + require!( + registrar.governance_program_id == allowed_program, + ErrorCode::DebugInstruction + ); + registrar.time_offset = time_offset; + Ok(()) +} diff --git a/programs/voter-stake-registry/src/instructions/update_max_vote_weight.rs b/programs/voter-stake-registry/src/instructions/update_max_vote_weight.rs new file mode 100644 index 0000000..37b60bc --- /dev/null +++ b/programs/voter-stake-registry/src/instructions/update_max_vote_weight.rs @@ -0,0 +1,51 @@ +use crate::account::*; +use crate::error::*; +use anchor_lang::prelude::*; +use anchor_spl::token::Mint; + +// Remaining accounts should all the token mints that have registered +// exchange rates. +#[derive(Accounts)] +pub struct UpdateMaxVoteWeight<'info> { + pub registrar: Box>, + // TODO: SPL governance has not yet implemented this. + pub max_vote_weight_record: UncheckedAccount<'info>, +} + +/// Calculates the max vote weight for the registry. This is a function +/// of the total supply of all exchange rate mints, converted into a +/// common currency with a common number of decimals. +/// +/// Note that this method is only safe to use if the cumulative supply for +/// all tokens fits into a u64 *after* converting into common decimals, as +/// defined by the registrar's `rate_decimal` field. +pub fn update_max_vote_weight<'info>(ctx: Context) -> Result<()> { + msg!("--------update_max_vote_weight--------"); + let registrar = &ctx.accounts.registrar; + let _max_vote_weight = { + let total: Result = ctx + .remaining_accounts + .iter() + .map(|acc| Account::::try_from(acc)) + .collect::>, ProgramError>>()? + .iter() + .try_fold(0u64, |sum, m| { + let er_idx = registrar + .rates + .iter() + .position(|r| r.mint == m.key()) + .ok_or(ErrorCode::ExchangeRateEntryNotFound)?; + let er_entry = registrar.rates[er_idx]; + let amount = er_entry.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. + // When it has, probably need to write the result into an account, + // similar to VoterWeightRecord. + Ok(()) +} diff --git a/programs/voter-stake-registry/src/instructions/update_voter_weight_record.rs b/programs/voter-stake-registry/src/instructions/update_voter_weight_record.rs new file mode 100644 index 0000000..5f73cc6 --- /dev/null +++ b/programs/voter-stake-registry/src/instructions/update_voter_weight_record.rs @@ -0,0 +1,42 @@ +use crate::account::*; +use crate::error::*; +use anchor_lang::prelude::*; + +pub const VOTER_WEIGHT_RECORD: [u8; 19] = *b"voter-weight-record"; + +#[derive(Accounts)] +pub struct UpdateVoterWeightRecord<'info> { + pub registrar: Box>, + + #[account(has_one = registrar)] + pub voter: AccountLoader<'info, Voter>, + + #[account( + mut, + seeds = [VOTER_WEIGHT_RECORD.as_ref(), registrar.key().as_ref(), voter.load()?.voter_authority.key().as_ref()], + bump = voter.load()?.voter_weight_record_bump, + constraint = voter_weight_record.realm == registrar.realm, + constraint = voter_weight_record.governing_token_owner == voter.load()?.voter_authority, + constraint = voter_weight_record.governing_token_mint == registrar.realm_governing_token_mint, + )] + pub voter_weight_record: Account<'info, VoterWeightRecord>, + + pub system_program: Program<'info, System>, +} + +/// Calculates the lockup-scaled, time-decayed voting power for the given +/// voter and writes it into a `VoteWeightRecord` account to be used by +/// the SPL governance program. +/// +/// This "revise" instruction should be called in the same transaction, +/// immediately before voting. +pub fn update_voter_weight_record(ctx: Context) -> Result<()> { + msg!("--------update_voter_weight_record--------"); + let registrar = &ctx.accounts.registrar; + let voter = ctx.accounts.voter.load()?; + let record = &mut ctx.accounts.voter_weight_record; + record.voter_weight = voter.weight(®istrar)?; + record.voter_weight_expiry = Some(Clock::get()?.slot); + + Ok(()) +} diff --git a/programs/voter-stake-registry/src/instructions/withdraw.rs b/programs/voter-stake-registry/src/instructions/withdraw.rs new file mode 100644 index 0000000..60b8a91 --- /dev/null +++ b/programs/voter-stake-registry/src/instructions/withdraw.rs @@ -0,0 +1,127 @@ +use crate::account::*; +use crate::error::*; +use anchor_lang::prelude::*; +use anchor_spl::token::{self, Mint, Token, TokenAccount}; +use spl_governance::state::token_owner_record; + +#[derive(Accounts)] +pub struct WithdrawOrClawback<'info> { + pub registrar: Box>, + + #[account(mut, has_one = registrar)] + pub voter: AccountLoader<'info, Voter>, + + // token_owner_record is validated in the instruction: + // - owned by registrar.governance_program_id + // - for the registrar.realm + // - for the registrar.realm_governing_token_mint + // - governing_token_owner is voter_authority + pub token_owner_record: UncheckedAccount<'info>, + + // The address is verified in the instructions. + // For withdraw: must be voter_authority + // For clawback: must be registrar.clawback_authority + pub authority: Signer<'info>, + + #[account( + mut, + associated_token::authority = registrar, + associated_token::mint = withdraw_mint, + )] + pub exchange_vault: Box>, + pub withdraw_mint: Box>, + + #[account(mut)] + pub destination: Box>, + + pub token_program: Program<'info, Token>, +} + +impl<'info> WithdrawOrClawback<'info> { + pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> { + let program = self.token_program.to_account_info(); + let accounts = token::Transfer { + from: self.exchange_vault.to_account_info(), + to: self.destination.to_account_info(), + authority: self.registrar.to_account_info(), + }; + CpiContext::new(program, accounts) + } +} + +/// Withdraws tokens from a deposit entry, if they are unlocked according +/// to a vesting schedule. +/// +/// `amount` is in units of the native currency being withdrawn. +pub fn withdraw(ctx: Context, deposit_id: u8, amount: u64) -> Result<()> { + msg!("--------withdraw--------"); + // Load the accounts. + let registrar = &ctx.accounts.registrar; + let voter = &mut ctx.accounts.voter.load_mut()?; + require!(voter.deposits.len() > deposit_id.into(), InvalidDepositId); + require!( + ctx.accounts.authority.key() == voter.voter_authority, + InvalidAuthority + ); + + // Governance may forbid withdraws, for example when engaged in a vote. + let token_owner_record_data = + token_owner_record::get_token_owner_record_data_for_realm_and_governing_mint( + ®istrar.governance_program_id, + &ctx.accounts.token_owner_record.to_account_info(), + ®istrar.realm, + ®istrar.realm_governing_token_mint, + )?; + let token_owner = voter.voter_authority; + require!( + token_owner_record_data.governing_token_owner == token_owner, + InvalidTokenOwnerRecord + ); + token_owner_record_data.assert_can_withdraw_governing_tokens()?; + + // Must not withdraw in the same slot as depositing, to prevent people + // depositing, having the vote weight updated, withdrawing and then + // voting. + require!( + voter.last_deposit_slot < Clock::get()?.slot, + ErrorCode::InvalidToDepositAndWithdrawInOneSlot + ); + + // Get the deposit being withdrawn from. + let curr_ts = registrar.clock_unix_timestamp(); + let deposit_entry = &mut voter.deposits[deposit_id as usize]; + require!(deposit_entry.is_used, InvalidDepositId); + require!( + deposit_entry.amount_withdrawable(curr_ts) >= amount, + InsufficientVestedTokens + ); + // technically unnecessary + require!( + deposit_entry.amount_deposited_native >= amount, + InsufficientVestedTokens + ); + + // Get the exchange rate for the token being withdrawn. + let er_idx = registrar + .rates + .iter() + .position(|r| r.mint == ctx.accounts.withdraw_mint.key()) + .ok_or(ErrorCode::ExchangeRateEntryNotFound)?; + let _er_entry = registrar.rates[er_idx]; + require!( + er_idx == deposit_entry.rate_idx as usize, + ErrorCode::InvalidMint + ); + + // Update deposit book keeping. + deposit_entry.amount_deposited_native -= amount; + + // Transfer the tokens to withdraw. + let registrar_seeds = registrar_seeds!(registrar); + token::transfer( + ctx.accounts.transfer_ctx().with_signer(&[registrar_seeds]), + amount, + )?; + + Ok(()) +} diff --git a/programs/voter-stake-registry/src/lib.rs b/programs/voter-stake-registry/src/lib.rs index 45f417d..f28bd83 100644 --- a/programs/voter-stake-registry/src/lib.rs +++ b/programs/voter-stake-registry/src/lib.rs @@ -1,17 +1,12 @@ -use access_control::*; use account::*; use anchor_lang::prelude::*; -use anchor_spl::token::{self, Mint}; -use context::*; use error::*; -use spl_governance::addins::voter_weight::VoterWeightAccountType; -use spl_governance::state::{realm, token_owner_record}; -use std::{convert::TryFrom, str::FromStr}; +use instructions::*; -mod access_control; +#[macro_use] pub mod account; -mod context; mod error; +mod instructions; // The program address. declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); @@ -64,46 +59,14 @@ declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); pub mod voter_stake_registry { use super::*; - /// Creates a new voting registrar. There can only be a single registrar - /// per governance realm. pub fn create_registrar( ctx: Context, vote_weight_decimals: u8, registrar_bump: u8, ) -> Result<()> { - msg!("--------create_registrar--------"); - let registrar = &mut ctx.accounts.registrar; - registrar.bump = registrar_bump; - registrar.governance_program_id = ctx.accounts.governance_program_id.key(); - registrar.realm = ctx.accounts.realm.key(); - registrar.realm_governing_token_mint = ctx.accounts.realm_governing_token_mint.key(); - registrar.realm_authority = ctx.accounts.realm_authority.key(); - registrar.clawback_authority = ctx.accounts.clawback_authority.key(); - registrar.vote_weight_decimals = vote_weight_decimals; - registrar.time_offset = 0; - - // Verify that "realm_authority" is the expected authority on "realm" - // and that the mint matches one of the realm mints too. - let realm = realm::get_realm_data_for_governing_token_mint( - ®istrar.governance_program_id, - &ctx.accounts.realm.to_account_info(), - ®istrar.realm_governing_token_mint, - )?; - require!( - realm.authority.unwrap() == ctx.accounts.realm_authority.key(), - ErrorCode::InvalidRealmAuthority - ); - - Ok(()) + instructions::create_registrar(ctx, vote_weight_decimals, registrar_bump) } - /// Creates a new exchange rate for a given mint. This allows a voter to - /// deposit the mint in exchange for vTokens. There can only be a single - /// exchange rate per mint. - /// - /// WARNING: This can be freely called when any of the rates are empty. - /// This should be called immediately upon creation of a Registrar. - #[access_control(rate_is_empty(&ctx, idx))] pub fn create_exchange_rate( ctx: Context, idx: u16, @@ -111,406 +74,59 @@ pub mod voter_stake_registry { rate: u64, decimals: u8, ) -> Result<()> { - msg!("--------create_exchange_rate--------"); - require!(rate > 0, InvalidRate); - let registrar = &mut ctx.accounts.registrar; - registrar.rates[idx as usize] = registrar.new_rate(mint, decimals, rate)?; - Ok(()) + instructions::create_exchange_rate(ctx, idx, mint, rate, decimals) } - /// Creates a new voter account. There can only be a single voter per - /// user wallet. pub fn create_voter( ctx: Context, voter_bump: u8, voter_weight_record_bump: u8, ) -> Result<()> { - msg!("--------create_voter--------"); - // Forbid creating voter accounts from CPI. The goal is to make automation - // impossible that weakens some of the limitations intentionally imposed on - // locked tokens. - { - use anchor_lang::solana_program::sysvar::instructions as tx_instructions; - let ixns = ctx.accounts.instructions.to_account_info(); - let current_index = tx_instructions::load_current_index_checked(&ixns)? as usize; - let current_ixn = tx_instructions::load_instruction_at_checked(current_index, &ixns)?; - require!( - current_ixn.program_id == *ctx.program_id, - ErrorCode::ForbiddenCpi - ); - } - - // Load accounts. - let registrar = &ctx.accounts.registrar; - let voter = &mut ctx.accounts.voter.load_init()?; - let voter_weight_record = &mut ctx.accounts.voter_weight_record; - - // Init the voter. - voter.voter_bump = voter_bump; - voter.voter_weight_record_bump = voter_weight_record_bump; - voter.voter_authority = ctx.accounts.voter_authority.key(); - voter.registrar = ctx.accounts.registrar.key(); - - // Init the voter weight record. - voter_weight_record.account_type = VoterWeightAccountType::VoterWeightRecord; - voter_weight_record.realm = registrar.realm; - voter_weight_record.governing_token_mint = registrar.realm_governing_token_mint; - voter_weight_record.governing_token_owner = ctx.accounts.voter_authority.key(); - - Ok(()) + instructions::create_voter(ctx, voter_bump, voter_weight_record_bump) } - /// Creates a new deposit entry. pub fn create_deposit_entry( ctx: Context, kind: LockupKind, periods: i32, allow_clawback: bool, ) -> Result<()> { - msg!("--------create_deposit--------"); - - // Load accounts. - let registrar = &ctx.accounts.registrar; - let voter = &mut ctx.accounts.voter.load_mut()?; - - // Set the lockup start timestamp. - let start_ts = registrar.clock_unix_timestamp(); - - // Get the exchange rate entry associated with this deposit. - let er_idx = registrar - .rates - .iter() - .position(|r| r.mint == ctx.accounts.deposit_mint.key()) - .ok_or(ErrorCode::ExchangeRateEntryNotFound)?; - - // Get and set up the first free deposit entry. - let free_entry_idx = voter - .deposits - .iter() - .position(|d_entry| !d_entry.is_used) - .ok_or(ErrorCode::DepositEntryFull)?; - let d_entry = &mut voter.deposits[free_entry_idx]; - d_entry.is_used = true; - d_entry.rate_idx = free_entry_idx as u8; - d_entry.rate_idx = er_idx as u8; - d_entry.amount_deposited_native = 0; - d_entry.amount_initially_locked_native = 0; - d_entry.allow_clawback = allow_clawback; - d_entry.lockup = Lockup { - kind, - start_ts, - end_ts: start_ts - .checked_add(i64::from(periods).checked_mul(kind.period_secs()).unwrap()) - .unwrap(), - padding: [0u8; 16], - }; - - Ok(()) + instructions::create_deposit_entry(ctx, kind, periods, allow_clawback) } - /// Adds tokens to a deposit entry by depositing tokens into the registrar. pub fn deposit(ctx: Context, id: u8, amount: u64) -> Result<()> { - msg!("--------update_deposit--------"); - let registrar = &ctx.accounts.registrar; - let voter = &mut ctx.accounts.voter.load_mut()?; + instructions::deposit(ctx, id, amount) + } - voter.last_deposit_slot = Clock::get()?.slot; - - // Get the exchange rate entry associated with this deposit. - let er_idx = registrar - .rates - .iter() - .position(|r| r.mint == ctx.accounts.deposit_mint.key()) - .ok_or(ErrorCode::ExchangeRateEntryNotFound)?; - let _er_entry = registrar.rates[er_idx]; - - require!(voter.deposits.len() > id as usize, InvalidDepositId); - let d_entry = &mut voter.deposits[id as usize]; - require!(d_entry.is_used, InvalidDepositId); - - // Deposit tokens into the registrar. - token::transfer(ctx.accounts.transfer_ctx(), amount)?; - d_entry.amount_deposited_native += amount; - - // Adding funds to a lockup that is already in progress can be complicated - // for linear vesting schedules because all added funds should be paid out - // gradually over the remaining lockup duration. - // The logic used is to wrap up the current lockup, and create a new one - // for the expected remainder duration. - let curr_ts = registrar.clock_unix_timestamp(); - d_entry.amount_initially_locked_native -= d_entry.vested(curr_ts)?; - d_entry.amount_initially_locked_native += amount; - d_entry.lockup.start_ts = d_entry - .lockup - .start_ts - .checked_add( - i64::try_from(d_entry.lockup.period_current(curr_ts)?) - .unwrap() - .checked_mul(d_entry.lockup.kind.period_secs()) - .unwrap(), - ) - .unwrap(); - - Ok(()) + pub fn withdraw(ctx: Context, deposit_id: u8, amount: u64) -> Result<()> { + instructions::withdraw(ctx, deposit_id, amount) } pub fn clawback(ctx: Context, deposit_id: u8) -> Result<()> { - msg!("--------clawback--------"); - // Load the accounts. - let registrar = &ctx.accounts.registrar; - let voter = &mut ctx.accounts.voter.load_mut()?; - require!(voter.deposits.len() > deposit_id.into(), InvalidDepositId); - require!( - ctx.accounts.authority.key() == registrar.clawback_authority, - InvalidAuthority - ); - - // Get the deposit being withdrawn from. - let curr_ts = registrar.clock_unix_timestamp(); - let deposit_entry = &mut voter.deposits[deposit_id as usize]; - require!(deposit_entry.is_used, InvalidDepositId); - require!( - deposit_entry.allow_clawback, - ErrorCode::ClawbackNotAllowedOnDeposit - ); - let unvested_amount = - deposit_entry.amount_initially_locked_native - deposit_entry.vested(curr_ts).unwrap(); - - // sanity check only - require!( - deposit_entry.amount_deposited_native >= unvested_amount, - InsufficientVestedTokens - ); - - // Transfer the tokens to withdraw. - let registrar_seeds = registrar_seeds!(registrar); - token::transfer( - ctx.accounts.transfer_ctx().with_signer(&[registrar_seeds]), - unvested_amount, - )?; - - // Update deposit book keeping. - deposit_entry.amount_deposited_native -= unvested_amount; - - // Now that all locked funds are withdrawn, end the lockup - deposit_entry.amount_initially_locked_native = 0; - deposit_entry.lockup.kind = LockupKind::None; - deposit_entry.lockup.start_ts = registrar.clock_unix_timestamp(); - deposit_entry.lockup.end_ts = deposit_entry.lockup.start_ts; - deposit_entry.allow_clawback = false; - - Ok(()) - } - /// Withdraws tokens from a deposit entry, if they are unlocked according - /// to a vesting schedule. - /// - /// `amount` is in units of the native currency being withdrawn. - pub fn withdraw(ctx: Context, deposit_id: u8, amount: u64) -> Result<()> { - msg!("--------withdraw--------"); - // Load the accounts. - let registrar = &ctx.accounts.registrar; - let voter = &mut ctx.accounts.voter.load_mut()?; - require!(voter.deposits.len() > deposit_id.into(), InvalidDepositId); - require!( - ctx.accounts.authority.key() == voter.voter_authority, - InvalidAuthority - ); - - // Governance may forbid withdraws, for example when engaged in a vote. - let token_owner_record_data = - token_owner_record::get_token_owner_record_data_for_realm_and_governing_mint( - ®istrar.governance_program_id, - &ctx.accounts.token_owner_record.to_account_info(), - ®istrar.realm, - ®istrar.realm_governing_token_mint, - )?; - let token_owner = voter.voter_authority; - require!( - token_owner_record_data.governing_token_owner == token_owner, - InvalidTokenOwnerRecord - ); - token_owner_record_data.assert_can_withdraw_governing_tokens()?; - - // Must not withdraw in the same slot as depositing, to prevent people - // depositing, having the vote weight updated, withdrawing and then - // voting. - require!( - voter.last_deposit_slot < Clock::get()?.slot, - ErrorCode::InvalidToDepositAndWithdrawInOneSlot - ); - - // Get the deposit being withdrawn from. - let curr_ts = registrar.clock_unix_timestamp(); - let deposit_entry = &mut voter.deposits[deposit_id as usize]; - require!(deposit_entry.is_used, InvalidDepositId); - require!( - deposit_entry.amount_withdrawable(curr_ts) >= amount, - InsufficientVestedTokens - ); - // technically unnecessary - require!( - deposit_entry.amount_deposited_native >= amount, - InsufficientVestedTokens - ); - - // Get the exchange rate for the token being withdrawn. - let er_idx = registrar - .rates - .iter() - .position(|r| r.mint == ctx.accounts.withdraw_mint.key()) - .ok_or(ErrorCode::ExchangeRateEntryNotFound)?; - let _er_entry = registrar.rates[er_idx]; - require!( - er_idx == deposit_entry.rate_idx as usize, - ErrorCode::InvalidMint - ); - - // Update deposit book keeping. - deposit_entry.amount_deposited_native -= amount; - - // Transfer the tokens to withdraw. - let registrar_seeds = registrar_seeds!(registrar); - token::transfer( - ctx.accounts.transfer_ctx().with_signer(&[registrar_seeds]), - amount, - )?; - - Ok(()) + instructions::clawback(ctx, deposit_id) } - /// Close an empty deposit, allowing it to be reused in the future pub fn close_deposit_entry(ctx: Context, deposit_id: u8) -> Result<()> { - msg!("--------close_deposit--------"); - let voter = &mut ctx.accounts.voter.load_mut()?; - - require!(voter.deposits.len() > deposit_id as usize, InvalidDepositId); - let d = &mut voter.deposits[deposit_id as usize]; - require!(d.is_used, InvalidDepositId); - require!(d.amount_deposited_native == 0, VotingTokenNonZero); - - // Deposits that have clawback enabled are guaranteed to live until the end - // of their locking period. That ensures a deposit can't be closed and reopenend - // with a different locking kind or locking end time before funds are deposited. - if d.allow_clawback { - require!( - d.lockup.end_ts < Clock::get()?.unix_timestamp, - DepositStillLocked - ); - } - - d.is_used = false; - Ok(()) + instructions::close_deposit_entry(ctx, deposit_id) } - /// Resets a lockup to start at the current slot timestamp and to last for - /// `periods`, which must be >= the number of periods left on the lockup. - /// This will re-lock any non-withdrawn vested funds. - pub fn reset_lockup(ctx: Context, deposit_id: u8, periods: i64) -> Result<()> { - msg!("--------reset_lockup--------"); - let registrar = &ctx.accounts.registrar; - let voter = &mut ctx.accounts.voter.load_mut()?; - require!(voter.deposits.len() > deposit_id as usize, InvalidDepositId); - - let d = &mut voter.deposits[deposit_id as usize]; - require!(d.is_used, InvalidDepositId); - - // 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 - ); - - // TODO: Check for correctness - d.amount_initially_locked_native = d.amount_deposited_native; - - d.lockup.start_ts = curr_ts; - d.lockup.end_ts = curr_ts - .checked_add(periods.checked_mul(d.lockup.kind.period_secs()).unwrap()) - .unwrap(); - - Ok(()) + pub fn reset_lockup(ctx: Context, deposit_id: u8, periods: i64) -> Result<()> { + instructions::reset_lockup(ctx, deposit_id, periods) } - /// Calculates the lockup-scaled, time-decayed voting power for the given - /// voter and writes it into a `VoteWeightRecord` account to be used by - /// the SPL governance program. - /// - /// This "revise" instruction should be called in the same transaction, - /// immediately before voting. pub fn update_voter_weight_record(ctx: Context) -> Result<()> { - msg!("--------update_voter_weight_record--------"); - let registrar = &ctx.accounts.registrar; - let voter = ctx.accounts.voter.load()?; - let record = &mut ctx.accounts.voter_weight_record; - record.voter_weight = voter.weight(®istrar)?; - record.voter_weight_expiry = Some(Clock::get()?.slot); - - Ok(()) + instructions::update_voter_weight_record(ctx) } - /// Calculates the max vote weight for the registry. This is a function - /// of the total supply of all exchange rate mints, converted into a - /// common currency with a common number of decimals. - /// - /// Note that this method is only safe to use if the cumulative supply for - /// all tokens fits into a u64 *after* converting into common decimals, as - /// defined by the registrar's `rate_decimal` field. - pub fn update_max_vote_weight<'info>( - ctx: Context<'_, '_, '_, 'info, UpdateMaxVoteWeight<'info>>, - ) -> Result<()> { - msg!("--------update_max_vote_weight--------"); - let registrar = &ctx.accounts.registrar; - let _max_vote_weight = { - let total: Result = ctx - .remaining_accounts - .iter() - .map(|acc| Account::::try_from(acc)) - .collect::>, ProgramError>>()? - .iter() - .try_fold(0u64, |sum, m| { - let er_idx = registrar - .rates - .iter() - .position(|r| r.mint == m.key()) - .ok_or(ErrorCode::ExchangeRateEntryNotFound)?; - let er_entry = registrar.rates[er_idx]; - let amount = er_entry.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. - // When it has, probably need to write the result into an account, - // similar to VoterWeightRecord. - Ok(()) + pub fn update_max_vote_weight<'info>(ctx: Context) -> Result<()> { + instructions::update_max_vote_weight(ctx) } - /// Closes the voter account, allowing one to retrieve rent exemption SOL. - /// Only accounts with no remaining deposits can be closed. pub fn close_voter(ctx: Context) -> Result<()> { - msg!("--------close_voter--------"); - let voter = &ctx.accounts.voter.load()?; - let amount = voter.deposits.iter().fold(0u64, |sum, d| { - sum.checked_add(d.amount_deposited_native).unwrap() - }); - require!(amount == 0, VotingTokenNonZero); - Ok(()) + instructions::close_voter(ctx) } pub fn set_time_offset(ctx: Context, time_offset: i64) -> Result<()> { - msg!("--------set_time_offset--------"); - let allowed_program = - Pubkey::from_str("GovernanceProgram11111111111111111111111111").unwrap(); - let registrar = &mut ctx.accounts.registrar; - require!( - registrar.governance_program_id == allowed_program, - ErrorCode::DebugInstruction - ); - registrar.time_offset = time_offset; - Ok(()) + instructions::set_time_offset(ctx, time_offset) } }