Skip to main content
Sigvex

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)

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.

References