F-2026-0003·centralization-risk

Single-step authority transfer without confirmation risks permanent loss of vault control on operator error

Fixedsolanavaulted25519
TL;DR

Authority transfer functions immediately move control without new-authority confirmation. A typo during transfer can permanently lock the vault with no recovery.

Severity
LOW
Impact
MEDIUM
Likelihood
LOW
Method
MManual review
CAT.
Complexity
LOW
Exploitability
LOW
02Section · Description

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.

03Section · Impact

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.

04Section · Recommendation

Recommendation

Implement a two-step authority transfer pattern requiring explicit acceptance from the new authority.

  1. Update CasinoVault struct (add 2 fields):
rust
#[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 this
pub pending_withdrawal_signer: Option<Pubkey>, // Add this
}

Note: Increase space in InitializeVault from 106 to 172 bytes (adding 66 bytes for two Option fields).

  1. Modify set_authority to nominate instead of transfer:
rust
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 transferring
msg!("Authority nominated: {}. Must call accept_authority to complete", new_authority);
Ok(())
}
  1. Add acceptance function (new authority must call this):
rust
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>,
}
  1. Apply same pattern to set_withdrawal_signer:
  • Rename to nominate_withdrawal_signer and set pending_withdrawal_signer
  • Add accept_withdrawal_signer function
05Section · Resolution

Resolution

Fair Casino: Fixed.

Zealynx: Verified.

Status
Fixed
F-2026-0003

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx