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 (
Openevent,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-solverWhat the generated YAML does
- Trigger โ
Openevents on the settlement, quoted inwhere:: both token identities are pinned (outputTokenmust be the configuredToken,inputTokenmust 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 escrowedinputAmountcovers the requestedoutputAmountcreate a run (the spread is the solver's fee). The network'sconfirmations: 12means a reorged intent cannot orphan a paid fill. unfilledโ race check: assertsisFilled(orderId) == false(another solver may have won between the event and now).inventoryโ asserts the solver wallet holds enough output tokens.approveโ allows the settlement to pull exactly this fill's output amount (gas-capped).fillโ delivers the output, collects the escrowed input: simulated first (a raced order reverts in simulation and nothing is broadcast),assert_simcheckssim.okplus asim.gas_usedcap, a deadlinerecheck(now() <= fillDeadline) runs immediately before broadcast so an intent that expired during the approve wait is dropped, then held untilconfirmed.
Inputs
| key | type | default |
|---|---|---|
project_name | string | erc7683-solver |
network / chain_id / rpc_env / rpc_url | network / chain_id / env_var / string | ethereum / 1 / ETH_RPC / a public RPC |
confirmations | int | 12 |
settlement_address | address | zero placeholder โ replace it |
output_token_address | address | zero placeholder โ replace it |
gas_max_price | string | 50 gwei |
sim_gas_cap | int | 500000 |
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 recheckre-evaluates the fill deadline immediately before broadcast- network
confirmations: 12,wait_for: confirmed concurrency.on_conflict: queueโ fills are serialized, never racing the same inventoryon_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 liveThe 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
- replace the
settlement_addressandoutput_token_addressplaceholders - replace the raw dev mnemonic with a production signer
- fund the
solverrelayer with output-token inventory AND gas - tune the quote: the default
where:ENFORCES same-token intents (both token identities pinned to your configuredToken) becauseoutputAmount <= inputAmountis only a quote within one token โ to fill cross-token intents, replace the pins with a real pricing source (e.g. acommand:quote step), never with a bare amount comparison - 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
solverrelayer (one wallet, cloned onto both chains) and point thefillstep'snetwork:at it โ see the commented variant inexamples/erc7683-solver. - Minimum spread: tighten the trigger to
outputAmount * 10050 <= inputAmount * 10000for a 0.5% floor. - Deadline guard: the fill's
recheckalready drops a send whosefillDeadlineexpired pre-broadcast; addand now() <= trigger.args.fillDeadlineto the triggerwhere:so expired intents never even create a run. - Standing allowance: replace the per-fill
approvewith a one-time manual allowance and delete the step (cheaper, less strict).