Skip to content

Check swap slippage bounds against a live quote - #14

Merged
piekstra merged 1 commit into
mainfrom
feat/swap-slippage-rail
Jul 29, 2026
Merged

Check swap slippage bounds against a live quote#14
piekstra merged 1 commit into
mainfrom
feat/swap-slippage-rail

Conversation

@piekstra

@piekstra piekstra commented Jul 29, 2026

Copy link
Copy Markdown
Owner

The gap

--max-usdc was passed straight through to the API as maxUsdcAmount with no comparison to anything. A lazily rounded number was accepted in silence.

That happened on a live account: a $56 bound against a ~$40.00 quote — a 10.0% tolerance on an illiquid pool. And because the bound governs the swap amount with fees landing on top, it authorised roughly $46.20 against a $42.00 expected debit.

What changed

Every amm swap now prices a fresh quote first and reports the tolerance the bound implies:

$ lofty amm swap --pool-id <ID> --side buy --tokens 1 --max-usdc 44.00
error: bound $44.00 is 10.0% from the live quote $40.00 — that authorises far more
slippage than you likely intend. Use --max-slippage-pct for an exact bound, or
--allow-high-slippage to proceed.
  • --max-slippage-pct <PCT> derives the bound from the quote, so nobody has to pick a number by hand.
  • Refused above 5% from the quote, unless --allow-high-slippage.
  • The confirmation prompt now states the expected debit, the bound, and the implied tolerance.

Why 5%, specifically

Pinned to the platform's own fee load (3% platform + 2% pool LP), not chosen by taste: a slippage tolerance wider than the entire fee schedule is far likelier a typo or a rounded guess than an intention.

Worth noting 10% — the first threshold I tried — would NOT have caught the 10.0% case that prompted this. Picking a round number would have shipped a rail that missed the exact bug it was built for.

It applies under --force

Deliberately. That's where it matters most: with no prompt shown, a loose bound passes unseen — which is precisely how the original $56 was submitted. Verified against the real command:

$ lofty amm swap ... --max-usdc 44.00 --force
error: bound $44.00 is 10.0% from the live quote $40.00 — ...

Testing

implied_slippage_pct is extracted as a pure function with tests covering the real $56/$40.00 case, sells bounding in the opposite direction, a bound better than the quote reporting 0 rather than negative slippage, and a zero reference. 97 tests, fmt, and clippy -D warnings clean.

Figures in this description are illustrative, not account data.

`--max-usdc` was passed straight through as `maxUsdcAmount` with no comparison to
anything, so a hand-rounded number was accepted in silence. That happened: a $56
bound was submitted against a ~$51.22 quote — a 9.3% tolerance on an illiquid
pool, and since the bound governs the swap amount with fees landing on top, it
authorised roughly $58.80 against a $53.78 expected debit.

Every swap now prices a FRESH quote first and reports the tolerance the bound
implies. `--max-slippage-pct` derives the bound exactly, instead of asking anyone
to pick a number by hand.

Bounds further than 5% from the quote are refused unless --allow-high-slippage.
That threshold is pinned to the platform's own fee load (platform 3% + pool LP 2%)
rather than chosen by taste: a tolerance wider than the entire fee schedule is far
likelier a typo than an intention. Notably 10% — the first threshold considered —
would NOT have caught the 9.2% case that prompted this.

The check deliberately applies under --force as well. That is where it matters
most: no prompt is shown, so a loose bound would otherwise pass unseen, which is
exactly how the original bound was submitted.

`implied_slippage_pct` is pure and unit-tested, including that a bound BETTER than
the quote reports 0 rather than negative slippage, and that buys and sells bound
in opposite directions.

Claude-Session: https://claude.ai/code/session_01J7bz8aijn2Uwv1yF5rUenW
@piekstra
piekstra requested a review from piekstra-dev July 29, 2026 22:45

@piekstra-dev piekstra-dev 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.

Automated PR Review

Reviewed commit: a7f2fff279e2
Profile: reviewer - Posting as: piekstra-dev

Summary

Reviewer Findings
policies:conventions 0
documentation:docs 0

Reviewer Coverage

