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

Token deposit relay

Template: token-deposit-relay ยท category: relayers ยท risk: money_moving

An ERC20 Transfer into a watched deposit address (at or above a minimum amount) triggers a relayed payout of the same amount from the payout wallet โ€” the classic exchange/treasury deposit-echo pattern. Runnable sibling: examples/token-transfer-relay.

When to use it

  • credit or forward user deposits to a downstream address automatically
  • bridge-style "mirror this deposit elsewhere" flows on a single chain
  • any event-driven send where the amount comes straight from the trigger

Generate it

rflow new --template token-deposit-relay
# or into an existing project (needs a signer; adds the `payout` relayer):
rflow add workflow token-deposit-relay

What the generated YAML does

  1. Trigger โ€” Transfer events on the token, filtered in where: to deposits >= min_deposit whole tokens whose to is the watched deposit_address. The network's confirmations: 12 means a reorged deposit never pays out.
  2. gate โ€” reads balanceOf(payout relayer) and asserts the wallet can cover the deposit (a short wallet fails the run into the dead-letter queue instead of firing a doomed send).
  3. payout โ€” transfer(payout_address, trigger.args.value) through the embedded relayer: simulated first (assert_sim: sim.ok), gas-capped, then held until confirmed. The simulation is also the drained-wallet guard โ€” a transfer the wallet cannot cover reverts in eth_call and dead-letters instead of broadcasting.

Inputs

keytypedefault
project_namestringtoken-deposit-relay
network / chain_id / rpc_env / rpc_urlnetwork / chain_id / env_var / stringethereum / 1 / ETH_RPC / a public RPC
confirmationsint12
token_address / token_decimalsaddress / intUSDC mainnet / 6
deposit_addressaddresszero placeholder โ€” replace it
payout_addressaddresszero placeholder โ€” replace it
min_deposittoken_amount100
gas_max_pricestring100 gwei

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)

  • inventory gate (read + assert) before any send
  • pre-flight simulation + assert_sim: ["${{ sim.ok }}"] โ€” this is the drained-wallet guard: an uncoverable transfer reverts in simulation instead of broadcasting (a recheck would only re-test journaled constants the gate already passed, so the template ships none)
  • gas cap: limit_from_simulation + max_price
  • network confirmations: 12, wait_for: confirmed
  • concurrency.on_conflict: queue โ€” payouts are serialized, never racing the same wallet balance
  • 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 token-deposit-relay --fixture fixtures/deposit-event.json  # dry-run rehearsal
rflow start                    # go live

The generated fixtures/deposit-event.json is a 250-token deposit into your watched address โ€” edit value to rehearse the min_deposit boundary.

Production checklist

  1. replace the deposit_address and payout_address placeholders
  2. replace the raw dev mnemonic with a production signer
  3. fund the payout relayer with tokens AND gas
  4. dry-run the fixture, then watch the first live deposit in rflow runs ls
  5. consider a budget on the workflow if you run budgets

Common modifications

  • Alert instead of failing when inventory is short: drop the assert on gate and add a notify step behind an if:.
  • Fee-taking relay: pay out trigger.args.value * 99 / 100 instead of the full amount.
  • Multiple deposit addresses: move deposit_address into a list: and match with contains(...) in the trigger where:.
  • Idle-deposit sweep: pair it with treasury-sweep-approval to move accumulated balances to cold storage behind an approval.