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.
Recommended Fix
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.