Skip to content

f(x) Protocol private fxUSD minting recipes - #53

Merged
zy0n merged 32 commits into
Railgun-Community:mainfrom
Squabble9:fxmint
May 26, 2026
Merged

f(x) Protocol private fxUSD minting recipes#53
zy0n merged 32 commits into
Railgun-Community:mainfrom
Squabble9:fxmint

Conversation

@Squabble9

Copy link
Copy Markdown
Contributor

This PR adds the fxmint recipes: Cookbook recipes for the first private decentralized stablecoin minting on Ethereum: fxUSD on f(x) Protocol, minted from shielded RAILGUN balances.

Thin glue over already-audited f(x) position-management contracts and RAILGUN cross-contract-call primitives. There is no new on-chain code.

Full doc: src/recipes/borrow/fx/README.md covers scope (6 recipes + shared step + read API), pool support (wstETH-Long / WBTC-Long), per-operation usage, calibration receipts (6 mainnet broadcasts), privacy properties, and file layout.

Tests: src/recipes/borrow/fx/__tests__/ and src/steps/borrow/fx/__tests__/.

Happy to iterate on whatever review comes back.

Kevin and others added 29 commits May 7, 2026 08:46
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lose math

Also adds viem@2.9.0 as a dependency and upgrades TypeScript to 5.x (needed
because viem 2.x type declarations use TS5 syntax incompatible with TS 4.9).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Port FxMintOpenRecipe and FxMintCloseRecipe from PrivateMint monorepo into
the cookbook fork; update imports to cookbook-internal relative paths and
wire the borrow barrel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…fxPositionNFT constant

The pool registry now carries each pool's native collateral decimals
(wstETH=18, WBTC=8) so cookbook amount metadata matches on-chain reality.
FxMintPoolRef's custom-pool branch widens to require collateralDecimals.

The previous FX_ADDRESSES.fxPositionNFT pinned wstETH-Long's pool address
and was wrong for WBTC-Long (different pool, different NFT contract).
Removed; recipes already used per-pool addresses for outputNFTs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FxMintRepayDebtRecipe needs the same fee accounting that computeFxClose
already uses for its repay leg. Extract computeFxRepay; have computeFxClose
delegate to it for the repay computation, then add the collateral-side
proportional withdraw on top. Behavior of computeFxClose is preserved
(golden-value test asserts equality with pre-refactor output).
…lateralDecimals

The borrow fee is per-(pool, operator) and governance-upgradable on f(x).
Hardcoding 0.5% would silently mis-account if governance moves the rate.
Step now takes borrowFeeRatio as a constructor arg; caller fetches via
PoolConfiguration.getPoolFeeRatio(pool, operator)[2] at recipe build time.

Also plumbs collateralDecimals through so cookbook's amount metadata is
correct for non-18-decimal collateral (8 for WBTC). Was hardcoded 18n,
which would mis-account by 10^10 for WBTC-Long.

Note: borrowFeeRatio and collateralDecimals are temporarily optional on
the step's data type (defaulted to current mainnet values: 5_000_000n
and 18n respectively) so FxMintOpenRecipe still compiles; Task 7
tightens them to required as part of the recipe-layer update.
Was hardcoded 18n for collateral output/spent lines, which would
mis-account by 10^10 for WBTC-Long. Now driven by the pool registry's
collateralDecimals (8 for WBTC, 18 for wstETH).

Note: collateralDecimals is temporarily optional on the step's data type
(default 18n) so FxMintCloseRecipe still compiles; Task 8 tightens to
required as part of the recipe-layer update.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New step that wraps PoolManager.operate() with signed collDelta/debtDelta
arguments. Used by the new topup, topup-and-borrow, borrow-more, and
repay-debt recipes (in subsequent commits). Position NFT is consumed and
re-emitted with the same tokenSubID (adjusts never burn the NFT).

