Single-step authority transfer without confirmation risks permanent loss of vault control on operator error
Authority transfer functions immediately move control without new-authority confirmation. A typo during transfer can permanently lock the vault with no recovery.
Description
The Fair Casino vault implements authority transfer functions set_authority and set_withdrawal_signer that immediately transfer control in a single transaction without requiring confirmation from the new authority. This creates a critical risk where a single typographical error can result in permanent loss of vault control and all deposited funds.
Impact
If an administrator enters an incorrect address during authority transfer, vault control would be permanently lost with no recovery mechanism. This would prevent all administrative operations including emergency pause and future key rotation. The vault manages real user funds, and code comments reference planned "Turnkey migration", making authority transfers an anticipated operational requirement rather than a theoretical edge case.
Recommendation
Implement a two-step authority transfer pattern requiring explicit acceptance from the new authority.
- Update
CasinoVaultstruct (add 2 fields):
#[account]pub struct CasinoVault {pub authority: Pubkey,pub withdrawal_signer: Pubkey,pub token_mint: Pubkey,pub vault_bump: u8,pub is_paused: bool,pub pending_authority: Option<Pubkey>, // Add thispub pending_withdrawal_signer: Option<Pubkey>, // Add this}
Note: Increase space in InitializeVault from 106 to 172 bytes (adding 66 bytes for two Option fields).
- Modify
set_authorityto nominate instead of transfer:
pub fn nominate_authority(ctx: Context<AdminControl>, new_authority: Pubkey) -> Result<()> {let vault = &mut ctx.accounts.vault;require!(ctx.accounts.authority.key() == vault.authority, ErrorCode::Unauthorized);require!(new_authority != Pubkey::default(), ErrorCode::InvalidAddress);vault.pending_authority = Some(new_authority); // Store pending instead of transferringmsg!("Authority nominated: {}. Must call accept_authority to complete", new_authority);Ok(())}
- Add acceptance function (new authority must call this):
pub fn accept_authority(ctx: Context<AcceptAuthority>) -> Result<()> {let vault = &mut ctx.accounts.vault;require!(Some(ctx.accounts.new_authority.key()) == vault.pending_authority, ErrorCode::Unauthorized);vault.authority = ctx.accounts.new_authority.key();vault.pending_authority = None;msg!("Authority transfer completed");Ok(())}#[derive(Accounts)]pub struct AcceptAuthority<'info> {#[account(mut, seeds = [VAULT_SEED], bump)]pub vault: Account<'info, CasinoVault>,pub new_authority: Signer<'info>,}
- Apply same pattern to
set_withdrawal_signer:
- Rename to
nominate_withdrawal_signerand setpending_withdrawal_signer - Add
accept_withdrawal_signerfunction
Resolution
Fair Casino: Fixed.
Zealynx: Verified.

