diff --git a/cross-chain/solana/programs/tbtc/src/processor/admin/add_minter.rs b/cross-chain/solana/programs/tbtc/src/processor/admin/add_minter.rs index ccb8620cb..b99925e85 100644 --- a/cross-chain/solana/programs/tbtc/src/processor/admin/add_minter.rs +++ b/cross-chain/solana/programs/tbtc/src/processor/admin/add_minter.rs @@ -1,6 +1,6 @@ use crate::{ error::TbtcError, - state::{Config, MinterInfo}, + state::{Config, MinterIndex, MinterInfo}, }; use anchor_lang::prelude::*; @@ -26,6 +26,15 @@ pub struct AddMinter<'info> { )] minter_info: Account<'info, MinterInfo>, + #[account( + init, + payer = authority, + space = 8 + MinterIndex::INIT_SPACE, + seeds = [MinterIndex::SEED_PREFIX, &[config.num_minters]], + bump + )] + minter_index: Account<'info, MinterIndex>, + /// CHECK: Required authority to mint tokens. This pubkey lives in `MinterInfo`. minter: AccountInfo<'info>, @@ -35,9 +44,15 @@ pub struct AddMinter<'info> { pub fn add_minter(ctx: Context) -> Result<()> { ctx.accounts.minter_info.set_inner(MinterInfo { minter: ctx.accounts.minter.key(), + index: ctx.accounts.config.num_minters, bump: ctx.bumps["minter_info"], }); + ctx.accounts.minter_index.set_inner(MinterIndex { + minter_info: ctx.accounts.minter_info.key(), + bump: ctx.bumps["minter_index"], + }); + ctx.accounts.config.num_minters += 1; Ok(()) } diff --git a/cross-chain/solana/programs/tbtc/src/processor/admin/remove_minter.rs b/cross-chain/solana/programs/tbtc/src/processor/admin/remove_minter.rs index 83aa5a07d..dcb037708 100644 --- a/cross-chain/solana/programs/tbtc/src/processor/admin/remove_minter.rs +++ b/cross-chain/solana/programs/tbtc/src/processor/admin/remove_minter.rs @@ -1,6 +1,6 @@ use crate::{ error::TbtcError, - state::{Config, MinterInfo}, + state::{Config, MinterIndex, MinterInfo}, }; use anchor_lang::prelude::*; @@ -25,6 +25,36 @@ pub struct RemoveMinter<'info> { )] minter_info: Account<'info, MinterInfo>, + // the minter info at the last index. + // This gets its index swapped to the position of the removed minter info. + #[account( + mut, + constraint = minter_info_swap.index == config.num_minters - 1, + seeds = [MinterInfo::SEED_PREFIX, minter_info_swap.minter.as_ref()], + bump = minter_info_swap.bump, + )] + minter_info_swap: Account<'info, MinterInfo>, + + // The index account of the minter to remove. + // We replace minter_info in this with minter_info_swap. + #[account( + mut, + seeds = [MinterIndex::SEED_PREFIX, &[minter_info.index]], + bump = minter_index_swap.bump, + )] + minter_index_swap: Account<'info, MinterIndex>, + + // The last minter index account. + // This gets removed, and its minter_info(_swap) gets put into minter_index_swap instead. + #[account( + mut, + close = authority, + seeds = [MinterIndex::SEED_PREFIX, &[config.num_minters - 1]], + bump = minter_index_tail.bump, + constraint = minter_index_tail.minter_info == minter_info_swap.key(), + )] + minter_index_tail: Account<'info, MinterIndex>, + /// CHECK: Required authority to mint tokens. This pubkey lives in `MinterInfo`. minter: AccountInfo<'info>, } diff --git a/cross-chain/solana/programs/tbtc/src/state/minter_index.rs b/cross-chain/solana/programs/tbtc/src/state/minter_index.rs new file mode 100644 index 000000000..b24cac001 --- /dev/null +++ b/cross-chain/solana/programs/tbtc/src/state/minter_index.rs @@ -0,0 +1,12 @@ +use anchor_lang::prelude::*; + +#[account] +#[derive(Debug, InitSpace)] +pub struct MinterIndex { + pub minter_info: Pubkey, + pub bump: u8, +} + +impl MinterIndex { + pub const SEED_PREFIX: &'static [u8] = b"minter-index"; +} diff --git a/cross-chain/solana/programs/tbtc/src/state/minter_info.rs b/cross-chain/solana/programs/tbtc/src/state/minter_info.rs index e8b6a6eeb..89717df2c 100644 --- a/cross-chain/solana/programs/tbtc/src/state/minter_info.rs +++ b/cross-chain/solana/programs/tbtc/src/state/minter_info.rs @@ -4,6 +4,7 @@ use anchor_lang::prelude::*; #[derive(Debug, InitSpace)] pub struct MinterInfo { pub minter: Pubkey, + pub index: u8, pub bump: u8, } diff --git a/cross-chain/solana/programs/tbtc/src/state/mod.rs b/cross-chain/solana/programs/tbtc/src/state/mod.rs index cb61742a2..d54275df4 100644 --- a/cross-chain/solana/programs/tbtc/src/state/mod.rs +++ b/cross-chain/solana/programs/tbtc/src/state/mod.rs @@ -7,5 +7,8 @@ pub use guardian_index::*; mod guardian_info; pub use guardian_info::*; +mod minter_index; +pub use minter_index::*; + mod minter_info; pub use minter_info::*; diff --git a/cross-chain/solana/tests/01__tbtc.ts b/cross-chain/solana/tests/01__tbtc.ts index bb94a5117..0b549f1e8 100644 --- a/cross-chain/solana/tests/01__tbtc.ts +++ b/cross-chain/solana/tests/01__tbtc.ts @@ -113,21 +113,41 @@ function getMinterPDA( ); } +function getMinterIndexPDA( + program: Program, + index +): [anchor.web3.PublicKey, number] { + let indexArr = new Uint8Array(1); + indexArr[0] = index; + return web3.PublicKey.findProgramAddressSync( + [ + Buffer.from('minter-index'), + indexArr, + ], + program.programId + ); +} + async function addMinter( program: Program, authority, - minter, - payer + minter ): Promise { const [config,] = getConfigPDA(program); const [minterInfoPDA, _] = getMinterPDA(program, minter); + + let configState = await program.account.config.fetch(config); + + const [minterIndexPDA, __] = getMinterIndexPDA(program, configState.numMinters); + await program.methods .addMinter() .accounts({ config, authority: authority.publicKey, - minter: minter.publicKey, minterInfo: minterInfoPDA, + minterIndex: minterIndexPDA, + minter: minter.publicKey, }) .signers(maybeAuthorityAnd(authority, [])) .rpc(); @@ -136,13 +156,22 @@ async function addMinter( async function checkMinter( program: Program, - minter + minter, + expectedIndex ) { const [minterInfoPDA, bump] = getMinterPDA(program, minter); let minterInfo = await program.account.minterInfo.fetch(minterInfoPDA); + const [minterIndexPDA, indexBump] = getMinterIndexPDA(program, minterInfo.index); + let minterIndex = await program.account.minterIndex.fetch(minterIndexPDA); + expect(minterInfo.minter).to.eql(minter.publicKey); expect(minterInfo.bump).to.equal(bump); + + expect(minterIndex.minterInfo).to.eql(minterInfoPDA); + expect(minterIndex.bump).to.equal(indexBump); + + expect(minterInfo.index).to.equal(expectedIndex); } async function removeMinter( @@ -152,12 +181,24 @@ async function removeMinter( minterInfo ) { const [config,] = getConfigPDA(program); + const configState = await program.account.config.fetch(config); + const minterInfoState = await program.account.minterInfo.fetch(minterInfo); + + const [lastIndex,] = getMinterIndexPDA(program, configState.numMinters - 1); + const [swapIndex,] = getMinterIndexPDA(program, minterInfoState.index); + + const lastIndexState = await program.account.minterIndex.fetch(lastIndex); + const swapInfo = lastIndexState.minterInfo; + await program.methods .removeMinter() .accounts({ config, authority: authority.publicKey, minterInfo: minterInfo, + minterInfoSwap: swapInfo, + minterIndexSwap: swapIndex, + minterIndexTail: lastIndex, minter: minter.publicKey }) .signers(maybeAuthorityAnd(authority, [])) @@ -382,8 +423,8 @@ describe("tbtc", () => { it('add minter', async () => { await checkState(program, authority, 0, 0, 0); - await addMinter(program, authority, minterKeys, authority); - await checkMinter(program, minterKeys); + await addMinter(program, authority, minterKeys); + await checkMinter(program, minterKeys, 0); await checkState(program, authority, 1, 0, 0); // Transfer lamports to imposter. @@ -400,7 +441,7 @@ describe("tbtc", () => { ); try { - await addMinter(program, impostorKeys, minter2Keys, authority); + await addMinter(program, impostorKeys, minter2Keys); chai.assert(false, "should've failed but didn't"); } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); @@ -413,7 +454,7 @@ describe("tbtc", () => { it('mint', async () => { await checkState(program, authority, 1, 0, 0); const [minterInfoPDA, _] = getMinterPDA(program, minterKeys); - await checkMinter(program, minterKeys); + await checkMinter(program, minterKeys, 0); // await setupMint(program, authority, recipientKeys); await mint(program, minterKeys, minterInfoPDA, recipientKeys, 1000, authority); @@ -434,7 +475,7 @@ describe("tbtc", () => { it('won\'t mint', async () => { await checkState(program, authority, 1, 0, 1000); const [minterInfoPDA, _] = getMinterPDA(program, minterKeys); - await checkMinter(program, minterKeys); + await checkMinter(program, minterKeys, 0); // await setupMint(program, authority, recipientKeys); @@ -452,9 +493,9 @@ describe("tbtc", () => { it('use two minters', async () => { await checkState(program, authority, 1, 0, 1000); const [minterInfoPDA, _] = getMinterPDA(program, minterKeys); - await checkMinter(program, minterKeys); - const minter2InfoPDA = await addMinter(program, authority, minter2Keys, authority); - await checkMinter(program, minter2Keys); + await checkMinter(program, minterKeys, 0); + const minter2InfoPDA = await addMinter(program, authority, minter2Keys); + await checkMinter(program, minter2Keys, 1); await checkState(program, authority, 2, 0, 1000); // await setupMint(program, authority, recipientKeys); @@ -487,7 +528,7 @@ describe("tbtc", () => { it('remove minter', async () => { await checkState(program, authority, 2, 0, 1500); const [minter2InfoPDA, _] = getMinterPDA(program, minter2Keys); - await checkMinter(program, minter2Keys); + await checkMinter(program, minter2Keys, 1); await removeMinter(program, authority, minter2Keys, minter2InfoPDA); await checkState(program, authority, 1, 0, 1500); }); @@ -495,7 +536,7 @@ describe("tbtc", () => { it('won\'t remove minter', async () => { await checkState(program, authority, 1, 0, 1500); const [minterInfoPDA, _] = getMinterPDA(program, minterKeys); - await checkMinter(program, minterKeys); + await checkMinter(program, minterKeys, 0); try { await removeMinter(program, impostorKeys, minterKeys, minterInfoPDA); @@ -514,10 +555,7 @@ describe("tbtc", () => { await removeMinter(program, authority, minterKeys, minterInfoPDA); chai.assert(false, "should've failed but didn't"); } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('AccountNotInitialized'); - expect(err.program.equals(program.programId)).is.true; + expect(_err.message).to.include('Account does not exist or has no data'); } }); @@ -604,7 +642,7 @@ describe("tbtc", () => { it('won\'t mint when paused', async () => { await checkState(program, authority, 0, 1, 1500); - const minterInfoPDA = await addMinter(program, authority, minterKeys, authority); + const minterInfoPDA = await addMinter(program, authority, minterKeys); await pause(program, guardianKeys); // await setupMint(program, authority, recipientKeys);