Skip to content

feat!: canonical Solana mainnet+devnet deployment, base58 helpers (v0.4.0) - #22

Draft
reednaa wants to merge 4 commits into
fix/fill-description-domain-magicfrom
feat/solana-canonical-deployment
Draft

feat!: canonical Solana mainnet+devnet deployment, base58 helpers (v0.4.0)#22
reednaa wants to merge 4 commits into
fix/fill-description-domain-magicfrom
feat/solana-canonical-deployment

Conversation

@reednaa

@reednaa reednaa commented Aug 13, 2026

Copy link
Copy Markdown
Member

Impact — high severity

Solana order construction was unsafe for both mainnet and devnet:

  • The published input- and output-settler constants referenced PDAs derived from superseded, pre-vanity program IDs. They remained valid-looking 32-byte values, so SDK construction and shape validation succeeded silently, but orders carrying them could not target the canonical deployment.
  • Polymer EVM→Solana orders built by buildMandateOutputs placed the input chain’s EVM oracle address in MandateOutput.oracle. The Solana fill could transfer the user’s requested tokens, but the subsequent Polymer submission rejected the attestation consumer, leaving the fill unprovable.

This affects consumers using @lifi/intent to construct orders involving the canonical Solana mainnet or devnet deployment. Solana testnet remains unsupported.

What changed

  • Replaces the stale Solana constants with the canonical LiFi vanity-key deployment used by catalyst-intent-svm.
  • Separates Solana program IDs from their state PDAs.
  • Correctly selects the Solana Polymer program ID for cross-chain Solana outputs.
  • Adds guarded accessors for input/output settler and Polymer program/PDA values.
  • Adds raw Solana base58 ↔ bytes32 conversion and validation helpers.
  • Allows 32-byte Solana solver public keys in exclusiveFor.
  • Uses the output-settler PDA as inputOracle for same-chain Solana orders, where the local-attestation branch does not read inputOracle.
  • Rejects duplicate outputs and Solana output amounts larger than u64.

Mainnet and devnet use identical program IDs in catalyst-intent-svm/Anchor.toml; consequently their PDAs are also identical. Testnet has no configured deployment.

Non-interchangeable Solana values

These values serve different protocol roles and must not be substituted for one another:

Order field Required value Base58 bytes32 Rust evidence
MandateOutput.settler Output-settler state PDA DHShHmVkTwCzUzAQbCu4GDqJmursuDscNR6o4hTBgeRy 0xb68296ce230150bb20190a46eb26a198b05bec8fc7f0fa893d690cf531fa9e54 programs/outputs/output_settler_base/src/base.rs:122 validates output.settler against the output-settler account key.
MandateOutput.oracle for a Solana output Polymer program ID LiFiBtfyPT1DnTHTAeZ2rwr5RgMrThwA5kt7KGT5nBV 0x050cae5588f8d907500199177ab1239f61b8557af2ffacd5defed0070b858ad4 programs/oracles/oracle_polymer/src/instructions/submit.rs:71-79 requires the local-attestation consumer to match ctx.program_id, Polymer’s returnedProgramId.
StandardSolana.inputOracle for a remote fill Polymer oracle state PDA 49zLKETMq34CUC2E2wL1xvv6uN2AUgyhjVX221mjE3Rw 0x2ee088ace4ac030d2266e50d829feeac73d1acb13e5825ef835d6f3318804796 programs/oracles/oracle_polymer/src/instructions/receive_attest.rs:142-150 creates the remote attestation through ctx.accounts.oracle_polymer; programs/inputs/input_settler_base/src/base.rs:125-130 validates that attestation using input_oracle.

For a same-chain Solana fill, inputOracle is instead set canonically to the output-settler PDA because validate_fill takes the local-attestation branch at programs/inputs/input_settler_base/src/base.rs:88-111 and does not read input_oracle.

Canonical Solana deployment

The following program IDs are declared identically for mainnet and devnet in catalyst-intent-svm/Anchor.toml:8-20.

