Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions crates/charon-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2349,6 +2349,44 @@ async fn process_opportunity(
sim,
provider: pipeline.provider.as_ref(),
};

// Issue #402: probe `eth_getCode` at the liquidator address before
// the simulation gate. If empty, label the drop
// `liquidator_not_deployed` and return early; otherwise fall
// through to `encode_and_simulate` whose Err arm now genuinely
// means the liquidation logic reverted (`sim_revert`).
match pipeline
.provider
.as_ref()
.get_code_at(pipeline.liquidator)
.block_id(alloy::eips::BlockId::latest())
.await
{
Ok(code) if code.is_empty() => {
charon_metrics::record_opportunity_dropped(chain, drop_stage::SIMULATION);
charon_metrics::record_opportunity_dropped_reason(
chain,
drop_reason::LIQUIDATOR_NOT_DEPLOYED,
);
warn!(
chain,
liquidator = %pipeline.liquidator,
borrower = %pos.borrower,
"liquidator contract has empty bytecode at head — opportunity dropped"
);
return Ok(false);
}
Ok(_) => {}
Err(err) => {
debug!(
chain,
liquidator = %pipeline.liquidator,
error = ?err,
"liquidator bytecode probe failed; deferring to simulator"
);
}
}

let sim_block = pipeline
.replay_block
.map(BlockNumberOrTag::Number)
Expand Down
13 changes: 13 additions & 0 deletions crates/charon-metrics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,14 @@ pub mod drop_reason {
/// opportunity passed every earlier gate but the broadcast
/// stage rejected it.
pub const SUBMIT_FAILED: &str = "submit_failed";
/// Simulation reverted with the well-known "no code at address"
/// payload — the configured CharonLiquidator address has empty
/// bytecode on the connected chain, distinct from the liquidation
/// logic itself reverting (#402). The startup gate added in
/// #399 catches this at boot for chains with a router config;
/// this counter covers the runtime self-destruct edge case and
/// any scan-only / replay path that bypasses the startup gate.
pub const LIQUIDATOR_NOT_DEPLOYED: &str = "liquidator_not_deployed";
}

/// Issue #397: outcome label on `EXECUTOR_BROADCASTS_TOTAL`. Mirrors
Expand Down Expand Up @@ -1034,6 +1042,10 @@ mod tests {
assert_eq!(drop_reason::GAS_CEILING, "gas_ceiling");
assert_eq!(drop_reason::TTL_EXPIRED, "ttl_expired");
assert_eq!(drop_reason::SUBMIT_FAILED, "submit_failed");
assert_eq!(
drop_reason::LIQUIDATOR_NOT_DEPLOYED,
"liquidator_not_deployed"
);
}

/// Issue #397: pin the public surface of the broadcast / inclusion
Expand Down Expand Up @@ -1088,6 +1100,7 @@ mod tests {
record_opportunity_dropped_reason("bnb", drop_reason::GAS_CEILING);
record_opportunity_dropped_reason("bnb", drop_reason::TTL_EXPIRED);
record_opportunity_dropped_reason("bnb", drop_reason::SUBMIT_FAILED);
record_opportunity_dropped_reason("bnb", drop_reason::LIQUIDATOR_NOT_DEPLOYED);
// Issue #397: broadcast / inclusion / realised-profit
record_broadcast("bnb", broadcast_result::SUBMITTED);
record_broadcast("bnb", broadcast_result::TIMEOUT);
Expand Down
39 changes: 38 additions & 1 deletion deploy/grafana/charon.json
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@
{
"datasource": { "type": "prometheus", "uid": "${datasource}" },
"editorMode": "code",
"expr": "sum by (stage) (rate(charon_executor_opportunities_dropped_total{instance=~\"$instance\",chain=~\"$chain\",job=~\"$job\"}[$__rate_interval])) * 60",
"expr": "sum by (stage) (rate(charon_executor_opportunities_dropped_total{instance=~\"$instance\",chain=~\"$chain\",job=~\"$job\",stage!=\"\"}[$__rate_interval])) * 60",
"legendFormat": "{{stage}}",
"range": true,
"refId": "B"
Expand All @@ -404,6 +404,43 @@
"title": "Executor — opportunities queued vs dropped",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "${datasource}" },
"description": "Drops broken out by root-cause reason label (issue #368 + #402). Distinguishes 'sim_revert' (the liquidation logic reverted) from 'liquidator_not_deployed' (no code at the configured CharonLiquidator address) so a misconfigured fork or a runtime self-destruct surfaces as its own series rather than collapsing into a generic SIM_REVERT.",
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisLabel": "opps / min",
"drawStyle": "bars",
"fillOpacity": 80,
"lineWidth": 1,
"showPoints": "never",
"stacking": { "group": "A", "mode": "normal" }
},
"unit": "short"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 },
"id": 12,
"options": {
"legend": { "displayMode": "list", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"datasource": { "type": "prometheus", "uid": "${datasource}" },
"editorMode": "code",
"expr": "sum by (reason) (rate(charon_opportunities_dropped_total{instance=~\"$instance\",chain=~\"$chain\",job=~\"$job\"}[$__rate_interval])) * 60",
"legendFormat": "{{reason}}",
"range": true,
"refId": "A"
}
],
"title": "Executor — drops by reason",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "${datasource}" },
"description": "Distribution of per-opportunity net profit (USD cents). Heat-map uses the underlying histogram buckets.",
Expand Down
Loading