Skip to content

Feat/support permanent delegate and pausable extensions + block transfer hook [EXO-7]#104

Merged
sebatustra merged 5 commits intomainfrom
feat/support-permanent-delegate-and-pausable-extensions
Apr 28, 2026
Merged

Feat/support permanent delegate and pausable extensions + block transfer hook [EXO-7]#104
sebatustra merged 5 commits intomainfrom
feat/support-permanent-delegate-and-pausable-extensions

Conversation

@sebatustra
Copy link
Copy Markdown
Collaborator

@sebatustra sebatustra commented Apr 24, 2026

Support Pausable + Permanent-Delegate Token-2022 Extensions

Summary

Accept Token-2022 PausableConfig and PermanentDelegate mints (previously rejected on-chain). The withdrawal operator pre-flights each ReleaseFunds: if the mint is paused, or a permanent delegate has drained the escrow ATA below the withdrawal amount, the row is routed to ManualReview and a webhook fires.

Also blocks the TransferHook extension on-chain — honoring hooks needs ExtraAccountMetaList resolution in the CPI, which the pinocchio TransferChecked builder does not support.

Changes

On-chain: validate_token2022_extensions accepts pausable + permanent-delegate, rejects transfer-hook. Removed now-dead error variants; enum renumbered; generated clients regenerated.

Indexer: DbMint gains is_pausable / has_permanent_delegate (lazy RPC resolution + DB write-back). New TransactionStatus::ManualReview. check_withdrawal_preflights runs after build_release_funds, short-circuits for non-2022 mints, and bails via quarantine_single for paused / drained mints.

Tests: LiteSVM coverage for all three extensions (accept / accept / block), indexer unit tests for the pre-flight, and two E2E tests (pausable_mint_integration, permanent_delegate_mint_integration).

Why the split

Pause state and delegate balance change over time, so static on-chain rejection cannot catch them — off-chain live checks are the only option. Transfer hooks would fail at CPI time anyway, so rejecting at AllowMint / Deposit avoids burning fees.

Coverage Report

Component Lines Hit Lines Total Coverage Artifact
Core 9,043 10,447 86.6% rust-unit-coverage-reports
Indexer 14,937 17,511 85.3% rust-unit-coverage-reports
Gateway 991 1,115 88.9% rust-unit-coverage-reports
Auth 541 596 90.8% rust-unit-coverage-reports
Withdraw Program 118 230 51.3% unit-coverage-reports
Escrow Program 1,169 1,947 60.0% unit-coverage-reports
E2E Integration 9,635 11,927 80.8% e2e-coverage-reports
Total 36,434 43,773 83.2%

Last updated: 2026-04-27 22:20:17 UTC by E2E Integration

@linear
Copy link
Copy Markdown

linear Bot commented Apr 24, 2026

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 24, 2026

Greptile Summary

This PR flips the Token-2022 extension policy: PermanentDelegate and PausableConfig mints are now accepted at AllowMint/deposit time, with the safety burden moved to a new check_withdrawal_preflights pre-flight in the operator that does live RPC checks before each release. TransferHook mints remain blocked on-chain because the pinocchio builder cannot resolve ExtraAccountMetaList.

  • P1 — closed-ATA restart loop: get_ata_balance maps every RPC error (including "account not found") to OperatorError::RpcError; if a permanent delegate closes the escrow ATA, the operator will classify it as Transient and restart indefinitely rather than routing to ManualReview.
  • P2 — deployment ordering: on-chain error codes were renumbered (removed two variants, shifted 12→11 and 13→12 for InvalidSmtProof/InvalidTransactionNonce); the new indexer binary must not be deployed before the program upgrade is finalized.

Confidence Score: 3/5

Safe to merge after fixing the closed-ATA error handling in get_ata_balance; remaining findings are P2 and non-blocking.

One P1 finding: when a permanent delegate fully closes the escrow ATA, get_ata_balance surfaces an RpcError instead of returning 0, causing the operator to classify the failure as Transient and restart indefinitely rather than routing to ManualReview. This directly affects the correctness of the permanent-delegate pre-flight that is the central new feature of this PR.

