LIQ yield flow: sysio.liq, msgch handlers, depot cranks, Solana relay shape - #634
Conversation
….swap Byte-identical snapshot of https://github.com/EOSArgentina/evolutiondex at 066feb3ccd8d406ba2e095ef9978f6859a89bb03 (master, 2020-11-10, MIT — LICENSE kept verbatim), laid out in the house contract shape and renamed to sysio.swap: evolutiondex.{hpp,cpp} -> include/sysio.swap/sysio.swap.hpp, sysio.swap.cpp utils.hpp, safe.hpp -> include/sysio.swap/ token_functions.cpp -> token_functions.cpp evolutiondex.{contracts,clauses}.md -> sysio.swap.{contracts,clauses}.md tests/, wevotethefee/ -> kept alongside for the port No source edits, no CMake wiring — nothing compiles yet; the port to the sysio CDT idioms lands on top of this so every change diffs cleanly against upstream. Also narrows .gitignore's vim-swap pattern from *.sw* to *.sw?: the old glob matched the directory name sysio.swap and silently ignored the whole contract tree. contracts_unit_test: No errors detected. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Change-Id: Icd32083b8c506efe914f70a7a89e73a840f01d77
…o contracts_unit_test
Minimal port of the pristine import so sysio.swap builds, deploys, and
passes every upstream test case on WIRE:
- eosio -> sysio throughout (headers, attributes, sysio::multi_index — a
drop-in over KV, so all four tables and both secondary indices are
unchanged); namespace evolution -> sysio, class evolutiondex -> swap,
contract name "sysio.swap". Logic untouched.
- CMake: contracts/sysio.swap registered as a two-TU add_contract; built
wasm/abi copied into the source tree per house convention.
- Ricardian .md files moved under ricardian/: the CDT's add_contract macro
passes its `${contracts}` glob unquoted to set_source_files_properties,
which fails whenever a directory carries BOTH <name>.contracts.md and
<name>.clauses.md. No house contract ships those in-dir, so the bug was
latent; needs a one-line quoting fix in wire-cdt's CDTMacros.cmake.in.
Tests: all 7 upstream cases move to contracts/tests/sysio.swap_tests.cpp,
with badtoken and wevotethefee as contracts/test_contracts/. Fixture
changes forced by WIRE semantics, not by the contract:
- sysio.token hard-codes ram_payer = "sysio", which only a privileged
contract may bill: every account hosting the token WASM (sysio.token,
anothertoken, carol) is set_privileged, as sysio.token_tests does.
- WIRE requires {payer, sysio.payer} in the action authorization when a
contract bills RAM to a user; inittoken's helper (the one place upstream
pushed a bare actor list) now carries it explicitly.
- Every setup step asserts success(). Upstream ignored those results, which
hid a real bug: memoexchange_test sends 0.0001 VOICE bob -> alice first,
so many_transfer's 0.0903 VOICE deposit silently overdrew; it is 0.0902.
- indextable's hard-coded id_256 embeds the token account name; the
eosio.token word is recomputed for sysio.token (verified independently).
- Tester API deltas: push_action takes no delay_sec; boost int256_t alias
collided with a chain type; edump((ex)) has no fmt formatter.
contracts_unit_test (full binary, --sys-vm): No errors detected.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Change-Id: Ia097ef394c0b4312c03e1d1bd3ff4499f74d86cc
…ized-invariant tests changefee accepted any int. A negative fee makes compute() subtract the fee (the pool pays out more than constant product allows), and a fee at or above 100% can push compute()'s result past the int64 range it checks BEFORE the fee is added. It now requires 0 <= fee <= MAX_FEE (FEE_DENOMINATOR - 1 = 9999); the 10000/9999 literals in compute() are the named FEE_DENOMINATOR. Three test cases close the gaps the upstream suite left on the swap math: - changefee_bounds: -1, 10000, INT_MIN, INT_MAX are rejected with state unchanged; 0, 9999 and every wevotethefee fee-vector value are accepted; at 9999 a swap still settles at exactly the spec quote. - compute_rounding_table: at each of the 15 fee-vector values and 7 amounts (1 unit to 1,000,000 units), both swap directions and exact-output withdrawals are pinned to the unit against a reference written from the spec (pay rounds up, receive rounds down, fee rounds up): one unit past the quote is refused, the quote lands, pool and user deltas match. Unit- share add/remove rows cover the liquidity path the same way. - invariants_under_random_sequences: a fixed-seed sequence of 400 ops over both pools and both users (swaps both ways, exact-output, add, remove, fee changes; log-uniform sizes from dust to full balance) asserts after every op that tokens are conserved and pool value per share never decreases, that any failure is one of the contract's own guards, and minimum success counts per op kind so the run cannot pass vacuously. The three cases run in ~4 s combined. contracts_unit_test (full binary, --sys-vm): No errors detected. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Change-Id: Ibb536d371e26f67b4a6377338b51f451d489436a
exchange now accepts exact-input swaps only: ext_asset_in must be positive and min_expected nonnegative. The implied-argument mode that let a negative ext_asset_in request an exact output amount is removed along with its documentation in Commands.md and the ricardian clause. Tests: the former exact-output success paths now assert rejection with unchanged state, the rounding table drops its exact-output block, and the randomized-invariant test replaces the exact-output op with a negative-input rejection op (>=40 rejections per run). Full contracts_unit_test green before commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Change-Id: I4e46020619d0bc55d2e262dff755531e4fd92550
process_exch now takes its gross output from the equal-weight constant-product path of sysio.opp.common/amm_math.hpp instead of the negative branch of compute(). The pair fee is unchanged (rounded up, via the new shared ceil_fee helper that compute() also uses), so every quote is bit-identical to before; the guards on a zero input or an empty pool keep their "invalid parameters" message. Characterization coverage added for the substitution, all written against the spec or the host-side amm_math model rather than the contract's arithmetic: differential agreement with the model across both pools, both directions, five fees and six amounts; fee-zero output equals the bare kernel and is the largest integer that keeps x*y from falling; round trips never profit; precision extremes with a one-unit dust pool, a whale pool grown past the init ceiling and whole-balance trades landing on the int64 asset ceiling; guard semantics through the memo path with badtoken's zero-amount notifications; the ABI surface pinned. sysio_swap_tests: 10 -> 16 cases. Full contracts_unit_test green before commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Change-Id: I8e9e53f80f4234a43e9f6136c2bdc5efeff0042a
The swap fee now comes from the depot-wide fee decomposition in sysio.opp.common/amm_math.hpp: split_wire_fee(gross, fee, 0).net, with no underwriter share because the fee stays in the pool for the liquidity providers. That convention rounds the fee DOWN, where the evolutiondex math rounded it up, so a quote nets one unit more whenever gross*fee is not a multiple of 10000 -- and a one-unit quote is no longer eaten by its own fee. The curve output is still floored, so the pool product never falls. Liquidity pricing (compute, ceil + 0.01%) is unchanged. MAX_FEE keeps its 9999 bound with the new rationale (below 100% a positive quote always nets a unit; split_wire_fee reports net 0 at 100%). The ricardian exchange clause and the README now state the floor-fee rule; the clause had described the result as x + y. Tests: the reference model splits into a floored swap fee and a ceiled liquidity fee, the host model composes out_given_in with split_wire_fee, and the pinned quotes in exchange_action, increasing_poolvalue and memoexchange_test move by one to three units (re-derived with an independent replay). The precision-extremes case now pins the one-unit dust quote reaching the trader. Full contracts_unit_test green before commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Change-Id: I2bfa45ca5d12cd62312c018b86b6c1aa33e6e006
split_wire_fee rounds the fee down, which leaves any quote below FEE_DENOMINATOR/fee units fee-free; at the 10 bps floor that is a thousand units, and "units" is precision-relative, so a zero-decimal token could trade a thousand whole tokens per swap without paying the pool. Trades that also divide the curve exactly then leave x*y untouched, and those amounts can be chosen deliberately. process_exch now takes max(floored fee, MIN_SWAP_FEE) whenever both the pair's rate and the gross quote are nonzero; a zero rate still charges nothing. Every fee-bearing trade grows x*y again. Reference and model in the tests carry the same rule, the pinned quotes move by one unit where a swap's floored fee was zero, and a new case walks the boundary: a 999-unit quote pays one unit, a 19956-unit quote is unaffected, a zero rate charges nothing, and x*y strictly grows on the smallest trades. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Change-Id: If2b6c93496278f3f25b55a335dab050effa03248
Every pair now keeps two 256-bit accumulators in a `priceaccum` row: the running sum of each side's Q64.64 spot price (the other side's balance over its own) times the microseconds that price held. They advance at the spot price in force since `last_update` immediately before the pools change -- swaps, adding and removing liquidity -- and on a new permissionless `sync` action, so a reader can bring a snapshot up to date without trading. A reader records the row at t0 and again at t; (r - r0) / (t - t0) is the time-weighted average price over the window. A trade that moves the spot price inside a block contributes nothing until time passes at the moved price. The kernel lives in sysio.opp.common/twap.hpp: integer only, host- compilable, with the 256-bit add (both carries), modular difference, and the 256-by-64 division that recovers the average. A Q64.64 price of int64 balances is below 2^126 and elapsed fits 64 bits, so the sum cannot wrap within the life of any chain. The row's limb type is spelled `uint128_t` so the ABI generator emits the `uint128` builtin. Tests: a host suite (twap_tests) checks the kernel against boost 256-bit arithmetic, including every carry and borrow path and 200 random multi-step windows; contract cases follow the accumulators through swap, add, remove and sync and recover the reader's average, show two trades in one transaction add nothing for the second, and hold the steepest int64 price for a week past 128 bits. The ABI pin gains the `sync` action, the `priceaccum` table and the two new structs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Change-Id: If0889e91e920c6a8e6ff305bfe78da1a31ea8891
The five tables leave the multi_index compatibility wrapper for the
native KV API, with explicit key structs and payer-first mutations:
accounts scoped_table by owner, key = symbol code (bytes unchanged,
so wevotethefee's foreign multi_index read still works)
evodexacnts scoped_table by user, keyed by the deposited token's
extended symbol -- the surrogate id, available_primary_key,
make128key and the uint128 secondary index all go; a
deposit lookup is one primary get
stat unscoped table keyed by the pair's symbol code (the old
scope duplicated the primary key)
evoindex unscoped table keyed by the pair identity itself, the two
legs in canonical (lower contract, symbol first) order --
the checksum256 hash, make256key and its secondary index
all go; a duplicate pair is a plain emplace conflict
priceaccum unscoped table keyed by the pair's symbol code; the
redundant `pair` field leaves the row
Table entries in the ABI now carry key_names/key_types/table_id, so an
explorer can decode the composite keys as named fields. The test
fixture reads deposits by extended symbol through a raw KV reader for
composite keys, the indextable case pins the canonical four-word key
(and the absence of the reversed one), and the ABI pin covers every
table's row type and key layout.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Change-Id: Ibce6a77e0da625217537923ab2fe23a7a8f42b4f
…thority
The account allowed to change a pair's fee was wired to the upstream
wevotethefee voting contract: inittoken accepted no other name and no
initial fee but wevotethefee's 10 bps floor, and every LP-token
transfer, add and remove notified it so it could weigh votes by stake.
WIRE governance executes approved proposals as sysio, so the fee
authority is now a plain permission holder:
* a `swapconfig` singleton, set at deployment by `setconfig` under
the contract's own authority, names the contract-wide fee
authority (sysio);
* inittoken takes `fee_authority`: an empty name adopts the
configured one, any other name becomes that pair's own -- pair
creation already needs the contract's authority, so an override
cannot be planted by an arbitrary caller;
* `initial_fee` may be anything in [0, MAX_FEE];
* changefee requires the pair's stored authority, as before;
* the pair row's `fee_contract` is renamed `fee_authority`, and the
require_recipient notifications go with the voter they served.
The wevotethefee test contract, its CMake and fixture hooks, its test
case and its fixture helpers are removed; badtoken stays for the guard
tests. New cases cover the unconfigured state (pair creation needs
either a configured or a named authority), per-pair authority
isolation, reconfiguration binding only pairs created afterwards, and
the open fee range at creation. The ABI pin gains setconfig, the
swapconfig singleton and the renamed field.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Change-Id: I495df05e030b7fe0b5b349e48bedc31215da307b
…share lock inittoken minted sqrt(pool1 * pool2) through the floating sqrt, which the consensus rules forbid: its rounding is platform-dependent. The seed supply now comes from amm::geometric_mean, an integer Newton floor(sqrt(x*y)) added to sysio.opp.common/amm_math.hpp, so every node mints the same share count on every input. inittoken also takes `locked_shares`: part of the minted supply that is credited to no account and can never be redeemed, the Uniswap v2 minimum-liquidity idea with the size left to the pair's creator. The lock keeps the pool from ever being emptied and re-seeded at another unit, and bounds how far one share's value can be pushed. Seed-time attacks only victimise the creator, so the size is the creator's decision and zero is allowed; the creator must keep at least one share. The pair row records the lock, and remliquidity refuses to take supply below it (ownership already makes that unreachable; the check is defense in depth beside "the pool cannot be left empty"). Tests: host cases pin isqrt across the int128 range (exact around 20000 random perfect squares and at the top of the range) and the geometric mean, the fixture pins the exact integer seed supply of both standard pools, and a contract case walks the lock: the three validation failures, the creator holding minted-less-locked, removing every creator share leaving the locked slice of the pools behind, and the pool trading and growing again from that floor. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Change-Id: I6edefc852f4653bacbbd50d728e8f5399693b92c
indexpair back-filled the pair-uniqueness index for pairs that upstream had created before the index existed. Every pair here is indexed by inittoken at creation and there are no pre-index pairs to migrate, so the action could only ever fail with "the pool is already indexed". The evoindex table and the placeindex helper stay: they are what enforce one pool per token pair. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Change-Id: Ib1ade4a5ac33c23c5f6da56a2777e2fa5fca1127
safe.hpp was evolutiondex's copy of the old fc::safe<T> checked-integer wrapper, 221 lines whose only consumer was the digit accumulation in asset_from_string, the parser for the min_expected asset of an "exchange:" memo. The parser now guards the same three arithmetic steps with __builtin_mul_overflow / __builtin_add_overflow (deterministic on WASM) and the header is gone. The parse also loses a dead branch: it negated the fraction on a leading '-', but the digit loop rejects a sign before it could run, so memo amounts have always been unsigned magnitudes, which they should be. Tests pin the overflow guard on the memo path: a digit string past int64, the last digit that overflows, a scaled integer part that overflows, one that fits int64 but not an asset (the asset's own magnitude refusal), more than 18 decimals, a sign, a bare decimal point and a missing separator. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Change-Id: I617c96039bc40d06a1f7cb299f75be8f616527c8
A pair may now name one of its legs as its shadow token, which makes it a yield pool; an empty yield leg is a plain pool with unchanged behaviour. A `yieldpairs` table keyed by the shadow's extended symbol enforces at most one yield pool per shadow symbol and doubles as the symbol-to-pair lookup the yield paths will use. The pair row gains the tick parameters (`conversion_horizon_sec`, `depth_cap_bps`, `last_tick`), set by the pair's fee authority through the new `setyield` action; both must be nonzero before a tick can run. Tests cover leg validation (must be one of the pair's legs, contract included), the uniqueness rule against a second pool over the same shadow, a plain pool alongside, and setyield's authority, target and range checks. The ABI pin gains the new fields, action and table. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Change-Id: Idefdd2d7a469a6da7366f1c4f905af8fc8285df5
A yield pool's shadow token pays WIRE yield to its holders through a cumulative index, and the contract, holding the pool's shadow, is such a holder. `sysio.opp.common/shadow_yield.hpp` is the read contract between the token and its holders: table names, row layouts, the index scale, the `owed` formula, and the two typed actions a holder calls (`claim`, `addyield`). The swap reads the token's tables in place through aliases local to its implementation file (an alias inside the contract class makes the ABI generator list the table as the contract's own). `accrue` computes what the contract is owed from that public state, credits it to the pool's other leg with no shares minted, records a receipt keyed by the shadow contract, and calls the token's `claim` inline; `ontransfer` matches the delivering transfer against the receipt and retires it, failing on any other amount. A receipt that outlives its transaction blocks further accrual through that contract. Accrual runs before every mint and burn (`add_signed_liq`), so yield always belongs to the shares that existed when it was earned, and the new `accrueyield` action lets anyone settle in between. A plain token in a yield leg has no index row and reads as owing nothing. Deposit accounts are not yield-bearing: the contract's whole holding earns for the pool. `contracts/test_contracts/shadowtoken` is the mock shadow token the suite drives: sysio.token's shape plus the distribution, settling every balance move, stamping new holders at the current index, and carrying the truncation remainder. Tests cover a single and a chained distribution against a hand-written reference, exactness of the delivered payout, the mint and burn pricing against the accrued pool, refusals, and the payout route being live only while a receipt exists. Docs and the ABI pin gain the yield leg, setyield and accrueyield. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Change-Id: Ic18f2d01bd283719a4c041bef139c8bd6cb90d3b
A yield pool gains a reservoir (`reservoirs` table, keyed by the pair, created at inittoken): shadow queued to be sold through the pool, held by the contract but in no pool and no deposit. It is its own row rather than a deposit row under a synthetic owner, so no account name can alias it. `fundyield(from, pair_token, quantity)` is the typed, memo-free announcement: `from` names the pair and the exact amount of its shadow it will transfer, and the transfer that matches (same contract, symbol and amount) fills the reservoir instead of `from`'s deposit, in the same transaction or a later one. One announcement per account is pending at a time, a new one replaces it, and while one is pending any other transfer from that account is refused. Both receipt routes in `ontransfer` now compare field-wise: asset's own == asserts on a symbol mismatch, which here must be an ordinary "does not match" failure. Tests cover the refusals, the pending state across transactions, the replacement, the one-transaction shape a contract would use inline, and that the reservoir earns yield for the pool like the rest of the contract's holding while accrual leaves the queue alone. Docs and the ABI pin gain fundyield, the receipt table and the reservoir. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Change-Id: Ia754e0e2fbd8cb36c10d6cfd2a9430ae90a06ea0
`tickyield(pair_token)` sells one clip of a yield pool's reservoir through the pool and hands the proceeds to the shadow's holders. The clip is the reservoir's share of the horizon elapsed since the pair's clock last advanced, rounded up so a nonempty reservoir always drains, capped by `depth_cap_bps` of the pool's shadow side and by what is queued; it is sold at the pool's own curve and fee (the fee stays with the providers) after the pool's owed yield has been settled, and the proceeds go out through the token's `addyield`, so the pool, a holder, takes its share back on the next accrual. An empty reservoir, no elapsed time, or a clip that rounds to nothing is a no-op. Ticking more often does not sell faster: a second tick in the same block does nothing. The clock (`last_tick`) advances on every tick that sells, restarts on `setyield`, and restarts when a funding fills a reservoir that was empty, so a queue sells over a fresh horizon from the moment it is funded rather than dumping against a stale timestamp. The token moves the proceeds out of the contract under the contract's authority, so deployment grants the shadow token's sysio.code seat on the contract's active permission; the fixture does the same (keeping the contract's own seat). The test drives the refusals, the empty no-op, the clock restarts, one clip against the hand-written pacing and curve references with the index advancing by the proceeds, the fee retained, the proceeds returning on the next accrual, same-block pounding, the cap after a long gap, a full drain with the cap lifted, and accrual-before-trade ordering. Docs cover the tick and the deployment steps. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Change-Id: I0dbbca1bfbf5745337494583c7f6216c01416dee
…r is pinned `setconfig` now also names the system token (WIRE), and `inittoken` requires every pair's second leg to be it; the first leg is the pair's own token, and since pairs are unique there is exactly one pair per token. That orientation lets `ontransfer` recognise a transfer without a table of its own: a token is accepted if it is the system token, or if the pair it forms with the system token exists, one lookup in the pair index. Anything else is refused, a look-alike symbol from another contract included, which is what the evolutiondex `badtoken` surface was about. A first leg's seed lands before its pair exists, so a transfer carrying the contract's own authority, the authority that creates pairs, is accepted regardless. Nothing works before `setconfig`: no deposit, no pair. A yield leg must now be the first leg, and the `yieldpairs` table is gone: one pair per first leg already makes one yield pool per shadow, and the pair index is the lookup. The tests configure EOS on sysio.token as the system token, so every upstream pair flips to (token, EOS) and the seed deposits of VOICE, TUSD, SHD and badtoken's EOS carry the contract's authority; pool-index expectations and the accumulator direction checks follow. New coverage: the refusal of an unpaired token with and without the authority, the second-leg rule in both orders and against a look-alike contract, the pair token itself being undepositable, and the unconfigured contract refusing everything until setconfig runs. Docs and the ABI pin follow. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Change-Id: I51e3337c65026a8977fa96a97e26329af8076192
…contract's authority Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Change-Id: If0417cbd720c561cf628797dfa85206b873b2b81
`sysio.swap::compute` held the last piece of the pool model that had not reached the shared kernel. The curve (`out_given_in`) and the seed (`geometric_mean`) were already there; what a share is worth against a pool side was not, because the header's original consumers, sysio.reserv and sysio.uwrit, have no tokenized pool share and never needed it. `in_given_shares` and `out_given_shares` are that slice, named off `out_given_in` and differing only in rounding: minting rounds up, burning rounds down, both the pool's way, which is why a mint-then-burn round trip cannot profit. They return `u128` because the value genuinely exceeds 64 bits (two 62-bit balances over a supply of one reaches about 2^124), so bounding it is the caller's job. The unsigned ceiling is written `(prod + supply - 1) / supply` rather than the contract's `1 + (prod - 1) / z`, which would wrap on a zero product. `compute` stays where it is and keeps everything the kernel deliberately does not do: the input guard, the sign that selects direction, the overflow and underflow bounds, and the liquidity fee. The bounds still run before the fee is added, as before. The kernel stays free of contract intrinsics and host-testable, which is the property that would have been lost by moving the `check()` calls with it. Behaviour is unchanged, and the swap suite's rounding table is what pins that: it computes every expectation from the hand-written spec rather than the contract. The ABI does not move. Four host cases cover the rounding directions, the exact-division agreement, dust in both directions, degenerate inputs, the result past 64 bits, and a random grid asserting each side is tight against the true quotient. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: Ibb36013965b1d7aa86bc572a695a01f6a623f0a6
The swap suite's two test contracts compiled only under BUILD_SYSTEM_CONTRACTS and committed nothing, so their wasm existed only on a machine that had built them. CI sets that flag OFF for every build that is not a version tag and compiles no contract at all, which left `contracts.hpp.in`'s embed with nothing to read: all 28 swap cases died in the fixture constructor on "wasm file cannot be found", on both the asan and asserton jobs. Every other contract here already handles this through `bootstrap_contract`, which copies the committed artifact into the build tree and, when contracts ARE being built, leaves the freshly compiled one alone. Both test contracts now use it, with their wasm and abi committed alongside, matching what sysio.swap and the rest do. Local behaviour is unchanged: add_contract still compiles both as a build-time check and overwrites the copy, so a change to either source means rebuilding and re-committing the artifact, which the comment on each file now says. Verified by configuring the contracts tree standalone with BUILD_SYSTEM_CONTRACTS=OFF, as a PR build does: all four artifacts land in the build tree, where before the change those CMakeLists were inert and produced nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: I93891438d64d85ee05a88e11156f3d6a2b704855
The tick rounded the clip UP so a nonempty reservoir always drained. That guaranteed a sale every tick, which had two consequences. A one-unit clip returns nothing on the curve whenever the WIRE side does not exceed the shadow side, and MIN_SWAP_FEE takes the rest up to an output of two, so across the whole below-parity range the clip moved into the pool and paid its holders zero: the reservoir drained to the liquidity providers a unit per block. It also made the elapsed-scaling unreachable, since the clock advanced on every tick and the window reset to one block forever, which is the opposite of the design's "pounding it yields zero-size no-ops". The clip is now floored, and `setyield` takes a third parameter: the least a clip may be. A clip short of `min(clip_floor, queued)` sells nothing and, the part that matters, leaves `last_tick` alone, so the interval is banked and the next tick offers a proportionally larger clip. Throughput is unchanged, because waiting N times as long sells N times as much; only the granularity is coarser. Yielding to `queued` keeps a remainder below the floor from stranding: it leaves as one sale once the time share reaches the whole queue, exactly one horizon later. The floor is in units of the shadow, so it is precision-relative and cannot be a constant. Size it where the pair's fee on a clip's output reaches a whole unit on its own, an output of FEE_DENOMINATOR/fee, since below that MIN_SWAP_FEE is the binding fee and a clip pays far above the pair's rate. Erring high is free; erring above the depth cap stops the pair selling, which is the one combination with no way out and is documented as such. A new case pins all of it: a floor above a block's share makes ten consecutive cranks no-ops that move nothing and hold the clock, the banked time then sells in one piece that actually pays, dust clears one horizon after funding, and a cap under the floor stalls until setyield widens one of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: I2b9ad271a61074acebc236cb0eb21061f97546a4
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: I30e4047742f37e623d7ecefe1afd5eb78451c18f
process_exch priced against the stored pool while accrueyield is permissionless, so one transaction could buy the shadow, settle, and sell it back, taking w·Y/(W+w) of the pending yield off the liquidity providers. It clears the round-trip fee once the pending yield passes about twice the fee rate against the WIRE side, which a pair reaches between ticks. The accrual now happens in process_exch rather than in exchange, so the memo route is covered by the same change, and a trade is always priced against a settled pool. tickyield no longer accrues for itself, because accrue must run at most once per action: its inline claim settles the token's row only after it returns, so a second call reads the same owed amount and collides with the receipt the first one left. That constraint is now on accrue's declaration. Nothing tickyield reads moves under accrual, which credits the other leg while the clip is measured against the shadow side. Covered by a new case, the first to swap against a yield pool at all: a buy prices against the pool including the owed yield and is refused at the stale quote, and the buy, settle, sell round trip returns less WIRE than it spent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: I34195d121b9c9c557872a0d33b57c91d88931367
The cap was taken against the shadow side as it stood, which a caller sets inside the same transaction by selling shadow into the pool. That is the same move that makes a clip worth sandwiching, so the bound scaled with the attack it exists to bound: the clip grew with the injection, the ratio the cap fixes stayed put, and sandwich profit went from self-limiting to linear in the attacker's position. The pair now records its shadow side at each setyield and each selling tick, and the cap is taken against the smaller of that and the current side. In-transaction inflation cannot widen it, a genuine shrink tightens it at once, and genuine growth takes effect one tick later. setyield always precedes a tick, so the recorded depth is never zero when it is read. The cap's sizing rule is now on setyield: keep depth_cap_bps below about twice the pair's fee, which is where a clip's price impact stops covering the round-trip fee a sandwicher pays on their own position. That is what makes the cap self-enforcing and is the argument the plan relies on in §09 when it leaves the band guard optional. The new case doubles the shadow side, then ticks, and pins the clip to the cap implied by the honest depth, which is half what the inflated side would have allowed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: I2e870488d98e0597d0cb076bdff41fb5ceaed2f8
fundyield billed the pending row to the contract and only the exact
matching transfer erased it, so an announcement nobody delivers sat on
sysio.swap's RAM forever, one per account that ever called the action and
no way to reclaim any of them. The row is now billed to `from`, which
puts the cost on whoever creates it and makes the accumulation
self-limiting; the action therefore carries `{from, sysio.payer}`
alongside its active permission, as inittoken already does.
`cancelyield(from)` drops a pending announcement and refunds the row.
That also fixes the state it left a funder in: while an announcement is
pending every other transfer from them is refused, so a funder who
changed their mind or named an amount they cannot send had no way back
except delivering it.
Covered by a case that watches both RAM balances across the
announcement, refuses a cancel signed by anyone else, refuses a cancel
with nothing pending, and checks ordinary deposits work again afterwards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Change-Id: Id0c32ce6e36a7cd580d99396d95cba8969cf5c49
Declared on the contract class and never defined or called, carried over from upstream. The memo path parses through utils.hpp's asset_from_string. Sweeping the rest of the class turned up no other declaration without a definition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: I13fe1db5c43318cba72926c13f38d84b574a3643
The clip floor is in shadow units, so the condition it stands in for -- that a clip's output is large enough for the pair's proportional fee to reach a whole unit -- it reaches only through the pool price and the precision gap between the two legs. Both move: the price with every trade, and the fee itself with changefee, after setyield has already fixed the floor. Below FEE_DENOMINATOR/fee units of output the proportional fee floors to zero and MIN_SWAP_FEE binds instead, so the clip pays far above the pair's rate -- an output of 2 pays half of itself -- out of the holders' distribution. tickyield now quotes the clip and declines it when the output falls short of that threshold, which is stated in output units, the one place the condition does not move with the price. The quote is taken against the pool before accrual, and accrual only ever raises the other leg, so a clip that clears it here clears it on the sale. Declining banks the elapsed time exactly as the clip floor's own skip does, so throughput is unchanged and a crank that finds nothing to do pays nothing for the attempt. Both gates return rather than assert, because a tick that asserted every block would accrue subjective CPU against the crank until its node stopped accepting the ticks that do clear; a pair that has stopped selling shows instead as a non-empty reservoir whose last_tick is not advancing. The remainder drain is exempt: when the whole queue is under the floor it is dust by construction, and stranding it forever is worse than selling it at a poor rate. process_exch's quote math moves to a pure quote_out so the tick can ask what a clip would fetch without moving anything, and clip_floor's documentation now describes it as the sale granularity it is rather than the fee-rate guarantee it cannot be. The new case drops the fee to 0.01%, which puts the fee-bearing output an order of magnitude above the clip floor, and pins the tick declining while the clip clears the floor alone, then selling in one piece measured from the original clock once the banked time carries the output over. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: I78597c27d6215ca27f8dc7427a5dc6dc4761749c
A uint64 index wraps once cumulative yield per shadow subunit passes ~1.8e7, which a thinly held symbol reaches on one add. shadow_yield.hpp now carries index and index_checkpoint as the ABI builtin uint128, owed() computes in 128 bits and checks the asset range, and the swap suite pins a supply-of-one add past 2^64. shadowtoken and sysio.swap rebuilt against the header. Change-Id: I167048937af536c89b2f6551682b02457a620a1d
sysio.liq shadows each outpost's liq token 1:1. SYNDICATE_LIQ credits a linked user or parks an unlinked pubkey; LIQ_YIELD lands in a pending balance that queueyield hands to the swap's reservoir; the WIRE that tickyield sells it for distributes through a uint128 cumulative index, with a T5 kicker drawn through fundclaim(recipient, amount); claim pays owed(row, index); desyndicate burns and queues DESYNDICATE_LIQ. sysio.msgch routes both inbound types behind the active-liq-token gate and never aborts an envelope: a malformed op_address now misses the authex lookup instead of aborting inside pubkey_to_checksum256, which the OPERATOR_ACTION path was exposed to as well. The bootstrap config gains liq_pools and syndications, replayed by regliqpool and importsynd inside the epoch-0 window; the liq suite replays the dev config end to end. Change-Id: I74b2101f74fdfebfef5f324eaaaa8e5acef114a7
batch_operator_plugin pushes sysio.swap::tickyield per yield pool with a queued reservoir, spaced per pool by --batch-yield-tick-interval-ms, and sysio.liq::queueyield per shadow with pending yield; both stay idle until sysio.swap and sysio.liq are deployed, so an undeployed contract never logs a failed table read every poll. outpost_solana_client carries the DESYNDICATE_LIQ effect shape, the handler's remaining accounts derived from the user's pubkey, the pool PDAs and the mint on DistributionState, and cranks report_liq_yield once per epoch after the elected operator's delivery, PostLaunch only. outpost_client gains crank_outpost for that; the Ethereum relay keeps the no-op default. Change-Id: If43f669eeb6b16c57a0a627d9e5994fafabdc9b6
The other exit: a holder deposits sysio.liq shadow on the swap, exchanges it for WIRE and withdraws it to their wallet, with both legs conserved exactly. The test support header gains payer_authorization for actions an unprivileged contract bills to the user. Change-Id: I3f0ac133a4641eb04ee6a698151b118e46c0a0b7
…laiming Change-Id: Iebec3be70b839c09208eb29510ace6c0e2dde7f6 # Conflicts: # contracts/sysio.msgch/sysio.msgch.wasm # contracts/sysio.system/sysio.system.wasm # contracts/tests/sysio.dispatch_tests.cpp
…laiming Change-Id: I01810fd22f108281fa3ea44791342b380a3e7e81 # Conflicts: # contracts/sysio.dclaim/sysio.dclaim.wasm # contracts/sysio.msgch/sysio.msgch.wasm # contracts/tests/sysio.dispatch_tests.cpp
….liq createlink sends sysio.liq::linkswept inline once sysio.liq is deployed and privileged, so a syndication parked against an unlinked key is delivered the moment the key links (the hook deferred until #594 landed). The shadowtoken stand-in is gone: the swap suite deploys the real sysio.liq, and its yield tests run on a fixture whose system token is 9,WIRE (the contract takes yield and pays claims in WIRE, and a pair's second leg is the system token), so SHEO is SHD/WIRE at precision 6. The AMM tests keep upstream's EOS fixture. recordlink carries native_address (#594) in the liq tests. Change-Id: Ie3dbb061257a402a2ee0ef475131728baedd7b34
| std::string{ CLAIM_MEMO })).send(); | ||
| } | ||
|
|
||
| void liq::addyield(name from, asset quantity, symbol_code target) { |
There was a problem hiding this comment.
[P1] Restrict the kicker-bearing intake to a trusted source. This action only requires from, but every caller can make sysio.liq invoke fundclaim against T5. If the caller owns all of the target supply, donating Q credits that caller Q plus the 2% kicker; a subsequent claim returns the donation and captures 0.02Q from the treasury. Please require sysio.swap as the source for this path, or split permissionless donations into an action that does not request a kicker.
There was a problem hiding this comment.
Gated in f043096: addyield requests the kicker only when from is sysio.swap — the one intake that is yield (tickyield's proceeds). Donations stay permissionless and distribute themselves alone, so the treasury is never drawn on a self-donation. The kicker unit test now donates as the swap and pins that a third-party donation carries no fundclaim; README, header doc and EMISSIONS.md say the same.
|
|
||
| // Minted to this contract and handed on in the same transaction, so its row is | ||
| // settled at one index and accrues nothing on the way through. | ||
| check(mint(sym, static_cast<uint64_t>(quantity.amount)), "supply exceeds the asset range"); |
There was a problem hiding this comment.
[P1] Keep reported yield out of circulation until Solana accounts for it as redeemable stake. On the current companion SHA (11c88c0), apply_yield raises yield_accumulated_liqsol and expected_pool_balance but not total_staked_liqsol; handle_desyndicate_liq refuses a payout once that principal counter is below the requested amount. Minting the reported yield here therefore creates transferable shadow that cannot all be redeemed: after aggregate exits consume the original principal, the remaining yield-backed shadow burns on WIRE and is logged unpaid even though the pool holds the yield. The PR body acknowledges the companion gap, but this path has no activation guard. Please land the Solana counter update first or gate queueyield until it is active.
There was a problem hiding this comment.
The fix is on the outpost: when report_liq_yield advances its watermark, the reported delta is added to total_staked_liqsol, so the outpost's redeemable total tracks the shadow supply the depot mints for it. That lands on the wire-solana companion (@valthon). The depot mints exactly what the outpost reports and stays as is.
There was a problem hiding this comment.
Rechecked against current wire-solana/next at cbf721887b1e (fetched 2026-09-23). GlobalState::apply_yield still updates the index, yield_accumulated_liqsol, and expected_pool_balance without incrementing total_staked_liqsol; the desyndication gate still refuses once that principal counter is exhausted. I also could not find an open companion PR containing the fold. This remains unresolved for this review head. Please link and land the companion fix before activation, or add a depot-side gate and an explicit hard deployment precondition.
| "connector_weight_bps": 5000, "is_private": false, "owner": "" } | ||
| ], | ||
| "liq_pools": [ | ||
| { "chain_code": "ETHEREUM", "token_code": "LIQETH", "pair_symbol": "LIQETHP", |
There was a problem hiding this comment.
[P1] Do not bootstrap LIQETH before Ethereum has LIQ handlers. The current e2e Ethereum branch (16e5e61) has no DESYNDICATE_LIQ handler or LIQ yield reporter, and OPPInbound irreversibly skips unregistered attestation types. Because sysio.liq::desyndicate burns before queuing the effect, every LIQETH outpost exit is dropped and requires governance recredit; the base outpost crank is also a no-op, so LIQETH never reports yield. Please remove or gate this LIQETH activation until the Ethereum handler and reporting path are deployed.
There was a problem hiding this comment.
The Ethereum side is wire-ethereum #207 (@qhool): SyndicationPool emits SYNDICATE_LIQ and LIQ_YIELD and registers an inline, never-reverting DESYNDICATE_LIQ handler with once-only settlement. The e2e ran on wne-41_next-fix only because tools master needs #205, and that branch predates #207. The dev config stays as it is: it is the dev cluster's, and the launch template carries no liq rows. One piece is in neither PR: realizeYield is access-restricted, so the batch operators will crank it through outpost_ethereum_client::crank_outpost, the counterpart of the Solana client's report_liq_yield crank, as a follow-up to this PR.
There was a problem hiding this comment.
Rechecked the cited Ethereum work. wire-ethereum #207 is still a draft at 2cd51ab, its body leaves live upgrade/deploy wiring out of scope, and this thread confirms the access-restricted realizeYield node crank is still a later follow-up. The current e2e branch predates #207, while this PR still bootstraps LIQETH in the dev config. Please remove/gate that activation or make the handler, upgrade wiring, and crank concrete prerequisites before this flow is enabled. I am keeping this finding open.
There was a problem hiding this comment.
The crank is in this PR after all, in fea8449: outpost_ethereum_client::crank_outpost sends realizeYield on the pool the outpost registers as its DESYNDICATE_LIQ handler (OPPInbound.attestationHandlers), so nothing has to name the pool; it stays idle until #207 deploys one and grants the relay's signer yield_operator.
| drop(path, "amount out of range"); | ||
| return std::nullopt; | ||
| } | ||
| if (amount > static_cast<uint64_t>(asset::max_amount - st->supply.amount)) { |
There was a problem hiding this comment.
[P2] Include existing pending yield in the supply-headroom invariant. This per-report check ignores liqpending, so individually valid reports can accumulate beyond the remaining mintable supply. A focused reproduction with supply at asset::max_amount - 100 accepts two reports of 60, leaves 120 pending, and then every queueyield attempt aborts while transaction rollback restores the same pending row. The saturation at lines 189-193 can also consume a sequence while discarding excess reported value. Please reserve pending amounts in every supply-growing path, or let queueyield mint available headroom and retain the remainder.
There was a problem hiding this comment.
Fixed in 16457e7: one headroom, the asset range net of supply and of what is pending, bounds every supply-growing path (mint and resolve_inbound), so queueyield cannot fail once the row is erased first. mintyield no longer saturates: a report past the headroom is dropped before its sequence is consumed, so a later report still admits. Pinned by pending_yield_is_reserved_against_the_asset_range: your two-reports-of-60 shape with the second dropped and the cursor unchanged, mintsynd and recredit refused while pending fills the range, and queueyield minting to exactly the maximum.
| return; | ||
| } | ||
| const uint64_t sequence = synd.sequence; | ||
| const name account = resolve_account_from_op_address(synd.user); |
There was a problem hiding this comment.
[P2] Validate synd.user.kind against the proven source chain before resolving AuthX. A valid wrong-family key that is already linked resolves an account here and proceeds through mintsynd, which receives no ChainKind and credits it. The same payload is rejected only when the key is unlinked, because that branch reaches park and its chain-kind check. Please compare the user kind with the registered kind of chain_code_slug before this lookup, or pass the kind through to mintsynd and enforce it there.
There was a problem hiding this comment.
Fixed in 8803bfa: dispatch_syndicate_liq now drops a user whose key family is not the proven outpost's registered kind, before the AuthX lookup, so a linked key of another family is refused exactly like an unlinked one. Same never-throw shape as the chain-binding check beside it. Pinned by syndicate_liq_refuses_a_key_of_another_chain_family: a linked and an unlinked Solana key in an Ethereum envelope are both dropped with nothing parked and no sequence consumed, and the EVM credit beside them lands.
addyield is permissionless, and the kicker rode on every intake: a holder of most of a shadow's supply could donate, claim it back with the treasury's 2% on top, and repeat. Only the swap's intake (tickyield's proceeds) is yield, so only it requests fundclaim now; a donation distributes itself alone. Review P1 on Change-Id: Ia0de4e049188ece04fb1c6ed96bbfb9a48f18a4c #634.
The range check on every supply-growing path read supply alone, so reports that each fit could together exceed what queueyield can mint, and its assertion then wedged the crank for that symbol for good. One headroom, the range net of supply and of what is pending, now bounds every mint and every intake; mintyield drops a report past it before consuming the sequence instead of clipping it after. Review P2 on #634. Change-Id: I5bdcabb7b57c2920004391bfa75e1d720225612d
The dispatch checked the payload's chain against the proven outpost and the token, then resolved the user's key through AuthX without comparing its family with the outpost's: a linked key of another family was credited by mintsynd while an unlinked one was refused by park. The family is now part of the provenance check, before the lookup. Review P2 on #634. Change-Id: If76a162d3144850cccabc609a3463b34b2d979cd
The Ethereum counterpart of the Solana relay's report_liq_yield crank. Once per epoch, after the delivery lands, the relay reads the pool's address off the outpost itself, OPPInbound.attestationHandlers(DESYNDICATE_LIQ), which wire-ethereum #207's SyndicationPool registers itself under, binds a wrapper to it, and sends realizeYield. No handler, or an ABI set without the pool, leaves the crank idle; the pool's own refusals (no yield, below the deadband, underbacked) are outcomes, anything else is a failed crank for the job to log. Change-Id: Iea3551beab38bf84f6d4cf5fcb4b5e4dc47bcec1
huangminghuang
left a comment
There was a problem hiding this comment.
Reviewed current head fea8449. The earlier sysio findings are addressed, including the Ethereum realizeYield crank. The remaining Solana accounting change and Ethereum contract/deployment work are companion-repository activation dependencies; this head remains backward-compatible and idles when the pool ABI or handler is absent. No blocking sysio findings.
The LIQ reward flow end to end on the WIRE side, stacked on the swap contract from #611 and merged with current
master(through #594 / WIRE-352). This PR supersedes #611: the swap contract itself is untouched except the uint128 index widening, and its open review items stay Chuy's after landing.The flow
SYNDICATE_LIQlands;sysio.msgchresolves the user's pubkey throughsysio.authex. A linked user is credited (sysio.liq::mintsynd); an unlinked pubkey is parked (park) and delivered on link —sysio.authex::createlinksendssysio.liq::linksweptinline, best-effort, oncesysio.liqis deployed and privileged — or by the permissionlesssweep. Pre-launch positions and the LCO liq arrive from the bootstrap config the same way (importsynd,regliqpool), inside the epoch-0 window.LIQ_YIELD, the outpost's claimed yield since its last report, is sequence-deduped and lands inliqpending, outside supply.queueyieldmints the pending amount as protocol-owned shadow and hands it to the swap's reservoir (fundyield) in one transaction.sysio.swap::tickyieldsells a time-share clip of the reservoir into the pool; the WIRE proceeds reach the shadow contract throughaddyield.addyieldbumps a uint128 cumulative index for every holder, with a 2% T5 kicker drawn throughfundclaim(recipient, amount);claimpays exactlyowed(row, index);closerefuses while anything is owed.desyndicatesettles, burns and queuesDESYNDICATE_LIQ; the Solana relay carries the handler's remaining accounts so the outpost pays the user's liqSOL inline. The other exit is selling shadow into the yield pool for WIRE on the depot.Depot
contracts/sysio.liq: the shadow token, the parked ledger, the inbound cursors, the kicker, the ingestion actions, and its suite, which includes a replay of the checked-in dev config.sysio.msgch:SYNDICATE_LIQ/LIQ_YIELDrouted behind an active-liq-token gate onsysio.tokens; every refusal is a drop, never an abort.sysio.liqjoins thequeueoutallow-list.sysio.authex:createlinksweeps a parked syndication beside the linked-rewards sweep (the hook is indocs/contract-upgrade-order.md; no ordering constraint). The header'spublic_key_from_op_addressreturns an optional for only the key shapes a link can hold: a malformedop_addressused to abort the whole envelope insidepubkey_to_checksum256, on theOPERATOR_ACTIONpath too; regression test added.sysio.system:fundclaim(name recipient, int64 amount), recipientssysio.dclaimandsysio.liq.bootstrap.proto:LiqPoolSpecandSyndicationSpec, validator rules V10 to V13, the dev config's liq pools and syndications.shadowtokenstand-in undertest_contractsis gone. The swap suite deploys the realsysio.liq(bound through the chain and token registries, minted into bysysio.msgch), and its yield tests run on a fixture whose system token is9,WIRE— the real contract takes its yield in WIRE and pays claims in WIRE, and a pair's second leg is the system token — soSHEOisSHD/WIREat the precisioninittokenderives. The AMM tests keep upstream's EOS fixture.Node
batch_operator_plugin:sysio.swap::tickyieldper yield pool with a queued reservoir, spaced per pool by--batch-yield-tick-interval-ms, andsysio.liq::queueyieldper shadow with pending yield. Both idle untilsysio.swapandsysio.liqrun code, so an undeployed contract never logs a failed table read every poll.outpost_solana_client:effect_shape::desyndicate_liq, in lock-step withhandle_desyndicate_liqon wire-solananext, and a once-per-epochreport_liq_yieldcrank, PostLaunch only, through the newoutpost_client::crank_outpost.outpost_ethereum_client: the same hook cranksSyndicationPool.realizeYield()once per epoch, on the pool discovered from the outpost'sDESYNDICATE_LIQhandler registration. Boot checks forDistributionStateandGlobalStatewhere the IDL declares them.Companion PRs
SysioContractTypesregeneration forswapandliq(plus master'sdelsnapprov).sysio.swap/sysio.liq, the opt-in mock yield pools, andflow-liq-yield, the acceptance flow.Not in this PR
total_staked_liqsolmust fold reported yield before yield-shadow circulates (David).Review (Huang-Ming, 2026-09-22)
f0430969358c:addyieldrequests the T5 kicker only on the swap's intake; a donation distributes itself alone.16457e738df4: supply plus pending yield never exceeds the asset range — oneheadroombehind every mint and every intake,mintyielddrops past it before consuming the sequence,queueyieldcan no longer fail.8803bfaaa38e:dispatch_syndicate_liqdrops a user whose key family is not the proven outpost's, before the AuthX lookup.fea844900990: the Ethereum crank.outpost_ethereum_client::crank_outpostsendsSyndicationPool.realizeYield()on the pool the outpost registers as itsDESYNDICATE_LIQhandler (OPPInbound.attestationHandlers), so nothing has to name the pool; idle until Wire-Network/wire-ethereum#207 deploys one, and the pool's own refusals (no yield, below the deadband, underbacked) are outcomes rather than failed cranks. The relay's signer needs the pool'syield_operatorrole.Verification
contracts_unit_test -- --sys-vm(full)8803bfaaa38e(2026-09-23)plugin_testtest_batch_operator_plugintest_outpost_solana_client_pluginlibraries/oppbootstrap-config validatorBRANCH_WIRE_SYSIO=feature/liq-yield-claimingBRANCH_WIRE_LIBRARIES_TS=feature/liq-yield-contract-typesBRANCH_WIRE_TOOLS_TS=feature/liq-yield-flowBRANCH_WIRE_ETHEREUM=wne-41_next-fixBRANCH_WIRE_SOLANA=codex/merge-develop-into-next-20260922)liq-yield(792 s) andliq-syndication(561 s), 2026-09-22; re-run on the review head8803bfaaa38e: run 35877211864 green, all 17 flows, 107 min (2026-09-23); re-run on the crank headfea844900990: run 35890209048: 16 of 17 flows green;flow-reserve-lifecyclefailed in its Solana bootstrap stepinit-token-mint(anchor run init-token-mint:ws error: connect ECONNREFUSED 127.0.0.1:39806, the validator's own pubsub port, before any batch operator ran; the validator log shows it alive and producing slots). Resolved refs identical to 35877211864 except the wire-sysio head, and the failing step never touches the changed plugin; re-dispatched as run 35905786811: green, all 17 flows, 122 min (2026-09-23)