Skip to content

feat(sdks): sealevel fee support svm sdk [IGNORE FOR NOW]#8647

Draft
xeno097 wants to merge 34 commits intomainfrom
xeno/sealevel-fee-support-svm-sdk
Draft

feat(sdks): sealevel fee support svm sdk [IGNORE FOR NOW]#8647
xeno097 wants to merge 34 commits intomainfrom
xeno/sealevel-fee-support-svm-sdk

Conversation

@xeno097
Copy link
Copy Markdown
Contributor

@xeno097 xeno097 commented Apr 21, 2026

Description

Implements Phase 2 of the SVM fee system: full SDK support for deploying and managing fee programs on Solana/SVM chains via the IRawFeeArtifactManager interface.

Requires #8642 (SVM fee program implementation) to be merged first. The fee program binary is temporarily injected via a droppable commit (0ae46733a6) that should be
removed once the program lands on main.

What's included:

  • Readers and writers for all 6 fee types: linear, regressive, progressive, offchainQuotedLinear, routing, crossCollateralRouting. Each type has a dedicated reader/writer following the existing artifact pattern. Leaf types
    (linear/regressive/progressive) share an abstract base to avoid duplication.
  • Instruction builders for all 18 fee program instructions: deployment/management instructions (InitFee, SetRoute, SetCrossCollateralRoute, UpdateFeeParams, SetBeneficiary, TransferOwnership, AddQuoteSigner, RemoveQuoteSigner,
    SetWildcardQuoteSigners, SetMinIssuedAt, RemoveRoute, RemoveCrossCollateralRoute) and runtime instructions (QuoteFee, SubmitQuote transient/standing, CloseTransientQuote, PruneExpiredQuotes, GetQuoteAccountMetas,
    GetSubmitQuoteAccountMetas).
  • PDA derivation functions for FeeAccount, RouteDomain, CrossCollateralRoute, TransientQuote, and StandingQuote PDAs.
  • Borsh codecs and account decoders for all fee program account types (FeeAccount, RouteDomain, CrossCollateralRoute, TransientQuote, StandingQuotePda).
  • SvmFeeArtifactManager implementing IRawFeeArtifactManager, wired into SvmProtocolProvider.createFeeArtifactManager().
  • Shared utilities: fee strategy mapping between provider-sdk and on-chain formats, wildcard signer computation, H160 signer conversion with validation, canonical BTreeSet encoding.
  • Stress tests that discovered on-chain limits via binary search:
    • InitFee max signers: 41 (tx size)
    • SetRoute max signers: 42 (tx size)
    • SetWildcardQuoteSigners max signers: 47 (tx size)
    • Incremental AddQuoteSigner: 409 (CU/heap)
    • Standing quotes per domain PDA: 113 (BTreeMap heap, Leaf and CC identical)

Drive-by changes

  • Breaking: ProtocolProvider.createFeeArtifactManager() now requires a FeeReadContext parameter to ensure routed fee types receive domain/router context for non-enumerable route PDA discovery. All protocol SDK implementations
    (tron, radix, cosmos, aleo, starknet) updated to accept the new parameter.
  • Added ByteCursor.readI64LE() and i64le() encoder for signed 64-bit integer support.
  • Added @noble/curves as a devDependency in svm-sdk for secp256k1 quote signing in stress tests.

Related issues

Backward compatibility

No — ProtocolProvider.createFeeArtifactManager() has a new required FeeReadContext parameter. This is a major version bump for @hyperlane-xyz/provider-sdk and @hyperlane-xyz/deploy-sdk. All downstream protocol SDK consumers need
to pass the context. The deploy-sdk factories (createFeeReader, createFeeWriter) already had context and now thread it through.

Testing