Validates at construction that at least one delta is non-zero and that
the relevant fee ratio is supplied for the active direction.

Matches FxMintClosePositionStep's partial-close branch for NFT handling
(only outputNFTs, not spentNFTs — the step-validator combines spent+output
into a single map and rejects duplicates).

7/7 new tests passing; full-suite failure count unchanged at 9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…alidation

Recipe now branches its step graph on swapQuote presence:
- wstETH-Long: requires swapQuote (WETH → wstETH path; existing behavior).
- WBTC-Long: forbids swapQuote (direct WBTC input).
- Custom pool ref: trusts caller's choice.

Borrow fee ratio is now a required opt (no more hardcoded 0.5%); caller
fetches it via PoolConfiguration.getPoolFeeRatio at recipe-build time.
Plumbs collateralDecimals through to the step.

validatePoolFlow is exported for reuse by close, topup, and
topup-and-borrow recipes in subsequent commits.

Tightens FxMintOpenPositionStep's borrowFeeRatio + collateralDecimals
back to required (Task 4 left them temporarily optional). Cleans up
the open-step test's non-null assertions and FEE_DENOM hardcode.
…imals plumbing

Recipe now branches on swapQuote presence:
- wstETH-Long (swap path): operate(close) → approve(0x) → swap(wstETH → WETH)
- WBTC-Long (direct path): operate(close) (collateral shields back as-is)

Reuses validatePoolFlow from fx-mint-open-recipe for per-pool flow checks.

Tightens FxMintClosePositionStep's collateralDecimals back to required
(Task 5 left it temporarily optional). Cleans up the close-step test's
NFT assertions and imprecise comment.
Adds collateral to an existing f(x) Long position without changing debt.
Two paths via shared validatePoolFlow:
- wstETH-Long (swap path): WETH -> wstETH -> operate(positionId, +coll, 0)
- WBTC-Long (direct path): WBTC -> operate(positionId, +coll, 0)

Position NFT consumed and re-emitted (same id). Builds on the new
FxMintAdjustPositionStep with collDelta > 0, debtDelta = 0.
Combines collateral top-up with additional fxUSD mint in a single
operate() call. Uses FxMintAdjustPositionStep with both deltas non-zero.

Same per-pool flow validation as FxMintTopupRecipe via shared
validatePoolFlow. Construction validates additionalDebt > 0 (otherwise
caller should use FxMintTopupRecipe).
…tion)

Single-step recipe: operate(positionId, 0, +additionalDebt). No swap leg,
no collateral side, pool-agnostic. Useful for extracting more fxUSD as
position appreciates.
Symmetric counterpart to FxMintTopupRecipe — together they form the
position's two risk-management dials. Burns fxUSD via PoolManager.operate
to reduce debt on an existing position; collateral unchanged.

Two-step recipe: ApproveERC20SpenderStep(PoolMgr, fxUSD, approveAmount)
→ FxMintAdjustPositionStep(positionId, 0, -repayAmount, repayFeeRatio).

Caller pre-computes amounts via computeFxRepay (extracted in Task 3).
Wraps PoolManager.getPoolInfo + Pool.getTotalRawCollaterals +
Pool.getLiquidateRatios + PoolConfiguration.getPoolFeeRatio into a single
sync function returning the FxPool shape. Default operator is the
Railgun relay-adapter; integrators can override.

Adds the getLiquidateRatios ABI fragment to FX_POOL_ABI (confirmed via
Task 1 discovery: lives on Pool, returns (debtRatio: 1e18, bonusRatio: 1e9);
mainnet readings 0.95e18 / 4e7 on both wstETH-Long and WBTC-Long).

Wallets call this once and feed borrowFeeRatio/repayFeeRatio into recipe
constructors directly — no more hardcoded fees anywhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Returns per-position state (rawColls, rawDebts, debtRatio) plus a
native-decimals collateralAmount derived via the rawColls × collateralBalance
/ totalRawColls formula computeFxClose uses internally.

