Decompose deposits into separate instructions
This commit is contained in:
parent
627d572774
commit
8db7c5b449
34
package.json
34
package.json
|
@ -1,18 +1,20 @@
|
||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "anchor test"
|
"lint:fix": "prettier tests/** -w",
|
||||||
},
|
"test": "anchor test"
|
||||||
"dependencies": {
|
},
|
||||||
"@project-serum/anchor": "^0.17.1-beta.1",
|
"dependencies": {
|
||||||
"@project-serum/common": "^0.0.1-beta.3",
|
"@project-serum/anchor": "^0.17.1-beta.1",
|
||||||
"@solana/spl-token": "^0.1.8",
|
"@project-serum/common": "^0.0.1-beta.3",
|
||||||
"bn.js": "^5.2.0"
|
"@solana/spl-token": "^0.1.8",
|
||||||
},
|
"bn.js": "^5.2.0"
|
||||||
"devDependencies": {
|
},
|
||||||
"@types/mocha": "^9.0.0",
|
"devDependencies": {
|
||||||
"chai": "^4.3.4",
|
"@types/mocha": "^9.0.0",
|
||||||
"mocha": "^9.0.3",
|
"chai": "^4.3.4",
|
||||||
"ts-mocha": "^8.0.0",
|
"mocha": "^9.0.3",
|
||||||
"typescript": "^4.3.5"
|
"prettier": "^2.4.1",
|
||||||
}
|
"ts-mocha": "^8.0.0",
|
||||||
|
"typescript": "^4.3.5"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,14 +10,59 @@ use std::ops::Deref;
|
||||||
|
|
||||||
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
||||||
|
|
||||||
|
/// # Introduction
|
||||||
|
///
|
||||||
|
/// The governance registry is an "addin" to the SPL governance program that
|
||||||
|
/// allows one to both many different ypes of tokens for voting and to scale
|
||||||
|
/// voting power as a linear function of time locked--subject to some maximum
|
||||||
|
/// upper bound.
|
||||||
|
///
|
||||||
|
/// The overall process is as follows:
|
||||||
|
///
|
||||||
|
/// - Create a SPL governance realm.
|
||||||
|
/// - Create a governance registry account.
|
||||||
|
/// - Add exchange rates for any tokens one wants to deposit. For example,
|
||||||
|
/// if one wants to vote with tokens A and B, where token B has twice the
|
||||||
|
/// voting power of token A, then the exchange rate of B would be 2 and the
|
||||||
|
/// exchange rate of A would be 1.
|
||||||
|
/// - Create a voter account.
|
||||||
|
/// - Deposit tokens into this program, with an optional lockup period.
|
||||||
|
/// - Vote.
|
||||||
|
///
|
||||||
|
/// Upon voting with SPL governance, a client is expected to call
|
||||||
|
/// `decay_voting_power` to get an up to date measurement of a given `Voter`'s
|
||||||
|
/// voting power for the given slot. If this is not done, then the transaction
|
||||||
|
/// will fail (since the SPL governance program will require the measurement
|
||||||
|
/// to be active for the current slot).
|
||||||
|
///
|
||||||
|
/// # Locked Tokens
|
||||||
|
///
|
||||||
|
/// Locked tokens scale voting power linearly. For example, if you were to
|
||||||
|
/// lockup your tokens for 10 years, with a single vesting period, then your
|
||||||
|
/// voting power would scale by 10x. If you were to lockup your tokens for
|
||||||
|
/// 10 years with two vesting periods, your voting power would scale by
|
||||||
|
/// 1/2 * deposit * 5 + 1/2 * deposit * 10--since the first half of the lockup
|
||||||
|
/// would unlock at year 5 and the other half would unlock at year 10.
|
||||||
|
///
|
||||||
|
/// # Decayed Voting Power
|
||||||
|
///
|
||||||
|
/// Although there is a premium awarded to voters for locking up their tokens,
|
||||||
|
/// that premium decays over time, since the lockup period decreases over time.
|
||||||
|
///
|
||||||
|
/// # Interacting with SPL Governance
|
||||||
|
///
|
||||||
|
/// This program does not directly interact with SPL governance via CPI.
|
||||||
|
/// Instead, it simply writes a `VoterWeightRecord` account with a well defined
|
||||||
|
/// format, which is then used by SPL governance as the voting power measurement
|
||||||
|
/// for a given user.
|
||||||
#[program]
|
#[program]
|
||||||
pub mod governance_registry {
|
pub mod governance_registry {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Creates a new voting registrar. There can only be a single regsitrar
|
/// Creates a new voting registrar. There can only be a single regsitrar
|
||||||
/// per governance realm.
|
/// per governance realm.
|
||||||
pub fn init_registrar(
|
pub fn create_registrar(
|
||||||
ctx: Context<InitRegistrar>,
|
ctx: Context<CreateRegistrar>,
|
||||||
registrar_bump: u8,
|
registrar_bump: u8,
|
||||||
voting_mint_bump: u8,
|
voting_mint_bump: u8,
|
||||||
_voting_mint_decimals: u8,
|
_voting_mint_decimals: u8,
|
||||||
|
@ -34,7 +79,7 @@ pub mod governance_registry {
|
||||||
|
|
||||||
/// Creates a new voter account. There can only be a single voter per
|
/// Creates a new voter account. There can only be a single voter per
|
||||||
/// user wallet.
|
/// user wallet.
|
||||||
pub fn init_voter(ctx: Context<InitVoter>, voter_bump: u8) -> Result<()> {
|
pub fn create_voter(ctx: Context<CreateVoter>, voter_bump: u8) -> Result<()> {
|
||||||
let voter = &mut ctx.accounts.voter.load_init()?;
|
let voter = &mut ctx.accounts.voter.load_init()?;
|
||||||
voter.voter_bump = voter_bump;
|
voter.voter_bump = voter_bump;
|
||||||
voter.authority = ctx.accounts.authority.key();
|
voter.authority = ctx.accounts.authority.key();
|
||||||
|
@ -46,7 +91,10 @@ pub mod governance_registry {
|
||||||
/// Creates a new exchange rate for a given mint. This allows a voter to
|
/// 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
|
/// deposit the mint in exchange for vTokens. There can only be a single
|
||||||
/// exchange rate per mint.
|
/// exchange rate per mint.
|
||||||
pub fn add_exchange_rate(ctx: Context<AddExchangeRate>, er: ExchangeRateEntry) -> Result<()> {
|
pub fn create_exchange_rate(
|
||||||
|
ctx: Context<CreateExchangeRate>,
|
||||||
|
er: ExchangeRateEntry,
|
||||||
|
) -> Result<()> {
|
||||||
require!(er.rate > 0, InvalidRate);
|
require!(er.rate > 0, InvalidRate);
|
||||||
|
|
||||||
let mut er = er;
|
let mut er = er;
|
||||||
|
@ -62,10 +110,55 @@ pub mod governance_registry {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deposits tokens into the registrar in exchange for *frozen* voting
|
/// Creates a new deposit entry and updates it by transferring in tokens.
|
||||||
/// tokens. These tokens are not used for anything other than displaying
|
pub fn create_deposit(
|
||||||
/// the amount in wallets.
|
ctx: Context<CreateDeposit>,
|
||||||
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
|
amount: u64,
|
||||||
|
lockup: Option<Lockup>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Creates the new deposit.
|
||||||
|
let deposit_id = {
|
||||||
|
let registrar = &ctx.accounts.deposit.registrar.load()?;
|
||||||
|
let voter = &mut ctx.accounts.deposit.voter.load_mut()?;
|
||||||
|
|
||||||
|
// Get the exchange rate entry associated with this deposit.
|
||||||
|
let er_idx = registrar
|
||||||
|
.rates
|
||||||
|
.iter()
|
||||||
|
.position(|r| r.mint == ctx.accounts.deposit.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;
|
||||||
|
|
||||||
|
if let Some(l) = lockup {
|
||||||
|
d_entry.start_ts = l.start_ts;
|
||||||
|
d_entry.end_ts = l.end_ts;
|
||||||
|
d_entry.period_count = l.period_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
free_entry_idx as u8
|
||||||
|
};
|
||||||
|
|
||||||
|
// Updates the entry by transferring in tokens.
|
||||||
|
let update_ctx = Context::new(ctx.program_id, &mut ctx.accounts.deposit, &[]);
|
||||||
|
update_deposit(update_ctx, deposit_id, amount)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates a deposit entry by depositing tokens into the registrar in
|
||||||
|
/// exchange for *frozen* voting tokens. These tokens are not used for
|
||||||
|
/// anything other than displaying the amount in wallets.
|
||||||
|
pub fn update_deposit(ctx: Context<UpdateDeposit>, id: u8, amount: u64) -> Result<()> {
|
||||||
let registrar = &ctx.accounts.registrar.load()?;
|
let registrar = &ctx.accounts.registrar.load()?;
|
||||||
let voter = &mut ctx.accounts.voter.load_mut()?;
|
let voter = &mut ctx.accounts.voter.load_mut()?;
|
||||||
|
|
||||||
|
@ -77,31 +170,10 @@ pub mod governance_registry {
|
||||||
.ok_or(ErrorCode::ExchangeRateEntryNotFound)?;
|
.ok_or(ErrorCode::ExchangeRateEntryNotFound)?;
|
||||||
let er_entry = registrar.rates[er_idx];
|
let er_entry = registrar.rates[er_idx];
|
||||||
|
|
||||||
// Get the deposit entry associated with this deposit.
|
require!(voter.deposits.len() > id as usize, InvalidDepositId);
|
||||||
let deposit_entry = {
|
let d_entry = &mut voter.deposits[id as usize];
|
||||||
match voter.deposits.iter().position(|deposit_entry| {
|
|
||||||
registrar.rates[deposit_entry.rate_idx as usize].mint
|
|
||||||
== ctx.accounts.deposit_mint.key()
|
|
||||||
}) {
|
|
||||||
// Lazily instantiate the deposit if needed.
|
|
||||||
None => {
|
|
||||||
let free_entry_idx = voter
|
|
||||||
.deposits
|
|
||||||
.iter()
|
|
||||||
.position(|deposit_entry| !deposit_entry.is_used)
|
|
||||||
.ok_or(ErrorCode::DepositEntryFull)?;
|
|
||||||
let entry = &mut voter.deposits[free_entry_idx];
|
|
||||||
entry.is_used = true;
|
|
||||||
entry.rate_idx = free_entry_idx as u8;
|
|
||||||
entry
|
|
||||||
}
|
|
||||||
// Use the existing deposit.
|
|
||||||
Some(e) => &mut voter.deposits[e],
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update the amount deposited.
|
d_entry.amount += amount;
|
||||||
deposit_entry.amount += amount;
|
|
||||||
|
|
||||||
// Calculate the amount of voting tokens to mint at the specified
|
// Calculate the amount of voting tokens to mint at the specified
|
||||||
// exchange rate.
|
// exchange rate.
|
||||||
|
@ -130,13 +202,53 @@ pub mod governance_registry {
|
||||||
|
|
||||||
/// Withdraws tokens from a deposit entry, if they are unlocked according
|
/// Withdraws tokens from a deposit entry, if they are unlocked according
|
||||||
/// to a vesting schedule.
|
/// to a vesting schedule.
|
||||||
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
|
///
|
||||||
// todo
|
/// `amount` is in units of the native currency being withdrawn.
|
||||||
|
pub fn withdraw(ctx: Context<Withdraw>, deposit_id: u8, amount: u64) -> Result<()> {
|
||||||
|
let registrar = &ctx.accounts.registrar.load()?;
|
||||||
|
let voter = &mut ctx.accounts.voter.load_mut()?;
|
||||||
|
require!(voter.deposits.len() > deposit_id.into(), InvalidDepositId);
|
||||||
|
|
||||||
|
// Update the deposit bookkeeping.
|
||||||
|
let deposit_entry = &mut voter.deposits[deposit_id as usize];
|
||||||
|
require!(deposit_entry.is_used, InvalidDepositId);
|
||||||
|
require!(deposit_entry.vested() >= amount, InsufficientVestedTokens);
|
||||||
|
deposit_entry.amount -= amount;
|
||||||
|
|
||||||
|
// 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];
|
||||||
|
|
||||||
|
let scaled_amount = er_entry.rate * amount;
|
||||||
|
|
||||||
|
// Transfer the tokens to withdraw.
|
||||||
|
token::transfer(
|
||||||
|
ctx.accounts
|
||||||
|
.transfer_ctx()
|
||||||
|
.with_signer(&[&[registrar.realm.as_ref(), &[registrar.bump]]]),
|
||||||
|
amount,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Unfreeze the voting mint.
|
||||||
|
token::thaw_account(
|
||||||
|
ctx.accounts
|
||||||
|
.thaw_ctx()
|
||||||
|
.with_signer(&[&[registrar.realm.as_ref(), &[registrar.bump]]]),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Burn the voting tokens.
|
||||||
|
token::burn(ctx.accounts.burn_ctx(), scaled_amount)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates a vesting schedule. Can only increase the lockup time. If all
|
/// Updates a vesting schedule. Can only increase the lockup time or reduce
|
||||||
/// tokens are unlocked, then the period count can also be updated.
|
/// the period count (since that has the effect of increasing lockup time).
|
||||||
|
/// If all tokens are unlocked, then both can be updated arbitrarily.
|
||||||
pub fn update_schedule(ctx: Context<UpdateSchedule>) -> Result<()> {
|
pub fn update_schedule(ctx: Context<UpdateSchedule>) -> Result<()> {
|
||||||
// todo
|
// todo
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -165,7 +277,7 @@ pub mod governance_registry {
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
#[instruction(registrar_bump: u8, voting_mint_bump: u8, voting_mint_decimals: u8)]
|
#[instruction(registrar_bump: u8, voting_mint_bump: u8, voting_mint_decimals: u8)]
|
||||||
pub struct InitRegistrar<'info> {
|
pub struct CreateRegistrar<'info> {
|
||||||
#[account(
|
#[account(
|
||||||
init,
|
init,
|
||||||
seeds = [realm.key().as_ref()],
|
seeds = [realm.key().as_ref()],
|
||||||
|
@ -180,8 +292,8 @@ pub struct InitRegistrar<'info> {
|
||||||
bump = voting_mint_bump,
|
bump = voting_mint_bump,
|
||||||
payer = payer,
|
payer = payer,
|
||||||
mint::authority = registrar,
|
mint::authority = registrar,
|
||||||
mint::decimals = voting_mint_decimals,
|
|
||||||
mint::freeze_authority = registrar,
|
mint::freeze_authority = registrar,
|
||||||
|
mint::decimals = voting_mint_decimals,
|
||||||
)]
|
)]
|
||||||
voting_mint: Account<'info, Mint>,
|
voting_mint: Account<'info, Mint>,
|
||||||
realm: UncheckedAccount<'info>,
|
realm: UncheckedAccount<'info>,
|
||||||
|
@ -194,7 +306,7 @@ pub struct InitRegistrar<'info> {
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
#[instruction(voter_bump: u8)]
|
#[instruction(voter_bump: u8)]
|
||||||
pub struct InitVoter<'info> {
|
pub struct CreateVoter<'info> {
|
||||||
#[account(
|
#[account(
|
||||||
init,
|
init,
|
||||||
seeds = [registrar.key().as_ref(), authority.key().as_ref()],
|
seeds = [registrar.key().as_ref(), authority.key().as_ref()],
|
||||||
|
@ -204,11 +316,11 @@ pub struct InitVoter<'info> {
|
||||||
)]
|
)]
|
||||||
voter: AccountLoader<'info, Voter>,
|
voter: AccountLoader<'info, Voter>,
|
||||||
#[account(
|
#[account(
|
||||||
init,
|
init,
|
||||||
payer = authority,
|
payer = authority,
|
||||||
associated_token::authority = authority,
|
associated_token::authority = authority,
|
||||||
associated_token::mint = voting_mint,
|
associated_token::mint = voting_mint,
|
||||||
)]
|
)]
|
||||||
voting_token: Account<'info, TokenAccount>,
|
voting_token: Account<'info, TokenAccount>,
|
||||||
voting_mint: Account<'info, Mint>,
|
voting_mint: Account<'info, Mint>,
|
||||||
registrar: AccountLoader<'info, Registrar>,
|
registrar: AccountLoader<'info, Registrar>,
|
||||||
|
@ -221,7 +333,7 @@ pub struct InitVoter<'info> {
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
#[instruction(rate: ExchangeRateEntry)]
|
#[instruction(rate: ExchangeRateEntry)]
|
||||||
pub struct AddExchangeRate<'info> {
|
pub struct CreateExchangeRate<'info> {
|
||||||
#[account(
|
#[account(
|
||||||
init,
|
init,
|
||||||
payer = payer,
|
payer = payer,
|
||||||
|
@ -241,34 +353,11 @@ pub struct AddExchangeRate<'info> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
pub struct Deposit<'info> {
|
pub struct CreateDeposit<'info> {
|
||||||
#[account(mut, has_one = authority)]
|
deposit: UpdateDeposit<'info>,
|
||||||
voter: AccountLoader<'info, Voter>,
|
|
||||||
#[account(
|
|
||||||
mut,
|
|
||||||
associated_token::authority = registrar,
|
|
||||||
associated_token::mint = deposit_mint,
|
|
||||||
)]
|
|
||||||
exchange_vault: Account<'info, TokenAccount>,
|
|
||||||
#[account(
|
|
||||||
mut,
|
|
||||||
constraint = deposit_token.mint == deposit_mint.key(),
|
|
||||||
)]
|
|
||||||
deposit_token: Account<'info, TokenAccount>,
|
|
||||||
#[account(
|
|
||||||
mut,
|
|
||||||
constraint = registrar.load()?.voting_mint == voting_token.mint,
|
|
||||||
)]
|
|
||||||
voting_token: Account<'info, TokenAccount>,
|
|
||||||
authority: Signer<'info>,
|
|
||||||
registrar: AccountLoader<'info, Registrar>,
|
|
||||||
deposit_mint: Account<'info, Mint>,
|
|
||||||
#[account(mut)]
|
|
||||||
voting_mint: Account<'info, Mint>,
|
|
||||||
token_program: Program<'info, Token>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'info> Deposit<'info> {
|
impl<'info> UpdateDeposit<'info> {
|
||||||
fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
|
fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
|
||||||
let program = self.token_program.to_account_info();
|
let program = self.token_program.to_account_info();
|
||||||
let accounts = token::Transfer {
|
let accounts = token::Transfer {
|
||||||
|
@ -301,8 +390,92 @@ impl<'info> Deposit<'info> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
pub struct Withdraw {
|
pub struct UpdateDeposit<'info> {
|
||||||
// todo
|
#[account(has_one = voting_mint)]
|
||||||
|
registrar: AccountLoader<'info, Registrar>,
|
||||||
|
#[account(mut, has_one = authority, has_one = registrar)]
|
||||||
|
voter: AccountLoader<'info, Voter>,
|
||||||
|
#[account(
|
||||||
|
mut,
|
||||||
|
associated_token::authority = registrar,
|
||||||
|
associated_token::mint = deposit_mint,
|
||||||
|
)]
|
||||||
|
exchange_vault: Account<'info, TokenAccount>,
|
||||||
|
#[account(
|
||||||
|
mut,
|
||||||
|
constraint = deposit_token.mint == deposit_mint.key(),
|
||||||
|
)]
|
||||||
|
deposit_token: Account<'info, TokenAccount>,
|
||||||
|
#[account(
|
||||||
|
mut,
|
||||||
|
associated_token::authority = authority,
|
||||||
|
associated_token::mint = voting_mint,
|
||||||
|
)]
|
||||||
|
voting_token: Account<'info, TokenAccount>,
|
||||||
|
authority: Signer<'info>,
|
||||||
|
deposit_mint: Account<'info, Mint>,
|
||||||
|
#[account(mut)]
|
||||||
|
voting_mint: Account<'info, Mint>,
|
||||||
|
token_program: Program<'info, Token>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct Withdraw<'info> {
|
||||||
|
#[account(has_one = voting_mint)]
|
||||||
|
registrar: AccountLoader<'info, Registrar>,
|
||||||
|
#[account(mut, has_one = registrar, has_one = authority)]
|
||||||
|
voter: AccountLoader<'info, Voter>,
|
||||||
|
#[account(
|
||||||
|
mut,
|
||||||
|
associated_token::authority = registrar,
|
||||||
|
associated_token::mint = withdraw_mint,
|
||||||
|
)]
|
||||||
|
exchange_vault: Account<'info, TokenAccount>,
|
||||||
|
withdraw_mint: Account<'info, Mint>,
|
||||||
|
#[account(
|
||||||
|
mut,
|
||||||
|
associated_token::authority = authority,
|
||||||
|
associated_token::mint = voting_mint,
|
||||||
|
)]
|
||||||
|
voting_token: Account<'info, TokenAccount>,
|
||||||
|
#[account(mut)]
|
||||||
|
voting_mint: Account<'info, Mint>,
|
||||||
|
#[account(mut)]
|
||||||
|
destination: Account<'info, TokenAccount>,
|
||||||
|
authority: Signer<'info>,
|
||||||
|
token_program: Program<'info, Token>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'info> Withdraw<'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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn thaw_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::ThawAccount<'info>> {
|
||||||
|
let program = self.token_program.to_account_info();
|
||||||
|
let accounts = token::ThawAccount {
|
||||||
|
account: self.voting_token.to_account_info(),
|
||||||
|
mint: self.voting_mint.to_account_info(),
|
||||||
|
authority: self.registrar.to_account_info(),
|
||||||
|
};
|
||||||
|
CpiContext::new(program, accounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn burn_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Burn<'info>> {
|
||||||
|
let program = self.token_program.to_account_info();
|
||||||
|
let accounts = token::Burn {
|
||||||
|
mint: self.voting_mint.to_account_info(),
|
||||||
|
to: self.voting_token.to_account_info(),
|
||||||
|
authority: self.authority.to_account_info(),
|
||||||
|
};
|
||||||
|
CpiContext::new(program, accounts)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
|
@ -370,7 +543,7 @@ pub struct DepositEntry {
|
||||||
pub amount: u64,
|
pub amount: u64,
|
||||||
|
|
||||||
// Locked state.
|
// Locked state.
|
||||||
pub period_count: u64,
|
pub period_count: u32,
|
||||||
pub start_ts: i64,
|
pub start_ts: i64,
|
||||||
pub end_ts: i64,
|
pub end_ts: i64,
|
||||||
}
|
}
|
||||||
|
@ -382,6 +555,19 @@ impl DepositEntry {
|
||||||
let locked_multiplier = 1; // todo
|
let locked_multiplier = 1; // todo
|
||||||
self.amount * locked_multiplier
|
self.amount * locked_multiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the amount of unlocked tokens for this deposit.
|
||||||
|
pub fn vested(&self) -> u64 {
|
||||||
|
// todo
|
||||||
|
self.amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AnchorSerialize, AnchorDeserialize)]
|
||||||
|
pub struct Lockup {
|
||||||
|
pub period_count: u32,
|
||||||
|
pub start_ts: i64,
|
||||||
|
pub end_ts: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Anchor wrapper for the SPL governance program's VoterWeightRecord type.
|
/// Anchor wrapper for the SPL governance program's VoterWeightRecord type.
|
||||||
|
@ -449,4 +635,6 @@ pub enum ErrorCode {
|
||||||
DepositEntryNotFound,
|
DepositEntryNotFound,
|
||||||
DepositEntryFull,
|
DepositEntryFull,
|
||||||
VotingTokenNonZero,
|
VotingTokenNonZero,
|
||||||
|
InvalidDepositId,
|
||||||
|
InsufficientVestedTokens,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,26 @@
|
||||||
import * as assert from 'assert';
|
import * as assert from "assert";
|
||||||
import * as anchor from '@project-serum/anchor';
|
import * as anchor from "@project-serum/anchor";
|
||||||
import { Program } from '@project-serum/anchor';
|
import { Program } from "@project-serum/anchor";
|
||||||
import { createMintAndVault } from '@project-serum/common';
|
import { createMintAndVault } from "@project-serum/common";
|
||||||
import BN from 'bn.js';
|
import BN from "bn.js";
|
||||||
import { PublicKey, Keypair, SystemProgram, SYSVAR_RENT_PUBKEY } from '@solana/web3.js';
|
import {
|
||||||
import { Token, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
PublicKey,
|
||||||
import { GovernanceRegistry } from '../target/types/governance_registry';
|
Keypair,
|
||||||
|
SystemProgram,
|
||||||
|
SYSVAR_RENT_PUBKEY,
|
||||||
|
} from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
Token,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
} from "@solana/spl-token";
|
||||||
|
import { GovernanceRegistry } from "../target/types/governance_registry";
|
||||||
|
|
||||||
describe('voting-rights', () => {
|
describe("voting-rights", () => {
|
||||||
anchor.setProvider(anchor.Provider.env());
|
anchor.setProvider(anchor.Provider.env());
|
||||||
|
|
||||||
const program = anchor.workspace.GovernanceRegistry as Program<GovernanceRegistry>;
|
const program = anchor.workspace
|
||||||
|
.GovernanceRegistry as Program<GovernanceRegistry>;
|
||||||
|
|
||||||
// Initialized variables shared across tests.
|
// Initialized variables shared across tests.
|
||||||
const realm = Keypair.generate().publicKey;
|
const realm = Keypair.generate().publicKey;
|
||||||
|
@ -22,15 +32,16 @@ describe('voting-rights', () => {
|
||||||
|
|
||||||
// Uninitialized variables shared across tests.
|
// Uninitialized variables shared across tests.
|
||||||
let registrar: PublicKey,
|
let registrar: PublicKey,
|
||||||
votingMint: PublicKey,
|
votingMint: PublicKey,
|
||||||
voter: PublicKey,
|
voter: PublicKey,
|
||||||
votingToken: PublicKey,
|
votingToken: PublicKey,
|
||||||
exchangeVaultA: PublicKey,
|
exchangeVaultA: PublicKey,
|
||||||
exchangeVaultB: PublicKey;
|
exchangeVaultB: PublicKey;
|
||||||
let registrarBump: number, votingMintBump: number, voterBump: number;
|
let registrarBump: number, votingMintBump: number, voterBump: number;
|
||||||
let mintA: PublicKey, mintB: PublicKey, godA: PublicKey, godB: PublicKey;
|
let mintA: PublicKey, mintB: PublicKey, godA: PublicKey, godB: PublicKey;
|
||||||
|
let tokenAClient: Token, tokenBClient: Token, votingTokenClient: Token;
|
||||||
|
|
||||||
it('Creates tokens and mints', async () => {
|
it("Creates tokens and mints", async () => {
|
||||||
const decimals = 6;
|
const decimals = 6;
|
||||||
const [_mintA, _godA] = await createMintAndVault(
|
const [_mintA, _godA] = await createMintAndVault(
|
||||||
program.provider,
|
program.provider,
|
||||||
|
@ -51,38 +62,38 @@ describe('voting-rights', () => {
|
||||||
godB = _godB;
|
godB = _godB;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Creates PDAs', async () => {
|
it("Creates PDAs", async () => {
|
||||||
const [_registrar, _registrarBump] = await PublicKey.findProgramAddress(
|
const [_registrar, _registrarBump] = await PublicKey.findProgramAddress(
|
||||||
[realm.toBuffer()],
|
[realm.toBuffer()],
|
||||||
program.programId,
|
program.programId
|
||||||
);
|
);
|
||||||
const [_votingMint, _votingMintBump] = await PublicKey.findProgramAddress(
|
const [_votingMint, _votingMintBump] = await PublicKey.findProgramAddress(
|
||||||
[_registrar.toBuffer()],
|
[_registrar.toBuffer()],
|
||||||
program.programId,
|
program.programId
|
||||||
);
|
);
|
||||||
const [_voter, _voterBump] = await PublicKey.findProgramAddress(
|
const [_voter, _voterBump] = await PublicKey.findProgramAddress(
|
||||||
[_registrar.toBuffer(), program.provider.wallet.publicKey.toBuffer()],
|
[_registrar.toBuffer(), program.provider.wallet.publicKey.toBuffer()],
|
||||||
program.programId,
|
program.programId
|
||||||
);
|
);
|
||||||
votingToken = await Token.getAssociatedTokenAddress(
|
votingToken = await Token.getAssociatedTokenAddress(
|
||||||
associatedTokenProgram,
|
associatedTokenProgram,
|
||||||
tokenProgram,
|
tokenProgram,
|
||||||
_votingMint,
|
_votingMint,
|
||||||
program.provider.wallet.publicKey,
|
program.provider.wallet.publicKey
|
||||||
);
|
);
|
||||||
exchangeVaultA = await Token.getAssociatedTokenAddress(
|
exchangeVaultA = await Token.getAssociatedTokenAddress(
|
||||||
associatedTokenProgram,
|
associatedTokenProgram,
|
||||||
tokenProgram,
|
tokenProgram,
|
||||||
mintA,
|
mintA,
|
||||||
_registrar,
|
_registrar,
|
||||||
true,
|
true
|
||||||
);
|
);
|
||||||
exchangeVaultB = await Token.getAssociatedTokenAddress(
|
exchangeVaultB = await Token.getAssociatedTokenAddress(
|
||||||
associatedTokenProgram,
|
associatedTokenProgram,
|
||||||
tokenProgram,
|
tokenProgram,
|
||||||
mintB,
|
mintB,
|
||||||
_registrar,
|
_registrar,
|
||||||
true,
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
registrar = _registrar;
|
registrar = _registrar;
|
||||||
|
@ -94,23 +105,52 @@ describe('voting-rights', () => {
|
||||||
voterBump = _voterBump;
|
voterBump = _voterBump;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Initializes a registrar', async () => {
|
it("Creates token clients", async () => {
|
||||||
await program.rpc.initRegistrar(registrarBump, votingMintBump, votingMintDecimals, {
|
tokenAClient = new Token(
|
||||||
accounts: {
|
program.provider.connection,
|
||||||
registrar,
|
mintA,
|
||||||
votingMint,
|
TOKEN_PROGRAM_ID,
|
||||||
realm,
|
// @ts-ignore
|
||||||
authority: program.provider.wallet.publicKey,
|
program.provider.wallet.payer
|
||||||
payer: program.provider.wallet.publicKey,
|
);
|
||||||
systemProgram,
|
tokenBClient = new Token(
|
||||||
tokenProgram,
|
program.provider.connection,
|
||||||
rent,
|
mintB,
|
||||||
},
|
TOKEN_PROGRAM_ID,
|
||||||
});
|
// @ts-ignore
|
||||||
|
program.provider.wallet.payer
|
||||||
|
);
|
||||||
|
votingTokenClient = new Token(
|
||||||
|
program.provider.connection,
|
||||||
|
votingMint,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
// @ts-ignore
|
||||||
|
program.provider.wallet.payer
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Initializes a voter', async () => {
|
it("Initializes a registrar", async () => {
|
||||||
await program.rpc.initVoter(voterBump, {
|
await program.rpc.createRegistrar(
|
||||||
|
registrarBump,
|
||||||
|
votingMintBump,
|
||||||
|
votingMintDecimals,
|
||||||
|
{
|
||||||
|
accounts: {
|
||||||
|
registrar,
|
||||||
|
votingMint,
|
||||||
|
realm,
|
||||||
|
authority: program.provider.wallet.publicKey,
|
||||||
|
payer: program.provider.wallet.publicKey,
|
||||||
|
systemProgram,
|
||||||
|
tokenProgram,
|
||||||
|
rent,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Initializes a voter", async () => {
|
||||||
|
await program.rpc.createVoter(voterBump, {
|
||||||
accounts: {
|
accounts: {
|
||||||
voter,
|
voter,
|
||||||
votingToken,
|
votingToken,
|
||||||
|
@ -121,17 +161,17 @@ describe('voting-rights', () => {
|
||||||
associatedTokenProgram,
|
associatedTokenProgram,
|
||||||
tokenProgram,
|
tokenProgram,
|
||||||
rent,
|
rent,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds an exchange rate', async () => {
|
it("Adds an exchange rate", async () => {
|
||||||
const er = {
|
const er = {
|
||||||
isUsed: false,
|
isUsed: false,
|
||||||
mint: mintA,
|
mint: mintA,
|
||||||
rate: new BN(1),
|
rate: new BN(1),
|
||||||
}
|
};
|
||||||
await program.rpc.addExchangeRate(er, {
|
await program.rpc.createExchangeRate(er, {
|
||||||
accounts: {
|
accounts: {
|
||||||
exchangeVault: exchangeVaultA,
|
exchangeVault: exchangeVaultA,
|
||||||
depositMint: mintA,
|
depositMint: mintA,
|
||||||
|
@ -142,36 +182,66 @@ describe('voting-rights', () => {
|
||||||
tokenProgram,
|
tokenProgram,
|
||||||
associatedTokenProgram,
|
associatedTokenProgram,
|
||||||
systemProgram,
|
systemProgram,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Deposits unlocked A tokens', async () => {
|
it("Deposits unlocked A tokens", async () => {
|
||||||
const amount = new BN(10);
|
const amount = new BN(10);
|
||||||
await program.rpc.deposit(amount, {
|
await program.rpc.createDeposit(amount, null, {
|
||||||
accounts: {
|
accounts: {
|
||||||
|
deposit: {
|
||||||
|
voter,
|
||||||
|
exchangeVault: exchangeVaultA,
|
||||||
|
depositToken: godA,
|
||||||
|
votingToken,
|
||||||
|
authority: program.provider.wallet.publicKey,
|
||||||
|
registrar,
|
||||||
|
depositMint: mintA,
|
||||||
|
votingMint,
|
||||||
|
tokenProgram,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const voterAccount = await program.account.voter.fetch(voter);
|
||||||
|
const deposit = voterAccount.deposits[0];
|
||||||
|
assert.ok(deposit.isUsed);
|
||||||
|
assert.ok(deposit.amount.toNumber() === 10);
|
||||||
|
assert.ok(deposit.rateIdx === 0);
|
||||||
|
|
||||||
|
const vtAccount = await votingTokenClient.getAccountInfo(votingToken);
|
||||||
|
assert.ok(vtAccount.amount.toNumber() === 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Withdraws unlocked A tokens", async () => {
|
||||||
|
const depositId = 0;
|
||||||
|
const amount = new BN(10);
|
||||||
|
await program.rpc.withdraw(depositId, amount, {
|
||||||
|
accounts: {
|
||||||
|
registrar,
|
||||||
voter,
|
voter,
|
||||||
exchangeVault: exchangeVaultA,
|
exchangeVault: exchangeVaultA,
|
||||||
depositToken: godA,
|
withdrawMint: mintA,
|
||||||
votingToken,
|
votingToken,
|
||||||
authority: program.provider.wallet.publicKey,
|
|
||||||
registrar,
|
|
||||||
depositMint: mintA,
|
|
||||||
votingMint,
|
votingMint,
|
||||||
|
destination: godA,
|
||||||
|
authority: program.provider.wallet.publicKey,
|
||||||
tokenProgram,
|
tokenProgram,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const voterAccount = await program.account.voter.fetch(voter);
|
const voterAccount = await program.account.voter.fetch(voter);
|
||||||
console.log(voterAccount);
|
const deposit = voterAccount.deposits[0];
|
||||||
|
assert.ok(deposit.isUsed);
|
||||||
|
assert.ok(deposit.amount.toNumber() === 0);
|
||||||
|
assert.ok(deposit.rateIdx === 0);
|
||||||
|
|
||||||
|
const vtAccount = await votingTokenClient.getAccountInfo(votingToken);
|
||||||
|
assert.ok(vtAccount.amount.toNumber() === 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it ('Deposits locked tokens', async () => {
|
it("Deposits locked A tokens", async () => {
|
||||||
|
// todo
|
||||||
});
|
|
||||||
|
|
||||||
it('Mints voting rights', async () => {
|
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -972,6 +972,11 @@ picomatch@^2.0.4, picomatch@^2.2.1:
|
||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
|
||||||
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
|
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
|
||||||
|
|
||||||
|
prettier@^2.4.1:
|
||||||
|
version "2.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c"
|
||||||
|
integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==
|
||||||
|
|
||||||
randombytes@^2.1.0:
|
randombytes@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
||||||
|
|
Loading…
Reference in New Issue