E2E tests (32 tests across 6 suites, all in CI):

  • fee-linear: 4 tests (create/read, no-op update, param update, beneficiary update)
  • fee-regressive: 4 tests (shared leaf suite)
  • fee-progressive: 4 tests (shared leaf suite)
  • fee-offchain-quoted-linear: 8 tests (shared leaf suite + signer create/add/remove + empty signer roundtrip)
  • fee-routing: 6 tests (create/read, add route, update params, change strategy type, remove route, offchainQuotedLinear route with signers)
  • fee-cross-collateral-routing: 5 tests (create/read, add pair, update params, change strategy, remove pair)

Open in Devin Review

@github-project-automation github-project-automation Bot moved this to In Review in Hyperlane Tasks Apr 21, 2026
@xeno097 xeno097 marked this pull request as draft April 21, 2026 21:43
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.02%. Comparing base (2496363) to head (22a40cd).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8647      +/-   ##
==========================================
- Coverage   79.33%   73.02%   -6.32%     
==========================================
  Files         143        7     -136     
  Lines        4278      708    -3570     
  Branches      436       32     -404     
==========================================
- Hits         3394      517    -2877     
+ Misses        855      190     -665     
+ Partials       29        1      -28     
Flag Coverage Δ
solidity ?
tron-sdk 73.02% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
core ∅ <ø> (∅)
hooks ∅ <ø> (∅)
isms ∅ <ø> (∅)
token ∅ <ø> (∅)
middlewares ∅ <ø> (∅)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread typescript/svm-sdk/src/accounts/fee.ts Outdated
}

function decodeFeeDataStrategy(c: ByteCursor): SvmFeeDataStrategy {
const kind = c.readU8() as FeeStrategyKind;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Unchecked as FeeStrategyKind cast on raw byte from on-chain data

c.readU8() returns number, but the cast as FeeStrategyKind narrows it to 0 | 1 | 2 without any validation. If the on-chain account contains a strategy kind value outside that range (e.g., a newer program version adds kind 3), the cast silently accepts it, violating the SvmFeeDataStrategy discriminated union type. Downstream pattern-match code in fee-strategy-utils.ts:35-46 uses an exhaustive never check that would throw at runtime, but the cast hides the real error origin. Per REVIEW.md mandatory type cast audit: every as must be flagged, and the only acceptable cast requires a // CAST: comment explaining why it's unavoidable.

Suggested change
const kind = c.readU8() as FeeStrategyKind;
const kind = c.readU8();
if (kind !== 0 && kind !== 1 && kind !== 2) {
throw new Error(`Unknown FeeDataStrategy kind: ${kind}`);
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

beneficiary,
maxFee: maxFee.toString(),
halfAmount: halfAmount.toString(),
} as C,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Unsafe as C type cast bypasses generic constraint in leaf fee reader

The object literal is cast as C where C extends LeafFeeConfig. The constructed object only has { type, owner, beneficiary, maxFee, halfAmount }, but LeafFeeConfig is a union that includes OffchainQuotedLinearFeeConfig which additionally requires quoteSigners: string[]. While current subclasses only instantiate C with types that match this shape, the cast circumvents TypeScript's ability to catch future misuse if a new leaf type with extra required fields is added. Per REVIEW.md mandatory type cast audit: "as X — flag it. The fix is almost always to fix the function signature, add a type guard, or restructure the code." No // CAST: comment is present.

Prompt for agents
In typescript/svm-sdk/src/fee/leaf-fee.ts line 91, the object literal is cast `as C`. The REVIEW.md mandates no `as` casts without a `// CAST:` comment. The fix is to either: (1) add a `// CAST: C is constrained to Linear/Regressive/Progressive which all share this exact shape; OffchainQuotedLinear has its own reader` comment if the cast is truly unavoidable, or (2) restructure to avoid the cast entirely, e.g. by having each concrete reader build its own config object and returning it without a cast.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +302 to +305
routerToBytes(router!),
),
],
annotation: `Remove CC route for domain ${domainStr} router ${router!.slice(0, 10)}...`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Non-null assertions router! without provable non-nullness on preceding line

