Skip to content

Transfers admin ownership of a Settings account to a new pubkey. Only the current admin can call this instruction.

pub fn transfer_admin(
ctx: Context<TransferAdmin>,
new_admin: Pubkey,
) -> Result<()>
ParameterTypeDescription
new_adminPubkeyPublic key of the new admin
AccountTypeDescription
adminSignerCurrent admin of the Settings account
settingsAccount<Settings>Settings PDA to transfer
import { buildTransferAdminInstruction } from './codama-ts-range-custom';
const instruction = await buildTransferAdminInstruction({
admin: currentAdminPublicKey,
newAdmin: newAdminPublicKey,
});
// Sign with current admin and send
const transaction = new Transaction().add(instruction);
await sendTransaction(transaction);
await program.methods
.transferAdmin(newAdminPublicKey)
.accounts({
admin: currentAdmin.publicKey,
settings: settingsPda,
})
.rpc();
  1. Verifies caller is the current admin
  2. Updates the admin field to new_admin
  3. Current admin immediately loses all control
  4. PDA address remains unchanged
ErrorCause
Constraint violationCaller is not the current admin
Account not foundSettings account doesn’t exist

Transferring control when changing team ownership:

// Current admin transfers to new team lead
const instruction = await buildTransferAdminInstruction({
admin: currentTeamLead.publicKey,
newAdmin: newTeamLead.publicKey,
});

Transferring to a multi-sig wallet for enhanced security:

// Transfer to a Squads multi-sig
const instruction = await buildTransferAdminInstruction({
admin: singleSignerAdmin.publicKey,
newAdmin: squadsMultisigPda,
});

Setting up a recovery pubkey before deployment:

// Create backup admin keypair, store securely offline
const backupAdmin = Keypair.generate();
console.log('Backup admin:', backupAdmin.publicKey.toBase58());
console.log('Store this keypair securely offline!');
// Later, if primary admin is compromised:
const instruction = await buildTransferAdminInstruction({
admin: primaryAdmin.publicKey,
newAdmin: backupAdmin.publicKey,
});

The Settings PDA is derived from the original admin pubkey. After transfer:

// PDA derivation uses original admin
const [settingsPda] = PublicKey.findProgramAddressSync(
[Buffer.from("settings"), originalAdmin.toBuffer()], // Original admin!
RANGE_PROGRAM_ID
);
// But the new admin controls it
// settings.admin === newAdminPublicKey

This means existing integrations that reference the PDA continue to work.

For high-value transfers, consider a two-step process:

// Step 1: Transfer to intermediate (verify new admin has access)
await buildTransferAdminInstruction({
admin: currentAdmin.publicKey,
newAdmin: intermediateAdmin.publicKey, // New admin's hot wallet
});
// Step 2: New admin transfers to their secure wallet
await buildTransferAdminInstruction({
admin: intermediateAdmin.publicKey,
newAdmin: secureMultisig.publicKey,
});