Skip to content
Merged

Stage #110

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 3
target-branch: "stage"
open-pull-requests-limit: 5
ignore:
Expand All @@ -13,11 +15,13 @@ updates:
- alexey-symbiotic
- arturSymbiotic

# Enable version updates for npm
# Enable version updates for Go modules
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 3
target-branch: "stage"
open-pull-requests-limit: 10
assignees:
Expand All @@ -32,9 +36,11 @@ updates:
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 3
target-branch: "stage"
open-pull-requests-limit: 5
assignees:
- oxsteins
- alexey-symbiotic
- arturSymbiotic
- arturSymbiotic
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # pin@v7.0.1

- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # pin@v6.5.0
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # pin@v7.0.0
with:
go-version-file: go.mod
cache: true
Expand All @@ -34,9 +34,9 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # pin@v7.0.1

- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # pin@v6.5.0
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # pin@v7.0.0
with:
go-version-file: go.mod
cache: true
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@ jobs:
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # pin@v7.0.1

- id: meta
run: echo "date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"

- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # pin@v3
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # pin@v4.2.0

- uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # pin@v4
- uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # pin@v4.5.1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}

- id: build
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # pin@v6
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # pin@v7.3.0
with:
context: .
file: deploy/Dockerfile
Expand Down
16 changes: 0 additions & 16 deletions .trivyignore.yaml

This file was deleted.

6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ SHELL := bash
.DEFAULT_GOAL := help

# Pinned codegen tool versions.
ABIGEN_VERSION ?= v1.16.1
ABIGEN_VERSION ?= v1.17.4
GOLANGCI_LINT_VERSION ?= v2.11.4
GENQLIENT_VERSION ?= v0.8.1
GQLFETCH_VERSION ?= v0.7.0
GENQLIENT_X_TOOLS_VERSION ?= v0.38.0
GENQLIENT_X_TOOLS_VERSION ?= v0.48.0
# Java openapi-generator (downloaded on demand by hack/openapi-generator-cli.sh). 7.12.0 is the floor:
# it ingests OpenAPI 3.1 (the RFQ backend spec); 5.4.0/7.0.1 fail on it.
OPENAPI_GENERATOR_VERSION ?= 7.12.0
OPENAPI_GENERATOR_VERSION ?= 7.24.0

# Foundry build output to vendor ABIs from (sibling rfq repo by default).
FORGE_OUT ?= ../rfq/out
Expand Down
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,15 @@ standing quotes from current adapter liquidity and receives matched, already-ope
LI.FI WebSocket feed. Before each fill it rechecks the canonical order status, adapter state, gas cost, and
strategy decision, then atomically claims the input, redeems it through LiquidLane, and fills the output via
`LiquidLaneLifiExecutor`. Capacity reserved by already-submitted fills is deducted from both later fill
decisions and standing quotes until those transactions complete. The published quote ladder is not replayed
at fill time: the solver greedily rebuilds the best current route plan, and redeemed output above the order
requirement remains executor surplus. The default strategy prices every standing range by running the shared
LiquidLane exact-input quote solver at both endpoints. It publishes the lower endpoint rate capped by a
linear conservative floor for interior route transitions, worst-case route gas, and rounding.
decisions and standing quotes until those transactions complete. Each token pair advertises the full currently
available capacity even when several pairs share one vault; accepting a fill reserves its shared `CapacityID`
and immediately refreshes every affected quote. A fill remains pending until the shared tx manager reaches
the configured confirmation depth; only then is its reservation released and quote refresh requested.
The published quote ladder is not replayed at fill time: the
solver greedily rebuilds the best current route plan, and redeemed output above the order requirement remains
executor surplus. The default strategy prices every standing range by running the shared LiquidLane exact-input
quote solver at both endpoints. It publishes the lower endpoint rate capped by a linear conservative floor for
interior route transitions, worst-case route gas, and rounding.
`strategy.config.rangeCount` sets the geometric curve resolution (default `8`, maximum `16`).

