Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

ERC-7683 intent solver

Template: erc7683-solver ยท category: intents ยท risk: money_moving

An ERC-7683 Open event fires the solver pipeline: profitability quote in the trigger, an on-chain race check, an inventory gate, an exact-amount approve, then the simulated, gas-capped fill. Runnable sibling: examples/erc7683-solver.

When to use it

  • run a filler against an ERC-7683-style settlement (Open event, isFilled(bytes32), fill(bytes32))
  • as the skeleton for any "event โ†’ quote โ†’ race check โ†’ send" solver, even off-standard ones (adapt the signatures)

Generate it

rflow new --template erc7683-solver
# or into an existing project (needs a signer; adds the `solver` relayer):
rflow add workflow erc7683-solver

What the generated YAML does

  1. Trigger โ€” Open events on the settlement, quoted in where:: both token identities are pinned (outputToken must be the configured Token, inputToken must be that same token โ€” comparing raw amounts of two different tokens is not a quote, and a hostile intent could otherwise trade worthless input for the solver's real inventory), then only intents whose escrowed inputAmount covers the requested outputAmount create a run (the spread is the solver's fee). The network's confirmations: 12 means a reorged intent cannot orphan a paid fill.
  2. unfilled โ€” race check: asserts isFilled(orderId) == false (another solver may have won between the event and now).
  3. inventory โ€” asserts the solver wallet holds enough output tokens.
  4. approve โ€” allows the settlement to pull exactly this fill's output amount (gas-capped).
  5. fill โ€” delivers the output, collects the escrowed input: simulated first (a raced order reverts in simulation and nothing is broadcast), assert_sim checks sim.ok plus a sim.gas_used cap, a deadline recheck (now() <= fillDeadline) runs immediately before broadcast so an intent that expired during the approve wait is dropped, then held until confirmed.

Inputs

keytypedefault
project_namestringerc7683-solver
network / chain_id / rpc_env / rpc_urlnetwork / chain_id / env_var / stringethereum / 1 / ETH_RPC / a public RPC
confirmationsint12
settlement_addressaddresszero placeholder โ€” replace it
output_token_addressaddresszero placeholder โ€” replace it
gas_max_pricestring50 gwei
sim_gas_capint500000

Required env vars

DATABASE_URL, the RPC env var, RAW_DANGEROUS_MNEMONIC. rflow new fills .env with a freshly generated DEV-ONLY mnemonic โ€” swap in a production signer before real funds ride on this config.

Safety defaults (all generated)

  • quote in the trigger pins both token identities before comparing amounts โ€” hostile and unprofitable intents never create a run
  • race check + inventory gate (read + assert) before any send
  • exact-amount approve (no unlimited allowances)
  • pre-flight simulation + assert_sim: ["${{ sim.ok }}", "${{ sim.gas_used < 500000 }}"] โ€” a raced order reverts in simulation and nothing is broadcast
  • gas caps (limit_from_simulation + max_price) on approve AND fill
  • recheck re-evaluates the fill deadline immediately before broadcast
  • network confirmations: 12, wait_for: confirmed
  • concurrency.on_conflict: queue โ€” fills are serialized, never racing the same inventory
  • on_failure: dead_letter

Run it locally

docker compose up -d           # postgres on :5448
rflow validate                 # should be green out of the box
rflow test fill-intent --fixture fixtures/open-event.json  # dry-run rehearsal
rflow start                    # go live

The generated fixtures/open-event.json is a profitable same-token intent (1050 in, 1000 out, both sides your configured output token) โ€” flip the amounts or swap in a foreign token address to rehearse the rejected paths (no run is created).

Production checklist

  1. replace the settlement_address and output_token_address placeholders
  2. replace the raw dev mnemonic with a production signer
  3. fund the solver relayer with output-token inventory AND gas
  4. tune the quote: the default where: ENFORCES same-token intents (both token identities pinned to your configured Token) because outputAmount <= inputAmount is only a quote within one token โ€” to fill cross-token intents, replace the pins with a real pricing source (e.g. a command: quote step), never with a bare amount comparison
  5. add a notification channel on the dead-letter path; a stream of raced fills means your latency budget needs work

Common modifications

  • Cross-chain fills: add the destination network, list it on the solver relayer (one wallet, cloned onto both chains) and point the fill step's network: at it โ€” see the commented variant in examples/erc7683-solver.
  • Minimum spread: tighten the trigger to outputAmount * 10050 <= inputAmount * 10000 for a 0.5% floor.
  • Deadline guard: the fill's recheck already drops a send whose fillDeadline expired pre-broadcast; add and now() <= trigger.args.fillDeadline to the trigger where: so expired intents never even create a run.
  • Standing allowance: replace the per-fill approve with a one-time manual allowance and delete the step (cheaper, less strict).