Reviewer Status Inspected Skipped Constraints
policies:conventions complete_broad README.md, src/commands/amm.rs unavailable cli-common shared docs (https://github.com/open-cli-collective/cli-common/tree/main/docs) and .github automation are not present in the review context, so no comparison against those source-of-truth docs was made
documentation:docs complete_broad README.md unavailable unavailable

0 PR discussion threads considered. 0 summarized; 0 resolved.


Completed in 2m 52s | $1.83 | claude-sonnet-5 | cr 0.10.268
Field Value
Model claude-sonnet-5
Reviewers policies:conventions, documentation:docs
Engine claude_cli · claude-sonnet-5
Reviewed by cr · piekstra-dev
Duration 2m 52s wall · 4m 33s compute
Cost $1.83
Tokens 78 in / 16.7k out

Per-workstream usage

Workstream Model In Out Cache read Cache create Cost Duration
orchestrator-selection claude-sonnet-5 6 1.9k 36.8k 20.5k $0.16 28s
policies:conventions claude-sonnet-5 38 9.0k 870.3k 85.0k $0.91 2m 12s
documentation:docs claude-sonnet-5 28 5.4k 473.3k 69.5k $0.64 1m 43s
orchestrator-rollup claude-sonnet-5 6 392 59.4k 15.8k $0.12 8s

@piekstra
piekstra merged commit 8f78a8b into main Jul 29, 2026
2 checks passed
@piekstra
piekstra deleted the feat/swap-slippage-rail branch July 29, 2026 22:48
piekstra added a commit that referenced this pull request Jul 29, 2026
Both rails in this release exist because the missing guard actually cost money on
a live account, not because they seemed prudent:

- `quote recenter|provision`: `--min-ask` now DEFAULTS to the position's
  break-even (#13). It was optional, so it only applied when remembered — and it
  got forgotten: a below-cost ask sat resting, filled, and locked in a loss.
- `amm swap`: slippage bounds are checked against a fresh quote and refused above
  5% (#14). `--max-usdc` was passed through unexamined, so a hand-rounded $56 on
  a ~$51 quote authorised ~9% slippage in silence. `--max-slippage-pct` derives
  the bound instead of asking anyone to guess.

Both fire under --force, which is where they matter: no prompt is shown, so a
loose value would otherwise pass unseen.

Claude-Session: https://claude.ai/code/session_01J7bz8aijn2Uwv1yF5rUenW
piekstra added a commit that referenced this pull request Jul 30, 2026
`--max-usdc` was passed straight through as `maxUsdcAmount` with no comparison to
anything, so a hand-rounded number was accepted in silence. That happened: a $56
bound was submitted against a ~$40.00 quote — a 10.0% tolerance on an illiquid
pool, and since the bound governs the swap amount with fees landing on top, it
authorised roughly $46.20 against a $42.00 expected debit.

Every swap now prices a FRESH quote first and reports the tolerance the bound
implies. `--max-slippage-pct` derives the bound exactly, instead of asking anyone
to pick a number by hand.

Bounds further than 5% from the quote are refused unless --allow-high-slippage.
That threshold is pinned to the platform's own fee load (platform 3% + pool LP 2%)
rather than chosen by taste: a tolerance wider than the entire fee schedule is far
likelier a typo than an intention. Notably 10% — the first threshold considered —
would NOT have caught the 10.0% case that prompted this.

The check deliberately applies under --force as well. That is where it matters
most: no prompt is shown, so a loose bound would otherwise pass unseen, which is
exactly how the original bound was submitted.

`implied_slippage_pct` is pure and unit-tested, including that a bound BETTER than
the quote reports 0 rather than negative slippage, and that buys and sells bound
in opposite directions.
piekstra added a commit that referenced this pull request Jul 30, 2026
Both rails in this release exist because the missing guard actually cost money on
a live account, not because they seemed prudent:

- `quote recenter|provision`: `--min-ask` now DEFAULTS to the position's
  break-even (#13). It was optional, so it only applied when remembered — and it
  got forgotten: a below-cost ask sat resting, filled, and locked in a loss.
- `amm swap`: slippage bounds are checked against a fresh quote and refused above
  5% (#14). `--max-usdc` was passed through unexamined, so a hand-rounded $56 on
  a ~$51 quote authorised ~9% slippage in silence. `--max-slippage-pct` derives
  the bound instead of asking anyone to guess.

Both fire under --force, which is where they matter: no prompt is shown, so a
loose value would otherwise pass unseen.
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.

2 participants