feat(cli): fund RFQ collateral from any held token with --pay-with (cli 0.4.0) - #59
Merged
Merged
Conversation
- Ignores CLI PRD/templates, agent tooling dirs (.claude, .superstack), and local handoff notes that were never meant to be tracked. - Adds codex_bugs.md alongside fable_5_fix.md: both are security-triage logs that reference unfixed vulnerabilities and must not be published.
- `id` is optionAddress + "-" + side, so printing it alongside optionAddress pushed the table past terminal width and wrapped pnl under the first column. - JSON output still carries `id`, so scripts keying on it are unaffected.
- `rfq request --pay-with <eth|token>` routes the request through `OptionFactory.swapAndCall`, wrapping native ETH 1:1 or swapping an ERC-20 through KyberSwap so the collateral is funded in the same transaction. The approval targets the OptionFactory, not the router, because the factory is what executes the swap. - The minimum shown at the confirmation prompt is the minimum enforced at broadcast: the re-quote can raise it but never lower it, and the aggregator's calldata is decoded and bound to the quoted trade (tokens, amount, recipient, and the router's own `minReturnAmount`) before anything is signed. - BUY-side and Base only — a SELL request escrows nothing at request time, so there is no deposit for a swap to fund.
- Adds docs/rfq/pay-with.md covering both funding paths, sizing, the rails, and why the approval goes to the OptionFactory rather than the swap router. - Cross-links it from the RFQ overview, create-rfq, SUMMARY, and the CLI README.
…-sdk into feat/cli-swapAndCall
- Cuts a 0.4.0 section for `--pay-with` and updates the README version banner. - Backfills the 0.3.0 and 0.3.1 headings: both releases bumped package.json but never cut their changelog entries, leaving shipped work under [Unreleased]. - Bumps cli to 0.4.0. Publishing to npm remains a separate decision.
- `--pay-with` refused a zero-deposit request by suggesting the flag be dropped, but a BUY with reservePrice 0 escrows nothing and cannot be filled either way, so that advice led nowhere. It now names the cause and the two real fixes. - Documents why it happens: the MM ask is only fetched when sizing with --collateral-amount, so --contracts leaves the reserve at 0 unless set.
This reverts commit 0d06a08.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
rfq request --pay-with, which funds an RFQ's collateral from an asset you already hold — in the same transaction — viaOptionFactory.swapAndCall. Native ETH wraps to WETH 1:1 inside the factory; an ERC-20 is swapped through KyberSwap. Cuts CLI 0.4.0.The collateral token an RFQ takes is dictated by the structure, not the user (single-strike ETH CALL is WETH, everything else USDC), so a user holding the other asset previously had to go acquire the right one first.
What's here
feat(cli)— the--pay-withflow:swapAndCall.ts(contract rules + aggregator client + router calldata decoder),payWith.ts(planning, rails, preview), and therfq requestwiring.docs(rfq)—docs/rfq/pay-with.md, cross-linked from the RFQ overview, create-rfq, SUMMARY, and the CLI README.fix(cli)— unrelated one-liner: theidcolumn pushedposition listpast terminal width.chore— gitignore for local working notes; 0.4.0 release prep.Safety properties
The approval targets the OptionFactory, not the router — the factory is what executes the swap. Beyond that:
minReturnAmountthe router actually enforces. That decoded minimum, not the API's plaintextamountOut, is what the floor is measured against, so a response cannot report one price and encode another. Routes carrying a router-level fee, or using an entrypoint the CLI can't decode, are refused. Selectors are pinned against the verified BaseMetaAggregationRouterV2(swap0xe21fd0e9,swapGeneric0x59e50fed,swapSimpleMode0x8af033fb).--pay-withsigns directly rather than through an SDK write method, which is where that check normally lives.authorizedRoutersstatus, and wallet balance are all checked before any approval is broadcast, so a rejected route costs no gas. A route with no USD pricing is refused rather than treated as zero impact.Scope notes
encodeSwapAndCalland sends it with the wallet's own signer.optionFactory.swapAndCall()dropsparams.value, breaking the direct native-wrap path. It has no CLI impact (the CLI usesencodeSwapAndCall, which preserves it) and fails closed. Worth a separate patch.package.jsonbut never cut their entries, leaving shipped work under[Unreleased].Testing
npm testincli— 14/14 pass, including two new suites covering the calldata decoder, every binding rejection (redirected recipient, wrong tokens, wrong size, smuggled fee, unknown selector, inflated plaintext minimum), theNaN/Infinityprice-impact fail-open, and the confirmed-floor invariant.npm run typecheck+npm run buildclean in bothcliand the SDK root.Verified end-to-end on Base mainnet (RFQ 123, ETH-21AUG26-2400-C, WETH-collateralized
INVERSE_CALL, funded from USDC):swapAndCall— USDC pulled, swapped via Kyber, WETH deposited, RFQ created atomically0xa099be…settleQuotationEarly— maker offer accepted0x474e9a…The decoded-calldata binding and the confirmed floor both ran on the live route and passed. The resulting position reports
contracts 0.016273772577950114— matching the request'snumContractsexactly — at a premium of0.000095459806942795 WETH, matching the accepted offer to the wei. Total cost ~$0.02 in gas.Expect occasional aborts on fast-moving pairs when drift exceeds 50bps between prompt and broadcast — clear error, nothing broadcast, re-run. That's the floor working as designed.