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

simulate

Rehearse arbitrary calldata and report the outcome, without a relayer, keys, or any intent to broadcast. A simulate: step runs the call through the network's read provider and journals the result as structured step output. It is the same eth_call-semantics simulation as send_transaction's pre-flight gate: eth_simulateV1 where the node supports it (adds gas + logs), plain eth_call + eth_estimateGas otherwise. No relayer handle is ever in scope, so a simulate: step cannot send money and is fully valid in keyless, monitor-only projects.

rflow_version: 1
name: governance-rehearsals
 
config:
  port: 3940
  db_connection: ${DATABASE_URL}
 
# keyless: no signer:, no relayers: — the relayer engine never boots
networks:
  - name: ethereum
    chain_id: 1
    rpc: ${ETH_RPC}
 
contracts:
  Governor:
    abi: ./abis/governor.json
    network: ethereum
    address: "0xGov..."
 
constants:
  timelock: "0xTim..."
  queued_proposals:
    - proposal_id: 42
 
workflows:
  rehearse-proposals:
    trigger:
      cron:
        expression: "0 */6 * * *"
    steps:
      - id: rehearse
        foreach: "${{ constants.queued_proposals }}"
        simulate: 
          network: ethereum
          from: "${{ constants.timelock }}"   # optional — default zero address 
          contract: Governor
          function: "execute(uint256)"
          args: ["${{ item.proposal_id }}"] 
          value: "0"

Fields

FieldRequiredDescription
network✅A declared network; the call runs on its read provider
contract◐A contract registry name, used with function/args
function◐Solidity signature, e.g. execute(uint256)
argsArguments; each may be an expression
to◐Raw target address, used with calldata (third-party payloads)
calldata◐Raw 0x-hex calldata: the exact bytes to rehearse, untouched
fromThe simulated sender (msg.sender); default the zero address. Set it to the address that would really execute (a timelock, a bridge executor) so balance/allowance/role checks see the truth
valueNative value to attach, e.g. "0" (the default)

contract/function/args and raw to/calldata are mutually exclusive: validation enforces exactly one target form and mirrors send_transaction's argument checks (arity, types, address shapes).

Output

The structured outcome becomes steps.<id>.output (and lands in the run journal, so rflow runs show and the history explorer carry it):

{
  "success": true,
  "revert_reason": null,
  "gas_used": 54321,
  "return_data": "0x0000000000000000000000000000000000000000000000000000000000000001",
  "logs": []
}
FieldDescription
successWhether the call succeeds against current chain state
revert_reasonThe decoded revert reason when it reverts (the standard Error(string) payload, the same decoding a send's simulation gate reports); null on success
gas_usedThe gas the simulated execution used (eth_simulateV1; eth_estimateGas on the fallback path); null when neither enrichment call could produce a figure (the key is always present)
return_dataThe raw 0x-hex return data
logsThe events the call would emit, decoded when the ABI is known (the same decoding the event trigger uses)

Only eth_simulateV1 can surface a call's logs, and not every RPC provider serves it (geth ≥ 1.14, recent reth, anvil do). Without it the step falls back to plain eth_call + eth_estimateGas: the report is just as valid, but logs stays empty.

The verdict eth_call and the gas/logs enrichment are separate round-trips, and state can move between them. Enrichment never fails a rehearsal the eth_call already proved successful: if the fallback eth_estimateGas reverts or errors, the successful report stands with gas_used: null (logs: []); if eth_simulateV1 instead reports the call reverting, the step re-runs the authoritative eth_call once and reports that fresh outcome: success, or success: false with the decoded reason.

Monitor-only by construction

simulate: needs a network and (for the registry form) a contract, nothing else. In a project without signer: or relayers: the relayer engine never boots. rflow explain classifies simulate: steps as read-only, and a workflow whose only chain interaction is a simulate: step keeps the monitor_only risk label.

The governance seatbelt pattern

The canonical use: every few hours, read the queued proposals, rehearse each proposal's calldata as the timelock, and post the outcome for human review before the timelock executes. A keyless project runs the whole loop:

rflow_version: 1
name: governance-seatbelt
 
config:
  port: 3940
  db_connection: ${DATABASE_URL}
 
networks:
  - name: ethereum
    chain_id: 1
    rpc: ${ETH_RPC}
 
contracts:
  Governor:
    abi: ./abis/governor.json
    network: ethereum
    address: "0xGov..."
 
constants:
  timelock: "0xTim..."
 
notifications:
  channels:
    gov-slack:
      slack:
        webhook_url: ${SLACK_WEBHOOK_URL}
 
workflows: 
  governance-seatbelt: 
    trigger: 
      cron: 
        expression: "0 */6 * * *"        # every 6 hours 
    steps: 
      - id: proposals
        http_call: 
          url: "https://your-indexer.example/governor/queued"
      - id: rehearse
        foreach: "${{ from_json(steps.proposals.output.body).proposals }}"
        simulate: 
          network: ethereum
          from: "${{ constants.timelock }}"
          contract: Governor
          function: "execute(uint256)"
          args: ["${{ item.id }}"] 
      - id: report
        foreach: "${{ from_json(steps.proposals.output.body).proposals }}"
        notify: 
          channel: gov-slack
          message: >-
            proposal ${{ item.id }}:
            ${{ 'SIMULATES OK (gas ' ~ steps.rehearse.output[item_index].gas_used ~ ')'
                if steps.rehearse.output[item_index].success
                else 'REVERTS — ' ~ steps.rehearse.output[item_index].revert_reason }}

A foreach: parent settles with output = the array of iteration outputs, so the report step indexes the rehearsal results by item_index. The same shape covers pre-flight checks on any third-party calldata: incoming bridge messages, queued timelock operations, partner-submitted payloads. Rehearse the exact bytes with the raw form:

rflow_version: 1
name: bridge-payload-check
 
config:
  port: 3940
  db_connection: ${DATABASE_URL}
 
networks:
  - name: ethereum
    chain_id: 1
    rpc: ${ETH_RPC}
 
contracts:
  Bridge:
    abi: ./abis/bridge.json
    network: ethereum
    address: "0xBri..."
 
constants:
  executor: "0xExe..."
 
workflows:
  rehearse-bridge-messages:
    trigger:
      event: { contract: Bridge, name: MessageReceived, network: ethereum }
    steps:
      - id: rehearse
        simulate: 
          network: ethereum
          from: "${{ constants.executor }}"
          to: "${{ trigger.args.target }}"
          calldata: "${{ trigger.args.payload }}"

Non-goals (v1)

  • No fork environments or state overrides — the call runs against the node's current state (Tenderly-Virtual-TestNet territory is out of scope).
  • No storage-diff output — success / revert reason / gas / logs is the deliberate 80%.
  • No bundle / multi-transaction simulation — one call per step; sequence steps (or foreach:) for several independent rehearsals.

A simulation is evidence about current state, not a guarantee about execution-time state. For sends, that gap is what recheck: and the send's own pre-flight gate exist for.