Decompose deposits into separate instructions

This commit is contained in:
armaniferrante 2021-10-16 09:47:57 -07:00
parent 627d572774
commit 8db7c5b449
No known key found for this signature in database
GPG Key ID: 58BEF301E91F7828
4 changed files with 413 additions and 148 deletions

View File

@ -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"
}
}

View File

@ -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<InitRegistrar>,
pub fn create_registrar(
ctx: Context<CreateRegistrar>,
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<InitVoter>, voter_bump: u8) -> Result<()> {
pub fn create_voter(ctx: Context<CreateVoter>, 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<AddExchangeRate>, er: ExchangeRateEntry) -> Result<()> {
pub fn create_exchange_rate(
ctx: Context<CreateExchangeRate>,
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<Deposit>, amount: u64) -> Result<()> {
/// Creates a new deposit entry and updates it by transferring in tokens.
pub fn create_deposit(
ctx: Context<CreateDeposit>,
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 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<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(())
}
/// 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<UpdateSchedule>) -> 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,
}

View File

@ -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<GovernanceRegistry>;
const program = anchor.workspace
.GovernanceRegistry as Program<GovernanceRegistry>;
// 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
});
});

View File

@ -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"