From 431565f73fd075748eb6c980ff575597938eec13 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Thu, 10 Sep 2026 20:29:08 +0200 Subject: [PATCH] bridge-monitor: capture exec-cost metrics (realized output, quote-vs-realized slippage, our gas, refund split) --- benchmarks/bridge-execution-latency.yml | 1 + .../bridge-monitor/cmd/monitor/executor.go | 36 ++++++++++++++++++- .../bridge-monitor/cmd/monitor/metrics.go | 29 +++++++++++++++ .../cmd/monitor/nearintents_exec.go | 1 + .../bridge-monitor/cmd/monitor/tx_executor.go | 23 ++++++++++++ 5 files changed, 89 insertions(+), 1 deletion(-) diff --git a/benchmarks/bridge-execution-latency.yml b/benchmarks/bridge-execution-latency.yml index 07d7d9860..e856cb522 100644 --- a/benchmarks/bridge-execution-latency.yml +++ b/benchmarks/bridge-execution-latency.yml @@ -56,6 +56,7 @@ methodology: - "Outcome classification: settled (funds received), reverted or refunded (capital returned to source), or errored (never broadcast). Only settled transactions contribute to the latency distribution; reverts and refunds count against the success rate." - "Region: EU-West only at present. The execution node runs a single wallet, so a second origin would race the same inventory; additional origins are not planned for the execution loop." - "Cohort: Mobula, Relay and LI.FI. deBridge is quote-only in this loop. Providers without a symmetric route on all three legs are excluded so the triangle conserves." + - "Cost metrics captured per real execution (beyond latency and success): realized output on-chain, execution slippage vs the quote (realized fee minus quote-projected fee), our own on-chain gas paid (approve + deposit, measured as source-chain native balance delta), and the refund rate split from hard-fail rate. These surface the true all-in cost and the gap between a quote and what actually settles." findings: - "{{name:mobula}} settles at {{p50:mobula}} (p50, broadcast to funds received) with a {{success:mobula}} success rate over the last 24 hours." diff --git a/harnesses/bridge-monitor/cmd/monitor/executor.go b/harnesses/bridge-monitor/cmd/monitor/executor.go index 9a09e271c..afd506c31 100644 --- a/harnesses/bridge-monitor/cmd/monitor/executor.go +++ b/harnesses/bridge-monitor/cmd/monitor/executor.go @@ -41,6 +41,7 @@ type ExecutionResult struct { E2ELatencyMs int64 // Time from quote start to funds received Success bool Reverted bool + Refunded bool // subset of Reverted: provider returned capital (status "refunded") Error error QuoteFeeUSD float64 // Fee from quote ActualFeeUSD float64 // Actual fee paid (input - output) @@ -55,6 +56,7 @@ type ExecutionResult struct { FeesPercent float64 CostUSD float64 OutputUSD float64 // What landed on destination (the fill) + ExecGasUSD float64 // Our on-chain gas paid (approve + deposit), source native delta } // Executor handles the execution loop @@ -374,6 +376,15 @@ func (e *Executor) executeOnBridge(bridge string, route TestRoute, amount, amoun log.Printf(" ⚠️ pre-execution balance read failed (%v) — falling back to quote-projected fill", preBalErr) } + // Source-chain native balance before execution, to measure the real gas we + // pay (approve + deposit) as the pre-minus-post delta. Single-flight (execMu) + // guarantees no other tx moves it during this one leg. + srcOwner := e.walletManager.EVMAddress + if route.FromChain == "Solana" { + srcOwner = e.walletManager.SolanaAddress + } + preNativeUSD, preNativeErr := e.txExecutor.nativeBalanceUSD(route.FromChain, srcOwner) + // PHASE 1: Get quote with TX data quoteStart := time.Now() var txHash string @@ -434,6 +445,15 @@ func (e *Executor) executeOnBridge(bridge string, route TestRoute, amount, amoun } } + // Real gas we paid this leg = source native balance delta (approve + deposit). + if preNativeErr == nil { + if postNativeUSD, nerr := e.txExecutor.nativeBalanceUSD(route.FromChain, srcOwner); nerr == nil { + if g := preNativeUSD - postNativeUSD; g > 0 { + result.ExecGasUSD = g + } + } + } + result.FeesUSD = result.ActualFeeUSD if amountUSD > 0 { result.FeesPercent = (result.ActualFeeUSD / amountUSD) * 100 @@ -618,6 +638,7 @@ func (e *Executor) executeMobula(route TestRoute, amount float64, quoteStart tim result.ActualFeeUSD = result.QuoteFeeUSD } else if status.Status == "refunded" { result.Reverted = true + result.Refunded = true log.Printf(" [mobula] ⚠️ Transaction was refunded!") } @@ -755,6 +776,7 @@ func (e *Executor) executeRelay(route TestRoute, rawUnits string, quoteStart tim result.ActualFeeUSD = result.QuoteFeeUSD } else if status.Status == "refunded" { result.Reverted = true + result.Refunded = true log.Printf(" [relay] ⚠️ Transaction was refunded!") } @@ -886,6 +908,7 @@ func (e *Executor) executeLiFi(route TestRoute, rawUnits string, quoteStart time result.ActualFeeUSD = result.QuoteFeeUSD } else if status.Status == "refunded" || status.Status == "failed" { result.Reverted = true + result.Refunded = status.Status == "refunded" log.Printf(" [lifi] ⚠️ Transaction was refunded/failed!") } @@ -925,6 +948,9 @@ func (e *Executor) recordExecutionMetrics(result *ExecutionResult) { bridgeReverts.WithLabelValues(labels...).Inc() bridgeConsecutiveFailures.WithLabelValues(result.Bridge, e.region).Inc() } + if result.Refunded { + bridgeRefunds.WithLabelValues(labels...).Inc() + } if result.Error != nil { bridgeErrors.WithLabelValues(append(labels, "execution_failed")...).Inc() if !result.Reverted { @@ -932,11 +958,19 @@ func (e *Executor) recordExecutionMetrics(result *ExecutionResult) { } } - // Record fees + // Record fees + the new execution-cost metrics bridgeFeesUSD.WithLabelValues(labels...).Set(result.ActualFeeUSD) if result.AmountUSD > 0 { bridgeFeesPercent.WithLabelValues(labels...).Set((result.ActualFeeUSD / result.AmountUSD) * 100) } + if result.OutputUSD > 0 { + bridgeRealizedOutputUSD.WithLabelValues(labels...).Set(result.OutputUSD) + } + // Execution slippage vs quote = realized fee - quote-projected fee. + bridgeQuoteSlippageUSD.WithLabelValues(labels...).Set(result.ActualFeeUSD - result.QuoteFeeUSD) + if result.ExecGasUSD > 0 { + bridgeExecGasUSD.WithLabelValues(labels...).Set(result.ExecGasUSD) + } } // getSourceTokenName returns the source token name for balance checking (legacy - fallback) diff --git a/harnesses/bridge-monitor/cmd/monitor/metrics.go b/harnesses/bridge-monitor/cmd/monitor/metrics.go index dca778afe..3f45b1c18 100644 --- a/harnesses/bridge-monitor/cmd/monitor/metrics.go +++ b/harnesses/bridge-monitor/cmd/monitor/metrics.go @@ -57,6 +57,35 @@ var ( Help: "Total number of reverted/refunded bridge transactions", }, []string{"bridge", "from_chain", "to_chain", "from_token", "to_token", "amount_usd", "region", "chain"}) + // Refund counter: subset of reverts where the bridge returned capital to + // source (status "refunded") rather than an on-chain revert. Lets us split + // refund-rate from hard-fail-rate (reverts minus refunds). + bridgeRefunds = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "bridge_refunds_total", + Help: "Bridge transactions the provider refunded (capital returned to source)", + }, []string{"bridge", "from_chain", "to_chain", "from_token", "to_token", "amount_usd", "region", "chain"}) + + // Realized output that actually landed on the destination (on-chain fill). + bridgeRealizedOutputUSD = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "bridge_realized_output_usd", + Help: "USD value that actually landed on the destination chain (realized fill)", + }, []string{"bridge", "from_chain", "to_chain", "from_token", "to_token", "amount_usd", "region", "chain"}) + + // Execution slippage vs the quote: realized fee minus quote-projected fee. + // Positive means the execution cost more than the quote promised. + bridgeQuoteSlippageUSD = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "bridge_quote_slippage_usd", + Help: "Realized fee minus quote-projected fee (execution cost above what the quote promised)", + }, []string{"bridge", "from_chain", "to_chain", "from_token", "to_token", "amount_usd", "region", "chain"}) + + // Our own on-chain gas cost for this execution (approve + deposit), measured + // as the source-chain native balance delta. This is the cost WE bear, on top + // of the bridge's own fee, for the true all-in cost. + bridgeExecGasUSD = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "bridge_exec_gas_usd", + Help: "On-chain gas we paid (approve + deposit), source-chain native balance delta in USD", + }, []string{"bridge", "from_chain", "to_chain", "from_token", "to_token", "amount_usd", "region", "chain"}) + // Error counter bridgeErrors = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "bridge_errors_total", diff --git a/harnesses/bridge-monitor/cmd/monitor/nearintents_exec.go b/harnesses/bridge-monitor/cmd/monitor/nearintents_exec.go index 39d342b50..6864a0606 100644 --- a/harnesses/bridge-monitor/cmd/monitor/nearintents_exec.go +++ b/harnesses/bridge-monitor/cmd/monitor/nearintents_exec.go @@ -303,6 +303,7 @@ func (e *Executor) executeNearIntents(route TestRoute, amountUSD float64, rawUni result.Success = true case "REFUNDED", "FAILED": result.Reverted = true + result.Refunded = status == "REFUNDED" default: // Timed out in PROCESSING/PENDING: terminal-ambiguous. Leave Success // false with the TxHash set so the caller treats it as in-flight. diff --git a/harnesses/bridge-monitor/cmd/monitor/tx_executor.go b/harnesses/bridge-monitor/cmd/monitor/tx_executor.go index 2dbf4b762..a79ecdf58 100644 --- a/harnesses/bridge-monitor/cmd/monitor/tx_executor.go +++ b/harnesses/bridge-monitor/cmd/monitor/tx_executor.go @@ -599,6 +599,29 @@ func (tx *TxExecutor) getRelayStatus(requestID string) (*BridgeStatus, error) { } // Close closes all connections + +// nativeBalanceUSD returns the native-token (ETH/SOL) balance of ownerAddr on +// chain, valued in USD. Used to measure the real gas we pay for an execution +// as the source-chain native balance delta (pre - post). +func (tx *TxExecutor) nativeBalanceUSD(chain, ownerAddr string) (float64, error) { + if strings.EqualFold(chain, "Solana") { + k, err := solana.PublicKeyFromBase58(ownerAddr) + if err != nil { + return 0, err + } + lamports, err := tx.solanaNativeBalance(k) + if err != nil { + return 0, err + } + return rawToFloat(lamports, 9) * TokenPriceUSD("SOL", 150), nil + } + wei, err := tx.evmNativeBalance(chain, common.HexToAddress(ownerAddr)) + if err != nil { + return 0, err + } + return rawToFloat(wei, 18) * TokenPriceUSD("ETH", 3600), nil +} + func (tx *TxExecutor) Close() { if tx.baseClient != nil { tx.baseClient.Close()