Protocol

Serrure the lock

Policy & guardrails

Serrure is the program that says no. Every Ombre settlement CPIs into serrure::check before moving funds. If the check fails, the parent transaction fails — no lamports leave the vault, no state is mutated anywhere. This is the guarantee that lets CFOs and auditors put agents on treasury.

What gets enforced

programs/serrure/src/lib.rs
pub struct PolicyParams {
    pub max_per_tx_lamports: u64,     // hard cap per single transaction
    pub max_per_day_lamports: u64,    // rolling 24h cap
    pub max_total_lamports: u64,      // lifetime cap (0 = unlimited)
    pub max_per_window: u32,          // rate limit: N tx / window
    pub window_seconds: i64,          // window length
    pub merchants_root: [u8; 32],     // merkle root of allowed merchants
}

Every field above is enforced independently. Failing any one fails the check. The window is a 16-slot sliding array stored on the account; merkle allowlists let you whitelist 10,000 merchants without on-chain rent bloat.

Rejection reasons

Rejections are typed so your UI can show meaningful messages:

rust
#[error_code]
pub enum SerrureError {
    NotPrincipal,
    WrongAgent,
    CircuitBreakerTripped,
    ExceedsPerTxLimit,
    RateLimited,
    DailyCapExceeded,
    TotalCapExceeded,
    MerchantNotAllowed,
}

Circuit breaker

The paused flag is a principal-only kill switch. Set it, and every future check fails with CircuitBreakerTripped until the principal calls unpause. This is the reversible middle step between "everything's fine" and "burn the Masque."

ts
// Anomaly detector raises:
await serrure.methods.pause().accounts({ serrure, principal }).rpc();

// After investigation, if it was a false alarm:
await serrure.methods.unpause().accounts({ serrure, principal }).rpc();

// If it was real, escalate to Masque revocation:
await masque.methods.revoke(2).accounts({ masque, principal }).rpc();

Merchant allowlists

You supply a merkle root; the agent supplies a merkle proof at settlement time. On-chain verification is 3-4 hash ops. To update the list, the principal calls update with a new policy (same PDA, new params).

Note
Leaf format: the domain-separator byte 0x00 concatenated with the merchant pubkey. Internal nodes use 0x01. See verify_merkle in the program for the exact hashing.

Typical policy shapes

Consumer agent

ts
{
  maxPerTxLamports:    5_000_000n,     // ~0.005 SOL (~$1)
  maxPerDayLamports:  50_000_000n,     // ~$10 / day
  maxTotalLamports:            0n,     // no lifetime cap
  maxPerWindow:   5,
  windowSeconds: 30,
  merchantsRoot: <allowlist root>,
}

Enterprise procurement agent

ts
{
  maxPerTxLamports:  500_000_000n,     // 0.5 SOL (~$100)
  maxPerDayLamports: 5_000_000_000n,   // ~$1000 / day
  maxTotalLamports:  50_000_000_000n,  // ~$10K lifetime, then reissue
  maxPerWindow:   20,
  windowSeconds:  60,
  merchantsRoot: <allowlist of approved vendors>,
}

Research / unbounded

ts
{
  maxPerTxLamports: 100_000_000n,
  maxPerDayLamports: 1_000_000_000n,
  maxTotalLamports: 0n,
  maxPerWindow: 100,
  windowSeconds: 10,
  merchantsRoot: '0x' + '00'.repeat(32), // allow any merchant
}
Tip
You can attach multiple Serrures to one Masque. A common pattern: one tight Serrure for general spending, one loose Serrure scoped to a specific high-trust merchant category.