indexer/src/operator/utils/mint_util.rs — get_ata_balance error handling for missing ATA; contra-escrow-program/program/src/error.rs + indexer/src/operator/utils/transaction_util.rs — verify deployment ordering for renumbered error codes.

Important Files Changed

Filename Overview
indexer/src/operator/utils/mint_util.rs Adds get_extension_flags, check_paused, and get_ata_balance to MintCache; get_ata_balance maps all RPC errors (including account-not-found) to OperatorError::RpcError, causing infinite restart if escrow ATA is closed by a permanent delegate.
indexer/src/operator/processor.rs Adds check_withdrawal_preflights for pause and permanent-delegate balance pre-flight checks; pre-flight runs after build_release_funds, creating an acknowledged TOCTOU window for permanent-delegate drains.
contra-escrow-program/program/src/error.rs Removes PermanentDelegateNotAllowed(6) and PausableMintNotAllowed(7), replaces with TransferHookNotAllowed(6), renumbering all subsequent error codes — requires coordinated deployment with the indexer.
indexer/src/operator/utils/transaction_util.rs Updates custom error code mappings from 12/13 to 11/12 to match renumbered on-chain program — must be deployed after the program upgrade to avoid misclassification of in-flight failures.
indexer/src/storage/postgres/db.rs Adds idempotent ALTER TABLE IF NOT EXISTS migrations for both new columns and a set_mint_extension_flags_internal write-back function guarded by rows_affected check.
indexer/src/storage/common/storage/mock.rs Fixes upsert_mints_batch to preserve extension flags on re-upsert, mirroring the ON CONFLICT DO UPDATE SET semantics in Postgres; adds set_mint_extension_flags mock.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[process_release_funds receives DbTransaction] --> B[build_release_funds\nwarms MintCache]
    B -->|InvalidBuilder| C[halt_withdrawal_pipeline]
    B -->|Ok| D[check_withdrawal_preflights]
    D --> E{Token-2022 mint?}
    E -->|No SPL Token| F[Skip pre-flight → dispatch]
    E -->|Yes| G[get_extension_flags\nCache → DB → RPC + write-back]
    G --> H{is_pausable?}
    H -->|Yes| I[check_paused via live RPC]
    I -->|paused=true| J[quarantine_single → ManualReview]
    I -->|paused=false| K{has_permanent_delegate?}
    H -->|No| K
    K -->|Yes| L[get_ata_balance via live RPC]
    L -->|AccountNotFound → RpcError| M[⚠️ Transient restart loop]
    L -->|on_chain < amount| J
    L -->|on_chain >= amount| F
    K -->|No| F
    F --> N[Scheduled rotation if boundary nonce]
    N --> O[Dispatch to sender]
Loading

Comments Outside Diff (1)

  1. indexer/src/operator/processor.rs, line 546-558 (link)

    P2 Pre-flight happens after build; TOCTOU window for permanent-delegate drain

    build_release_funds is called first, then check_withdrawal_preflights samples the ATA balance. Any drain that occurs between the balance check and on-chain dispatch will cause the TransferChecked CPI to fail on-chain rather than being caught here. The failure is still handled by the normal confirmation/retry path, but the comment explaining why the ordering is intentional ("warms MintCache") doesn't acknowledge this window.

    Consider adding an explicit note in the comment about the known race so future maintainers understand why a failed on-chain submission is still possible even when the pre-flight passed.

Reviews (1): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment thread indexer/src/operator/utils/mint_util.rs
Comment thread indexer/src/indexer/transaction_processor.rs Outdated
Comment thread indexer/src/operator/utils/transaction_util.rs
@sebatustra sebatustra requested review from amilz and dev-jodee April 27, 2026 22:28
@sebatustra sebatustra merged commit 14ac0cf into main Apr 28, 2026
12 checks passed
@sebatustra sebatustra deleted the feat/support-permanent-delegate-and-pausable-extensions branch April 28, 2026 19:30
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.

2 participants