diff --git a/cross-chain/solana/programs/tbtc/src/error.rs b/cross-chain/solana/programs/tbtc/src/error.rs index 434c27b05..5e1b279d5 100644 --- a/cross-chain/solana/programs/tbtc/src/error.rs +++ b/cross-chain/solana/programs/tbtc/src/error.rs @@ -5,4 +5,6 @@ pub enum TbtcError { IsPaused, IsNotPaused, IsNotAuthority, + IsNotPendingAuthority, + NoPendingAuthorityChange, } diff --git a/cross-chain/solana/programs/tbtc/src/lib.rs b/cross-chain/solana/programs/tbtc/src/lib.rs index 593302351..daa07cdd7 100644 --- a/cross-chain/solana/programs/tbtc/src/lib.rs +++ b/cross-chain/solana/programs/tbtc/src/lib.rs @@ -34,6 +34,14 @@ pub mod tbtc { processor::change_authority(ctx) } + pub fn cancel_authority_change(ctx: Context) -> Result<()> { + processor::cancel_authority_change(ctx) + } + + pub fn take_authority(ctx: Context) -> Result<()> { + processor::take_authority(ctx) + } + pub fn add_minter(ctx: Context) -> Result<()> { processor::add_minter(ctx) } diff --git a/cross-chain/solana/programs/tbtc/src/processor/admin/cancel_authority_change.rs b/cross-chain/solana/programs/tbtc/src/processor/admin/cancel_authority_change.rs new file mode 100644 index 000000000..92000cca0 --- /dev/null +++ b/cross-chain/solana/programs/tbtc/src/processor/admin/cancel_authority_change.rs @@ -0,0 +1,22 @@ +use crate::{error::TbtcError, state::Config}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] + +pub struct CancelAuthorityChange<'info> { + #[account( + mut, + seeds = [Config::SEED_PREFIX], + bump, + has_one = authority @ TbtcError::IsNotAuthority, + constraint = config.pending_authority.is_some() @ TbtcError::NoPendingAuthorityChange + )] + config: Account<'info, Config>, + + authority: Signer<'info>, +} + +pub fn cancel_authority_change(ctx: Context) -> Result<()> { + ctx.accounts.config.pending_authority = None; + Ok(()) +} diff --git a/cross-chain/solana/programs/tbtc/src/processor/admin/change_authority.rs b/cross-chain/solana/programs/tbtc/src/processor/admin/change_authority.rs index d2d0d1669..0581396c0 100644 --- a/cross-chain/solana/programs/tbtc/src/processor/admin/change_authority.rs +++ b/cross-chain/solana/programs/tbtc/src/processor/admin/change_authority.rs @@ -13,10 +13,11 @@ pub struct ChangeAuthority<'info> { authority: Signer<'info>, - new_authority: Signer<'info>, + /// CHECK: New authority. + new_authority: AccountInfo<'info>, } pub fn change_authority(ctx: Context) -> Result<()> { - ctx.accounts.config.authority = ctx.accounts.new_authority.key(); + ctx.accounts.config.pending_authority = Some(ctx.accounts.new_authority.key()); Ok(()) } diff --git a/cross-chain/solana/programs/tbtc/src/processor/admin/initialize.rs b/cross-chain/solana/programs/tbtc/src/processor/admin/initialize.rs index 9b418ddfd..e32ebc5bb 100644 --- a/cross-chain/solana/programs/tbtc/src/processor/admin/initialize.rs +++ b/cross-chain/solana/programs/tbtc/src/processor/admin/initialize.rs @@ -36,6 +36,7 @@ pub fn initialize(ctx: Context) -> Result<()> { ctx.accounts.config.set_inner(Config { bump: ctx.bumps["config"], authority: ctx.accounts.authority.key(), + pending_authority: None, mint: ctx.accounts.mint.key(), mint_bump: ctx.bumps["mint"], num_minters: 0, diff --git a/cross-chain/solana/programs/tbtc/src/processor/admin/mod.rs b/cross-chain/solana/programs/tbtc/src/processor/admin/mod.rs index bc1f98170..4e9b75a5c 100644 --- a/cross-chain/solana/programs/tbtc/src/processor/admin/mod.rs +++ b/cross-chain/solana/programs/tbtc/src/processor/admin/mod.rs @@ -4,6 +4,9 @@ pub use add_guardian::*; mod add_minter; pub use add_minter::*; +mod cancel_authority_change; +pub use cancel_authority_change::*; + mod change_authority; pub use change_authority::*; @@ -19,5 +22,8 @@ pub use remove_guardian::*; mod remove_minter; pub use remove_minter::*; +mod take_authority; +pub use take_authority::*; + mod unpause; pub use unpause::*; diff --git a/cross-chain/solana/programs/tbtc/src/processor/admin/take_authority.rs b/cross-chain/solana/programs/tbtc/src/processor/admin/take_authority.rs new file mode 100644 index 000000000..292d2d726 --- /dev/null +++ b/cross-chain/solana/programs/tbtc/src/processor/admin/take_authority.rs @@ -0,0 +1,24 @@ +use crate::{error::TbtcError, state::Config}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct TakeAuthority<'info> { + #[account( + mut, + seeds = [Config::SEED_PREFIX], + bump, + constraint = config.pending_authority.is_some() @ TbtcError::NoPendingAuthorityChange + )] + config: Account<'info, Config>, + + #[account( + constraint = pending_authority.key() == config.pending_authority.unwrap() @ TbtcError::IsNotPendingAuthority + )] + pending_authority: Signer<'info>, +} + +pub fn take_authority(ctx: Context) -> Result<()> { + ctx.accounts.config.authority = ctx.accounts.pending_authority.key(); + ctx.accounts.config.pending_authority = None; + Ok(()) +} \ No newline at end of file diff --git a/cross-chain/solana/programs/tbtc/src/state/config.rs b/cross-chain/solana/programs/tbtc/src/state/config.rs index 4dd81b69c..497200848 100644 --- a/cross-chain/solana/programs/tbtc/src/state/config.rs +++ b/cross-chain/solana/programs/tbtc/src/state/config.rs @@ -7,6 +7,7 @@ pub struct Config { /// The authority over this program. pub authority: Pubkey, + pub pending_authority: Option, // Mint info. pub mint: Pubkey, diff --git a/cross-chain/solana/tests/01__tbtc.ts b/cross-chain/solana/tests/01__tbtc.ts index 0b549f1e8..bca0fd8ac 100644 --- a/cross-chain/solana/tests/01__tbtc.ts +++ b/cross-chain/solana/tests/01__tbtc.ts @@ -64,10 +64,57 @@ async function changeAuthority( authority: authority.publicKey, newAuthority: newAuthority.publicKey, }) - .signers(maybeAuthorityAnd(authority, [newAuthority])) + .signers(maybeAuthorityAnd(authority, [])) .rpc(); } +async function takeAuthority( + program: Program, + newAuthority, +) { + const [config,] = getConfigPDA(program); + await program.methods + .takeAuthority() + .accounts({ + config, + pendingAuthority: newAuthority.publicKey, + }) + .signers(maybeAuthorityAnd(newAuthority, [])) + .rpc(); +} + +async function cancelAuthorityChange( + program: Program, + authority, +) { + const [config,] = getConfigPDA(program); + await program.methods + .cancelAuthorityChange() + .accounts({ + config, + authority: authority.publicKey, + }) + .signers(maybeAuthorityAnd(authority, [])) + .rpc(); +} + +async function checkPendingAuthority( + program: Program, + pendingAuthority, +) { + const [config,] = getConfigPDA(program); + let configState = await program.account.config.fetch(config); + expect(configState.pendingAuthority).to.eql(pendingAuthority.publicKey); +} + +async function checkNoPendingAuthority( + program: Program, +) { + const [config,] = getConfigPDA(program); + let configState = await program.account.config.fetch(config); + expect(configState.pendingAuthority).to.equal(null); +} + async function checkPaused( program: Program, paused: boolean @@ -415,9 +462,61 @@ describe("tbtc", () => { it('change authority', async () => { await checkState(program, authority, 0, 0, 0); + await checkNoPendingAuthority(program); + try { + await cancelAuthorityChange(program, authority); + 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('NoPendingAuthorityChange'); + expect(err.program.equals(program.programId)).is.true; + } + try { + await takeAuthority(program, newAuthority); + 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('NoPendingAuthorityChange'); + expect(err.program.equals(program.programId)).is.true; + } + await changeAuthority(program, authority, newAuthority); + await checkPendingAuthority(program, newAuthority); + await takeAuthority(program, newAuthority); + await checkNoPendingAuthority(program); await checkState(program, newAuthority, 0, 0, 0); await changeAuthority(program, newAuthority, authority.payer); + try { + await takeAuthority(program, impostorKeys); + 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('IsNotPendingAuthority'); + expect(err.program.equals(program.programId)).is.true; + } + try { + await takeAuthority(program, newAuthority); + 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('IsNotPendingAuthority'); + expect(err.program.equals(program.programId)).is.true; + } + try { + await cancelAuthorityChange(program, authority); + 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('IsNotAuthority'); + expect(err.program.equals(program.programId)).is.true; + } + await takeAuthority(program, authority); + await checkState(program, authority, 0, 0, 0); })