Component Kind Base58 bytes32
Intents protocol Program ID LiFixdGLT5CMdLsHBvaijTXpPy4Uux9Y53SXkuR4HaK 0x050cae668656471e02181ee1e6c52ee00a779ca7c481a04fb0e2ab9085f5c304
Input settler escrow Program ID LiFiRp8RM7nJUZyUYC9FPPpDr7sAy5XPfBN6ABzBgT7 0x050cae5ad286a6a6227a55fd650d63a3eccc866d6c293c94b87839e5cb6dba86
Output settler simple Program ID LiFiEDFjz5x1jJe9gSXNDHQW4dWt4yLXdp2VN4EiQUt 0x050cae566a93270d66dbfe1915b80dc06e4a80178992592b3d8f51e888d0afe5
Polymer oracle Program ID LiFiBtfyPT1DnTHTAeZ2rwr5RgMrThwA5kt7KGT5nBV 0x050cae5588f8d907500199177ab1239f61b8557af2ffacd5defed0070b858ad4
Chain ID PDA, seed chain_id, bump 255 BkEw3WHFvJR9a5deUcwPLJ79yK3r9YGdQ61TehFXzAQ 0x02c0b32f8be4a5319a95cca17bd05eba48e7195ce52f1608723b51e575457df5
Input settler escrow PDA, seed input_settler_escrow, bump 253 Cj3mSoPJtgi5bubC9rLz8oM1DuXoJj97RMw1uC6ev9zm 0xae3613f974fc9cd94682bbff7bd7f229697616c5b6fdb6e0c16d1f02607242ae
Output settler simple PDA, seed output_settler_simple, bump 255 DHShHmVkTwCzUzAQbCu4GDqJmursuDscNR6o4hTBgeRy 0xb68296ce230150bb20190a46eb26a198b05bec8fc7f0fa893d690cf531fa9e54
Polymer oracle PDA, seed polymer, bump 253 49zLKETMq34CUC2E2wL1xvv6uN2AUgyhjVX221mjE3Rw 0x2ee088ace4ac030d2266e50d829feeac73d1acb13e5825ef835d6f3318804796

buildMandateOutputs bug

The existing Polymer rule was:

output.oracle = oracle configured for the input chain

That is correct for EVM→EVM orders because the EVM PolymerOracle is deployed CREATE2-identically across supported EVM chains. Proofs are therefore keyed under the same address(this) value, and selecting the input chain’s configured oracle yields the expected identifier.

That assumption does not extend to Solana. For a Solana output:

  1. The output settler creates a LocalAttestation whose consumer is MandateOutput.oracle.
  2. The Solana Polymer program is represented to Polymer as its program ID via returnedProgramId.
  3. oracle_polymer::submit validates the local attestation with ctx.program_id as the expected consumer.
  4. The previous builder supplied the input chain’s EVM oracle address instead, so submission failed after the output transfer had already occurred.

buildMandateOutputs now handles Polymer Solana outputs before applying the generic EVM rule:

Solana output: output.oracle = Solana Polymer program ID
EVM output:    output.oracle = input chain’s Polymer oracle

Breaking changes

This package is still 0.x. The release moves from 0.3.0 to 0.4.0 because existing exported Solana values change and previously constructed orders may encode different bytes.

Existing exported names whose values change:

  • SOLANA_MAINNET_INPUT_SETTLER_ESCROW
  • SOLANA_DEVNET_INPUT_SETTLER_ESCROW
  • SOLANA_MAINNET_OUTPUT_SETTLER_PDA
  • SOLANA_DEVNET_OUTPUT_SETTLER_PDA
  • Mainnet/devnet entries in SOLANA_INPUT_SETTLER_PROGRAMS
  • Mainnet/devnet entries in SOLANA_OUTPUT_SETTLER_PDAS
  • inputSettlerForSolana(...) consequently returns the canonical escrow PDA instead of the superseded PDA.

New exported deployment names and registries:

  • SOLANA_INTENTS_PROTOCOL_PROGRAM
  • SOLANA_INPUT_SETTLER_ESCROW_PROGRAM
  • SOLANA_OUTPUT_SETTLER_SIMPLE_PROGRAM
  • SOLANA_POLYMER_ORACLE_PROGRAM
  • SOLANA_CHAIN_ID_PDA
  • SOLANA_INPUT_SETTLER_ESCROW_PDA
  • SOLANA_OUTPUT_SETTLER_PDA
  • SOLANA_POLYMER_ORACLE_PDA
  • SOLANA_INPUT_SETTLER_ESCROW_PROGRAMS
  • SOLANA_OUTPUT_SETTLER_PROGRAMS
  • SOLANA_POLYMER_ORACLES
  • SOLANA_POLYMER_ORACLE_PROGRAMS

