Skip to content

feat(tooling): anvil BSC mainnet fork + README seed#52

Merged
obchain merged 7 commits intomainfrom
feat/24-anvil-fork
Apr 24, 2026
Merged

feat(tooling): anvil BSC mainnet fork + README seed#52
obchain merged 7 commits intomainfrom
feat/24-anvil-fork

Conversation

@obchain
Copy link
Copy Markdown
Owner

@obchain obchain commented Apr 22, 2026

Summary

  • scripts/anvil_fork.sh — boots an anvil fork of BNB mainnet against a probed upstream (dRPC primary, PublicNode fallback), preserves chainId 56, 3s block time
  • config/fork.toml — mirrors mainnet addresses but points the RPC at 127.0.0.1:8545 and lowers the profit gate for demo staging
  • First README.md on the repo (the single tracked markdown per policy) with the quickstart for all three run modes and the metrics-series reference
  • Profile test gains fork_profile_parses_and_targets_localhost so the new TOML can't regress silently

Why this PR matters for the demo

Path B of the Grafana demo plan (the one that actually exercises the full liquidation pipeline) needs a local archive node. Free public BSC archive RPCs are rare; this script picks from the two that work keyless and falls back on failure so the demo is reproducible from a clean laptop.

Smoke-tested manually

  • bash -n scripts/anvil_fork.sh — OK
  • Script boots: upstream probe succeeds, fork mines 3s blocks, chainId 56 preserved
  • charon --config config/fork.toml test-connection --chain bnb → live forked head block
  • Metrics exporter binds :9091, charon_build_info{version,git_sha} scrapes cleanly

Not yet smoke-tested (follow-up)

  • Acceptance clause "fork survives >100 blocks of charon scanning without crash" needs a longer soak in a session that can keep two terminals alive. The initial 45s smoke confirmed startup + Venus adapter (48 markets enumerated) + metrics exporter — the 100-block soak is a next-session manual check.
  • .stale chainlink feed warnings fire against the fork because Chainlink updatedAt is frozen at fork time while wall-clock keeps moving. Not a script bug; a known consequence of forking without advancing the fork's time. Either relax DEFAULT_MAX_AGE for fork profile or advance anvil's time — flagged for a follow-up tweak, not blocking this PR.

Test plan

  • cargo build --workspace clean
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --all --check clean
  • cargo test --workspace — 3 profile tests pass (default, testnet, fork)
  • Fork boots + test-connection returns forked head
  • 100-block soak on the fork (next session)

Stacked PR

