From 59ddc226b358f75b076e4aec15f8e224073b106b Mon Sep 17 00:00:00 2001 From: Zach Petersen Date: Mon, 28 Oct 2024 22:26:35 -0500 Subject: [PATCH] Use Token-2022 --- README.md | 32 ++++- .../src/instructions/accept_admin_transfer.rs | 6 +- .../src/instructions/add_minter.rs | 6 +- .../instructions/add_whitelisted_address.rs | 6 +- .../src/instructions/get_remaining_amount.rs | 6 +- .../src/instructions/mint_token.rs | 16 +-- .../remove_whitelisted_address.rs | 6 +- .../src/instructions/start_admin_transfer.rs | 6 +- .../src/instructions/update_rate_limit.rs | 6 +- programs/minter-controller/src/lib.rs | 2 +- tests/minter_controller.ts | 111 +++++++++++++----- 11 files changed, 136 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index a30285c..532e517 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,39 @@ # Solana Programs Solana programs developed at Paxos +## Minter Controller +`minter_controller` is used to add transaction controls to Solana mint authorities for the token and token-2022 program. +The transaction controls include rate limitings and whitelisting. The program requires the token mint authority +to be a [spl token multisig](https://spl.solana.com/token#example-mint-with-multisig-authority). + +### Usage + +#### Add Minter +The program is first used by calling the `add_minter` instruction which creates a PDA that can be used to mint tokens. +The PDA is derived from the `minter_authority` and `mint_account`. +The minter PDA should be added as a signer on the spl token multisig. +An `admin` Pubkey is also specified to update the transaction controls. + +#### Whitelisting +Whitelisted addresses which can be minted to can be added via `add_whitelisted_address`. Whitelisted addresses can be +removed via `remove_whitelisted_address`. These instructions can only be called by the `admin`. + +#### Rate limiting +The initial rate limit is specified when calling `add_minter`. It can also be updated via `update_rate_limit`. +These instructions can only be called by the `admin`. + +#### Update admin +Updating the admin is a two step process. Step 1 is to call `start_admin_transfer` with the current `admin`. +Step 2 is for the new admin to call `accept_admin_transfer`. + +#### Minting tokens +Minting tokens is done by calling `mint_tokens`. This instruction must be signed with the `minter_authority`. + + ## Development ### Required installs -- rust 1.79.0 (Installed via asdf) +- rust 1.79.0 - [solana](https://docs.solanalabs.com/cli/install) 1.18.18 - [anchor](https://book.anchor-lang.com/getting_started/installation.html) 0.30.1 @@ -13,6 +42,7 @@ Solana programs developed at Paxos ### Test `npm install` + `anchor test` ### Deploy diff --git a/programs/minter-controller/src/instructions/accept_admin_transfer.rs b/programs/minter-controller/src/instructions/accept_admin_transfer.rs index 140c576..9b4877b 100644 --- a/programs/minter-controller/src/instructions/accept_admin_transfer.rs +++ b/programs/minter-controller/src/instructions/accept_admin_transfer.rs @@ -1,9 +1,7 @@ use { anchor_lang::prelude::*, - anchor_spl::{ - token::{ Mint }, - }, + anchor_spl::token_interface::{Mint}, }; use crate::*; @@ -21,7 +19,7 @@ pub struct AcceptAdminTransfer<'info> { // Mint account address is a PDA #[account()] - pub mint_account: Account<'info, Mint>, + pub mint_account: InterfaceAccount<'info, Mint>, #[account( mut, diff --git a/programs/minter-controller/src/instructions/add_minter.rs b/programs/minter-controller/src/instructions/add_minter.rs index 4fdba19..8fb67c3 100644 --- a/programs/minter-controller/src/instructions/add_minter.rs +++ b/programs/minter-controller/src/instructions/add_minter.rs @@ -1,8 +1,6 @@ use { anchor_lang::prelude::*, - anchor_spl::{ - token::{ Mint }, - }, + anchor_spl::token_interface::{Mint}, }; use crate::*; @@ -16,7 +14,7 @@ pub struct AddMinter<'info> { // Mint account address is a PDA #[account()] - pub mint_account: Account<'info, Mint>, + pub mint_account: InterfaceAccount<'info, Mint>, #[account( init, diff --git a/programs/minter-controller/src/instructions/add_whitelisted_address.rs b/programs/minter-controller/src/instructions/add_whitelisted_address.rs index 9dcfb63..1e4538f 100644 --- a/programs/minter-controller/src/instructions/add_whitelisted_address.rs +++ b/programs/minter-controller/src/instructions/add_whitelisted_address.rs @@ -1,8 +1,6 @@ use { anchor_lang::prelude::*, - anchor_spl::{ - token::{ Mint }, - }, + anchor_spl::token_interface::{Mint}, }; use crate::*; @@ -20,7 +18,7 @@ pub struct AddWhitelistedAddress<'info> { // Mint account address is a PDA #[account()] - pub mint_account: Account<'info, Mint>, + pub mint_account: InterfaceAccount<'info, Mint>, #[account( has_one = admin, diff --git a/programs/minter-controller/src/instructions/get_remaining_amount.rs b/programs/minter-controller/src/instructions/get_remaining_amount.rs index 8eb472d..00d51af 100644 --- a/programs/minter-controller/src/instructions/get_remaining_amount.rs +++ b/programs/minter-controller/src/instructions/get_remaining_amount.rs @@ -1,8 +1,6 @@ use { anchor_lang::prelude::*, - anchor_spl::{ - token::{ Mint }, - }, + anchor_spl::token_interface::{Mint}, }; use crate::*; @@ -17,7 +15,7 @@ pub struct GetRemainingAmount<'info> { // Mint account address is a PDA #[account()] - pub mint_account: Account<'info, Mint>, + pub mint_account: InterfaceAccount<'info, Mint>, #[account( mut, diff --git a/programs/minter-controller/src/instructions/mint_token.rs b/programs/minter-controller/src/instructions/mint_token.rs index 1693f67..9fcbe12 100644 --- a/programs/minter-controller/src/instructions/mint_token.rs +++ b/programs/minter-controller/src/instructions/mint_token.rs @@ -2,11 +2,10 @@ use { anchor_lang::prelude::*, anchor_spl::{ associated_token::AssociatedToken, - token::{Mint, Token, TokenAccount}, + token_interface::{Mint, TokenInterface, TokenAccount} }, }; -pub use anchor_spl::token::spl_token; -pub use anchor_spl::token::spl_token::ID as splId; +pub use anchor_spl::token_2022::spl_token_2022; use crate::*; #[derive(Accounts)] @@ -23,7 +22,7 @@ pub struct MintToken<'info> { // Mint account address is a PDA #[account(mut)] - pub mint_account: Account<'info, Mint>, + pub mint_account: InterfaceAccount<'info, Mint>, #[account( mut, @@ -48,10 +47,11 @@ pub struct MintToken<'info> { payer = payer, associated_token::mint = mint_account, associated_token::authority = to_address, + associated_token::token_program = token_program )] - pub associated_token_account: Account<'info, TokenAccount>, + pub associated_token_account: InterfaceAccount<'info, TokenAccount>, - pub token_program: Program<'info, Token>, + pub token_program: Interface<'info, TokenInterface>, pub associated_token_program: Program<'info, AssociatedToken>, pub system_program: Program<'info, System>, } @@ -68,8 +68,8 @@ pub fn mint_token(ctx: Context, amount: u64) -> Result<()> { // Invoke the mint_to instruction on the token program // Anchor does not implement the token multisig so we have to do it here manually. - let ix = spl_token::instruction::mint_to( - &splId, + let ix = spl_token_2022::instruction::mint_to( + ctx.accounts.token_program.to_account_info().key, ctx.accounts.mint_account.to_account_info().key, ctx.accounts.associated_token_account.to_account_info().key, ctx.accounts.mint_multisig.to_account_info().key, diff --git a/programs/minter-controller/src/instructions/remove_whitelisted_address.rs b/programs/minter-controller/src/instructions/remove_whitelisted_address.rs index 772733d..c08f99a 100644 --- a/programs/minter-controller/src/instructions/remove_whitelisted_address.rs +++ b/programs/minter-controller/src/instructions/remove_whitelisted_address.rs @@ -1,8 +1,6 @@ use { anchor_lang::prelude::*, - anchor_spl::{ - token::{ Mint }, - }, + anchor_spl::token_interface::{Mint}, }; use crate::*; @@ -20,7 +18,7 @@ pub struct RemoveWhitelistedAddress<'info> { // Mint account address is a PDA #[account()] - pub mint_account: Account<'info, Mint>, + pub mint_account: InterfaceAccount<'info, Mint>, #[account( has_one = admin, diff --git a/programs/minter-controller/src/instructions/start_admin_transfer.rs b/programs/minter-controller/src/instructions/start_admin_transfer.rs index 520de0a..563cf21 100644 --- a/programs/minter-controller/src/instructions/start_admin_transfer.rs +++ b/programs/minter-controller/src/instructions/start_admin_transfer.rs @@ -1,9 +1,7 @@ use { anchor_lang::prelude::*, - anchor_spl::{ - token::{ Mint }, - }, + anchor_spl::token_interface::{Mint}, }; use crate::*; @@ -21,7 +19,7 @@ pub struct StartAdminTransfer<'info> { // Mint account address is a PDA #[account()] - pub mint_account: Account<'info, Mint>, + pub mint_account: InterfaceAccount<'info, Mint>, #[account( mut, diff --git a/programs/minter-controller/src/instructions/update_rate_limit.rs b/programs/minter-controller/src/instructions/update_rate_limit.rs index fac728a..4df3513 100644 --- a/programs/minter-controller/src/instructions/update_rate_limit.rs +++ b/programs/minter-controller/src/instructions/update_rate_limit.rs @@ -1,9 +1,7 @@ use { anchor_lang::prelude::*, - anchor_spl::{ - token::{ Mint }, - }, + anchor_spl::token_interface::{Mint}, }; use crate::*; @@ -21,7 +19,7 @@ pub struct UpdateRateLimit<'info> { // Mint account address is a PDA #[account()] - pub mint_account: Account<'info, Mint>, + pub mint_account: InterfaceAccount<'info, Mint>, #[account( mut, diff --git a/programs/minter-controller/src/lib.rs b/programs/minter-controller/src/lib.rs index af75952..876a17d 100644 --- a/programs/minter-controller/src/lib.rs +++ b/programs/minter-controller/src/lib.rs @@ -6,7 +6,7 @@ use state::*; pub mod instructions; pub mod state; -declare_id!("wKv26ghpb9Dtxd4ba8RtAndZKuGh1HjrEU2FRSFSPN6"); +declare_id!("DWGSMEEzjofw9Xd1rSUHr71HNgtmotoUbWuqRyfLcMWk"); #[program] diff --git a/tests/minter_controller.ts b/tests/minter_controller.ts index 45ef825..c52d7d8 100644 --- a/tests/minter_controller.ts +++ b/tests/minter_controller.ts @@ -1,7 +1,7 @@ import * as anchor from '@coral-xyz/anchor' import { BorshCoder, EventParser, Program } from '@coral-xyz/anchor' import { Keypair, PublicKey, ConfirmOptions, Transaction, sendAndConfirmTransaction, TransactionSignature } from '@solana/web3.js' -import { getAssociatedTokenAddressSync, createMultisig, createMint, createSetAuthorityInstruction, AuthorityType } from '@solana/spl-token'; +import { getAssociatedTokenAddressSync, createMultisig, createMint, createSetAuthorityInstruction, AuthorityType, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token'; import { expect, assert } from 'chai' import * as borsh from "borsh"; import { minterController } from '../target/types/minter_controller' @@ -97,7 +97,10 @@ describe('minter_controller', () => { minterAuthorityKeypair, //Payer adminKeypair.publicKey, //Mint authority adminKeypair.publicKey, //Freeze authority - 9 + 9, + undefined, + undefined, + TOKEN_2022_PROGRAM_ID ); mintPDAToken2 = await createMint( @@ -105,7 +108,10 @@ describe('minter_controller', () => { minterAuthorityKeypair, //Payer adminKeypair.publicKey, //Mint authority adminKeypair.publicKey, //Freeze authority - 9 + 9, + undefined, + undefined, + TOKEN_2022_PROGRAM_ID ); //Create PDAs @@ -120,12 +126,18 @@ describe('minter_controller', () => { minterAuthorityKeypair, [adminKeypair.publicKey, minterPDA], 1, + undefined, + undefined, + TOKEN_2022_PROGRAM_ID ) mintMultisigAddr2 = await createMultisig( provider.connection, minterAuthorityKeypair, [adminKeypair.publicKey, minterPDAToken2], 1, + undefined, + undefined, + TOKEN_2022_PROGRAM_ID ) //Update token mint authority @@ -134,7 +146,9 @@ describe('minter_controller', () => { mintPDA, adminKeypair.publicKey, //Current authority AuthorityType.MintTokens, //Mint Authority type - mintMultisigAddr + mintMultisigAddr, + [], + TOKEN_2022_PROGRAM_ID )); await sendAndConfirmTransaction(provider.connection, transaction, [adminKeypair]); @@ -143,12 +157,14 @@ describe('minter_controller', () => { mintPDAToken2, adminKeypair.publicKey, //Current authority AuthorityType.MintTokens, //Mint Authority type - mintMultisigAddr2 + mintMultisigAddr2, + [], + TOKEN_2022_PROGRAM_ID )); await sendAndConfirmTransaction(provider.connection, transaction2, [adminKeypair]); - associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, badMinterAuthorityKeypair.publicKey); - associatedTokenAccountAddressToken2 = getAssociatedTokenAddressSync(mintPDAToken2, badMinterAuthorityKeypair.publicKey); + associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, badMinterAuthorityKeypair.publicKey, true, TOKEN_2022_PROGRAM_ID); + associatedTokenAccountAddressToken2 = getAssociatedTokenAddressSync(mintPDAToken2, badMinterAuthorityKeypair.publicKey, true, TOKEN_2022_PROGRAM_ID); }) @@ -485,7 +501,7 @@ describe('minter_controller', () => { it('Can successfully mint tokens', async () => { // Derive the associated token address account for the mint and payer. - const associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, payer.publicKey); + const associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, payer.publicKey, true, TOKEN_2022_PROGRAM_ID); let foundEvent = false try { @@ -497,7 +513,8 @@ describe('minter_controller', () => { toAddress: payer.publicKey, associatedTokenAccount: associatedTokenAccountAddress, mintAccount: mintPDA, - mintMultisig: mintMultisigAddr + mintMultisig: mintMultisigAddr, + tokenProgram: TOKEN_2022_PROGRAM_ID, }) .signers([minterAuthorityKeypair]) .rpc(); @@ -511,7 +528,7 @@ describe('minter_controller', () => { } catch (err) { console.log('Got an error') console.log(err) - assert.fail('Error not expected while minting with minterPDA') + assert.fail('Error not expected while minting with minterPDA') } let tokenAmount = await provider.connection.getTokenAccountBalance(associatedTokenAccountAddress); @@ -519,9 +536,33 @@ describe('minter_controller', () => { assert.isTrue(foundEvent) }); + it('Cannot mint using token program with TOKEN-2022 mint account', async () => { + // Derive the associated token address account for the mint and payer. + const associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, payer.publicKey, true, TOKEN_PROGRAM_ID); + + try { + const mintTokenSignature = await minterControllerProgram.methods + .mintToken(capacity) + .accounts({ + payer: minterAuthorityKeypair.publicKey, + minterAuthority: minterAuthorityKeypair.publicKey, + toAddress: payer.publicKey, + associatedTokenAccount: associatedTokenAccountAddress, + mintAccount: mintPDA, + mintMultisig: mintMultisigAddr, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .signers([minterAuthorityKeypair]) + .rpc(); + assert.fail('Should fail when using TOKEN_PROGRAM_ID') + } catch (err) { + assert.isTrue(err.toString().includes('incorrect program id for instruction')) + } + }); + it('Cannot mint if rate limit exceeded', async () => { // Derive the associated token address account for the mint and payer. - const associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, payer.publicKey); + const associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, payer.publicKey, true, TOKEN_2022_PROGRAM_ID); try { const mintTokenSignature = await minterControllerProgram.methods @@ -532,7 +573,8 @@ describe('minter_controller', () => { toAddress: payer.publicKey, associatedTokenAccount: associatedTokenAccountAddress, mintAccount: mintPDA, - mintMultisig: mintMultisigAddr + mintMultisig: mintMultisigAddr, + tokenProgram: TOKEN_2022_PROGRAM_ID, }) .signers([minterAuthorityKeypair]) .rpc(); @@ -547,7 +589,7 @@ describe('minter_controller', () => { it('Cannot mint if rate limit exceeded over time period', async () => { // Derive the associated token address account for the mint and payer. - const associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, payer.publicKey); + const associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, payer.publicKey, true, TOKEN_2022_PROGRAM_ID); try { const mintTokenSignature = await minterControllerProgram.methods @@ -558,7 +600,8 @@ describe('minter_controller', () => { toAddress: payer.publicKey, associatedTokenAccount: associatedTokenAccountAddress, mintAccount: mintPDA, - mintMultisig: mintMultisigAddr + mintMultisig: mintMultisigAddr, + tokenProgram: TOKEN_2022_PROGRAM_ID, }) .signers([minterAuthorityKeypair]) .rpc(); @@ -575,7 +618,8 @@ describe('minter_controller', () => { toAddress: payer.publicKey, associatedTokenAccount: associatedTokenAccountAddress, mintAccount: mintPDA, - mintMultisig: mintMultisigAddr + mintMultisig: mintMultisigAddr, + tokenProgram: TOKEN_2022_PROGRAM_ID, }) .signers([minterAuthorityKeypair]) .rpc(); @@ -596,7 +640,8 @@ describe('minter_controller', () => { toAddress: payer.publicKey, associatedTokenAccount: associatedTokenAccountAddress, mintAccount: mintPDA, - mintMultisig: mintMultisigAddr + mintMultisig: mintMultisigAddr, + tokenProgram: TOKEN_2022_PROGRAM_ID, }) .signers([minterAuthorityKeypair]) .rpc(); @@ -607,7 +652,7 @@ describe('minter_controller', () => { it('Can successfully mint tokens for second token using other multisig', async () => { // Derive the associated token address account for the mint and payer. - const associatedTokenAccountAddressToken2 = getAssociatedTokenAddressSync(mintPDAToken2, payer.publicKey); + const associatedTokenAccountAddressToken2 = getAssociatedTokenAddressSync(mintPDAToken2, payer.publicKey, true, TOKEN_2022_PROGRAM_ID); try { const mintTokenSignature = await minterControllerProgram.methods .mintToken(amount) @@ -617,7 +662,8 @@ describe('minter_controller', () => { toAddress: payer.publicKey, associatedTokenAccount: associatedTokenAccountAddressToken2, mintAccount: mintPDAToken2, - mintMultisig: mintMultisigAddr2 + mintMultisig: mintMultisigAddr2, + tokenProgram: TOKEN_2022_PROGRAM_ID, }) .signers([minterAuthorityKeypair]) .rpc(); @@ -634,7 +680,7 @@ describe('minter_controller', () => { it('Cannot mint tokens with non minter', async () => { // Derive the associated token address account for the mint and payer. - const associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, badMinterAuthorityKeypair.publicKey); + const associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, badMinterAuthorityKeypair.publicKey, true, TOKEN_2022_PROGRAM_ID); try { const mintTokenSignature = await minterControllerProgram.methods @@ -645,7 +691,8 @@ describe('minter_controller', () => { toAddress: badMinterAuthorityKeypair.publicKey, associatedTokenAccount: associatedTokenAccountAddress, mintAccount: mintPDA, - mintMultisig: mintMultisigAddr + mintMultisig: mintMultisigAddr, + tokenProgram: TOKEN_2022_PROGRAM_ID, }) .signers([badMinterAuthorityKeypair]) .rpc(); @@ -666,7 +713,8 @@ describe('minter_controller', () => { toAddress: badMinterAuthorityKeypair.publicKey, associatedTokenAccount: associatedTokenAccountAddress, mintAccount: mintPDA, - mintMultisig: mintMultisigAddr + mintMultisig: mintMultisigAddr, + tokenProgram: TOKEN_2022_PROGRAM_ID, }) .rpc(); @@ -678,7 +726,7 @@ describe('minter_controller', () => { }) it('Cannot mint tokens with invalid user for minter authority', async () => { - const associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, payer.publicKey); + const associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, payer.publicKey, true, TOKEN_2022_PROGRAM_ID); try { const mintTokenSignature = await minterControllerProgram.methods @@ -690,7 +738,8 @@ describe('minter_controller', () => { associatedTokenAccount: associatedTokenAccountAddress, minter: minterPDA2, mintAccount: mintPDA, - mintMultisig: mintMultisigAddr + mintMultisig: mintMultisigAddr, + tokenProgram: TOKEN_2022_PROGRAM_ID, }) .signers([minterAuthorityKeypair]) .rpc(); @@ -702,7 +751,7 @@ describe('minter_controller', () => { }) it('Cannot mint tokens with mismatched minter authority signature', async () => { - const associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, payer.publicKey); + const associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, payer.publicKey, true, TOKEN_2022_PROGRAM_ID); try { const mintTokenSignature = await minterControllerProgram.methods @@ -714,7 +763,8 @@ describe('minter_controller', () => { associatedTokenAccount: associatedTokenAccountAddress, minter: minterPDA, mintAccount: mintPDA, - mintMultisig: mintMultisigAddr + mintMultisig: mintMultisigAddr, + tokenProgram: TOKEN_2022_PROGRAM_ID, }) .signers([badMinterAuthorityKeypair]) .rpc(); @@ -736,7 +786,8 @@ describe('minter_controller', () => { associatedTokenAccount: associatedTokenAccountAddress, minter: minterPDA, mintAccount: mintPDA, - mintMultisig: mintMultisigAddr + mintMultisig: mintMultisigAddr, + tokenProgram: TOKEN_2022_PROGRAM_ID, }) .signers([minterAuthorityKeypair]) .rpc(); @@ -758,7 +809,8 @@ describe('minter_controller', () => { associatedTokenAccount: associatedTokenAccountAddressToken2, minter: minterPDA, mintAccount: mintPDAToken2, - mintMultisig: mintMultisigAddr2 + mintMultisig: mintMultisigAddr2, + tokenProgram: TOKEN_2022_PROGRAM_ID, }) .signers([minterAuthorityKeypair]) .rpc(); @@ -770,7 +822,7 @@ describe('minter_controller', () => { }) it('Cannot mint tokens if not whitelisted', async () => { - const associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, badMinterAuthorityKeypair.publicKey); + const associatedTokenAccountAddress = getAssociatedTokenAddressSync(mintPDA, badMinterAuthorityKeypair.publicKey, true, TOKEN_2022_PROGRAM_ID); try { const mintTokenSignature = await minterControllerProgram.methods .mintToken(amount) @@ -780,7 +832,8 @@ describe('minter_controller', () => { toAddress: badMinterAuthorityKeypair.publicKey, associatedTokenAccount: associatedTokenAccountAddress, mintAccount: mintPDA, - mintMultisig: mintMultisigAddr + mintMultisig: mintMultisigAddr, + tokenProgram: TOKEN_2022_PROGRAM_ID, }) .signers([minterAuthorityKeypair]) .rpc();