New package-root helpers:

  • inputSettlerForSolana
  • inputSettlerProgramForSolana
  • outputSettlerForSolana
  • polymerOracleForSolana
  • polymerOracleProgramForSolana
  • solanaBase58ToBytes32
  • bytes32ToSolanaBase58
  • isSolanaBase58Address
  • SOLANA_ADDRESS_BYTES

Validation also gains OUTPUT_AMOUNT_EXCEEDS_U64 and DUPLICATE_OUTPUTS, so orders previously accepted by the generic validator can now be rejected when they cannot be safely filled.

Testing

The new specs prove:

  • Every canonical base58 address round-trips to its expected bytes32 representation.
  • Every PDA is recomputed from its program ID, seed, canonical bump, and Solana’s ProgramDerivedAddress marker.
  • The old pre-vanity input/output PDAs and output-settler program ID are explicitly pinned as forbidden anti-regression values.
  • Program IDs and their state PDAs remain distinct.
  • Mainnet and devnet registries point to the shared canonical deployment, while testnet remains unsupported.
  • EVM→Solana construction uses the Polymer program ID and output-settler PDA without querying the Solana output chain for an oracle.
  • Same-chain Solana construction works without a configured cross-chain oracle.
  • Solana exclusiveFor accepts an unpadded 32-byte solver public key.
  • Validation accepts the correct Solana input/output oracle roles and rejects the EVM oracle, Polymer PDA, and Polymer program ID when placed in the wrong fields.
  • Validation rejects superseded settler PDAs, duplicate outputs, and amounts above u64::MAX, while accepting u64::MAX and supported native SOL outputs.
  • Base58 helpers preserve leading zero bytes and reject malformed alphabets, non-32-byte payloads, EVM addresses, and Tron Base58Check addresses. The tests also document that a typo which still decodes to 32 bytes cannot be detected because Solana base58 has no checksum.

Full suite (bun test), plus bun run check and bun run build including the
ESM/CJS export smoke check:

258 pass
2 skip
0 fail
607 expect() calls
Ran 260 tests across 20 files
smoke ok — 97 named exports match across ESM and CJS

Merge order / release

This branch is based on fix/fill-description-domain-magic (v0.3.0, commit c2f32a1), which has not yet been merged to main.

Merge fix/fill-description-domain-magic first, then merge this PR. Releasing this branch independently would also include the pending v0.3.0 domain-magic changes.

The package version is bumped to 0.4.0. Publication is triggered by pushing the v0.4.0 tag after the merge-order dependency and release questions below are resolved.

Review notes / open questions

  • OrderContainer.inputSettler semantics for Solana require owner confirmation before release. SOLANA_INPUT_SETTLER_PROGRAMS and inputSettlerForSolana(...) currently hold/return the input-settler escrow PDA, and this PR updates that PDA to its canonical value. It is not yet confirmed whether the order service expects OrderContainer.inputSettler to contain that escrow PDA or the callable input-settler program ID. The order-service owner should confirm the contract before v0.4.0 is tagged.
  • On-chain initialization has not been verified. Program IDs are cross-referenced against catalyst-intent-svm/Anchor.toml, and the PDAs are deterministically re-derived, but this PR does not verify on-chain that every program and required state PDA has been deployed and initialized correctly on each advertised cluster. Mainnet and devnet initialization state, chain IDs, ownership, and required Polymer mappings should be checked before release.

….4.0)

The shipped Solana constants were PDAs derived under the pre-vanity program
ids, superseded when catalyst-intent-svm 21fc982 rotated every program to its
LiFi vanity key. They are correctly-shaped and correctly-derived, which is why
they went unnoticed: an order carrying them can never be filled.

Replaces them with the canonical deployment, and separates the three values
that all get called "the Solana Polymer oracle" but are not interchangeable:

  MandateOutput.settler       -> output settler PDA
  MandateOutput.oracle        -> polymer PROGRAM ID
  StandardSolana.inputOracle  -> polymer oracle PDA

