Skip to main content
Sigvex

Timelock Operations Remediation

How to fix missing timelock delays, timestamp validation issues, and unprotected cancellation.

Timelock Operations Remediation

Overview

Related Detector: Timelock Operations

Timelock issues arise when critical admin operations execute immediately without a delay period, or when time-dependent logic lacks proper timestamp validation. The fix involves implementing a propose-then-execute pattern with an enforced delay, validating timestamps against the Solana clock sysvar, and restricting cancellation to authorized accounts.

Before (Vulnerable)

pub fn update_fee(accounts: &[AccountInfo], new_fee: u64) -> ProgramResult {
    let admin = &accounts[0];
    let config = &accounts[1];

    if !admin.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Immediate execution -- no delay
    let mut data = config.try_borrow_mut_data()?;
    data[0..8].copy_from_slice(&new_fee.to_le_bytes());
    Ok(())
}

After (Fixed)

const DELAY: i64 = 86400; // 24 hours

pub fn propose_fee_change(accounts: &[AccountInfo], new_fee: u64) -> ProgramResult {
    let admin = &accounts[0];
    let proposal = &accounts[1];
    let clock = Clock::get()?;

    if !admin.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    let mut data = proposal.try_borrow_mut_data()?;
    data[0..8].copy_from_slice(&new_fee.to_le_bytes());
    let execute_after = clock.unix_timestamp + DELAY;
    data[8..16].copy_from_slice(&execute_after.to_le_bytes());
    Ok(())
}

pub fn execute_fee_change(accounts: &[AccountInfo]) -> ProgramResult {
    let admin = &accounts[0];
    let proposal = &accounts[1];
    let config = &accounts[2];
    let clock = Clock::get()?;

    if !admin.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    let prop_data = proposal.try_borrow_data()?;
    let execute_after = i64::from_le_bytes(prop_data[8..16].try_into().unwrap());

    if clock.unix_timestamp < execute_after {
        return Err(ProgramError::InvalidArgument); // Too early
    }

    let new_fee = u64::from_le_bytes(prop_data[0..8].try_into().unwrap());
    drop(prop_data);

    let mut data = config.try_borrow_mut_data()?;
    data[0..8].copy_from_slice(&new_fee.to_le_bytes());
    Ok(())
}

Alternative Mitigations

1. Anchor with time constraint

#[derive(Accounts)]
pub struct ExecuteProposal<'info> {
    pub admin: Signer<'info>,
    #[account(
        mut,
        has_one = admin,
        constraint = proposal.execute_after <= Clock::get()?.unix_timestamp @ ErrorCode::TooEarly
    )]
    pub proposal: Account<'info, Proposal>,
    #[account(mut)]
    pub config: Account<'info, ProgramConfig>,
}

2. Governance integration

Use SPL Governance for community-governed timelocks with voting:

// Proposals require community vote + timelock before execution
// See SPL Governance for full implementation

3. Cancellation with authorization

pub fn cancel_proposal(accounts: &[AccountInfo]) -> ProgramResult {
    let admin = &accounts[0];
    let proposal = &accounts[1];

    if !admin.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Verify admin is authorized to cancel
    let prop_data = proposal.try_borrow_data()?;
    let proposer = Pubkey::try_from(&prop_data[16..48])
        .map_err(|_| ProgramError::InvalidAccountData)?;
    if *admin.key != proposer {
        return Err(ProgramError::InvalidArgument);
    }

    drop(prop_data);
    // Zero out proposal data to cancel
    let mut data = proposal.try_borrow_mut_data()?;
    data.fill(0);
    Ok(())
}

Common Mistakes

Mistake 1: Using a delay that is too short

A delay of seconds or minutes does not give users meaningful time to react. Use at least 24 hours for critical operations like admin transfers and fee changes.

Mistake 2: Not validating the clock source

Always use Clock::get()? (the Solana clock sysvar) rather than allowing the caller to pass a timestamp. User-supplied timestamps can be arbitrary.

Mistake 3: Allowing anyone to cancel queued proposals

Cancellation must be restricted to the proposer or an authorized admin. Otherwise, an attacker can cancel legitimate pending actions.

References