Signature Replay Remediation (SVM)
How to prevent signature replay in Solana programs by including a nonce and deadline in the signed message and incrementing the nonce after each successful verification.
Signature Replay Remediation (SVM)
Overview
Signature replay vulnerabilities in Solana programs arise when a program verifies a cryptographic signature without ensuring the same signature cannot be reused. The remediation is to include a monotonically incrementing nonce and an expiry slot or timestamp in the signed message, and to increment the nonce after each successful verification.
Related Detector: Signature Replay (SVM)
Recommended Fix
Before (Vulnerable)
use solana_program::secp256k1_recover::secp256k1_recover;
// VULNERABLE: signature verified but no nonce or deadline
pub fn execute(accounts: &[AccountInfo], sig: [u8; 64], rec_id: u8, hash: [u8; 32], amount: u64) -> ProgramResult {
let key = secp256k1_recover(&hash, rec_id, &sig).map_err(|_| MyError::BadSig)?;
require!(key.0 == EXPECTED_KEY, MyError::WrongKey);
// Same signature replayable indefinitely
transfer_funds(accounts, amount)
}
After (Fixed)
use anchor_lang::prelude::*;
use solana_program::secp256k1_recover::secp256k1_recover;
#[account]
pub struct AuthState {
pub authority_key: [u8; 64], // secp256k1 public key bytes
pub nonce: u64,
}
#[derive(Accounts)]
pub struct Execute<'info> {
#[account(mut)]
pub auth_state: Account<'info, AuthState>,
pub clock: Sysvar<'info, Clock>,
}
pub fn execute(
ctx: Context<Execute>,
sig: [u8; 64],
rec_id: u8,
amount: u64,
deadline_slot: u64,
) -> Result<()> {
let state = &mut ctx.accounts.auth_state;
let clock = &ctx.accounts.clock;
// Check deadline
require!(clock.slot <= deadline_slot, MyError::Expired);
// Build message that includes nonce and deadline
let message = build_message(amount, state.nonce, deadline_slot);
let hash = solana_program::hash::hash(&message);
// Verify signature
let recovered = secp256k1_recover(hash.as_ref(), rec_id, &sig)
.map_err(|_| MyError::InvalidSignature)?;
require!(recovered.0 == state.authority_key, MyError::WrongKey);
// Increment nonce — makes this signature invalid for future transactions
state.nonce = state.nonce.checked_add(1).ok_or(MyError::NonceOverflow)?;
transfer_funds(&ctx.accounts, amount)
}
Alternative Mitigations
Solana durable nonce accounts — for meta-transactions and gasless flows, use Solana’s built-in durable nonce mechanism. The runtime rejects any transaction that reuses a consumed nonce account, providing replay protection at the transaction level without program-level nonce management.
Spent signature bitmap — when out-of-order execution is required (sequential nonces do not work):
#[account]
pub struct NonceStore {
pub used_nonces: Vec<u64>, // or a bitmap for fixed range
}
pub fn execute(ctx: Context<Execute>, nonce: u64, ...) -> Result<()> {
let store = &mut ctx.accounts.nonce_store;
require!(!store.used_nonces.contains(&nonce), MyError::NonceReused);
// ... verify signature ...
store.used_nonces.push(nonce);
Ok(())
}
Common Mistakes
Incrementing the nonce before verifying the signature — if verification fails and the nonce was already incremented, it is permanently consumed and the legitimate user may be locked out.
Not including the program ID in the signed message — a signature for one program that uses the same key and message format can be replayed against another program.
Checking nonce equality without incrementing — verifying nonce == stored without stored += 1 allows unlimited replay of the same nonce.