Wallet integrators feed collateralAmount into their own price feeds for
LTV display; rawColls/rawDebts feed into computeFxClose / computeFxRepay
for close/repay flows.
…+ read API + repay math

Wires up the four new recipes (Topup / TopupAndBorrow / BorrowMore / RepayDebt),
FxMintAdjustPositionStep, the getFxPool / getFxPosition read API, and the
computeFxRepay helper through the package barrel chain so consumers can import
them all from '@railgun-community/cookbook' root.

Also adds the missing api/borrow and api/borrow/fx index barrels (no api/borrow
existed before) and threads './borrow' into src/api/index.ts.
- Move DEFAULT_FXMINT_OPERATOR to fx-mint-util.ts (single source of truth).
- Restore slippage-pair test in FxMintTopupAndBorrowRecipe.
- Backfill real positionId in getFxPosition fork test.
- Export FxPoolEntry; spread {...repay} in computeFxClose return.
- Strengthen golden-value test in computeFxClose.
- Add runtime-throw test for FxMintAdjustPositionStep no-input branch.
- Misc comment + cast tightening (no behavioral changes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expands the recipe README for v0.1 surface:
- Top-up / top-up-and-borrow / borrow-more / repay-debt usage sections
- collateralDecimals column in pool support table
- Swap-vs-direct path explanation per pool
- Read API section (getFxPool + getFxPosition + DEFAULT_FXMINT_OPERATOR)
- Dynamic-fees note (no constants hardcoded; caller fetches via getFxPool)
- File layout updated for new files

Calibration receipts section stubbed for v0.1 — actual tx hashes appended
once mainnet calibration broadcasts complete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the stubbed prose with a 7-row table; each row has a <FILL B.x>
placeholder matching the Phase B step IDs in the calibration runbook.
After mainnet broadcasts, user sed-replaces each placeholder with the tx
hash from the corresponding broadcast.
…y path

Mirrors FxMintClosePositionStep's pattern: when debtDelta < 0, the
input fxUSD is split by the preceding ApproveERC20SpenderStep into
(approveAmount with approvedSpender=PoolManager) + any leftover change
(approvedSpender=undefined). Cookbook's step-validator demands per-step
input == spent + outputs + fees; the recipe-engine epilogue shield-back
happens AFTER all steps, so the leftover must be declared as a step
output for the balance to close.

Caught by FxMintRepayDebtRecipe mainnet broadcast B.7 (5.94 fxUSD input,
2.004 fxUSD spent → validator rejected). Fix verified by repeat of all
8 FxMintAdjustPositionStep unit tests; test repay-input updated to
simulate post-approve state (approvedSpender=PoolManager for the exact
spend amount, no orphan in unit test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six mainnet calibrations landed (12 May 2026): wstETH-Long open re-cal
proving dynamic borrowFeeRatio plumbing, topup (swap), borrow-more,
partial repay-debt; WBTC-Long open (first-ever WBTC direct-path
fxmint position on mainnet) and partial close (direct, WBTC out).

WBTC-Long topup skipped — its step-graph is identical to wstETH-Long
topup with the leading approve target swapped, and the WBTC direct
input path is exercised by the WBTC-Long open above. Redundant for
v0.1 calibration scope.
f(x) Pool exposes a rebalance threshold BELOW the liquidation threshold.
When a position's debtRatio crosses the rebalance ratio, the protocol's
rebalancer service progressively unwinds collateral to keep the position
from ever reaching the liquidation ratio (and getting fully seized).

Adds:
- getRebalanceRatios() ABI fragment to FX_POOL_ABI
- rebalanceDebtRatio + rebalanceBonusRatio fields on FxPool
- Read step in getFxPool()

Mainnet values (May 2026, wstETH-Long): (0.88e18, 2.5e7) — 88% rebalance
threshold, 2.5% rebalancer bonus (vs 95% / 4% for liquidation).

Wallets should treat [rebalanceDebtRatio, liquidationDebtRatio) as a
yellow risk zone: the position is alive but f(x) is actively unwinding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tened guards

Pre-upstream-PR cleanup batch addressing f(x) dev's code review:

- Drop viem dep entirely; migrate 4 call sites to ethers (Interface for
  encodeFunctionData; Contract for readContract; JsonRpcProvider in tests).
  Address type now exported from fx-mint-util.
- Replace `as never` casts in three step files with proper ContractTransaction
  typing — the previous cast silently silenced downstream type checks.
- Pin TypeScript back to ^4.9.5 (was ^5.9.3). fxmint only needs 4.9 for
  `as const satisfies`; the 5.x jump was out of scope for the feature PR.
- FxMintRepayDebtRecipe: enforce strict approveAmount equality against the
  fee-uplifted target (was >= repayAmount). Errors now name the expected
  value and point at computeFxRepay instead of surfacing as a downstream
  step-validator failure.
- FxMintOpenPositionStep and FxMintAdjustPositionStep: set minBalance =
  fxUSDNet on the deterministic fxUSD output (was 0n). Unblocks future
  v0.2 steps that want to consume this fxUSD with a fixed amount.
- README: integrator note on DEFAULT_FXMINT_OPERATOR — recommend wallets
  pass `operator` explicitly rather than relying on the pinned default.
- Run prettier over all fxmint files (single quotes / trailing commas /
  arrow parens to match cookbook house style).

v0.2 follow-ups (reviewer agreed defer): rename `positionWstETH` field,
multicall batching in getFxPool/getFxPosition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous review-fix commit moved cookbook's Address type from viem's
`\`0x\${string}\`` brand to plain `string`. That broke wallet integrators
(and our CLI) that import `Address` from viem for their own internal use
— passing a cookbook-returned address into viem's strict-typed APIs no
longer typechecks.

Restore the brand natively in fx-mint-util (no viem dep — purely a
type-level convention) plus a small `getAddress` wrapper around ethers'
checksum function that casts the returned plain string back to the brand
so FX_ADDRESSES / KNOWN_POOLS / DEFAULT_FXMINT_OPERATOR stay tidy.

Runtime is unchanged. Type-system change only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Squabble9 pushed a commit to Squabble9/cookbook that referenced this pull request May 18, 2026
- Replace placeholder with live upstream PR link
  (Railgun-Community#53).
- Fix the recipe-docs pointer from non-existent docs/fx/ to the
  actual src/recipes/borrow/fx/ location.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zy0n
zy0n self-requested a review May 18, 2026 14:03
@zy0n

zy0n commented May 18, 2026

Copy link
Copy Markdown
Collaborator

Thank you, will review this soon!

Kevin and others added 2 commits May 25, 2026 15:44
Replaces three positional `const [, /* label */ value, ...]` destructures
in getFxPool and getFxPosition with fully-named slots (`_`-prefixed for
unused ones). The old form attached inline-comment labels to the value
that followed them, but the labels actually named the slot before — the
reverse of how the eye reads top-to-bottom.

Pure cosmetic change. No behavior delta; tests unchanged.

Addresses items raised in PR Railgun-Community#53 review:
- getFxPool poolInfo destructure
- getFxPosition poolInfo destructure
- fees tuple in getFxPool (same anti-pattern, same file)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The README's getFxPool field-list comment was out of date —
commit 00fed54 added `rebalanceDebtRatio` and `rebalanceBonusRatio`
to the FxPool return shape but didn't update this block. The
FxPool type docstring (fx-position-reader.ts) already covers
both fields; this just brings the integrator-facing README in sync.

Order matches the actual return-object order in getFxPool
(liquidation* → rebalance* → fees).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@socket-security

socket-security Bot commented May 25, 2026

Copy link
Copy Markdown

No dependency changes detected. Learn more about Socket for GitHub.

👍 No dependency changes detected in pull request

Revert package.json + yarn.lock to origin/main. Rewrite
fx-mint-util's `as const satisfies` to plain `as const` so
the type still narrows under typescript@4.8.3.
@zy0n

zy0n commented May 26, 2026

Copy link
Copy Markdown
Collaborator

tests confirmed locally.

@zy0n
zy0n merged commit 99e3425 into Railgun-Community:main May 26, 2026
2 of 4 checks passed
Squabble9 pushed a commit to Squabble9/cookbook that referenced this pull request May 27, 2026
Follow-up to PR Railgun-Community#53 (item Railgun-Community#3 from @zy0n's review).

validatePoolFlow now performs a direction-aware shape check on the
provided swapQuote, in addition to the existing presence check. The
shape check runs on BOTH named-pool and custom-pool refs (zy0n's
"both of those logic branches" comment):

  'deposit'  (open / topup / topup-and-borrow):
    sellTokenAddress           must equal WETH
    buyERC20Amount.tokenAddress must equal pool.collateralToken

  'withdraw' (close):
    sellTokenAddress           must equal pool.collateralToken
    buyERC20Amount.tokenAddress must equal WETH

The direction asymmetry surfaced during implementation: open/topup
recipes hardcode `inputToken = FX_ADDRESSES.WETH` and target
pool.collateralToken, while close-recipe reverses (withdraws collateral,
swaps to WETH). A single direction-agnostic check would reject every
valid close-recipe quote, so validatePoolFlow takes a third arg.

Compares are case-insensitive. Mismatches raise a clear `fxmint:`
error at recipe construction time, replacing what previously
surfaced as a confusing on-chain gas-estimate revert.

Tests: 9 new cases in the validatePoolFlow describe block, covering
both directions (deposit + withdraw), both branches (named + custom),
and the checksum-vs-lowercase case-insensitivity path. Existing
presence-check tests pass unchanged after adding the direction arg.
Fixture sweep across the four recipe test files added the previously-
missing sellTokenAddress (the fixtures were cast `as unknown as
SwapQuoteData` to bypass the type check; the new shape check would
otherwise throw TypeError on undefined).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zy0n pushed a commit that referenced this pull request May 29, 2026
)

Follow-up to PR #53 (item #3 from @zy0n's review).

validatePoolFlow now performs a direction-aware shape check on the
provided swapQuote, in addition to the existing presence check. The
shape check runs on BOTH named-pool and custom-pool refs (zy0n's
"both of those logic branches" comment):

  'deposit'  (open / topup / topup-and-borrow):
    sellTokenAddress           must equal WETH
    buyERC20Amount.tokenAddress must equal pool.collateralToken

  'withdraw' (close):
    sellTokenAddress           must equal pool.collateralToken
    buyERC20Amount.tokenAddress must equal WETH

The direction asymmetry surfaced during implementation: open/topup
recipes hardcode `inputToken = FX_ADDRESSES.WETH` and target
pool.collateralToken, while close-recipe reverses (withdraws collateral,
swaps to WETH). A single direction-agnostic check would reject every
valid close-recipe quote, so validatePoolFlow takes a third arg.

Compares are case-insensitive. Mismatches raise a clear `fxmint:`
error at recipe construction time, replacing what previously
surfaced as a confusing on-chain gas-estimate revert.

Tests: 9 new cases in the validatePoolFlow describe block, covering
both directions (deposit + withdraw), both branches (named + custom),
and the checksum-vs-lowercase case-insensitivity path. Existing
presence-check tests pass unchanged after adding the direction arg.
Fixture sweep across the four recipe test files added the previously-
missing sellTokenAddress (the fixtures were cast `as unknown as
SwapQuoteData` to bypass the type check; the new shape check would
otherwise throw TypeError on undefined).

Co-authored-by: Kevin <kevin@openclaw.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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