Skip to content

Commit

Permalink
Two-stage authority change
Browse files Browse the repository at this point in the history
  • Loading branch information
eth-r committed Aug 1, 2023
1 parent 73964de commit 5c00165
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 3 deletions.
2 changes: 2 additions & 0 deletions cross-chain/solana/programs/tbtc/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ pub enum TbtcError {
IsPaused,
IsNotPaused,
IsNotAuthority,
IsNotPendingAuthority,
NoPendingAuthorityChange,
}
8 changes: 8 additions & 0 deletions cross-chain/solana/programs/tbtc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ pub mod tbtc {
processor::change_authority(ctx)
}

pub fn cancel_authority_change(ctx: Context<CancelAuthorityChange>) -> Result<()> {
processor::cancel_authority_change(ctx)
}

pub fn take_authority(ctx: Context<TakeAuthority>) -> Result<()> {
processor::take_authority(ctx)
}

pub fn add_minter(ctx: Context<AddMinter>) -> Result<()> {
processor::add_minter(ctx)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CancelAuthorityChange>) -> Result<()> {
ctx.accounts.config.pending_authority = None;
Ok(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChangeAuthority>) -> Result<()> {
ctx.accounts.config.authority = ctx.accounts.new_authority.key();
ctx.accounts.config.pending_authority = Some(ctx.accounts.new_authority.key());
Ok(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub fn initialize(ctx: Context<Initialize>) -> 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,
Expand Down
6 changes: 6 additions & 0 deletions cross-chain/solana/programs/tbtc/src/processor/admin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand All @@ -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::*;
Original file line number Diff line number Diff line change
@@ -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<TakeAuthority>) -> Result<()> {
ctx.accounts.config.authority = ctx.accounts.pending_authority.key();
ctx.accounts.config.pending_authority = None;
Ok(())
}
1 change: 1 addition & 0 deletions cross-chain/solana/programs/tbtc/src/state/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub struct Config {

/// The authority over this program.
pub authority: Pubkey,
pub pending_authority: Option<Pubkey>,

// Mint info.
pub mint: Pubkey,
Expand Down
101 changes: 100 additions & 1 deletion cross-chain/solana/tests/01__tbtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Tbtc>,
newAuthority,
) {
const [config,] = getConfigPDA(program);
await program.methods
.takeAuthority()
.accounts({
config,
pendingAuthority: newAuthority.publicKey,
})
.signers(maybeAuthorityAnd(newAuthority, []))
.rpc();
}

async function cancelAuthorityChange(
program: Program<Tbtc>,
authority,
) {
const [config,] = getConfigPDA(program);
await program.methods
.cancelAuthorityChange()
.accounts({
config,
authority: authority.publicKey,
})
.signers(maybeAuthorityAnd(authority, []))
.rpc();
}

async function checkPendingAuthority(
program: Program<Tbtc>,
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<Tbtc>,
) {
const [config,] = getConfigPDA(program);
let configState = await program.account.config.fetch(config);
expect(configState.pendingAuthority).to.equal(null);
}

async function checkPaused(
program: Program<Tbtc>,
paused: boolean
Expand Down Expand Up @@ -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);
})

Expand Down

0 comments on commit 5c00165

Please sign in to comment.