On lines 302 and 305, router! is used after const [domainStr, router] = key.split(':') on line 293. TypeScript types the second destructured element as string | undefined since split returns string[] and destructuring beyond the array length yields undefined. While the key format ${domain}:${router} guarantees a colon exists, the non-null assertion is not provably safe from the immediately preceding line — it depends on reasoning about how the key was constructed several lines earlier. Per REVIEW.md: "! (non-null assertion) — flag unless the value is provably non-null on the preceding line."

Suggested change
routerToBytes(router!),
),
],
annotation: `Remove CC route for domain ${domainStr} router ${router!.slice(0, 10)}...`,
routerToBytes(router ?? ''),
),
],
annotation: `Remove CC route for domain ${domainStr} router ${(router ?? '').slice(0, 10)}...`,
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +36 to 48
const primaryPath = join(PROGRAMS_DIR, filename);
const fallbackPath = join(LOCAL_SO_DIR, filename);
let bytes;
try {
const bytes = readFileSync(path);
programBytes[key] = Array.from(bytes);
console.log(` ✅ ${key}: ${bytes.length.toLocaleString()} bytes`);
bytes = readFileSync(primaryPath);
} catch {
console.error(` ❌ ${key}: required .so file not found at ${path}`);
process.exit(1);
try {
bytes = readFileSync(fallbackPath);
} catch {
console.error(` ❌ ${key}: .so not found at ${primaryPath} or ${fallbackPath}`);
process.exit(1);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Fee program binary added to program-bytes generator with fallback path mechanism

In scripts/generate-program-bytes.mjs, the new tokenFee entry was added at line 28, and lines 36-48 introduce a fallback mechanism that checks LOCAL_SO_DIR (so-binaries/) when the primary path (rust/sealevel/target/deploy/) doesn't have the binary. This is a deviation from the existing pattern where all programs came from the same build directory. The fallback could mask build issues if a stale local binary is used instead of a freshly compiled one. The check-program-bytes-hash.mjs script should catch staleness via the source hash, but only if it covers the fee program sources.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

xeno097 added 23 commits April 22, 2026 14:28
…el-token-fee-support

Copies the compiled fee program .so into so-binaries/ and updates the
generation script to include tokenFee with a fallback to the local directory.
Drop this commit once the fee program lands on main and is built normally.
Added deriveFeeAccountPda, deriveRouteDomainPda, deriveCrossCollateralRoutePda,
deriveTransientQuotePda, and deriveStandingQuotePda to the PDA utilities module.
Added SvmDeployedFee, SvmFeeWriterConfig, DEFAULT_FEE_SALT, deriveFeeSalt,
FeeStrategyKind/FeeDataKind enums, and H160 signer address conversion utilities.
Added FeeParams, FeeDataStrategy, FeeData, LeafFeeConfig, RoutingFeeConfig,
CrossCollateralRoutingFeeConfig, RouteKey types with Borsh encoding for
instruction data serialization. Includes BTreeSet<H160> and discriminator constants.
Added ByteCursor.readI64LE() and i64le() encoder for signed 64-bit integers.
Added Borsh decoders for FeeAccount, RouteDomain, CrossCollateralRoute,
TransientQuote, and StandingQuotePda account types. Fixed RoutingFeeConfig
and CrossCollateralRoutingFeeConfig wildcard_signers encoding to match
on-chain BTreeSet (non-optional) layout.
Added InitFee, UpdateFeeParams, SetBeneficiary, and TransferOwnership
instruction builders with FeeInstructionKind enum covering all 18 instructions.
Added fetchFeeAccount for reading on-chain fee state and detectSvmFeeType
for mapping on-chain FeeData variants to provider-sdk FeeType values.
SvmLinearFeeReader reads Leaf(Linear) fee accounts and returns LinearFeeConfig.
SvmLinearFeeWriter creates and updates linear fee accounts with param, beneficiary,
and ownership diffing using existing address comparison utilities.
Tests create, read, no-op update, fee param update, and beneficiary update.
Each test deploys a fresh program instance for full isolation.
…iter

Extracted SvmLeafFeeReader/Writer abstract base with abstract feeType and
strategyKind properties. Slimmed down linear-fee.ts to thin subclass.
Added SvmRegressiveFeeReader/Writer following the same pattern.
…tests

Extracted defineLeafFeeTests shared suite for leaf fee types. Refactored
linear test to use it. Added regressive fee E2E tests using the same suite.
Added SvmProgressiveFeeReader/Writer extending the leaf fee base.
E2E tests validate create, read, no-op update, param update, and beneficiary update.
…uilders

Handles all three route variants: Leaf (fee_account writable, no route PDA),
Routing (route PDA writable), and CC (cc_route PDA writable).
Reads Leaf(Linear) with signers and maps to OffchainQuotedLinearFeeConfig.
Writer creates with signers via InitFee, update diffs params, beneficiary,
signer set (add/remove), and ownership.
Reuses shared leaf test suite for common cases (create/read, params, beneficiary).
Adds signer-specific tests: read signers after create, add signer, remove signer.
Widened LeafFeeConfig union to include OffchainQuotedLinearFeeConfig.
… instruction builders

SetRoute creates/updates per-domain RouteDomain PDAs for Routing mode.
RemoveRoute closes a RouteDomain PDA returning rent to owner.
SetWildcardQuoteSigners sets the wildcard signer set on the fee account.
Fetches and decodes a RouteDomain PDA for a given fee account and destination domain.
Reader uses FeeReadContext to discover which domain route PDAs to query
(route PDAs are not enumerable on-chain). Maps on-chain RouteDomain data
to provider-sdk FeeStrategy including offchainQuotedLinear detection.

Writer deploys Routing fee with per-domain routes and computed wildcard
signers. Update diffs routes (add/update/remove), wildcard signers,
beneficiary, and ownership.
Tests create/read with multiple routes, offchainQuotedLinear route with signers,
add new route, update existing route params, change route strategy type, and
remove route.
…oute instruction builders

SetCrossCollateralRoute creates/updates per-(destination, targetRouter) CC route PDAs.
RemoveCrossCollateralRoute closes a CC route PDA returning rent to owner.
Fetches and decodes a CrossCollateralRoute PDA for a given fee account,
destination domain, and target router address.
…ract shared strategy utils

Extracted routeDataToFeeStrategy, feeStrategyToOnChain, and
computeWildcardSignersFromStrategies into fee-strategy-utils.ts, shared by
both routing and CC routing implementations.

CC routing reader uses FeeReadContext (domain, router) pairs to discover
CC route PDAs. Writer deploys CC routing fee with per-(domain, router) routes
and computed wildcard signers. Update diffs routes, wildcard signers,
beneficiary, and ownership.
Tests create/read with multiple (domain, router) pairs, add new pair,
update existing route params, change strategy type, and remove route.
All update tests assert exact expected transaction counts.
xeno097 added 11 commits April 22, 2026 15:07
Added SetMinIssuedAt, SubmitQuote (transient and standing variants),
CloseTransientQuote, PruneExpiredQuotes, GetQuoteAccountMetas,
GetSubmitQuoteAccountMetas, and QuoteFee instruction builders.
Includes SvmSignedQuoteData type and Borsh encoder for the quote payload.
Implements IRawFeeArtifactManager with readFee (auto-detects fee type),
createReader, and createWriter factory methods for all 6 fee types.
…add exports

SvmProtocolProvider.createFeeArtifactManager() now returns a real
SvmFeeArtifactManager instead of null. Added all fee reader/writer,
type, PDA, and artifact manager exports to the public API following
the existing export pattern.
Binary search + incremental growth tests that find the maximum:
- InitFee (Leaf) signers: 41 (tx size limit)
- SetRoute signers: 42 (tx size limit)
- Incremental AddQuoteSigner on Leaf: 409 (CU/heap limit)
- SetWildcardQuoteSigners: 47 (tx size limit)

Tx size bottleneck: ~1232 byte legacy tx limit, each H160 signer = 20 bytes.
CU/heap bottleneck: BTreeSet deserialization at ~400 entries exhausts compute.
Standing quote limits require secp256k1 signing infrastructure (noted as TODO).
Binary search + incremental growth tests that find the maximum:

TX SIZE LIMITS (~1232 byte legacy tx payload, each H160 = 20 bytes):
- InitFee (Leaf) max signers:          41
- SetRoute max signers:                42
- SetWildcardQuoteSigners max signers: 47

CU / HEAP LIMITS (BTreeSet/BTreeMap deser on 32KB heap):
- AddQuoteSigner incremental on Leaf:  409
- Standing quotes per domain PDA:      113 (Leaf and CC identical)

Added @noble/curves as devDependency for secp256k1 quote signing in tests.
- High: fix empty signer sets misread as no signers. Use isNullish() to
  distinguish None from Some(empty) — on-chain Some([]) means offchain
  quoting enabled with zero signers, not on-chain-only mode.

- Medium: fix ownerless fee deployment. Use isZeroishAddress() instead of
  truthiness to detect zero-address sentinel in all create paths (leaf,
  offchain-quoted, routing, CC routing).

- Medium: fix unsafe signer/router hex parsing. Use addressBytes() +
  ensureLength() from codecs/binary.ts for validated conversion in
  signerToH160() and routerToBytes().

- Low: fix BTreeSet codec to sort entries before encoding, matching Rust
  canonical BTreeSet serialization order.

- Medium (documented): signers are orthogonal to strategy on-chain but
  provider-sdk only models offchainQuotedLinear. Added comments noting
  this limitation in detectSvmFeeType and routeDataToFeeStrategy.
… createFeeArtifactManager

Made FeeReadContext a required parameter on ProtocolProvider.createFeeArtifactManager()
so that the deploy-sdk factories pass the context from the warp config all the way
through to protocol-specific artifact managers. This fixes the high-severity issue
where SVM routing/CC fee updates saw empty routes because context was lost.

The deploy-sdk FeeReader/FeeWriter factories now pass context to the protocol provider.
All protocol SDK implementations updated to accept the new parameter.
…uotedLinear type

Validates that removing the last signer results in Some(empty set) on-chain,
not None, and the fee type reads back as offchainQuotedLinear (not linear).
Added fee-linear, fee-regressive, fee-progressive, fee-offchain-quoted-linear,
fee-routing, and fee-cross-collateral-routing to the SVM SDK e2e test matrix.
Stress tests excluded (exploratory, long-running).
- Replace unsafe `as FeeStrategyKind` cast with `isFeeStrategyKind()` type
  guard that validates at runtime and narrows the type via `value is`.
- Add CAST comment on `as C` in leaf-fee reader explaining why
  OffchainQuotedLinearFeeConfig never flows through this path.
- Replace `router!` non-null assertions with `assert(router)` in CC
  routing update diff.
- Replace `!` non-null assertions in BTreeSet sort comparator with
  nullish coalescing.
- Replace `expected.routes[domain]!` with Object.entries iteration.
@xeno097 xeno097 force-pushed the xeno/sealevel-fee-support-svm-sdk branch from 79ebd4c to 22a40cd Compare April 22, 2026 20:20
@hyper-gonk
Copy link
Copy Markdown
Contributor

hyper-gonk Bot commented Apr 22, 2026

Node Services Docker Image Built Successfully

Service Tag
node-services 22a40cd-20260422-202056
Full image paths
ghcr.io/hyperlane-xyz/hyperlane-node-services:22a40cd-20260422-202056

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

1 participant