feat: multi-liquidation batcher (contracts + executor)#45
Merged
Conversation
Adds per-chain batching so multiple liquidatable positions land in one transaction instead of N. The base tx cost (21 000 gas + signature + calldata header) amortises across the whole batch; each flash-loan still pays its own 0.05% premium. On-chain (CharonLiquidator.sol): - New `batchExecute(LiquidationParams[] calldata items) external onlyOwner nonReentrant` entrypoint, capped at `MAX_BATCH_SIZE = 10` (uint256 internal constant) - Refactor: the seven input-validation guards + the `flashLoanSimple` kickoff move from `executeLiquidation` into a new internal helper `_initiateFlashLoan(LiquidationParams memory)`. `executeLiquidation` becomes a thin wrapper that delegates to it so both entrypoints share one validated flow - Emits `BatchExecuted(uint256 count)` after the loop completes - `nonReentrant` stays on `batchExecute`; `_initiateFlashLoan` is internal so the guard is held across iterations without deadlock - Four new Foundry tests (access control, empty-array guard, too-large-array guard, first-item-validation atomic revert); total suite now 20/0/1 (skipped = the existing fork scaffold) Rust (charon-executor/batcher.rs): - `Batcher` struct, stateless planner. `plan(opps)` partitions by `chain_id`, chunks into groups of `≤ max_batch_size` (default 3), and omits single-item groups (they belong on the plain path) - `LiquidationBatch` — chain + ordered opps + summed net profit - `encode_calldata(batch, params)` ABI-encodes `batchExecute(LiquidationParams[])` via alloy `sol!` with a `BatchParams` struct pinned to the Solidity shape; selector test asserts lockstep - Six unit tests: single → no batch, same-chain grouping, cross-chain split, size-limit chunking, selector pin, param-length mismatch rejection
This was referenced Apr 23, 2026
Closed
Closed
Closed
Closed
Closed
Closed
solc 0.8.24 defaults to Shanghai codegen and emits PUSH0 (0x5f). BSC mainnet runs a pre-Shanghai EVM, so every deploy of CharonLiquidator (batchExecute included) faults at the first PUSH0 site. forge-test runs against revm which accepts PUSH0, so local green does not imply on-chain green — a hidden false signal. Add `evm_version = "paris"` to contracts/foundry.toml and strip the caret from every `pragma solidity ^0.8.24;` so a future solc patch cannot silently shift codegen under the same source tree. Same defect as #113/#114/#118/#119 in prior PRs, now closed at the foundry-config layer across all Solidity files on this branch. Closes #203 Closes #204
- Hoist unsafe_code=forbid, arithmetic_side_effects=deny, cast_possible_truncation=deny, unwrap_used=deny into [workspace.lints.rust] and [workspace.lints.clippy]. - Add thiserror to [workspace.dependencies] for the batcher error enum landing alongside this change. - Opt charon-executor in via [lints] workspace = true. - Reshape gas_cost_usd_cents to saturating/checked arithmetic so the arithmetic_side_effects lint passes without loosening behaviour. - Replace unwrap() on JoinHandle::join in the nonce concurrency test with expect() carrying a diagnostic message. Closes #211
- Introduce BatcherError (ParamLengthMismatch, BatchTooLarge, UnsupportedChain, AbiEncodeError) so the public batcher API no longer returns anyhow::Result. - Remove the cross-chain HashMap partitioning from Batcher::plan. v0.1 is BSC-only; the planner now rejects any opportunity whose chain_id is not 56 via BatcherError::UnsupportedChain. - Enforce the on-chain MAX_BATCH_SIZE ceiling (10) inside encode_calldata via a new SOLIDITY_MAX_BATCH_SIZE constant; prevents a misconfigured caller from signing a tx that reverts with "batch too large" on-chain. - Batcher::new now clamps the caller-supplied size to [1, SOLIDITY_MAX_BATCH_SIZE]. - Pin the batchExecute selector to an externally-computed keccak256 digest of the canonical Solidity signature; the earlier test was self-referential and would silently pass even if both the macro and the on-chain signature drifted together. - Document encode_calldata's Safety contract: callers MUST pass the returned calldata through Simulator::simulate before broadcast, per the CLAUDE.md eth_call gate invariant. Batch-path sim wiring is tracked in #298. - Drop the different_chains_produce_separate_batches test; add plan_rejects_non_bsc_chain_id, assert_single_chain, new_clamps_max_batch_size_to_solidity_cap, and encode_calldata_rejects_oversize_batch. Closes #206 Closes #207 Closes #208 Closes #209 Closes #210
- Add NatSpec on batchExecute stating the EVM-atomicity contract explicitly: any item reverting rolls the full batch back and BatchExecuted is emitted only on full-batch success. - Expand BatchExecuted NatSpec so observers know absence of the event equals no partial progress. - Add test_batchExecute_revertsOnSecondItemValidation: 2-item batch where item[0] is valid (flashLoanSimple mocked to no-op) and item[1].borrower == address(0). Asserts revert with "!borrower" and, via vm.recordLogs, that BatchExecuted was never emitted. Closes #212
Wraps Batcher::encode_calldata's output in an opaque UnsimulatedBatchCalldata newtype. The only promotion path is Batcher::simulate, which runs the buffer through Simulator::simulate and returns SimulatedBatchCalldata on success. A broadcaster written against this crate that accepts only SimulatedBatchCalldata cannot be handed raw encoder output by mistake, so the CLAUDE.md invariant "no broadcast without a passing eth_call" becomes a compile-time guarantee for the batch path (mirroring the UnverifiedPreSigned guard on the mempool pre-sign path). BatcherError gains a SimulationFailed variant carrying the node's revert string. Foundry-side batchExecute fork test stays on feat/25 / PR #53. Closes #298
…cher # Conflicts: # Cargo.lock # Cargo.toml # contracts/foundry.toml # contracts/src/CharonLiquidator.sol # crates/charon-executor/Cargo.toml # crates/charon-executor/src/gas.rs # crates/charon-executor/src/lib.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #18
Adds per-chain batching so multiple liquidatable positions land in one transaction instead of N. Base tx cost (21 000 gas + signature + calldata header) amortises across the whole batch; each flash-loan still pays its own 0.05% premium individually.
On-chain —
CharonLiquidator.sol:batchExecute(LiquidationParams[] calldata items)entrypoint —onlyOwner+nonReentrant, capped atMAX_BATCH_SIZE = 10flashLoanSimplekickoff moved fromexecuteLiquidationinto internal_initiateFlashLoan. Both entrypoints now share one validated flowBatchExecuted(uint256 count)after the loopnonReentrantstays onbatchExecute;_initiateFlashLoanis internal so the guard is held across iterations without deadlockRust —
charon-executor/batcher.rs:Batcherstateless planner.plan(opps)partitions bychain_id, chunks into groups of≤ max_batch_size(default 3), omits single-item groupsLiquidationBatch— chain + ordered opps + summed net profitencode_calldata(batch, params)ABI-encodesbatchExecute(LiquidationParams[])viaalloy::sol!; selector test asserts lockstep with Solidity structDepends on
feat/19-private-rpc-submit.