The executor contract is the registered LI.FI solver account. It is registered once through EIP-1271 using
Expand Down Expand Up @@ -178,9 +182,10 @@ ladders, amount ranges, or quote-time route reservation. Omitting the entire `ga
accounting in both quote and fill decisions and skips gas-state and Chainlink reads. The tx manager still
prices and pays actual transaction gas, so that cost is then subsidized by the solver. Uniswap deliberately
makes indicative and hard RFQ requests
indistinguishable, so the solver echoes `quoteId` but does not guess the phase. Capacity is reserved only
after a fill transaction is accepted for submission; every posted order gets a fresh route plan from the
current chain state and is simulated before sending. The reservation remains effective while txmanager waits
indistinguishable, so the solver echoes `quoteId` but does not guess the phase. As soon as a polled order is
admitted to the fill queue, quote publication pauses until planning either rejects it or atomically hands
capacity ownership to an accepted transaction reservation. Every posted order gets a fresh route plan from
the current chain state and is simulated before sending. The reservation remains effective while txmanager waits
for the configured confirmations. On completion the quote snapshot is invalidated before capacity is
released, and that capacity is not advertised again until a fresh post-fill chain snapshot is published.
A quote is returned only if its snapshot epoch and every blocking condition are unchanged after the strategy
Expand Down
22 changes: 16 additions & 6 deletions docs/LIFI-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,12 @@ type Strategy interface {
when at most `max(quoteInterval, quoteTtl / 3)` remains, even when no new block is observed or the head poll
fails. The strategy
may only shorten that expiry to `discount deadline - executionDeadlineBuffer`.
Capacity allocation is scoped to one token pair before the range curve is built. Different pairs backed
by the same vault therefore each advertise the full currently unreserved `CapacityID` instead of receiving
static shares. This is deliberately optimistic: an accepted fill reserves the shared domain and wakes quote
refresh immediately, but two orders matched against the previous curves can still race. Fresh fill planning,
the shared reservation ledger, and inclusion-time adapter checks prevent double spending; they do not promise
that every concurrently matched order can be filled.
- **`FillInput`** = the matched signed `StandardOrder` output facts (`output.amount`, raw
`output.context`) plus fresh `getAmountOut`, `minDiscount`, `getMaxAssets`, pending fill reservations
by shared `CapacityID`, and the same latest LiquidLane gas facts. Direct candidates require current
Expand Down Expand Up @@ -377,9 +383,11 @@ type Strategy interface {
The order worker owns pending fills and their capacity reservations. It reserves each direct route's
target output and each private route's upward-buffered output against its shared `CapacityID` while an
accepted fill tx is in flight, passes the aggregate reservation snapshot to every later fill decision,
and releases it when that send completes. A single shared `CapacityLedger` is the source for both fill
planning and quote refresh; the quote coordinator receives only a coalesced refresh signal and does not
keep a second copy of per-order reservations. On startup, when any economic payload changes, or when expiry enters the renewal
and releases it only when the shared tx manager returns after the globally configured confirmation depth.
A successful tx-manager admission immediately sends a coalesced refresh signal; confirmed completion and
reservation release send another. A single shared `CapacityLedger` is the source for
both fill planning and quote refresh, and the quote coordinator does not keep a second copy of per-order
reservations. On startup, when any economic payload changes, or when expiry enters the renewal
window, it submits the replacement curve directly; LI.FI overwrites the old quote for the pair. When a pair
stops quoting, it submits the last curve with an expiry in the past, which overwrites and immediately expires
the old server-side quote. An unchanged pair is not reposted on every calculation tick.
Expand Down Expand Up @@ -508,9 +516,11 @@ LI.FI order server ──(WS: opened/funded StandardOrder)──▶ lifi solver
to cover gas, both quote-time price windows, and rounding.
The strategy charges complete-plan gas after route allocation and omits any capacity range whose lower
boundary is not economically positive.
- **Capacity safety** — routes sharing a vault share one conservative capacity domain. Both quote and
fill planning subtract in-flight buffered outputs before allocating that shared capacity. Each fill
still uses a fresh chain snapshot and the adapter enforces execution at inclusion. An economic change removes
- **Capacity safety** — routes sharing a vault share one conservative capacity domain. Each pair may advertise
the full domain, while quote and fill planning subtract every in-flight buffered output from it. This
optimistic publication can overbook across simultaneously matched pairs, but each fill still uses a fresh
chain snapshot and the adapter enforces execution at inclusion. An accepted fill immediately requests quote
replacement; its reservation remains until configured confirmations complete, and an economic change removes
old server ranges before replacement.
- **Authorization safety** — startup validates executor immutables and requires the framework signer to be
authorized by `executor.isCaller`. Startup and every admitted order also require
Expand Down
13 changes: 11 additions & 2 deletions docs/LIQUIDLANE-CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ Core field rules:
- Direct inventory `MaxRate` is `getMaxRate(tokenIn)` and already includes `minDiscount`. A `FillQuote`
derives the same conservative fixed-point fact from `MaxAmountOut / AmountIn`, so fill-time private
offers are bounded without another RPC call.
- Discount `MaxRate` comes from the discounts backend and already includes its advertised discount.
- Discount `MaxRate` comes from the discounts backend and already includes its advertised discount. It
arrives already floored, while the adapter floors `getAmountOut` first and applies the discount
second, so pricing directly at it can predict one unit above what the adapter pays. Re-derive it for
the concrete `amountIn` with `liquidlane.ConservativeAdvertisedRate` before quoting or sizing a leg.
- `GrossAmountOut` is raw `getAmountOut`; `MaxAmountOut` is the executable amount after discount.
- `MinDiscount` is the adapter's current lower bound for a fill.
- `ValidUntil` is an external offer deadline. Inventory does not carry a duplicate read timestamp;
Expand Down Expand Up @@ -154,11 +157,17 @@ only when that deployment deliberately accepts different freshness and availabil
Direct and signed-discount inventory for the same route are alternative ways to use the same capacity.
Never sum them. `internal/liquidlane/strategies/greedy` encodes the one-candidate-per-route rule for quote and fill
tasks across RFQ, LI.FI, and UniswapX; execution reservations use the shared `CapacityID`.
Concrete RFQ and UniswapX requests allocate capacity after filtering to their pair. LI.FI does the same for
each standing pair curve, so multiple curves may advertise the same unreserved vault capacity; accepted-fill
reservations are then subtracted from every curve sharing that `CapacityID`.

For signed discounts:

1. List and validate advertised offers for quote construction.
2. Never apply `discount` to backend `maxRate` a second time.
2. Never apply `discount` to backend `maxRate` a second time — but do re-derive the rate for the
concrete `amountIn` with `ConservativeAdvertisedRate`. The backend floors the discount into the
rate while the adapter floors `getAmountOut` first, so the raw rate can price a unit above what the
adapter pays, and an over-predicted leg leaves the filler short of the order's signed outputs.
3. Resolve signatures again immediately before fill.
4. Recheck id, adapter, tokens, current discount bounds, and deadlines.
5. Reserve capacity for upward price movement: discount swaps release their full computed output and
Expand Down
22 changes: 21 additions & 1 deletion docs/RFQ-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,25 @@ refresh uses (`paused`, `getMaxAssets`, `getMaxRate`) — each adapter's `vault`
single-route constraint above. A richer quoting strategy is a later follow-up (mirrors the
3F pricing TODO), or an operator can plug their own via the `webhook` strategy (see the strategy
layer below).
- **Discount-leg rate rounding** — a discount leg prices off the backend's advertised `maxRate`, which
is the adapter oracle price with the discount already applied *and floored*. The adapter rounds down
in the opposite order: `getAmountOut` floors `amountIn × price × 10^outDec / (1e18 × 10^inDec)`
first, then `swap(DiscountSwap, ...)` applies the discount and floors again. The two nested roundings
differ by at most one unit, and the difference falls our way often (roughly a fifth to a half of
amounts at a non-zero discount) — so pricing at the raw `maxRate` predicts one unit more output than
the adapter delivers. That is not an adapter revert (the adapter computes `amountOut` itself and
`InvalidSwapRate` cannot trigger, since `discount ≥ minDiscount`); it reverts in
`Reactor._fill`, which pulls the order's *signed* outputs out of the Executor after `execute()`
returns. With no `priceBufferBps` in RFQ and `Finalize` distributing the full achievable output, the
slack is exactly zero whenever the price has not moved since the quote, so the fill fails gas
estimation and the order retries until it expires. `NormalizeOracleInventory` therefore re-derives
every discount candidate's rate through `liquidlane.ConservativeAdvertisedRate`, which shaves one
unit off the predicted output and converts it back to a rate; the round trip through
`RateForAmountOut` floors, so downstream `AmountOutForRate` call sites need no change. Direct legs
are unaffected — they already re-derive their rate from a live `getAmountOut` read. The exact
alternative (clamp against `AmountOutAfterDiscount(GrossAmountOut, discount)`, as
`discounts.AdvertisedFillQuotes` does) needs the discount ppm, which the `/quote` request's
`adapters[]` entries do not carry; revisit if that field is ever added to the backend contract.
- **Quote latency** — `/quote` is synchronous in the backend's fan-out, so keep it cheap: pricing is
one `getAmountOut` multicall, and `tokenIn` decimals are read once and cached. A warm quote is a
single multicall; only the first quote for a not-yet-seen `tokenIn` adds a one-off `decimals` read.
Expand All @@ -321,7 +340,8 @@ endpoints actually used (`GET /orders` ×3 query shapes, `GET /discounts`, `POST
and the fill-time RPC read/authorization set are all 1:1. The Go port adds a few **fail-closed
hardenings the TS filler lacks** — an order-deadline check before fill, a strategy↔order
`tokenIn`/`tokenOut`/`amountIn` binding, txHash validation on reconcile, a single-entry guard on the
batch discount-resolve shape, and TTL eviction of stale terminal orders (TS maps grow unbounded).
batch discount-resolve shape, a conservative discount-leg rate that cannot out-predict the adapter's
nested rounding (§5), and TTL eviction of stale terminal orders (TS maps grow unbounded).
A few **intentional, non-fund-moving divergences** remain, by design:

- **Quote-time oracle revert** — a reverting `getAmountOut` makes the Go quote *skip that asset and
Expand Down
Loading