DEV Community

Christian Pichichero
Christian Pichichero

Posted on

What Each Test Layer Proves When Code Moves Funds

Your onchain executor can pass its unit tests, produce valid calldata, and still send funds through an unintended path. The difficult part is not writing more tests. It is knowing what each test environment proves and where its evidence stops.

A useful testing ladder has distinct stages: pure simulation, forked-chain execution, a production canary using a trivial amount, and staged production use. None is a substitute for the others.

The most dangerous design is often a dryRun boolean scattered across the codebase. If some transaction paths respect it while another path broadcasts, the dry run creates false confidence. A system with no dry-run mode is visibly dangerous. A partially enforced one looks safe until the missed branch moves funds.

Start with a deterministic execution plan

Separate deciding what to do from doing it. The planner should consume explicit inputs and produce an execution plan without holding signing or broadcast capabilities.

A plan might contain:

  • chain identifier
  • contract and token addresses
  • calldata
  • value to send
  • approval requirements
  • slippage bounds
  • deadline
  • expected state transitions
  • reasons the action should be rejected

The same planner should run in simulation, fork tests, canaries, and production. Environment-specific code should sit behind the transport boundary.

Conceptually:

market snapshot + strategy state
              |
              v
       deterministic planner
              |
              v
       validated transaction plan
              |
      +-------+--------+
      |                |
 simulation transport  live transport
 no signing material   signing + broadcast
Enter fullscreen mode Exit fullscreen mode

This is stronger than checking a flag immediately before an RPC call. In simulation mode, the process should not load a signer capable of broadcasting. The restriction is structural rather than conventional.

Pure planning tests are good at checking arithmetic, rounding, token decimals, state transitions, allocation rules, and invariants such as “a rejected plan cannot contain an executable transaction.” Property-based tests are useful here because many failures live at boundaries: zero balances, minimum trade sizes, stale timestamps, or values just inside a slippage limit.

What this layer does badly is represent external state. A mock contract behaves as its author expects, not necessarily as the deployed contract behaves. A simulated quote does not expose an RPC outage, a changed pool state, or a transaction competing in the public mempool.

Use a fork to test contract interactions

A forked chain lets the executor interact with deployed bytecode and a captured state snapshot. This catches errors that pure simulation tends to miss:

  • incorrect ABI encoding
  • approval and allowance assumptions
  • token decimal mistakes
  • revert conditions in deployed contracts
  • multi-call ordering problems
  • unexpected behavior from non-standard tokens

Fork tests should execute the complete sequence, not isolated calls. If production performs an approval followed by a swap, the test should run that sequence through the same transaction-building code.

Pin the block used by the test. Without a pinned state, a passing test can become a failing test because liquidity, balances, or contract state changed rather than because the code changed. It is also useful to maintain fixtures for awkward historical states instead of testing only a convenient recent snapshot.

A fork still does not reproduce production. It usually omits real RPC latency, provider rate limits, signer failures, relayer behavior, gas sponsorship, mempool ordering, and reorgs. Local mining also tends to be more predictable than public block production. A fork proves that a transaction can execute under the captured state. It does not prove that the transaction will execute under the state present when production broadcasts it.

Make dry run a capability boundary

A safe dry run should be incapable of broadcasting, not merely instructed not to.

Keep the live signer and broadcast client out of the dry-run process. Give that process a simulation transport that supports calls and gas estimation but has no method or credentials for sending transactions. If a code path unexpectedly requests a broadcast, it should fail closed and emit an observable error.

Then test the boundary itself. Exercise every action type through dry-run mode and assert that no live transport was constructed. Include cancellation, retries, approval refreshes, cleanup jobs, and administrative paths. Background workers are easy to overlook because the visible strategy path may respect dry run while a retry queue still holds an executable payload.

Audit logs should record the plan, validation decisions, simulation result, selected transport, and whether signing material was loaded. Logs are not an enforcement mechanism, but they make boundary violations easier to diagnose.

The trade-off is duplication pressure. Simulation and live transports can drift. Reduce that risk by sharing the planner, serializer, and validators while keeping the final send capability separate.

Use a canary to test the production path

A canary sends a real transaction using the production RPC, signer, policy checks, monitoring, and confirmation logic, but with the smallest practical exposure permitted by the venue and token precision.

This stage can reveal configuration errors that earlier layers cannot: wrong chain selection, unavailable signing credentials, broken gas handling, provider restrictions, stale configuration, and monitoring that watches the wrong address.

A canary is still weak evidence. It covers a particular route under a particular market state. It may not exercise retries, cancellation, partial completion, or adverse ordering. Success should not cause the system to bypass limits or broaden access automatically.

Canaries also need containment. Use a dedicated wallet, strict token permissions, bounded transaction value, and alerts tied to expected transaction hashes and state changes. Stop after unexpected behavior rather than repeatedly retrying an uncertain action.

Production needs continuing controls

After a canary, staged activation is safer than treating deployment as the end of testing. Keep explicit limits on value, action frequency, allowed contracts, tokens, and slippage. Monitor both submitted transactions and intended plans so that “nothing happened” is detectable alongside incorrect execution.

Plan for ambiguous outcomes. An RPC timeout does not establish that a transaction was rejected. Before retrying, inspect the transaction hash, sender nonce, and chain state. Blind retries can duplicate an action when the initial request reached the network but its response did not reach the executor.

Pausing should block new plans at the scheduler and block broadcasts at the transport boundary. Relying on one check leaves another path available.

Disclosure: I build Tradevo, software where creators describe crypto strategies in plain English, historically test and paper-trade the compiled deterministic rules, submit them for human review, set a free or monthly price, and publish approved strategies, while other users can discover, subscribe to, allocate USDC to, and automatically run those rules from their own wallets.

The testing ladder works because each stage answers a narrower question. Simulation checks planning logic. Forks check interaction with captured contract state. Canaries check the real operational path with contained exposure. Production controls limit what happens when assumptions fail. Treating any stage as proof of the next is where expensive mistakes begin.

Top comments (0)