buildMandateOutputs set output.oracle to the *input* chain's oracle for every
cross-chain Polymer output. That rule only holds because PolymerOracle is
CREATE2-identical across EVM chains; on Solana, oracle_polymer::submit compares
the fill's LocalAttestation consumer against its own program id. Every
EVM->Solana order the builder produced was therefore unprovable — the tokens
move, then proving fails. Solana outputs now resolve their own program id.

Also:
- new src/helpers/solana.ts: solanaBase58ToBytes32, bytes32ToSolanaBase58,
  isSolanaBase58Address. No new dependency (@scure/base was already present).
  Documents that Solana base58 is raw — no checksum, so a typo decodes to a
  different valid key and the 32-byte length check is the only defence.
- PDA/program-id accessors for symmetry with the Tron ones.
- Solana same-chain orders no longer require a configured oracle; they point
  inputOracle at the output settler like EVM and Tron, since validate_fill
  takes the LocalAttestation branch and never reads it.
- exclusiveFor accepts a 32-byte Solana solver pubkey; the 20-byte-only guard
  made exclusive fills structurally impossible on Solana outputs.
- validation rejects duplicate outputs (one transfer satisfies both fill
  records) and Solana output amounts above u64.
- constants.spec re-derives every PDA from (program id, seed, canonical bump)
  and pins the superseded values as forbidden.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2dcd06d2-6573-462c-832d-5f0207f4c936

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

reednaa added a commit to lifinance/lintent that referenced this pull request Aug 13, 2026
Temporary local `file:` dependency on ../intent.ts so this branch can build
against the Solana work in lifinance/intent.ts#22 before it is published.

This MUST be repinned to a published version before merge:
  "@lifi/intent": "0.4.0"

CI and the Cloudflare Worker build cannot resolve a file: path, so this branch
is expected to be red until the library lands.

Restores the Solana branch of formatAddressForChain, which was stubbed in the
previous commit because bytes32ToSolanaBase58 did not exist in 0.2.1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
reednaa and others added 3 commits August 14, 2026 14:06
The exports map listed `"node": "./_cjs/index.js"` ahead of `"import"`.
Condition order is first-match, so `node` won for every Node-side consumer
regardless of how they imported — an `import` from an ESM context got the
CommonJS build.

Node itself tolerates that, but a bundler evaluating the package in an ESM
module runner does not: SvelteKit's SSR inlines the CJS entry and dies on
`ReferenceError: require is not defined`, taking down the whole route that
imported it. Found in lintent, where a server route importing this package
returned 500 for every request including malformed ones — the module never
loaded, so no handler code ran.

`node` now branches on import/require rather than short-circuiting both to
CJS. `_esm/package.json` already declares `{"type":"module"}`, so the ESM
build was always correct to hand out; nothing else changes and `require()`
still resolves to `_cjs`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The quote API reads every address and asset in the CAIP-2 namespace its
sibling `chain` field declares. A Solana mint sent as 32-byte hex is
rejected with `bytes32 value has non-zero upper bytes`, because an
`eip155` asset is read as a left-padded 20-byte EVM address and a full
Solana key never fits. The same error appears under a correct `solana:`
chain still carrying hex, so the response cannot tell a wrong namespace
from a wrong encoding.

`toQuoteAddress` converts on the way out, from the `namespace` already
supplied alongside each chain id, so callers keep one internal
representation. Native base58 passes through unchanged. EVM and Tron are
deliberately untouched: the API accepts their hex today, and Tron base58
is a different (checksummed) encoding that should only be introduced
against a verified API expectation.

The address fields widen from `0x${string}` to `string` so a caller
holding a native Solana address needs no conversion of its own.

Verified against order.li.fi: the corrected payload returns HTTP 200.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An omitted `namespace` silently defaults to eip155, which sends
`eip155:1151111081099710` with an unconverted 32-byte asset. The API
then answers `bytes32 value has non-zero upper bytes`, naming the asset
rather than the namespace that actually caused it. Fail locally instead,
where the message can say which field to set.

EVM and Tron are both 20 bytes, so a 32-byte value under either is
always a caller mistake and never a legitimate address.

Also records why Tron is not converted: order.li.fi was probed live with
hex, base58 and mixed asset/receiver on a `tron:` output and returned 200
for all three. Solana is the only namespace that hard-rejects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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