Skip to content

feat(opp): add SYNDICATE_LIQ / LIQ_YIELD / DESYNDICATE_LIQ attestation types (simple_swap) - #609

Merged
valthon merged 1 commit into
masterfrom
simple_swap
Sep 15, 2026
Merged

valthon merged 1 commit into
masterfrom
simple_swap

Conversation

@valthon

@valthon valthon commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Protocol-only half of the cross-repo simple_swap feature (identically-named branches in wire-solana and wire-tools-ts; they land together).

What

Three new OPP attestation types plus their messages, so liq-token syndication moves onto OPP with the WIRE depot as ledger of record:

Type Value Direction Message
SYNDICATE_LIQ 60963 outpost → depot SyndicateLIQ { chain_code, user: ChainAddress, amount: TokenAmount, sequence }
LIQ_YIELD 60964 outpost → depot LIQYield { chain_code, amount: TokenAmount, sequence, epoch }
DESYNDICATE_LIQ 60965 depot → outpost DesyndicateLIQ { chain_code, user, amount: TokenAmount, request_id }
  • A holder hands liq tokens to the outpost (liqsol-core::synd in PostLaunch); the tokens become outpost property and the outpost keeps no per-user state.
  • LIQ_YIELD is the outpost's global report of yield claimed for the syndicated pool since its last report. It replaces the per-user STAKING_REWARD reports for syndicated liqSOL (per-user did not scale). Non-syndicated liqSOL keeps its normal on-chain yield.
  • DESYNDICATE_LIQ is depot-originated; the outpost pays inline at dispatch and log-and-skips refusals.
  • user is the holder's native pubkey in both directions (SVM: 32-byte Ed25519; EVM: 33-byte compressed secp256k1), never a derived address — the depot has exactly one identity for the AuthX-linked holder and the outpost derives the transfer destination. Every amount is a TokenAmount whose token_code names the chain's liq token as the depot registered it (e.g. LIQSOL) and whose amount is base units (liqSOL: 9 decimals, 1:1 with the depot frame). sequence is one per-outpost strictly-increasing counter shared by the two outbound types.

Deliberately not in this PR

Depot-side handling and emission: sysio.msgch::dispatch_attestation drops the two inbound types through its default: break (verified), nothing calls queueout with DESYNDICATE_LIQ, and the Solana relay has no effect_shape for it. Those land with the depot ledger. Ethereum is untouched (parity later).

Landing order

wire-solana's standalone CI resolves the published wire-opp-solana-models crate, so its PR stays red on that job until this merges and the opp-bundles publish runs. Local/e2e builds use the sibling paths override and are unaffected.

Cross-repo links

🤖 Generated with Claude Code

https://claude.ai/code/session_01NoaXitAvpNSNj7114S9338

@heifner

heifner commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

LGTM — one comment request, one question.

This unblocks the yield design's stated blocker (committed attestation types + messages). LIQ_YIELD being global rather than per-user is the right call — it's what keeps depot-side distribution O(1), where a per-user push is 100k row writes per tick × 172,800 ticks/day.

Verified the deferral claim, since it's the one that'd be dangerous if wrong: sysio.msgch::dispatch_attestation has default: break, returns void, doesn't throw on that arm. Inbound SYNDICATE_LIQ/LIQ_YIELD drop silently and can't stall an epoch.

Request: document what ChainAddress.address carries for each user

The protocol already uses both readings for EVM under the same field name — BAR.sol:305 packs abi.encodePacked(actor) (20-byte address), OperatorRegistry.sol:368 packs compressedPubkey (33 bytes). Both correct in context, but the convention lives in scattered comments rather than in the type.

These two fields want opposite contents:

  • SyndicateLiq.user is what the depot parks against and later matches an AuthX link on. sysio.authex's links stores a public_key and indexes only by_pub_key / bynamechain — no address field, no address index — and the depot can't derive an address from a compressed key (that needs the uncompressed point; msgch:735 only goes the other way). On EVM this must be the 33-byte compressed secp256k1 point.
  • DesyndicateLiq.user is a transfer destination → on EVM the 20-byte address.

Invisible for liqSOL, since an SVM address is the 32-byte pubkey. It bites when liqEth parity is written, and the natural thing to reach for — abi.encodePacked(msg.sender) — is the one that silently never matches a link. The rule follows the VM family rather than the chain, so Eclipse gets the SVM form and Polygon the EVM one.

// The syndicating user's native identity: what the depot parks the syndication
// against and later matches an AuthX link on. SVM: the 32-byte pubkey.
// EVM: the 33-byte COMPRESSED secp256k1 point, not the 20-byte address --
// sysio.authex indexes links by pubkey only, and the depot cannot derive an
// address from a compressed key.
sysio.opp.types.ChainAddress user = 2;

// Recipient's transfer destination on the outpost chain.
// SVM: the 32-byte pubkey. EVM: the 20-byte address.
sysio.opp.types.ChainAddress user = 2;

No code change. When parity lands, the pubkey is the existing one-liner Secp256k1.addressFromCompressedPubkey(pk) == msg.sender that OperatorRegistry.withdraw already uses.

Question: which ledger does the onboarding gate read?

SyndicateLiq.user parks by native address in the new depot ledger. sysio.dclaim also parks by native address, in unmapped_tokens, but holds WIRE owed rather than liq balances.

The onboarding flow creates a WIRE account only if a parked credit exists — and it reads unmapped_tokens. A user who only ever syndicated has no row there, so the gate would refuse an account to exactly the population this feature serves. Either the gate checks both ledgers, or syndication parks its WIRE-denominated yield into unmapped_tokens the way onreward does. Probably a depot-ledger-PR question, but it's decided by where that ledger lands.

