Protocol Registration
Initialize the SentinelState PDA to authorize the off-chain watcher to pause your protocol.
1. The SentinelState PDA
To allow SentinelGuard to protect your vaults, your program must expose a PDA that stores the pause state and the authorized watcher key.
#[account]
pub struct SentinelState {
pub paused: bool,
pub authorized_watcher: Pubkey,
pub last_alert_slot: u64,
pub bump: u8,
}2. Initialization Instruction
Create an instruction that the protocol admin calls once during setup.
#[derive(Accounts)]
pub struct InitializeSentinel<'info> {
#[account(mut)]
pub admin: Signer<'info>,
#[account(
init,
payer = admin,
space = 8 + 1 + 32 + 8 + 1,
seeds = [b"sentinel_state"],
bump
)]
pub sentinel_state: Account<'info, SentinelState>,
pub system_program: Program<'info, System>,
}
pub fn initialize_sentinel(ctx: Context<InitializeSentinel>, watcher_key: Pubkey) -> Result<()> {
let state = &mut ctx.accounts.sentinel_state;
state.paused = false;
state.authorized_watcher = watcher_key;
state.last_alert_slot = 0;
state.bump = ctx.bumps.sentinel_state;
Ok(())
}3. Pause Instruction
Expose a pause_withdrawals instruction that the off-chain watcher calls via CPI when a critical alert fires. The constraint ensures only the authorized watcher keypair can trigger it.
#[derive(Accounts)]
pub struct PauseWithdrawals<'info> {
pub watcher: Signer<'info>,
#[account(
mut,
seeds = [b"sentinel_state"],
bump = sentinel_state.bump,
constraint = sentinel_state.authorized_watcher == watcher.key()
@ ErrorCode::UnauthorizedWatcher
)]
pub sentinel_state: Account<'info, SentinelState>,
}
pub fn pause_withdrawals(ctx: Context<PauseWithdrawals>) -> Result<()> {
let state = &mut ctx.accounts.sentinel_state;
state.paused = true;
state.last_alert_slot = Clock::get()?.slot;
Ok(())
}The watcher keypair must match authorized_watcher set during initialization. If the keys don't match, the CPI fails and the pause does not execute.
4. Circuit Breaker Check
In every instruction where funds leave your protocol — withdraw, borrow, flash_loan — pass the SentinelState account and assert it is not paused. This is a single require! at the top of the handler.
#[derive(Accounts)]
pub struct Withdraw<'info> {
// ... your standard accounts ...
#[account(
seeds = [b"sentinel_state"],
bump = sentinel_state.bump
)]
pub sentinel_state: Account<'info, SentinelState>,
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
// Circuit breaker — one line blocks all withdrawals when paused
require!(!ctx.accounts.sentinel_state.paused, ErrorCode::ProtocolPaused);
// ... rest of your withdrawal logic unchanged ...
Ok(())
}Once paused = true is set on the PDA, every subsequent withdrawal reverts instantly with ProtocolPaused — no further action required until an admin resets the state.