penumbra_sdk_dex/component/
arb.rs1use std::sync::Arc;
2
3use crate::component::{metrics, StateReadExt};
4use anyhow::Result;
5use async_trait::async_trait;
6use cnidarium::{StateDelta, StateWrite};
7use penumbra_sdk_asset::{asset, Value};
8use penumbra_sdk_proto::{DomainType as _, StateWriteProto as _};
9use penumbra_sdk_sct::component::clock::EpochRead;
10use tracing::instrument;
11
12use crate::{
13 component::{ExecutionCircuitBreaker, InternalDexWrite, ValueCircuitBreaker},
14 event, SwapExecution,
15};
16
17use super::router::{RouteAndFill, RoutingParams};
18
19#[async_trait]
20pub trait Arbitrage: StateWrite + Sized {
21 #[instrument(skip(self, arb_token, routing_params))]
24 async fn arbitrage(
25 self: &mut Arc<Self>,
26 arb_token: asset::Id,
27 routing_params: RoutingParams,
28 ) -> Result<Option<Value>>
29 where
30 Self: 'static,
31 {
32 tracing::debug!(?arb_token, ?routing_params, "beginning arb search");
33 let arb_start = std::time::Instant::now();
34
35 let mut this = Arc::new(StateDelta::new(self.clone()));
39
40 let flash_loan = Value {
42 asset_id: arb_token,
43 amount: u64::MAX.into(),
44 };
45
46 let execution_budget = self.get_dex_params().await?.max_execution_budget;
47 let execution_circuit_breaker = ExecutionCircuitBreaker::new(execution_budget);
48 let Some(swap_execution) = this
49 .route_and_fill(
50 arb_token,
51 arb_token,
52 flash_loan.amount,
53 routing_params,
54 execution_circuit_breaker,
55 )
56 .await?
57 else {
58 return Ok(None);
59 };
60
61 let filled_input = swap_execution.input.amount;
62 let output = swap_execution.output.amount;
63 let unfilled_input = flash_loan
64 .amount
65 .checked_sub(&filled_input)
66 .expect("filled input should always be <= flash loan amount");
67
68 metrics::histogram!(metrics::DEX_ARB_DURATION).record(arb_start.elapsed());
72
73 let total_output = output + unfilled_input;
76
77 let Some(arb_profit) = total_output.checked_sub(&flash_loan.amount) else {
79 tracing::debug!("mis-estimation in route-and-fill led to unprofitable arb, discarding");
83 return Ok(None);
84 };
85
86 if arb_profit == 0u64.into() {
87 tracing::debug!("found 0-profit arb, discarding");
90 return Ok(None);
91 } else {
92 tracing::debug!(
93 ?filled_input,
94 ?output,
95 ?unfilled_input,
96 ?total_output,
97 ?arb_profit,
98 "arb search detected surplus!"
99 );
100 }
101
102 let (self2, cache) = Arc::try_unwrap(this)
105 .map_err(|_| ())
106 .expect("no more outstanding refs to state after routing")
107 .flatten();
108 std::mem::drop(self2);
109 let mut self_mut = Arc::get_mut(self).expect("self was unique ref");
111 cache.apply_to(&mut self_mut);
112
113 let height = self_mut.get_block_height().await?;
115 let se = SwapExecution {
116 traces: swap_execution.traces,
117 input: Value {
118 asset_id: arb_token,
119 amount: filled_input,
120 },
121 output: Value {
122 amount: filled_input + arb_profit,
123 asset_id: arb_token,
124 },
125 };
126 self_mut.set_arb_execution(height, se.clone());
127
128 self_mut
130 .dex_vcb_debit(Value {
131 amount: arb_profit,
132 asset_id: arb_token,
133 })
134 .await?;
135
136 self_mut.record_proto(
138 event::EventArbExecution {
139 height,
140 swap_execution: se,
141 }
142 .to_proto(),
143 );
144 return Ok(Some(Value {
145 amount: arb_profit,
146 asset_id: arb_token,
147 }));
148 }
149}
150
151impl<T: StateWrite> Arbitrage for T {}