For the record

Syndicating just before a LIQ_YIELD bump to capture a full reporting window's yield is real in shape but small in size: one ~2-day Solana window is 0.022% of pool value at 4%/yr, and an attacker doubling the syndicated pool captures half of that. Dilution, not an exploit — and a per-holder "syndicated at" timestamp would cost more than it recovers. Noting it as considered-and-accepted rather than unnoticed.

@valthon

valthon commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — both points land.

Doc request: applied in the proto itself, folded into the single commit (80e907b1, force-pushed). SyndicateLiq.user now says it is the native identity the depot parks against and matches an AuthX link on — SVM: 32-byte pubkey; EVM: the 33-byte compressed secp256k1 point, not the address, because sysio.authex indexes links by pubkey only and the depot can't derive an address from a compressed key. DesyndicateLiq.user says it is the transfer destination — SVM: 32-byte pubkey; EVM: 20-byte address — and calls out that it is the opposite reading. Encoding follows the VM family, as you said.

Onboarding gate: agreed this is decided by where the depot ledger lands, so it goes on that PR. My recommendation there: the gate reads both ledgers rather than syndication writing into unmapped_tokens. The syndication ledger is liq-denominated principal, unmapped_tokens is WIRE owed; folding one into the other to satisfy the gate would blur the two and force a conversion the depot has no reason to do at park time. A syndicated-only user should be able to create an account off the syndication row alone.

Dilution note: agreed, considered-and-accepted. The per-holder timestamp costs more than the 0.02%/window it protects.

heifner
heifner previously approved these changes Sep 12, 2026
@valthon

valthon commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Pushing back on the asymmetric reading after discussing it with David: both user fields carry the pubkey, byte-identical in each direction. Amended in place (74eadd7643).

The reasoning cuts the other way from the request. A de-syndication is not an arbitrary transfer destination — it pays only the AuthX-linked holder of the syndicated position, so the depot has exactly one identity for that holder: the pubkey it parked the SyndicateLiq against. The fact that the depot cannot derive an address from a compressed key is the argument for a single symmetric reference, not for a second encoding: if DesyndicateLiq.user were an address, the depot would have to produce something it has no way to compute. The outpost, on the other hand, already does the derivation — OPPInboundLib.sol:163 resolves an inbound remit's compressed pubkey with Secp256k1.addressFromCompressedPubkey, the same one-liner OperatorRegistry uses in three places — and on SVM the pubkey is the address.

Both comments now say pubkey explicitly, that DesyndicateLiq.user is byte-identical to the recorded SyndicateLiq.user, and that the outpost owns the address derivation. The VM-family encoding rule (32-byte Ed25519 / 33-byte compressed secp256k1) is kept as you wrote it; only the "destination = address" reading is dropped.

heifner
heifner previously approved these changes Sep 14, 2026

@jglanz jglanz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

amount shold never be bare, aside from the few fixes, this looks good

Comment thread libraries/opp/proto/sysio/opp/attestations/attestations.proto Outdated
Comment thread libraries/opp/proto/sysio/opp/attestations/attestations.proto Outdated
Comment thread libraries/opp/proto/sysio/opp/attestations/attestations.proto Outdated
Comment thread libraries/opp/proto/sysio/opp/attestations/attestations.proto Outdated
…n types (simple_swap)

Protocol-only half of the cross-repo `simple_swap` feature: liq-token
syndication moves onto OPP so the WIRE depot becomes the ledger of record
for syndicated liqSOL (and, at parity, liqEth).

- `ATTESTATION_TYPE_SYNDICATE_LIQ = 60963` (outpost -> depot):
  `SyndicateLIQ { chain_code, user: ChainAddress, amount: TokenAmount, sequence }` — a
  holder handed `amount` liq tokens to the outpost; from that moment the
  tokens are outpost property.
- `ATTESTATION_TYPE_LIQ_YIELD = 60964` (outpost -> depot):
  `LIQYield { chain_code, amount: TokenAmount, sequence, epoch }` — the outpost's GLOBAL
  report of liq yield claimed for the syndicated pool since its previous
  report. No user address: the depot splits it across its own ledger. This
  replaces the per-user `StakingReward` reports for syndicated liqSOL.
- `ATTESTATION_TYPE_DESYNDICATE_LIQ = 60965` (depot -> outpost):
  `DesyndicateLIQ { chain_code, user, amount: TokenAmount, request_id }` — depot-originated
  release; the outpost pays inline at dispatch, log-and-skip on refusal.

`user` is the holder's native pubkey in both directions (SVM: 32-byte
Ed25519; EVM: 33-byte compressed secp256k1), never a derived address: the
depot has one identity for the AuthX-linked holder and the outpost derives
the transfer destination. Every `amount` is a `TokenAmount` whose
`token_code` names the chain's liq token as the depot registered it (the
packed slug, LIQSOL on Solana) and whose `amount` is base units (liqSOL:
9 decimals, 1:1 with the depot frame). `sequence` is one per-outpost
strictly-increasing counter shared by SyndicateLIQ and LIQYield, the depot's
dedupe key.

Depot-side handling and emission are deliberately NOT part of this change:
`sysio.msgch::dispatch_attestation` drops the two inbound types through its
`default: break`, nothing calls `queueout` with DESYNDICATE_LIQ, and the
Solana relay has no `effect_shape` for it yet. Those land with the depot
ledger. The host `FC_REFLECT_ENUM` mirror is extended so the plugins can
name the new values.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NoaXitAvpNSNj7114S9338
Change-Id: I3c0d32add50f0f051573a9b4b5a6d9e360fdb57c
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.

3 participants