| name | simulate-flow |
| description | Dry-run user flows to catch reverts before users hit them. Uses `cast call` and `cast estimate` to verify contract interactions will succeed, estimate gas costs, and compare simulated outcomes against frontend display. |
| user-invocable | true |
| allowed-tools | Bash, Read, Glob, Grep |
simulate-flow
Dry-run user flows to catch reverts before users hit them.
You are executing the simulate-flow skill. Your job is to simulate contract interactions using cast call and cast estimate to verify that user flows will succeed, estimate gas costs, and compare simulated outcomes against what the frontend would display. This is proactive QA — catching failures before deployment or before users encounter them.
Prerequisites
Verify cast is available:
cast --version
Phase 1: Identify the Flow to Simulate
Determine what user action to simulate. Common flows:
| Flow Type | Key Functions | Common Failure Points |
|---|
| Bid/Auction | createBid(uint256) | Reserve not met, auction ended, outbid threshold |
| Swap/Trade | swap(...), exactInputSingle(...) | Slippage, deadline, insufficient liquidity |
| Deposit/Stake | deposit(uint256), stake(uint256) | Missing approval, cap reached, paused |
| Withdraw | withdraw(uint256), unstake(...) | Insufficient balance, timelock, cooldown |
| Mint/NFT | mint(uint256), claim(...) | Max supply, wrong price, not whitelisted |
| Approve | approve(address,uint256) | Usually succeeds (but check for approval caps) |
| Transfer | transfer(address,uint256) | Insufficient balance, blacklisted |
| Governance | propose(...), castVote(...) | Insufficient voting power, proposal expired |
If the user specifies a flow (e.g., "simulate a bid"), use that. If they say "simulate everything" or "test the main flows", identify the primary user flows from the frontend code.
Discover flows from frontend
Search for transaction-sending patterns:
Grep for "useWriteContract" or "writeContract" or "sendTransaction" in src/, app/
Grep for "functionName:" near these calls to identify the contract functions
For each write function found, that's a flow to simulate.
Phase 2: Gather Parameters
For each flow, you need:
- Contract address — from config, .env, or constants
- Function signature — exact Solidity signature with types
- Parameters — realistic values for simulation
- Sender address — who is calling (affects balance/permission checks)
- Value — ETH to send (for payable functions)
Determine realistic parameters
DO NOT use zero or dummy values. The point of simulation is to test realistic conditions.
Read the frontend to find what parameters it would send:
Grep for the function name to find where the frontend constructs the call
Look for:
- Form input validation (min/max values)
- Computed parameters (e.g., slippage calculations)
- Constants used as defaults
If the frontend constructs parameters dynamically, read the relevant on-chain state to compute realistic values:
cast call <auction> "auction()(uint256,uint256,address,uint256,uint256,bool)" --rpc-url <rpc>
cast call <auction> "minBidIncrementPercentage()(uint8)" --rpc-url <rpc>
Sender address
Use a realistic sender. Options:
- An address from the user (if they specify)
- An address that has the right token balances (find one from recent events)
- The zero address (for basic call simulation — but won't catch balance/permission issues)
To find a realistic sender, check recent events:
cast logs --from-block -1000 --address <contract> --rpc-url <rpc>
Or use the --from flag with impersonation in cast call (this works because cast call is a simulation, not a real transaction):
cast call <contract> "function(args)" <params> --from <any-address> --rpc-url <rpc>
Phase 2.5: Frontend Simulation Verification
Before running agent-side simulation (Phase 4), check whether the frontend ALREADY has client-side simulation for each write flow. This determines whether the flow is self-protecting or relies entirely on agent/manual verification.
Why This Matters
Client-side simulation (via useSimulateContract / useSimulate*) can catch reverts BEFORE the user signs — the tx button stays disabled until simulation passes. Agent-side simulation (cast call) is a point-in-time check that may not reflect the user's actual parameters. Both are valuable; neither replaces the other.
Detection Steps
For each write flow discovered in Phase 1:
-
Find the hook file containing the writeContract / useWrite* call
-
Search for corresponding simulation hook:
Grep for "useSimulate" in the same file
Also check if the simulation hook is imported from a generated file:
Grep for "useSimulate.*{ContractName}" in hooks/generated* or generated/
-
Check if simulation gates the write:
- Does the write reference
simData.request or simulation.data.request? → GATED
- Does the write construct args inline while simulation exists separately? → PARALLEL (dangerous)
- Does the simulation error get checked before write execution? → GATED
- Is the write button disabled when simulation hasn't completed? → GATED
-
Classify each flow:
| Classification | Meaning | Severity |
|---|
| GATED | Simulation exists and gates the write invocation | No action needed — flow is self-protecting |
| PARALLEL | Simulation exists but doesn't gate the write | CRITICAL — simulation provides zero protection |
| AGENT_ONLY | No client-side simulation exists | INFO — recommend adding useSimulate* for the flow |
Report Format
PHASE 2.5: Frontend Simulation Verification
Flow: closeAccount (MoneycombVault)
Client simulation: useSimulateMoneycombVaultCloseAccount ✓
Simulation gates write: YES (pendingIndex dependency, simError check) ✓
Classification: GATED — self-protecting
Flow: openAccount (MoneycombVault)
Client simulation: NONE
Classification: AGENT_ONLY
→ Recommendation: Add useSimulateMoneycombVaultOpenAccount to catch reverts before signing
→ Risk: User can trigger openAccount with invalid honeycombId — revert at wallet confirmation
Flow: approve (ERC721)
Client simulation: NONE
Classification: AGENT_ONLY
→ Note: ERC721 approve rarely reverts (owner check only). Low priority for simulation.
Grounding
This phase was added because the MCV audit found useMoneycombClose had useSimulateMoneycombVaultCloseAccount co-existing with closeAccountWrite but firing in PARALLEL — the simulation provided zero protection because the write didn't depend on the simulation result. The fix was to accept pendingIndex as a prop (simulation runs when modal opens) and check simError before executing the write. Without this phase, simulate-flow would run agent-side cast call simulation and declare the flow "passes" — missing the fact that the frontend's own simulation was broken.
Phase 3: Pre-flight Checks
Before simulating the main flow, check prerequisites that commonly cause failures:
3A: Check if contract is paused
cast call <contract> "paused()(bool)" --rpc-url <rpc>
If paused, report immediately — all user flows will fail.
3B: Check token approvals (for flows that transfer tokens)
If the flow involves depositing/spending tokens:
cast call <token> "allowance(address,address)(uint256)" <sender> <spender-contract> --rpc-url <rpc>
cast call <token> "balanceOf(address)(uint256)" <sender> --rpc-url <rpc>
If allowance is insufficient, note that an approve transaction is needed first. Simulate the approve too:
cast call <token> "approve(address,uint256)(bool)" <spender-contract> <amount> --from <sender> --rpc-url <rpc>
3C: Check ETH balance (for payable flows)
cast balance <sender> --rpc-url <rpc>
3D: Check time-dependent conditions
cast block latest --rpc-url <rpc> --json 2>/dev/null | jq -r '.timestamp'
Compare against deadlines, auction end times, cooldown periods, etc.
Phase 4: Simulate the Transaction
Basic simulation with cast call
cast call simulates a transaction without broadcasting it. It runs against the current state and returns the result (or reverts).
cast call <contract> "functionName(type1,type2)(returnType)" <arg1> <arg2> \
--from <sender> \
--value <wei-amount> \
--rpc-url <rpc>
IMPORTANT: cast call returns the return value of the function. For state-changing functions, this may not be meaningful (often returns nothing or a bool). The key information is whether it REVERTS or not.
Gas estimation with cast estimate
cast estimate <contract> "functionName(type1,type2)" <arg1> <arg2> \
--from <sender> \
--value <wei-amount> \
--rpc-url <rpc>
This returns the estimated gas in units. Convert to cost:
cast gas-price --rpc-url <rpc>
Handling simulation results
If simulation succeeds:
- Record the return value
- Record the gas estimate
- Calculate the gas cost in ETH and USD (if possible)
- Compare return value against what the frontend would show
If simulation reverts:
- Decode the revert reason (same as tx-forensics Phase 3)
- Determine the root cause
- Suggest how to fix the parameters or prerequisites
Phase 5: Multi-Step Flow Simulation
Many user flows require multiple transactions. Simulate the full sequence:
Common multi-step flows
Approve + Deposit:
cast call <token> "approve(address,uint256)(bool)" <vault> <amount> --from <sender> --rpc-url <rpc>
cast call <vault> "deposit(uint256)(uint256)" <amount> --from <sender> --rpc-url <rpc>
NOTE: cast call doesn't persist state between calls. The approve simulation won't affect the deposit simulation. You need to check if the allowance is ALREADY sufficient, or note that the approve is needed.
Swap with permit:
Mint + List (NFT flow):
cast call <nft> "mint(uint256)" --value <price> --from <sender> --rpc-url <rpc>
For each step:
- Simulate the call
- Estimate gas
- Note dependencies (Step 2 requires Step 1 to succeed first)
- Calculate total gas cost across all steps
Phase 6: Compare Against Frontend Display
This is where simulation becomes truly valuable. Check whether what the frontend SHOWS matches what the simulation PRODUCES.
Find frontend display values
Grep for components that display transaction previews, estimates, or confirmations
Look for:
- Gas estimate displays
- Output amount displays (for swaps)
- Price impact calculations
- Fee displays
- Expected return calculations
Common mismatches
| Frontend Shows | Simulation Shows | Cause |
|---|
| Lower gas estimate | Higher actual gas | Frontend uses hardcoded gas limit |
| Different swap output | Actual output amount | Frontend quote is stale (price moved) |
| "Transaction will succeed" | Simulation reverts | Frontend doesn't check all preconditions |
| Lower fee | Higher fee | Fee parameter changed on-chain |
Specific checks
For swaps/trades:
cast call <router> "getAmountsOut(uint256,address[])(uint256[])" <amountIn> "[<tokenIn>,<tokenOut>]" --rpc-url <rpc>
Compare against the frontend's displayed quote.
For deposits/yields:
cast call <vault> "previewDeposit(uint256)(uint256)" <amount> --rpc-url <rpc>
Compare against the frontend's displayed expected shares.
Phase 7: Edge Case Simulations
Test boundary conditions that normal usage wouldn't hit but attackers might:
Zero amount
cast call <contract> "deposit(uint256)" 0 --from <sender> --rpc-url <rpc>
Should revert. If it doesn't, that's a potential bug.
Max uint256 amount
cast call <contract> "deposit(uint256)" 115792089237316195423570985008687907853269984665640564039457584007913129639935 --from <sender> --rpc-url <rpc>
Should revert cleanly. If it overflows, that's critical.
Already-used parameters
For operations that should be one-time (claim, initialize):
cast call <contract> "claim()" --from <already-claimed-address> --rpc-url <rpc>
Should revert. If it doesn't, double-claim is possible.
Expired deadlines
cast call <router> "swap(...,uint256 deadline)" ... 1 --from <sender> --rpc-url <rpc>
Should revert with "expired" or similar.
Phase 8: Generate Report
Write the simulation report to grimoires/protocol/simulate-report.md:
# Flow Simulation Report
**Generated**: <timestamp>
**Contract**: <address>
**Chain**: <chain>
**RPC**: <rpc url (redact API keys)>
## Summary
| Flow | Status | Gas (units) | Gas (ETH) | Notes |
|------|--------|-------------|-----------|-------|
| <flow name> | PASS / FAIL | <gas> | <cost> | <note> |
## Detailed Results
### Flow 1: <name>
**Steps**:
1. <step description>
- Command: `cast call ...`
- Result: <success/revert>
- Gas: <estimate>
2. <step description>
...
**Pre-flight checks**:
- Contract paused: <No>
- Token approval sufficient: <Yes/No>
- Sender balance sufficient: <Yes/No>
**Simulation result**: <PASS/FAIL>
**Return value**: <decoded>
**Gas estimate**: <units> (<ETH cost>)
**Frontend comparison**: <match/mismatch details>
### Flow 2: <name>
...
## Edge Case Results
| Test | Expected | Actual | Status |
|------|----------|--------|--------|
| Zero amount | Revert | <result> | PASS/FAIL |
| Max amount | Revert | <result> | PASS/FAIL |
| Duplicate action | Revert | <result> | PASS/FAIL |
| Expired deadline | Revert | <result> | PASS/FAIL |
## Frontend Discrepancies
| What Frontend Shows | What Simulation Shows | Impact |
|--------------------|----------------------|--------|
| <value> | <value> | <user-facing impact> |
## Recommendations
1. <recommendation>
2. <recommendation>
Error Handling
"execution reverted" with no data
The function may require specific conditions. Check:
- Is the sender whitelisted/authorized?
- Is the contract in the right state (not paused, auction active, etc.)?
- Are all prerequisite steps completed?
Gas estimation fails
If cast estimate fails, the transaction WOULD revert. This is equivalent to a simulation failure. Decode the revert reason.
RPC timeout on simulation
Complex simulations (especially with many internal calls) can timeout on public RPCs. Suggest the user use a dedicated RPC with higher compute limits.
Fork simulation (advanced)
If available, suggest using anvil for more complex simulations:
anvil --fork-url <rpc> --fork-block-number <block>
cast send <contract> "function()" --from <sender> --rpc-url http://localhost:8545 --private-key <anvil-test-key>
This allows multi-step simulations with state persistence between calls. Only suggest this for complex flows that can't be adequately tested with cast call.
Completion Criteria
You are done when:
- You have identified all flows to simulate
- You have gathered realistic parameters for each flow
- You have run pre-flight checks
- You have simulated each flow and recorded results
- You have estimated gas for successful flows
- You have compared results against frontend display
- You have tested edge cases
- You have written the report
- You have highlighted any FAIL results or frontend discrepancies to the user
The single most important output is: which flows will revert and why. Lead with failures.