Base is feat/23-testnet-config (PR #51). Merge order: #46#50#51 → this.

Closes #48.

obchain added 2 commits April 22, 2026 16:49
New `scripts/anvil_fork.sh` boots an anvil fork of BNB Smart Chain
mainnet so the full liquidation path (flashLoan → liquidate → swap)
can be demonstrated locally against real Venus + Aave V3 state
without touching real capital.

Upstream RPC strategy:
- primary: https://bsc.drpc.org  (free, keyless, archive)
- fallback: https://bsc-rpc.publicnode.com  (ditto)
The script probes the primary with a short-timeout eth_blockNumber
call and falls back automatically on timeout or non-JSON responses.
`FORK_RPC` overrides the probe entirely for operators running their
own archive node.

Knobs:
- FORK_BLOCK  — pin the fork at a specific block (default: latest)
- FORK_PORT   — override the host port (default: 8545)
- FORK_CHAIN_ID — override the preserved chain id (default: 56)

Block-time is set to 3s so the scanner's block-duration histogram
and gas-oracle refresh cadence read sensibly during a demo.

New `config/fork.toml` mirrors `default.toml`'s mainnet addresses
(Aave pool, Venus Unitroller, Chainlink feeds) but points the RPC at
the local fork, lowers the profit gate so synthetic test positions
cross, and leaves the private-RPC entry unset — the local anvil IS
the submission surface.

Smoke-tested locally:
- bash -n passes syntax check
- Script boots anvil, mines 3-second blocks against the dRPC fork
- curl probe against the fork returns eth_chainId = 0x38 (56)
- `charon --config config/fork.toml test-connection --chain bnb`
  returns a live forked head block

`config_profiles.rs` gains a `fork_profile_parses_and_targets_localhost`
case so the fork TOML can't regress silently.

Closes #48.
The repo's only tracked markdown file per the project's markdown
policy. Covers the three shipped run modes (mainnet / BSC testnet /
local anvil fork), the Prometheus exporter surface, and the
crate-by-crate layout so a reviewer landing on GitHub doesn't have
to dig through Cargo.toml to orient themselves.

Metrics table matches the constants exported by `charon-metrics`;
kept short so it doesn't drift out of sync with the source of truth.
This was referenced Apr 22, 2026
obchain added 4 commits April 23, 2026 18:10
anvil_fork.sh previously forked at upstream head whenever FORK_BLOCK
was unset, which is every default invocation — CI runs, soak tests,
and the Grafana demo all landed on a moving baseline. Introduce
DEFAULT_FORK_BLOCK=94_000_000 (matches the fork-test suite pin on
feat/25, captured 2026-04-23; past every Aave V3 reserve and Venus
vToken activation the demo needs). `FORK_BLOCK=latest` remains
available as an opt-in escape hatch; the help text flags it as not
reproducible.

Drop the PublicNode RPC fallback. PublicNode retains ~128 blocks of
state; any forked historical eth_call against it returns "missing
trie node" — a silently broken fork is worse than a hard-error.
When the dRPC primary fails the probe the script now exits with a
message directing the operator to set FORK_RPC=<archive-url>.

Closes #242
Closes #246
… placeholder, Chainlink keep-alive

config/fork.toml:
- bind metrics to 127.0.0.1:9091 so a laptop demo on public Wi-Fi
  doesn't silently publish /metrics (unauthenticated) to the LAN.
- drop [liquidator.bnb] with its address(0) placeholder; the zero
  address would reach TxBuilder and fail with an opaque encoding
  error on the first opportunity. Omission flips the CLI onto the
  read-only arm in run_listen, matching the pattern testnet.toml
  already uses for Chapel. Comment block documents how to re-enable
  after forge create.

scripts/anvil_fork.sh:
- restructure the tail: anvil now runs in the background with a
  tracked PID, `trap cleanup EXIT INT TERM` tears both the node and
  the mine loop down on any exit path, and `wait "\$ANVIL_PID"`
  keeps the script foreground so Ctrl-C still propagates.
- add a background keep-alive loop that issues `cast rpc anvil_mine 1`
  every FORK_MINE_INTERVAL_SECS (default 30s) so block.timestamp keeps
  walking forward and Chainlink feeds stay inside PriceCache's
  freshness window — without this the scanner degrades to zero
  liquidatable positions within ~10 minutes of fork time and the
  Grafana demo looks dead.
- gate the loop on `cast` availability and expose the
  CHARON_PRICE_MAX_AGE_SECS fallback in the header comment so an
  operator without Foundry can still run a read-only demo.
- add an eth_blockNumber readiness probe against the local RPC before
  kicking off the mine loop, so the first anvil_mine call doesn't
  race node startup and waste a retry budget on connection-refused.

Closes #249
Closes #252
Closes #244
Closes #240
fork.toml now reads `${CHARON_ANVIL_PORT:-8545}` on both ws_url and
http_url so a dev can run a secondary fork on 8546 (or any port)
without editing TOML. To support the `:-default` shape, the config
loader's `substitute_env_vars` was extended to POSIX-ish parameter
expansion semantics: `${VAR}` still errors if unset (preserving the
secret-bearing default profile's contract), but `${VAR:-default}`
falls back when the variable is unset or empty. Unit-tested for set,
unset-with-default, unset-without-default (still errors), empty-with-
default, default-overridden, and unterminated-expression cases.

Added `bot.profile_tag: Option<String>` with `profile_tag = "fork"`
on fork.toml. New `Config::validate()` returns the typed enum
`ConfigError::ForkProfileNonLoopbackRpc { chain, field, url }` when
the fork profile points at anything other than 127.0.0.1 / ::1 /
localhost. main.rs now calls `validate` right after load and exits
non-zero if it fails, so a misconfigured env cannot silently land a
0.01 USD profit gate on a mainnet RPC. Non-fork profiles are not
restricted — local-geth dev runs remain supported.

Extended `fork_profile_parses_and_targets_localhost` to lock in:
- chain_id == 56
- ws_url and http_url both start with loopback
- metrics.bind is a loopback address (not 0.0.0.0)
- min_profit_usd is strictly lower than default.toml's gate
- profile_tag is Some("fork") and Config::validate accepts it
- liquidator map is empty (no placeholder address(0) regression)

Resolves #247, #254, #256.
Added a non-fatal version check near the top of the script that
compares `anvil --version` output against a substring configured by
`CHARON_REQUIRED_FOUNDRY_VERSION` (default: "stable"). A mismatch
prints a loud warning with the remediation (`foundryup -i <ver>`)
and sleeps 2s; a missing / empty output is a hard 127 exit. The
check is disabled by `CHARON_SKIP_FOUNDRY_VERSION_CHECK=1` for CI
images that pin Foundry out of band.

Renamed the port knob from `FORK_PORT` to `CHARON_ANVIL_PORT` so
the script reads the same variable `config/fork.toml` substitutes
via `${CHARON_ANVIL_PORT:-8545}`. `FORK_PORT` kept as a legacy
fallback for muscle memory. Header updated to document both.

Resolves #259.
@obchain obchain changed the base branch from feat/23-testnet-config to main April 24, 2026 14:11
# Conflicts:
#	README.md
#	crates/charon-cli/src/main.rs
#	crates/charon-core/src/config.rs
#	crates/charon-core/src/lib.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[config] BSC testnet profile (Chapel) with Venus deployment addresses

1 participant