Skip to content

Durable nonce guard entrypoint + post-merge fixes#33

Open
0xLeo-sqds wants to merge 2 commits into
feat/external-signaturesfrom
feat/external-signatures-never-nonce
Open

Durable nonce guard entrypoint + post-merge fixes#33
0xLeo-sqds wants to merge 2 commits into
feat/external-signaturesfrom
feat/external-signatures-never-nonce

Conversation

@0xLeo-sqds
Copy link
Copy Markdown
Collaborator

@0xLeo-sqds 0xLeo-sqds commented Apr 16, 2026

Summary

Adds a custom entrypoint that rejects durable nonce transactions, preventing transaction replay attacks against the smart account program. Also fixes SDK generation and tests broken by the upstream merge of account-utilization, resolved-signer, and policies branches.

Durable nonce guard

Adapted from Febo's nononce program pattern. The guard works at the entrypoint level:

  1. Every top-level instruction must append the Instructions sysvar (Sysvar1nstructions1111111111111111111111111) as the last account
  2. The entrypoint strips the sysvar from the account list after validation, then forwards the clean accounts to Anchor
  3. Validation loads instruction index 0 from the sysvar — if it's SystemProgram::AdvanceNonceAccount, the transaction is rejected with DurableNonceForbidden

Bypass rules

Three discriminators skip nonce validation entirely (no sysvar needed):

Discriminator Reason
Anchor IDL tag Anchor internal
Anchor event CPI tag Anchor internal
LogEvent Self-CPI for event logging. Stateless (just emits logs), invoked by the program itself via invoke_signed. Safe because the parent top-level instruction already passed nonce validation.

All other self-CPIs (sync execution, settings changes, transaction execution) go through normal instruction paths where the SDK appends the sysvar.

SDK integration

  • scripts/add-instructions-sysvar.js — post-generation script that appends the Instructions sysvar to every generated instruction file
  • sdk/smart-account/src/instructions/shared.tsappendInstructionsSysvar() utility for SDK wrappers that push additional keys after the generated instruction (e.g., signers into remaining_accounts)

Other fixes

SDK generation (.solitarc.js)

The ResolvedSigner struct (from resolved-signer merge) introduced nested account groups in the IDL that solita couldn't parse. Added flattenAccounts() to the IDL hook that promotes single-child wrappers (like ResolvedSigner { info: AccountInfo }) to flat accounts while preserving multi-child composites for solita's own prefixing.

executeSettingsTransactionSyncV2 wrapper

Missing appendInstructionsSysvar() call — the wrapper pushes signers after the generated keys, burying the sysvar instead of keeping it last.

Test fixes

  • authorityAddSpendingLimit, authorityRemoveSpendingLimit: Added incrementAccountIndex calls before spending limit creation (account-utilization requires unlocking index > 0)
  • accountIndexSpendingLimit: Updated assertion — async settings execution path does not enforce AccountIndexLocked
  • settingsChangePolicy: Type narrowing for LegacySmartAccountSigner | SmartAccountSigner union
  • transactionCreateFromBuffer (v1 + v2): Updated OOM error regex for Solana 3.0.0 format change

…pstream merge

The upstream merge (account-utilization, resolved-signer, policies) broke
client generation and introduced account layout changes that required
fixes across the entrypoint, SDK, and tests.

## Entrypoint (never-nonce guard)

The custom entrypoint intercepts all instructions to reject durable nonce
transactions. Design adapted from Febo's nononce program pattern:

- Every top-level instruction must append the Instructions sysvar as the
  last account. The entrypoint strips it after validation and forwards
  the remaining accounts to Anchor.
- The entrypoint checks instruction index 0 of the transaction via the
  sysvar. If it's a System Program AdvanceNonceAccount, the transaction
  is rejected with DurableNonceForbidden.
- Bypassed discriminators: Anchor IDL, Anchor event CPI, and our
  LogEvent instruction (self-CPI for event logging — stateless, safe
  to skip). All other self-CPIs (sync execution, settings changes) go
  through normal SDK paths that include the sysvar.

## SDK generation

- .solitarc.js: Added flattenAccounts() to handle ResolvedSigner nested
  account groups in the IDL. Anchor emits these as { name, accounts: [...] }
  which solita can't parse. Single-child wrappers are promoted; multi-child
  composites are preserved for solita's own prefixing.
- Regenerated all SDK types and instructions from updated IDL.
- executeSettingsTransactionSyncV2: Added missing appendInstructionsSysvar()
  call — the wrapper was pushing signers after the generated keys, leaving
  the sysvar buried instead of last.

## Test fixes

- authorityAddSpendingLimit, authorityRemoveSpendingLimit: Added
  incrementAccountIndex calls before spending limit creation (required
  by account-utilization merge for index > 0).
- accountIndexSpendingLimit: Updated to match actual behavior — the async
  settings transaction execution path does not enforce AccountIndexLocked.
- settingsChangePolicy: Added type narrowing for LegacySmartAccountSigner
  | SmartAccountSigner union.
- transactionCreateFromBuffer (v1 + v2): Updated OOM error regex to match
  Solana 3.0.0 error format.
@0xLeo-sqds 0xLeo-sqds force-pushed the feat/external-signatures-never-nonce branch from 88b928e to 7868345 Compare April 16, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant