Conversation
✅ Deploy Preview for vrtx-dashboard canceled.
|
✅ Deploy Preview for vortex-sandbox ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for vortexfi ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Additive only: ConversionExecutionPricing and the required execution block on DepositConvertedWebhookPayload.conversions[], mirrored in WebhookPayload and WebhookDeliveryAttempt.
_settle returned the vault's claimed subsidy without observing it, and setSubsidyVault is guardian-only, untimelocked and unvalidated: a no-op vault let a below-floor fill pass the net floor on paper while the client received the raw fill. Count the subsidy only after exactly the shortfall landed at destination, the same exact-delta pattern _swap uses.
The only net-floor tests landed in the subsidy branch; a reference 100 bps under Chainlink with a fill above its target exercises the fee branch and must still revert / defer on the oracle floor.
The floor on the net binds at SLIPPAGE_BPS - floorPpm (~25 bps) below Chainlink, well inside the 100 bps band; the weekend-gap open item framed it purely as a band question.
The monitor compares the raw quote to SLIPPAGE_BPS, but settlement now enforces the floor on the client's net: a raw impact above it is subsidized up to the vault cap and the keeper still executes. Keep the raw depth-vs-oracle signal, name what it means (every swap draws a subsidy, the permissionless path reverts) instead of calling for a pause, and fix the stale _minOut reference.
flatten treated arrays as leaves, so String([...]) collapsed every route to "[object Object]" and a same-length path or enabled change produced no diff.
The partner docs said five-minute VWAP unconditionally; the keeper widens to sixty minutes when the five carry no volume.
…arder-fee-subsidy # Conflicts: # docs/api/pages/14-managed-profiles.md
…venue Coinbase Exchange delisted EURC-USD on 2024-08-29; its candles endpoint still answers with two-year-old data, so every keeper swap deferred on "no volume in the last 60 minutes". EURC-USDC is the live market (USDC = USD is already assumption A4). A delisting is invisible to the keeper, so the monitoring pass now probes the product status every half hour.
…ecovery Product wants one USDC transfer per bank payment and an automatic refund when a payment cannot be converted inside the promised window, with Vortex holding the funds for that refund. The clone now keeps every chunk's USDC (subsidy included) until the keeper forwards the whole payment, and can move a stuck payment to the immutable Vortex recovery wallet once its batch has been open for RECOVERY_DELAY. The client-held fallback role, its sweep and the dead-man sweep go away; the permissionless swap/forwardAll path after TRIGGER_DELAY stays as the liveness guarantee. SLIPPAGE_BPS fixtures move to 60 so weekend Chainlink drift no longer turns into refunds. Manifest v4 follows the new immutables.
…ecoveries The keeper now serves one deposit at a time: chunk swaps bound to it (1 deposit : N executions, explicit amountIn), one forward of the summed net once every chunk is confirmed, and a recover for a deposit an operator marked for the refund path. The N:M allocation join and the R04 snapshot attribution have no job left and go; every keeper transaction shares the execution row, the nonce-before-broadcast identity and the calldata-exact crash recovery, per kind. Deposits gain the settlement and refund states, DEPOSIT_CONVERTED fires on the confirmed forward and carries its hash, and the client-held fallback role leaves the account model, the provisioning read-back and the monitors (a destination change is now an incident; stranded batches warn past RECOVERY_DELAY). Two admin endpoints mark a deposit for recovery and close or retry it by hand until the refund is automated.
Second ADR-0005 amendment (chunks accumulate on the clone, one forward per payment, Vortex-held recovery wallet behind an on-chain 2 h delay, client fallback role removed, SLIPPAGE_BPS 60), architecture flow and data model, security-spec invariants and threat rows, runbook §2.7 refund procedure and triage rows, rollout terms and ledger, webhook page and OpenAPI deposit shape.
The promised conversion window counts from the mint block, and a refund needs the payer's IBAN and name; both come from data the flow already sees (the mint log's block, the issue order's counterpart) and were not kept. Adds the monerium_recoveries table the automated refund drives.
MONERIUM_B2B_AUTO_RECOVERY=alert reports deposits past the deadline, auto marks them and drives the refund: once the keeper's recover is confirmed, one recovery at a time swaps the USDC back on the reversed whitelisted route, tops the dedicated recovery wallet up from the EURe float to the exact issue amount (or sweeps a surplus back), places the Monerium redeem order to the payer's IBAN with a memo that makes placement exactly-once, and marks the deposit refunded when Monerium processes it. Every step re-derives its work from the wallet's balances so a lost hash never repeats a send; the executor refuses a second recover while a refund is in flight. Large amounts, a missing payer, rejected orders and exhausted retries park the deposit for the operator with the phase preserved. A refund monitor watches the queue and the float.
DEPOSIT_RETURNED joins the account-scoped deposit event family: fired once per refunded deposit with the EUR amount refunded, the payer's IBAN masked to its first and last four characters, Monerium's redeem order and the recover transaction. The deposits read API gains the same facts as a refund block.
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.
Why
Two things changed the forwarder design before launch, and both land here because the contracts from #1272 are not deployed yet (no clone migration, no "v2").
feeBpsskim on whatever the DEX returns cannot express that.Decisions are recorded in the two dated amendments of
docs/adr-0005-monerium-b2b-onramp.md; behaviour indocs/architecture-monerium-b2b-onramp.md; the design rationale for the second part (approaches compared, feasibility findings) indocs/proposal-monerium-b2b-settlement-and-recovery.md.What changes
Contracts (
contracts/monerium-forwarder)targetPpm1250,floorPpm1500 at launch) behind the existing 24 h increase timelock;MAX_FEE_PPMcaps both. Three bands against a keeper-supplied reference the contract bounds toMAX_REFERENCE_DEVIATION_BPSaround Chainlink: surplus above the target is the fee, between floor and target passes through, below the floor is topped up from the new sharedVortexSubsidyVault(treasury-funded, pays only factory clones, per-swap cap and daily budget, pausable, withdraws only to the treasury).swap(reference, route, amountIn)converts an explicit chunk and keeps the USDC (subsidy included) on the clone;forward(amount)pushes the whole converted payment todestinationin one transfer;forwardAll()is permissionless afterTRIGGER_DELAY(a Vortex outage cannot trap converted funds).recover(eure, usdc): keeper-only, pays only the immutableRECOVERY_WALLET, and only once the clone's batch marker (batchOpenedAt, never re-timed by a chunk swap) isRECOVERY_DELAY(2 h) old. The contract, not the keeper, enforces the promised window. Not blocked by pauses (pause-then-recover is the incident sequence).fallbackAddress,sweep,setDestination,setClientPaused, the dead-man sweep andSWEEP_DELAY. A destination change is a new clone.SLIPPAGE_BPS(on the client's net after fee and subsidy) is 60 bps in every fixture: with a 2 h promise a weekend Chainlink gap that defers a swap turns into a refund, and the twelve-month replay shows ~80 h/year of floor-cause deferral at 40 bps vs two five-minute blips at 60. Factory route whitelist validated on chain; manifest scripts learn all of it (manifest v4).Backend (
apps/api)kind(swap|forward|recover) anddeposit_id, the N:M allocation join is dropped (the migration refuses an execution that spanned several deposits instead of guessing),fallback_addressgoes; deposits gain the settlement and refund states (converting,forwarded,recovering,refunded,recovery_failed),minted_at, the payer's IBAN/name from the issue order's counterpart, and areturned_event_atmarker; newmonerium_recoveries.projectSwap) and defers rather than sending when the vault could not cover, the net would breach the floor, the reference is unavailable or out of band. It now serves one deposit at a time: chunk swaps bound to the deposit (never leaving sub-minimum dust when avoidable), oneforwardof the summed net, and arecoverfor a deposit marked for the refund path. Every keeper transaction shares the execution row, the nonce-before-broadcast identity and the calldata-exact crash recovery, per kind.recovery.ts,MONERIUM_B2B_AUTO_RECOVERY=off|alert|auto): deposits pastMONERIUM_B2B_RECOVERY_DEADLINE_MINUTES(120, from the mint) are reported or marked; once the keeper'srecoveris confirmed, one recovery at a time swaps the USDC back on the reversed route with a Chainlink-derived minimum, tops the dedicated recovery wallet up from the EURe float to the exact issue amount (or sweeps a surplus back), places the Monerium redeem order to the payer's IBAN with a memo that makes placement exactly-once, and marks the depositrefundedwhen Monerium processes it. Every step re-derives its work from the wallet's balances, so a lost hash never repeats a send; the executor refuses a secondrecoverwhile a refund is in flight. Amounts of EUR 15,000 or more, a missing payer, rejected orders and exhausted retries park the deposit for the operator with the phase preserved. Admin endpoints mark a deposit for recovery and close or retry one by hand.RECOVERY_DELAY, error pastTRIGGER_DELAY), reference venue, refund queue and float; config drift treats a destination change as an incident.DEPOSIT_CONVERTEDfires on the confirmed forward and carriesforwardTxHash; newDEPOSIT_RETURNED(refunded amount, masked payer IBAN, redeem order, recover tx); the deposits read API gainsforwardTxHashand arefundblock; deposit statuses and the wire-contract snapshot follow.Docs
SLIPPAGE_BPS60, dormancy refunds), architecture, security spec (invariants, threat rows, audit checklist), runbook (§2.7 refund procedure and automation, triage rows), rollout (G1 re-approval items, terms rewrite incl. the custody disclosure, deploy checklist, ledger), API pages and OpenAPI.Verification
forge test: 76 pass (unit, vault, invariants incl. pricing bounds, EURe/USDC exit exhaustiveness, no recovery beforeRECOVERY_DELAY, no chunk re-times a batch); the 4 mainnet fork tests compile but were not run (noETH_RPC_URLlocally).bun test(apps/api) on an isolated test database: 1976 pass, 0 fail (incl. the refund state machine with fakes, deadline marking, one-at-a-time gating, operator retry,DEPOSIT_RETURNED). API typecheck, Biome,bun docs:api:check,bun wire-contract:checkclean.Reviewer notes
contracts/monerium-forwarder/manifests/is v2 and failsverify-manifestuntil the contracts are redeployed; expected.alert→autoswitch.DEPOSIT_RECEIVEDmay now reportstatus: "converting"when conversion started within the same keeper cycle (documented)..d.ts; whichever merges second re-runsbun docs:api:types.