From 8db7c5b4491c1234eab72a2d63223ff9959fae2a Mon Sep 17 00:00:00 2001 From: armaniferrante Date: Sat, 16 Oct 2021 09:47:57 -0700 Subject: [PATCH] Decompose deposits into separate instructions --- package.json | 34 +-- programs/governance-registry/src/lib.rs | 336 ++++++++++++++++++------ tests/governance-registry.ts | 186 +++++++++---- yarn.lock | 5 + 4 files changed, 413 insertions(+), 148 deletions(-) diff --git a/package.json b/package.json index 250c94e..2c8b1ec 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,20 @@ { - "scripts": { - "test": "anchor test" - }, - "dependencies": { - "@project-serum/anchor": "^0.17.1-beta.1", - "@project-serum/common": "^0.0.1-beta.3", - "@solana/spl-token": "^0.1.8", - "bn.js": "^5.2.0" - }, - "devDependencies": { - "@types/mocha": "^9.0.0", - "chai": "^4.3.4", - "mocha": "^9.0.3", - "ts-mocha": "^8.0.0", - "typescript": "^4.3.5" - } + "scripts": { + "lint:fix": "prettier tests/** -w", + "test": "anchor test" + }, + "dependencies": { + "@project-serum/anchor": "^0.17.1-beta.1", + "@project-serum/common": "^0.0.1-beta.3", + "@solana/spl-token": "^0.1.8", + "bn.js": "^5.2.0" + }, + "devDependencies": { + "@types/mocha": "^9.0.0", + "chai": "^4.3.4", + "mocha": "^9.0.3", + "prettier": "^2.4.1", + "ts-mocha": "^8.0.0", + "typescript": "^4.3.5" + } } diff --git a/programs/governance-registry/src/lib.rs b/programs/governance-registry/src/lib.rs index 946427f..5addc3c 100644 --- a/programs/governance-registry/src/lib.rs +++ b/programs/governance-registry/src/lib.rs @@ -10,14 +10,59 @@ use std::ops::Deref; 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] pub mod governance_registry { use super::*; /// Creates a new voting registrar. There can only be a single regsitrar /// per governance realm. - pub fn init_registrar( - ctx: Context, + pub fn create_registrar( + ctx: Context, registrar_bump: u8, voting_mint_bump: 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 /// user wallet. - pub fn init_voter(ctx: Context, voter_bump: u8) -> Result<()> { + pub fn create_voter(ctx: Context, voter_bump: u8) -> Result<()> { let voter = &mut ctx.accounts.voter.load_init()?; voter.voter_bump = voter_bump; 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 /// deposit the mint in exchange for vTokens. There can only be a single /// exchange rate per mint. - pub fn add_exchange_rate(ctx: Context, er: ExchangeRateEntry) -> Result<()> { + pub fn create_exchange_rate( + ctx: Context, + er: ExchangeRateEntry, + ) -> Result<()> { require!(er.rate > 0, InvalidRate); let mut er = er; @@ -62,10 +110,55 @@ pub mod governance_registry { Ok(()) } - /// Deposits 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 deposit(ctx: Context, amount: u64) -> Result<()> { + /// Creates a new deposit entry and updates it by transferring in tokens. + pub fn create_deposit( + ctx: Context, + amount: u64, + lockup: Option, + ) -> 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, id: u8, amount: u64) -> Result<()> { let registrar = &ctx.accounts.registrar.load()?; let voter = &mut ctx.accounts.voter.load_mut()?; @@ -77,31 +170,10 @@ pub mod governance_registry { .ok_or(ErrorCode::ExchangeRateEntryNotFound)?; let er_entry = registrar.rates[er_idx]; - // Get the deposit entry associated with this deposit. - let deposit_entry = { - 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], - } - }; + require!(voter.deposits.len() > id as usize, InvalidDepositId); + let d_entry = &mut voter.deposits[id as usize]; - // Update the amount deposited. - deposit_entry.amount += amount; + d_entry.amount += amount; // Calculate the amount of voting tokens to mint at the specified // exchange rate. @@ -130,13 +202,53 @@ pub mod governance_registry { /// Withdraws tokens from a deposit entry, if they are unlocked according /// to a vesting schedule. - pub fn withdraw(ctx: Context, amount: u64) -> Result<()> { - // todo + /// + /// `amount` is in units of the native currency being withdrawn. + pub fn withdraw(ctx: Context, 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(()) } - /// Updates a vesting schedule. Can only increase the lockup time. If all - /// tokens are unlocked, then the period count can also be updated. + /// Updates a vesting schedule. Can only increase the lockup time or reduce + /// 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) -> Result<()> { // todo Ok(()) @@ -165,7 +277,7 @@ pub mod governance_registry { #[derive(Accounts)] #[instruction(registrar_bump: u8, voting_mint_bump: u8, voting_mint_decimals: u8)] -pub struct InitRegistrar<'info> { +pub struct CreateRegistrar<'info> { #[account( init, seeds = [realm.key().as_ref()], @@ -180,8 +292,8 @@ pub struct InitRegistrar<'info> { bump = voting_mint_bump, payer = payer, mint::authority = registrar, - mint::decimals = voting_mint_decimals, mint::freeze_authority = registrar, + mint::decimals = voting_mint_decimals, )] voting_mint: Account<'info, Mint>, realm: UncheckedAccount<'info>, @@ -194,7 +306,7 @@ pub struct InitRegistrar<'info> { #[derive(Accounts)] #[instruction(voter_bump: u8)] -pub struct InitVoter<'info> { +pub struct CreateVoter<'info> { #[account( init, seeds = [registrar.key().as_ref(), authority.key().as_ref()], @@ -204,11 +316,11 @@ pub struct InitVoter<'info> { )] voter: AccountLoader<'info, Voter>, #[account( - init, - payer = authority, - associated_token::authority = authority, - associated_token::mint = voting_mint, - )] + init, + payer = authority, + associated_token::authority = authority, + associated_token::mint = voting_mint, + )] voting_token: Account<'info, TokenAccount>, voting_mint: Account<'info, Mint>, registrar: AccountLoader<'info, Registrar>, @@ -221,7 +333,7 @@ pub struct InitVoter<'info> { #[derive(Accounts)] #[instruction(rate: ExchangeRateEntry)] -pub struct AddExchangeRate<'info> { +pub struct CreateExchangeRate<'info> { #[account( init, payer = payer, @@ -241,34 +353,11 @@ pub struct AddExchangeRate<'info> { } #[derive(Accounts)] -pub struct Deposit<'info> { - #[account(mut, has_one = authority)] - 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>, +pub struct CreateDeposit<'info> { + deposit: UpdateDeposit<'info>, } -impl<'info> Deposit<'info> { +impl<'info> UpdateDeposit<'info> { fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> { let program = self.token_program.to_account_info(); let accounts = token::Transfer { @@ -301,8 +390,92 @@ impl<'info> Deposit<'info> { } #[derive(Accounts)] -pub struct Withdraw { - // todo +pub struct UpdateDeposit<'info> { + #[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)] @@ -370,7 +543,7 @@ pub struct DepositEntry { pub amount: u64, // Locked state. - pub period_count: u64, + pub period_count: u32, pub start_ts: i64, pub end_ts: i64, } @@ -382,6 +555,19 @@ impl DepositEntry { let locked_multiplier = 1; // todo 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. @@ -449,4 +635,6 @@ pub enum ErrorCode { DepositEntryNotFound, DepositEntryFull, VotingTokenNonZero, + InvalidDepositId, + InsufficientVestedTokens, } diff --git a/tests/governance-registry.ts b/tests/governance-registry.ts index 4250aef..7aa800d 100644 --- a/tests/governance-registry.ts +++ b/tests/governance-registry.ts @@ -1,16 +1,26 @@ -import * as assert from 'assert'; -import * as anchor from '@project-serum/anchor'; -import { Program } from '@project-serum/anchor'; -import { createMintAndVault } from '@project-serum/common'; -import BN from 'bn.js'; -import { PublicKey, 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'; +import * as assert from "assert"; +import * as anchor from "@project-serum/anchor"; +import { Program } from "@project-serum/anchor"; +import { createMintAndVault } from "@project-serum/common"; +import BN from "bn.js"; +import { + PublicKey, + 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()); - const program = anchor.workspace.GovernanceRegistry as Program; + const program = anchor.workspace + .GovernanceRegistry as Program; // Initialized variables shared across tests. const realm = Keypair.generate().publicKey; @@ -22,15 +32,16 @@ describe('voting-rights', () => { // Uninitialized variables shared across tests. let registrar: PublicKey, - votingMint: PublicKey, - voter: PublicKey, - votingToken: PublicKey, - exchangeVaultA: PublicKey, - exchangeVaultB: PublicKey; + votingMint: PublicKey, + voter: PublicKey, + votingToken: PublicKey, + exchangeVaultA: PublicKey, + exchangeVaultB: PublicKey; let registrarBump: number, votingMintBump: number, voterBump: number; 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 [_mintA, _godA] = await createMintAndVault( program.provider, @@ -51,38 +62,38 @@ describe('voting-rights', () => { godB = _godB; }); - it('Creates PDAs', async () => { + it("Creates PDAs", async () => { const [_registrar, _registrarBump] = await PublicKey.findProgramAddress( [realm.toBuffer()], - program.programId, + program.programId ); const [_votingMint, _votingMintBump] = await PublicKey.findProgramAddress( [_registrar.toBuffer()], - program.programId, + program.programId ); const [_voter, _voterBump] = await PublicKey.findProgramAddress( [_registrar.toBuffer(), program.provider.wallet.publicKey.toBuffer()], - program.programId, + program.programId ); votingToken = await Token.getAssociatedTokenAddress( associatedTokenProgram, tokenProgram, _votingMint, - program.provider.wallet.publicKey, + program.provider.wallet.publicKey ); exchangeVaultA = await Token.getAssociatedTokenAddress( associatedTokenProgram, tokenProgram, mintA, _registrar, - true, + true ); exchangeVaultB = await Token.getAssociatedTokenAddress( associatedTokenProgram, tokenProgram, mintB, _registrar, - true, + true ); registrar = _registrar; @@ -94,23 +105,52 @@ describe('voting-rights', () => { voterBump = _voterBump; }); - it('Initializes a registrar', async () => { - await program.rpc.initRegistrar(registrarBump, votingMintBump, votingMintDecimals, { - accounts: { - registrar, - votingMint, - realm, - authority: program.provider.wallet.publicKey, - payer: program.provider.wallet.publicKey, - systemProgram, - tokenProgram, - rent, - }, - }); + it("Creates token clients", async () => { + tokenAClient = new Token( + program.provider.connection, + mintA, + TOKEN_PROGRAM_ID, + // @ts-ignore + program.provider.wallet.payer + ); + tokenBClient = new Token( + program.provider.connection, + 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 () => { - await program.rpc.initVoter(voterBump, { + it("Initializes a registrar", async () => { + 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: { voter, votingToken, @@ -121,17 +161,17 @@ describe('voting-rights', () => { associatedTokenProgram, tokenProgram, rent, - } + }, }); }); - it('Adds an exchange rate', async () => { + it("Adds an exchange rate", async () => { const er = { isUsed: false, mint: mintA, rate: new BN(1), - } - await program.rpc.addExchangeRate(er, { + }; + await program.rpc.createExchangeRate(er, { accounts: { exchangeVault: exchangeVaultA, depositMint: mintA, @@ -142,36 +182,66 @@ describe('voting-rights', () => { tokenProgram, associatedTokenProgram, systemProgram, - } - }) + }, + }); }); - it('Deposits unlocked A tokens', async () => { + it("Deposits unlocked A tokens", async () => { const amount = new BN(10); - await program.rpc.deposit(amount, { + await program.rpc.createDeposit(amount, null, { 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, exchangeVault: exchangeVaultA, - depositToken: godA, + withdrawMint: mintA, votingToken, - authority: program.provider.wallet.publicKey, - registrar, - depositMint: mintA, votingMint, + destination: godA, + authority: program.provider.wallet.publicKey, tokenProgram, }, }); + 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('Mints voting rights', async () => { - + it("Deposits locked A tokens", async () => { + // todo }); }); diff --git a/yarn.lock b/yarn.lock index 2a45239..73f2954 100644 --- a/yarn.lock +++ b/yarn.lock @@ -972,6 +972,11 @@ picomatch@^2.0.4, picomatch@^2.2.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" 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: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"