Skip to content

Fix "max" liquidity crash-loop: tolerate consumed unlimited allowance - #1

Closed
tomer-ba-text wants to merge 1 commit into
textile-protocol:mainfrom
tomer-ba-text:fix/max-liquidity-approval-coverage
Closed

Fix "max" liquidity crash-loop: tolerate consumed unlimited allowance#1
tomer-ba-text wants to merge 1 commit into
textile-protocol:mainfrom
tomer-ba-text:fix/max-liquidity-approval-coverage

Conversation

@tomer-ba-text

Copy link
Copy Markdown

What this is

A proposed fix for the operator crash-loop that happens when a side is configured with *_total_liquidity = "max". Take it or leave it, just flagging the root cause with a candidate patch. No urgency from my end since the numeric-ceiling config is a clean workaround (see below).

Symptom

On v0.1.43, running with buy_total_liquidity_debt = "max" (and/or the collateral equivalent) crash-loops at startup with:

Error: missing Permit2 approvals for 1 token(s); run `stitch approve` first, or pass --dry-run to test without them

...even right after stitch approve succeeds and reports the token approved. Numeric liquidity values (e.g. "12000000000") do not hit this.

Root cause

required_approvals sets a "max"-liquidity token's required allowance to U256::MAX (add_required_amount, the LiquidityAmount::Max arm). The live-start preflight (unapproved_tokens -> approval_action) then only counts the token as approved when the ERC20-allowance-to-Permit2 is >= U256::MAX.

But Permit2 decrements that ERC20 allowance by each filled order's input amount, so an "unlimited" approval never stays exactly U256::MAX. After the very first fill, current_allowance < U256::MAX -> preflight reports the token unapproved -> with KeepAlive the operator crash-loops. Re-running stitch approve tops it back to U256::MAX, but the next fill breaks it again, so it's not durable.

Confirmed on Base mainnet for maker 0x4C6E…384d:

  • cNGN ERC20 allowance to Permit2 == U256::MAX -> passes
  • USDC ERC20 allowance to Permit2 == U256::MAX - 50_000 (~$0.05 consumed by fills) -> fails -> "missing Permit2 approvals for 1 token(s)"

The change

In approval_action, for the max sentinel (required == U256::MAX) treat any allowance above half of max as effectively unlimited. Falling below that floor would take 2^255 atomic units of fills, which no real balance can reach. A genuinely under-approved wallet (allowance below the floor) still gets a fresh Approve(U256::MAX). Exact-mode coverage is untouched.

One function changed + one regression test that encodes the real consumed-allowance scenario. Existing max_liquidity_* tests (which assert required == U256::MAX) are intentionally left as-is.

Notes / open questions for you

  • I went with a "half of max" floor for simplicity. If you'd rather make the intent explicit, the alternative is to use the existing RequiredApproval.uses_max_liquidity flag in the preflight instead of keying on required == U256::MAX. Happy to switch it if you prefer that shape.
  • I couldn't compile locally (no Rust toolchain on this machine, the operator was installed as a prebuilt binary), so this hasn't been cargo test'd. Please run CI / tests before trusting it.
  • Workaround if you don't want to touch this now: a high numeric ceiling above wallet balance (e.g. "12000000000") makes the bot cap live depth to funded - committed every tick and use the full balance, with none of this fragility. That's what we're running.

🤖 Generated with Claude Code

With `*_total_liquidity = "max"`, required_approvals sets the per-token
required allowance to U256::MAX. The live-start preflight then only treats
a token as approved when its ERC20-allowance-to-Permit2 is >= U256::MAX.

But Permit2 decrements that allowance by each filled order's input, so an
"unlimited" approval never stays exactly U256::MAX. After the first fill the
preflight reports "missing Permit2 approvals" and the operator crash-loops,
even though `stitch approve` was run and the allowance is effectively
unlimited. Observed live on Base: cNGN allowance still == U256::MAX (passes),
USDC allowance == U256::MAX - 50_000 after ~$0.05 of fills (fails) ->
"missing Permit2 approvals for 1 token(s)".

Fix: for the max sentinel (required == U256::MAX), treat any allowance above
half of max as effectively unlimited. Reaching that floor would require 2^255
atomic units of fills, unreachable by any real balance; a genuinely
under-approved wallet (below the floor) still gets a fresh max approval.
Exact-mode coverage is unchanged.

Adds a regression test using the real-world consumed-allowance scenario.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bnolens

bnolens commented Jun 11, 2026

Copy link
Copy Markdown
Member

Thanks for the detailed report and candidate patch. I investigated it against the current packages/stitch-bot code in textile-monorepo.

The root cause is valid: for a "max" liquidity side, the approval requirement becomes U256::MAX, but some tokens spend down the ERC20 allowance granted to Permit2 as fills execute. Requiring the live-start preflight allowance to remain exactly U256::MAX can therefore false-positive after the first fill and trigger the crash loop you described.

I handled this in textile-monorepo instead, because that is where the maintained stitch-bot source now lives. The implementation follows the approach you called out in the PR notes: it threads the existing RequiredApproval.uses_max_liquidity flag into the approval coverage check, so the relaxed “effectively unlimited” floor applies only when the config actually used the "max" liquidity sentinel. That avoids keying behavior solely on required == U256::MAX, which is an implementation detail and could also represent a literal exact numeric requirement.

I also added regression coverage for both cases:

  • a consumed once-max allowance such as U256::MAX - 50_000 still counts as approved for max-liquidity config;
  • an exact U256::MAX requirement without max-liquidity intent still requires literal coverage.

Verified with:

cargo test --manifest-path packages/stitch-bot/Cargo.toml

That passed locally. Closing this PR as superseded by the monorepo fix.

@bnolens bnolens closed this Jun 11, 2026
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