Move all instructions to separate files
This commit is contained in:
parent
d1471a2820
commit
f5ea3180d1
|
@ -1,10 +0,0 @@
|
||||||
use crate::context::*;
|
|
||||||
use crate::error::*;
|
|
||||||
use anchor_lang::prelude::*;
|
|
||||||
|
|
||||||
pub fn rate_is_empty(ctx: &Context<CreateExchangeRate>, 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(())
|
|
||||||
}
|
|
|
@ -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::<Registrar>()
|
|
||||||
)]
|
|
||||||
pub registrar: Box<Account<'info, Registrar>>,
|
|
||||||
|
|
||||||
// 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<Account<'info, Registrar>>,
|
|
||||||
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<'info, Registrar>>,
|
|
||||||
|
|
||||||
#[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::<Voter>(),
|
|
||||||
)]
|
|
||||||
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<'info, Registrar>>,
|
|
||||||
|
|
||||||
#[account(mut, has_one = registrar, has_one = voter_authority)]
|
|
||||||
pub voter: AccountLoader<'info, Voter>,
|
|
||||||
pub voter_authority: Signer<'info>,
|
|
||||||
|
|
||||||
pub deposit_mint: Box<Account<'info, Mint>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Accounts)]
|
|
||||||
pub struct Deposit<'info> {
|
|
||||||
pub registrar: Box<Account<'info, Registrar>>,
|
|
||||||
|
|
||||||
#[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<Account<'info, TokenAccount>>,
|
|
||||||
pub deposit_mint: Box<Account<'info, Mint>>,
|
|
||||||
#[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<Account<'info, TokenAccount>>,
|
|
||||||
|
|
||||||
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<'info, Registrar>>,
|
|
||||||
|
|
||||||
#[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<Account<'info, TokenAccount>>,
|
|
||||||
pub withdraw_mint: Box<Account<'info, Mint>>,
|
|
||||||
|
|
||||||
#[account(mut)]
|
|
||||||
pub destination: Box<Account<'info, TokenAccount>>,
|
|
||||||
|
|
||||||
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<'info, Registrar>>,
|
|
||||||
|
|
||||||
#[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<'info, Registrar>>,
|
|
||||||
|
|
||||||
#[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<Account<'info, Registrar>>,
|
|
||||||
// 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<Account<'info, Registrar>>,
|
|
||||||
pub realm_authority: Signer<'info>,
|
|
||||||
}
|
|
|
@ -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<WithdrawOrClawback>, 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(())
|
||||||
|
}
|
|
@ -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<CloseDepositEntry>, 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(())
|
||||||
|
}
|
|
@ -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<CloseVoter>) -> 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(())
|
||||||
|
}
|
|
@ -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<'info, Registrar>>,
|
||||||
|
|
||||||
|
#[account(mut, has_one = registrar, has_one = voter_authority)]
|
||||||
|
pub voter: AccountLoader<'info, Voter>,
|
||||||
|
pub voter_authority: Signer<'info>,
|
||||||
|
|
||||||
|
pub deposit_mint: Box<Account<'info, Mint>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new deposit entry.
|
||||||
|
pub fn create_deposit_entry(
|
||||||
|
ctx: Context<CreateDepositEntry>,
|
||||||
|
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(())
|
||||||
|
}
|
|
@ -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<Account<'info, Registrar>>,
|
||||||
|
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<CreateExchangeRate>,
|
||||||
|
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(())
|
||||||
|
}
|
|
@ -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::<Registrar>()
|
||||||
|
)]
|
||||||
|
pub registrar: Box<Account<'info, Registrar>>,
|
||||||
|
|
||||||
|
// 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<CreateRegistrar>,
|
||||||
|
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(())
|
||||||
|
}
|
|
@ -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<'info, Registrar>>,
|
||||||
|
|
||||||
|
#[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::<Voter>(),
|
||||||
|
)]
|
||||||
|
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<CreateVoter>,
|
||||||
|
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(())
|
||||||
|
}
|
|
@ -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<'info, Registrar>>,
|
||||||
|
|
||||||
|
#[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<Account<'info, TokenAccount>>,
|
||||||
|
pub deposit_mint: Box<Account<'info, Mint>>,
|
||||||
|
#[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<Account<'info, TokenAccount>>,
|
||||||
|
|
||||||
|
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<Deposit>, 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(())
|
||||||
|
}
|
|
@ -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;
|
|
@ -0,0 +1,42 @@
|
||||||
|
use crate::account::*;
|
||||||
|
use crate::error::*;
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct ResetLockup<'info> {
|
||||||
|
pub registrar: Box<Account<'info, Registrar>>,
|
||||||
|
|
||||||
|
#[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<ResetLockup>, 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(())
|
||||||
|
}
|
|
@ -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<Account<'info, Registrar>>,
|
||||||
|
pub realm_authority: Signer<'info>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_time_offset(ctx: Context<SetTimeOffset>, 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(())
|
||||||
|
}
|
|
@ -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<Account<'info, Registrar>>,
|
||||||
|
// 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<UpdateMaxVoteWeight>) -> Result<()> {
|
||||||
|
msg!("--------update_max_vote_weight--------");
|
||||||
|
let registrar = &ctx.accounts.registrar;
|
||||||
|
let _max_vote_weight = {
|
||||||
|
let total: Result<u64> = ctx
|
||||||
|
.remaining_accounts
|
||||||
|
.iter()
|
||||||
|
.map(|acc| Account::<Mint>::try_from(acc))
|
||||||
|
.collect::<std::result::Result<Vec<Account<Mint>>, ProgramError>>()?
|
||||||
|
.iter()
|
||||||
|
.try_fold(0u64, |sum, m| {
|
||||||
|
let 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(())
|
||||||
|
}
|
|
@ -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<'info, Registrar>>,
|
||||||
|
|
||||||
|
#[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<UpdateVoterWeightRecord>) -> 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(())
|
||||||
|
}
|
|
@ -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<'info, Registrar>>,
|
||||||
|
|
||||||
|
#[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<Account<'info, TokenAccount>>,
|
||||||
|
pub withdraw_mint: Box<Account<'info, Mint>>,
|
||||||
|
|
||||||
|
#[account(mut)]
|
||||||
|
pub destination: Box<Account<'info, TokenAccount>>,
|
||||||
|
|
||||||
|
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<WithdrawOrClawback>, 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(())
|
||||||
|
}
|
|
@ -1,17 +1,12 @@
|
||||||
use access_control::*;
|
|
||||||
use account::*;
|
use account::*;
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use anchor_spl::token::{self, Mint};
|
|
||||||
use context::*;
|
|
||||||
use error::*;
|
use error::*;
|
||||||
use spl_governance::addins::voter_weight::VoterWeightAccountType;
|
use instructions::*;
|
||||||
use spl_governance::state::{realm, token_owner_record};
|
|
||||||
use std::{convert::TryFrom, str::FromStr};
|
|
||||||
|
|
||||||
mod access_control;
|
#[macro_use]
|
||||||
pub mod account;
|
pub mod account;
|
||||||
mod context;
|
|
||||||
mod error;
|
mod error;
|
||||||
|
mod instructions;
|
||||||
|
|
||||||
// The program address.
|
// The program address.
|
||||||
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
||||||
|
@ -64,46 +59,14 @@ declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
||||||
pub mod voter_stake_registry {
|
pub mod voter_stake_registry {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Creates a new voting registrar. There can only be a single registrar
|
|
||||||
/// per governance realm.
|
|
||||||
pub fn create_registrar(
|
pub fn create_registrar(
|
||||||
ctx: Context<CreateRegistrar>,
|
ctx: Context<CreateRegistrar>,
|
||||||
vote_weight_decimals: u8,
|
vote_weight_decimals: u8,
|
||||||
registrar_bump: u8,
|
registrar_bump: u8,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
msg!("--------create_registrar--------");
|
instructions::create_registrar(ctx, vote_weight_decimals, registrar_bump)
|
||||||
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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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(
|
pub fn create_exchange_rate(
|
||||||
ctx: Context<CreateExchangeRate>,
|
ctx: Context<CreateExchangeRate>,
|
||||||
idx: u16,
|
idx: u16,
|
||||||
|
@ -111,406 +74,59 @@ pub mod voter_stake_registry {
|
||||||
rate: u64,
|
rate: u64,
|
||||||
decimals: u8,
|
decimals: u8,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
msg!("--------create_exchange_rate--------");
|
instructions::create_exchange_rate(ctx, idx, mint, rate, decimals)
|
||||||
require!(rate > 0, InvalidRate);
|
|
||||||
let registrar = &mut ctx.accounts.registrar;
|
|
||||||
registrar.rates[idx as usize] = registrar.new_rate(mint, decimals, rate)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new voter account. There can only be a single voter per
|
|
||||||
/// user wallet.
|
|
||||||
pub fn create_voter(
|
pub fn create_voter(
|
||||||
ctx: Context<CreateVoter>,
|
ctx: Context<CreateVoter>,
|
||||||
voter_bump: u8,
|
voter_bump: u8,
|
||||||
voter_weight_record_bump: u8,
|
voter_weight_record_bump: u8,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
msg!("--------create_voter--------");
|
instructions::create_voter(ctx, voter_bump, voter_weight_record_bump)
|
||||||
// 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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new deposit entry.
|
|
||||||
pub fn create_deposit_entry(
|
pub fn create_deposit_entry(
|
||||||
ctx: Context<CreateDepositEntry>,
|
ctx: Context<CreateDepositEntry>,
|
||||||
kind: LockupKind,
|
kind: LockupKind,
|
||||||
periods: i32,
|
periods: i32,
|
||||||
allow_clawback: bool,
|
allow_clawback: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
msg!("--------create_deposit--------");
|
instructions::create_deposit_entry(ctx, kind, periods, allow_clawback)
|
||||||
|
|
||||||
// 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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds tokens to a deposit entry by depositing tokens into the registrar.
|
|
||||||
pub fn deposit(ctx: Context<Deposit>, id: u8, amount: u64) -> Result<()> {
|
pub fn deposit(ctx: Context<Deposit>, id: u8, amount: u64) -> Result<()> {
|
||||||
msg!("--------update_deposit--------");
|
instructions::deposit(ctx, id, amount)
|
||||||
let registrar = &ctx.accounts.registrar;
|
}
|
||||||
let voter = &mut ctx.accounts.voter.load_mut()?;
|
|
||||||
|
|
||||||
voter.last_deposit_slot = Clock::get()?.slot;
|
pub fn withdraw(ctx: Context<WithdrawOrClawback>, deposit_id: u8, amount: u64) -> Result<()> {
|
||||||
|
instructions::withdraw(ctx, deposit_id, amount)
|
||||||
// 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 clawback(ctx: Context<WithdrawOrClawback>, deposit_id: u8) -> Result<()> {
|
pub fn clawback(ctx: Context<WithdrawOrClawback>, deposit_id: u8) -> Result<()> {
|
||||||
msg!("--------clawback--------");
|
instructions::clawback(ctx, deposit_id)
|
||||||
// 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<WithdrawOrClawback>, 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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close an empty deposit, allowing it to be reused in the future
|
|
||||||
pub fn close_deposit_entry(ctx: Context<CloseDepositEntry>, deposit_id: u8) -> Result<()> {
|
pub fn close_deposit_entry(ctx: Context<CloseDepositEntry>, deposit_id: u8) -> Result<()> {
|
||||||
msg!("--------close_deposit--------");
|
instructions::close_deposit_entry(ctx, deposit_id)
|
||||||
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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets a lockup to start at the current slot timestamp and to last for
|
pub fn reset_lockup(ctx: Context<ResetLockup>, deposit_id: u8, periods: i64) -> Result<()> {
|
||||||
/// `periods`, which must be >= the number of periods left on the lockup.
|
instructions::reset_lockup(ctx, deposit_id, periods)
|
||||||
/// This will re-lock any non-withdrawn vested funds.
|
|
||||||
pub fn reset_lockup(ctx: Context<UpdateSchedule>, 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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<UpdateVoterWeightRecord>) -> Result<()> {
|
pub fn update_voter_weight_record(ctx: Context<UpdateVoterWeightRecord>) -> Result<()> {
|
||||||
msg!("--------update_voter_weight_record--------");
|
instructions::update_voter_weight_record(ctx)
|
||||||
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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculates the max vote weight for the registry. This is a function
|
pub fn update_max_vote_weight<'info>(ctx: Context<UpdateMaxVoteWeight>) -> Result<()> {
|
||||||
/// of the total supply of all exchange rate mints, converted into a
|
instructions::update_max_vote_weight(ctx)
|
||||||
/// 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<u64> = ctx
|
|
||||||
.remaining_accounts
|
|
||||||
.iter()
|
|
||||||
.map(|acc| Account::<Mint>::try_from(acc))
|
|
||||||
.collect::<std::result::Result<Vec<Account<Mint>>, ProgramError>>()?
|
|
||||||
.iter()
|
|
||||||
.try_fold(0u64, |sum, m| {
|
|
||||||
let 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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<CloseVoter>) -> Result<()> {
|
pub fn close_voter(ctx: Context<CloseVoter>) -> Result<()> {
|
||||||
msg!("--------close_voter--------");
|
instructions::close_voter(ctx)
|
||||||
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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_time_offset(ctx: Context<SetTimeOffset>, time_offset: i64) -> Result<()> {
|
pub fn set_time_offset(ctx: Context<SetTimeOffset>, time_offset: i64) -> Result<()> {
|
||||||
msg!("--------set_time_offset--------");
|
instructions::set_time_offset(ctx, 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(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue