Skip to content

feat(executor): transaction builder + eth_call simulator#41

Merged
obchain merged 3 commits intomainfrom
feat/16-executor-builder-and-sim
Apr 24, 2026
Merged

feat(executor): transaction builder + eth_call simulator#41
obchain merged 3 commits intomainfrom
feat/16-executor-builder-and-sim

Conversation

@obchain
Copy link
Copy Markdown
Owner

@obchain obchain commented Apr 21, 2026

Closes #15

New charon-executor crate. Two modules, both small and passive (no broadcast, no signing of arbitrary inputs, no state held beyond config). Pipeline glue wires them together in a later PR.

builder.rsTxBuilder:

  • encode_calldata(opp, params) — packs LiquidationParams::Venus + underlying-token addresses from the opportunity into the on-chain CharonLiquidationParams struct (lockstep with Solidity) and ABI-encodes the outer executeLiquidation(...) call via alloy::sol! + SolCall::abi_encode
  • build_tx(provider, calldata, max_fee, priority_fee, gas_limit) — assembles EIP-1559 TransactionRequest, fetches the latest nonce for the bot signer
  • sign(tx) — signs via EthereumWallet, returns raw EIP-2718 envelope bytes. Does not broadcast.
  • Unit test pins selector against executeLiquidationCall::SELECTOR (catches accidental drift between Rust + Solidity struct shapes)

simulation.rsSimulator:

  • simulate(provider, calldata)eth_call against latest block. Ok = caller may broadcast; Err = revert reason logged at WARN, caller drops the opportunity. Zero gas spent.

Constants pinned: PROTOCOL_VENUS = 3 matches Solidity constant. CharonLiquidationParams field order/types must match contracts/src/CharonLiquidator.sol exactly — selector test fails if either side drifts.

Depends on feat/15-profit-calc-and-queue.

New `charon-executor` crate. Two modules — both deliberately small,
both passive (no broadcast / no signing of arbitrary inputs / no
state held beyond config). Pipeline glue (#15) wires them together.

builder.rs — `TxBuilder`:
- `encode_calldata(opp, params)` — packs the protocol-specific
  `LiquidationParams::Venus` plus underlying-token addresses from
  the `LiquidationOpportunity` into the on-chain
  `CharonLiquidationParams` struct (lockstep with the Solidity
  source) and ABI-encodes the outer `executeLiquidation(...)` call
  via `alloy::sol!` + `SolCall::abi_encode`
- `build_tx(provider, calldata, max_fee, priority_fee, gas_limit)`
  — assembles an EIP-1559 `TransactionRequest`, fetching the latest
  nonce for the bot signer
- `sign(tx)` — signs via `EthereumWallet`, returns raw EIP-2718
  envelope bytes ready for `eth_sendRawTransaction` or a Flashbots
  bundle. **Does not broadcast.**
- Test pins selector against `executeLiquidationCall::SELECTOR`
  (catches accidental drift between Rust + Solidity struct shapes)

simulation.rs — `Simulator`:
- `simulate(provider, calldata)` — `eth_call` against latest block.
  Ok = caller may broadcast; Err = revert reason logged at WARN,
  caller drops the opportunity. Zero gas spent.

Constants pinned: `PROTOCOL_VENUS = 3` matches the Solidity
constant. `CharonLiquidationParams` field order/types must match
`contracts/src/CharonLiquidator.sol` exactly — the selector test
will fail if either side drifts.
This was referenced Apr 23, 2026
obchain added a commit that referenced this pull request Apr 23, 2026
…able

the swap leg hard-coded `fee: 3000` (0.30 %). that tier does not
exist or has near-zero liquidity for several Venus-collateral pairs
on PCS V3: BTCB/USDT sits in the 0.05 % (500) pool, ETH/USDT in the
0.01 % (100) pool, XVS/WBNB in the 1 % (10000) pool. routing those
through the 0.30 % pool would revert on `SPL` or eat unbounded
slippage.

add `uint24 swapPoolFee` to LiquidationParams, validate non-zero in
executeLiquidation, and pass it to ExactInputSingleParams.fee. the
off-chain opportunity router now selects the deepest pool per pair.

abi layout note: this extends LiquidationParams with a new tail
field. the companion Rust `LiquidationParams` builder in the
charon-executor crate (not yet present on this branch — lands with
PR #41) must mirror the added field.

closes #122
- Redact PrivateKeySigner in TxBuilder Debug to avoid leaking the
  k256 scalar into logs (#158).
- Pull nonce from the pending block tag so queued tx don't collide
  with newly built ones; TODO flags NonceManager in PR #43 (#159).
- Reject build_tx calls where priority tip exceeds max fee per gas
  before any RPC round-trip (#160).
- Require an explicit gas_limit on Simulator::simulate so the
  simulation burns the same gas ceiling as the real broadcast (#161).
- Add Simulator::from_builder so the simulated sender is always the
  builder's hot wallet (onlyOwner alignment), plus debug_assert on
  non-zero sender (#162).
- Replace anyhow on the public lib surface with BuilderError and
  SimulationError thiserror enums (#163).
- Pin PROTOCOL_VENUS to the Solidity constant with an ABI-level unit
  test and line-number comment referencing CharonLiquidator.sol:49
  (#164).
- Adopt workspace lints (unsafe_code=forbid, arithmetic_side_effects,
  cast_possible_truncation, unwrap_used) and opt charon-executor in
  via [lints] workspace = true (#165).
- Decode revert payload in Simulator::simulate into a 4-byte selector
  plus full hex body so logs are greppable cross-protocol (#166).
- Add #[ignore]d fork tests covering the happy path and the
  onlyOwner adversarial path to pin the sim gate's safety invariant
  (#167).

Closes #158
Closes #159
Closes #160
Closes #161
Closes #162
Closes #163
Closes #164
Closes #165
Closes #166
Closes #167
@obchain obchain changed the base branch from feat/15-profit-calc-and-queue to main April 24, 2026 11:21
@obchain obchain merged commit 7ca3749 into main Apr 24, 2026
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.

[executor] Transaction builder: EIP-1559 gas + eth_call simulation gate + nonce management

1 participant