6
6
ChainId ,
7
7
} ,
8
8
chain:: {
9
+ eth_gas_oracle:: eip1559_default_estimator,
9
10
ethereum:: {
10
11
InstrumentedPythContract ,
11
12
InstrumentedSignablePythContract ,
@@ -84,6 +85,8 @@ const POLL_INTERVAL: Duration = Duration::from_secs(2);
84
85
const TRACK_INTERVAL : Duration = Duration :: from_secs ( 10 ) ;
85
86
/// Check whether we need to conduct a withdrawal at this interval.
86
87
const WITHDRAW_INTERVAL : Duration = Duration :: from_secs ( 300 ) ;
88
+ /// Check whether we need to adjust the fee at this interval.
89
+ const ADJUST_FEE_INTERVAL : Duration = Duration :: from_secs ( 30 ) ;
87
90
/// Rety last N blocks
88
91
const RETRY_PREVIOUS_BLOCKS : u64 = 100 ;
89
92
@@ -99,6 +102,7 @@ pub struct KeeperMetrics {
99
102
pub end_sequence_number : Family < AccountLabel , Gauge > ,
100
103
pub balance : Family < AccountLabel , Gauge < f64 , AtomicU64 > > ,
101
104
pub collected_fee : Family < AccountLabel , Gauge < f64 , AtomicU64 > > ,
105
+ pub current_fee : Family < AccountLabel , Gauge < f64 , AtomicU64 > > ,
102
106
pub total_gas_spent : Family < AccountLabel , Gauge < f64 , AtomicU64 > > ,
103
107
pub requests : Family < AccountLabel , Counter > ,
104
108
pub requests_processed : Family < AccountLabel , Counter > ,
@@ -153,6 +157,12 @@ impl KeeperMetrics {
153
157
keeper_metrics. collected_fee . clone ( ) ,
154
158
) ;
155
159
160
+ writable_registry. register (
161
+ "current_fee" ,
162
+ "Current fee charged by the provider" ,
163
+ keeper_metrics. current_fee . clone ( ) ,
164
+ ) ;
165
+
156
166
writable_registry. register (
157
167
"total_gas_spent" ,
158
168
"Total gas spent revealing requests" ,
@@ -288,6 +298,23 @@ pub async fn run_keeper_threads(
288
298
. in_current_span ( ) ,
289
299
) ;
290
300
301
+ // Spawn a thread that periodically adjusts the provider fee.
302
+ spawn (
303
+ adjust_fee_wrapper (
304
+ contract. clone ( ) ,
305
+ chain_state. provider_address . clone ( ) ,
306
+ ADJUST_FEE_INTERVAL ,
307
+ chain_eth_config. legacy_tx ,
308
+ chain_eth_config. gas_limit ,
309
+ chain_eth_config. min_profit_pct ,
310
+ chain_eth_config. target_profit_pct ,
311
+ chain_eth_config. max_profit_pct ,
312
+ chain_eth_config. fee ,
313
+ )
314
+ . in_current_span ( ) ,
315
+ ) ;
316
+
317
+
291
318
// Spawn a thread to track the provider info and the balance of the keeper
292
319
spawn (
293
320
async move {
@@ -841,6 +868,7 @@ pub async fn track_provider(
841
868
// The f64 conversion is made to be able to serve metrics with the constraints of Prometheus.
842
869
// The fee is in wei, so we divide by 1e18 to convert it to eth.
843
870
let collected_fee = provider_info. accrued_fees_in_wei as f64 / 1e18 ;
871
+ let current_fee: f64 = provider_info. fee_in_wei as f64 / 1e18 ;
844
872
845
873
let current_sequence_number = provider_info. sequence_number ;
846
874
let end_sequence_number = provider_info. end_sequence_number ;
@@ -853,6 +881,14 @@ pub async fn track_provider(
853
881
} )
854
882
. set ( collected_fee) ;
855
883
884
+ metrics
885
+ . current_fee
886
+ . get_or_create ( & AccountLabel {
887
+ chain_id : chain_id. clone ( ) ,
888
+ address : provider_address. to_string ( ) ,
889
+ } )
890
+ . set ( current_fee) ;
891
+
856
892
metrics
857
893
. current_sequence_number
858
894
. get_or_create ( & AccountLabel {
@@ -940,3 +976,205 @@ pub async fn withdraw_fees_if_necessary(
940
976
941
977
Ok ( ( ) )
942
978
}
979
+
980
+ #[ tracing:: instrument( name = "adjust_fee" , skip_all) ]
981
+ pub async fn adjust_fee_wrapper (
982
+ contract : Arc < InstrumentedSignablePythContract > ,
983
+ provider_address : Address ,
984
+ poll_interval : Duration ,
985
+ legacy_tx : bool ,
986
+ gas_limit : u64 ,
987
+ min_profit_pct : u64 ,
988
+ target_profit_pct : u64 ,
989
+ max_profit_pct : u64 ,
990
+ min_fee_wei : u128 ,
991
+ ) {
992
+ // The maximum balance of accrued fees + provider wallet balance. None if we haven't observed a value yet.
993
+ let mut high_water_pnl: Option < U256 > = None ;
994
+ // The sequence number where the keeper last updated the on-chain fee. None if we haven't observed it yet.
995
+ let mut sequence_number_of_last_fee_update: Option < u64 > = None ;
996
+ loop {
997
+ if let Err ( e) = adjust_fee_if_necessary (
998
+ contract. clone ( ) ,
999
+ provider_address,
1000
+ legacy_tx,
1001
+ gas_limit,
1002
+ min_profit_pct,
1003
+ target_profit_pct,
1004
+ max_profit_pct,
1005
+ min_fee_wei,
1006
+ & mut high_water_pnl,
1007
+ & mut sequence_number_of_last_fee_update,
1008
+ )
1009
+ . in_current_span ( )
1010
+ . await
1011
+ {
1012
+ tracing:: error!( "Withdrawing fees. error: {:?}" , e) ;
1013
+ }
1014
+ time:: sleep ( poll_interval) . await ;
1015
+ }
1016
+ }
1017
+
1018
+ /// Adjust the fee charged by the provider to ensure that it is profitable at the prevailing gas price.
1019
+ /// This method targets a fee as a function of the maximum cost of the callback,
1020
+ /// c = (gas_limit) * (current gas price), with min_fee_wei as a lower bound on the fee.
1021
+ ///
1022
+ /// The method then updates the on-chain fee if all of the following are satisfied:
1023
+ /// - the on-chain fee does not fall into an interval [c*min_profit, c*max_profit]. The tolerance
1024
+ /// factor prevents the on-chain fee from changing with every single gas price fluctuation.
1025
+ /// Profit scalars are specified in percentage units, min_profit = (min_profit_pct + 100) / 100
1026
+ /// - either the fee is increasing or the keeper is earning a profit -- i.e., fees only decrease when the keeper is profitable
1027
+ /// - at least one random number has been requested since the last fee update
1028
+ ///
1029
+ /// These conditions are intended to make sure that the keeper is profitable while also minimizing the number of fee
1030
+ /// update transactions.
1031
+ pub async fn adjust_fee_if_necessary (
1032
+ contract : Arc < InstrumentedSignablePythContract > ,
1033
+ provider_address : Address ,
1034
+ legacy_tx : bool ,
1035
+ gas_limit : u64 ,
1036
+ min_profit_pct : u64 ,
1037
+ target_profit_pct : u64 ,
1038
+ max_profit_pct : u64 ,
1039
+ min_fee_wei : u128 ,
1040
+ high_water_pnl : & mut Option < U256 > ,
1041
+ sequence_number_of_last_fee_update : & mut Option < u64 > ,
1042
+ ) -> Result < ( ) > {
1043
+ let provider_info = contract
1044
+ . get_provider_info ( provider_address)
1045
+ . call ( )
1046
+ . await
1047
+ . map_err ( |e| anyhow ! ( "Error while getting provider info. error: {:?}" , e) ) ?;
1048
+
1049
+ if provider_info. fee_manager != contract. wallet ( ) . address ( ) {
1050
+ return Err ( anyhow ! ( "Fee manager for provider {:?} is not the keeper wallet. Fee manager: {:?} Keeper: {:?}" , contract. provider( ) , provider_info. fee_manager, contract. wallet( ) . address( ) ) ) ;
1051
+ }
1052
+
1053
+ // Calculate target window for the on-chain fee.
1054
+ let max_callback_cost: u128 = estimate_tx_cost ( contract. clone ( ) , legacy_tx, gas_limit. into ( ) )
1055
+ . await
1056
+ . map_err ( |e| anyhow ! ( "Could not estimate transaction cost. error {:?}" , e) ) ?;
1057
+ let target_fee_min = std:: cmp:: max (
1058
+ ( max_callback_cost * ( 100 + u128:: from ( min_profit_pct) ) ) / 100 ,
1059
+ min_fee_wei,
1060
+ ) ;
1061
+ let target_fee = std:: cmp:: max (
1062
+ ( max_callback_cost * ( 100 + u128:: from ( target_profit_pct) ) ) / 100 ,
1063
+ min_fee_wei,
1064
+ ) ;
1065
+ let target_fee_max = std:: cmp:: max (
1066
+ ( max_callback_cost * ( 100 + u128:: from ( max_profit_pct) ) ) / 100 ,
1067
+ min_fee_wei,
1068
+ ) ;
1069
+
1070
+ // Calculate current P&L to determine if we can reduce fees.
1071
+ let current_keeper_balance = contract
1072
+ . provider ( )
1073
+ . get_balance ( contract. wallet ( ) . address ( ) , None )
1074
+ . await
1075
+ . map_err ( |e| anyhow ! ( "Error while getting balance. error: {:?}" , e) ) ?;
1076
+ let current_keeper_fees = U256 :: from ( provider_info. accrued_fees_in_wei ) ;
1077
+ let current_pnl = current_keeper_balance + current_keeper_fees;
1078
+
1079
+ let can_reduce_fees = match high_water_pnl {
1080
+ Some ( x) => current_pnl >= * x,
1081
+ None => false ,
1082
+ } ;
1083
+
1084
+ // Determine if the chain has seen activity since the last fee update.
1085
+ let is_chain_active: bool = match sequence_number_of_last_fee_update {
1086
+ Some ( n) => provider_info. sequence_number > * n,
1087
+ None => {
1088
+ // We don't want to adjust the fees on server start for unused chains, hence false here.
1089
+ false
1090
+ }
1091
+ } ;
1092
+
1093
+ let provider_fee: u128 = provider_info. fee_in_wei ;
1094
+ if is_chain_active
1095
+ && ( ( provider_fee > target_fee_max && can_reduce_fees) || provider_fee < target_fee_min)
1096
+ {
1097
+ tracing:: info!(
1098
+ "Adjusting fees. Current: {:?} Target: {:?}" ,
1099
+ provider_fee,
1100
+ target_fee
1101
+ ) ;
1102
+ let contract_call = contract. set_provider_fee_as_fee_manager ( provider_address, target_fee) ;
1103
+ let pending_tx = contract_call
1104
+ . send ( )
1105
+ . await
1106
+ . map_err ( |e| anyhow ! ( "Error submitting the set fee transaction: {:?}" , e) ) ?;
1107
+
1108
+ let tx_result = pending_tx
1109
+ . await
1110
+ . map_err ( |e| anyhow ! ( "Error waiting for set fee transaction receipt: {:?}" , e) ) ?
1111
+ . ok_or_else ( || {
1112
+ anyhow ! ( "Can't verify the set fee transaction, probably dropped from mempool" )
1113
+ } ) ?;
1114
+
1115
+ tracing:: info!(
1116
+ transaction_hash = & tx_result. transaction_hash. to_string( ) ,
1117
+ "Set provider fee. Receipt: {:?}" ,
1118
+ tx_result,
1119
+ ) ;
1120
+
1121
+ * sequence_number_of_last_fee_update = Some ( provider_info. sequence_number ) ;
1122
+ } else {
1123
+ tracing:: info!(
1124
+ "Skipping fee adjustment. Current: {:?} Target: {:?} [{:?}, {:?}] Current Sequence Number: {:?} Last updated sequence number {:?} Current pnl: {:?} High water pnl: {:?}" ,
1125
+ provider_fee,
1126
+ target_fee,
1127
+ target_fee_min,
1128
+ target_fee_max,
1129
+ provider_info. sequence_number,
1130
+ sequence_number_of_last_fee_update,
1131
+ current_pnl,
1132
+ high_water_pnl
1133
+ )
1134
+ }
1135
+
1136
+ // Update high water pnl
1137
+ * high_water_pnl = Some ( std:: cmp:: max (
1138
+ current_pnl,
1139
+ high_water_pnl. unwrap_or ( U256 :: from ( 0 ) ) ,
1140
+ ) ) ;
1141
+
1142
+ // Update sequence number on server start.
1143
+ match sequence_number_of_last_fee_update {
1144
+ Some ( _) => ( ) ,
1145
+ None => {
1146
+ * sequence_number_of_last_fee_update = Some ( provider_info. sequence_number ) ;
1147
+ }
1148
+ } ;
1149
+
1150
+
1151
+ Ok ( ( ) )
1152
+ }
1153
+
1154
+ /// Estimate the cost (in wei) of a transaction consuming gas_used gas.
1155
+ pub async fn estimate_tx_cost (
1156
+ contract : Arc < InstrumentedSignablePythContract > ,
1157
+ use_legacy_tx : bool ,
1158
+ gas_used : u128 ,
1159
+ ) -> Result < u128 > {
1160
+ let middleware = contract. client ( ) ;
1161
+
1162
+ let gas_price: u128 = if use_legacy_tx {
1163
+ middleware
1164
+ . get_gas_price ( )
1165
+ . await
1166
+ . map_err ( |e| anyhow ! ( "Failed to fetch gas price. error: {:?}" , e) ) ?
1167
+ . try_into ( )
1168
+ . map_err ( |e| anyhow ! ( "gas price doesn't fit into 128 bits. error: {:?}" , e) ) ?
1169
+ } else {
1170
+ let ( max_fee_per_gas, max_priority_fee_per_gas) = middleware
1171
+ . estimate_eip1559_fees ( Some ( eip1559_default_estimator) )
1172
+ . await ?;
1173
+
1174
+ ( max_fee_per_gas + max_priority_fee_per_gas)
1175
+ . try_into ( )
1176
+ . map_err ( |e| anyhow ! ( "gas price doesn't fit into 128 bits. error: {:?}" , e) ) ?
1177
+ } ;
1178
+
1179
+ Ok ( gas_price * gas_used)
1180
+ }
0 commit comments