Skip to content

Commit eb071ad

Browse files
authored
fix(CA): using the bundled simulation for allowance and bridging transactions (#919)
1 parent dcae08b commit eb071ad

File tree

5 files changed

+144
-108
lines changed

5 files changed

+144
-108
lines changed

src/error.rs

+14
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,12 @@ pub enum RpcError {
247247

248248
#[error("Bridging final amount is less then expected")]
249249
BridgingFinalAmountLess,
250+
251+
#[error("Simulation provider unavailable")]
252+
SimulationProviderUnavailable,
253+
254+
#[error("Simulation failed: {0}")]
255+
SimulationFailed(String),
250256
}
251257

252258
impl IntoResponse for RpcError {
@@ -623,6 +629,14 @@ impl IntoResponse for RpcError {
623629
)),
624630
)
625631
.into_response(),
632+
Self::SimulationProviderUnavailable => (
633+
StatusCode::SERVICE_UNAVAILABLE,
634+
Json(new_error_response(
635+
"".to_string(),
636+
"Simulation provider is temporarily unavailable".to_string(),
637+
)),
638+
)
639+
.into_response(),
626640
Self::CosignerPermissionDenied(e) => (
627641
StatusCode::UNAUTHORIZED,
628642
Json(new_error_response(

src/handlers/chain_agnostic/mod.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,13 @@ pub async fn get_assets_changes_from_simulation(
287287
{
288288
if asset_changed.asset_type.clone() == AssetChangeType::Transfer
289289
&& asset_changed.token_info.standard.clone() == TokenStandard::Erc20
290+
&& asset_changed.to.is_some()
290291
{
291292
asset_changes.push(Erc20AssetChange {
292293
chain_id: transaction.chain_id.clone(),
293294
asset_contract: asset_changed.token_info.contract_address,
294295
amount: asset_changed.raw_amount,
295-
receiver: asset_changed.to,
296+
receiver: asset_changed.to.unwrap_or_default(),
296297
})
297298
}
298299
}

src/handlers/chain_agnostic/route.rs

+22-104
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use {
1515
utils::{
1616
crypto::{
1717
convert_alloy_address_to_h160, decode_erc20_transfer_data, get_erc20_balance,
18-
get_gas_estimate, get_nonce, Erc20FunctionType,
18+
get_nonce, Erc20FunctionType,
1919
},
2020
network,
2121
},
@@ -29,6 +29,7 @@ use {
2929
hyper::HeaderMap,
3030
serde::{Deserialize, Serialize},
3131
std::{
32+
collections::HashMap,
3233
net::SocketAddr,
3334
str::FromStr,
3435
sync::Arc,
@@ -48,7 +49,7 @@ use {
4849
};
4950

5051
// Slippage for the gas estimation
51-
const ESTIMATED_GAS_SLIPPAGE: i8 = 100; // 100%, x2 slippage to cover the volatility
52+
const ESTIMATED_GAS_SLIPPAGE: i8 = 20; // 20% slippage to cover the volatility
5253

5354
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5455
#[serde(rename_all = "camelCase")]
@@ -410,7 +411,7 @@ async fn handler_internal(
410411
)
411412
.await?;
412413

413-
let mut approval_transaction = Transaction {
414+
let approval_transaction = Transaction {
414415
from: approval_tx.from,
415416
to: approval_tx.to,
416417
value: U256::ZERO,
@@ -419,70 +420,14 @@ async fn handler_internal(
419420
nonce: current_nonce,
420421
chain_id: format!("eip155:{}", bridge_tx.chain_id),
421422
};
422-
423-
// Estimate (or get cached) the gas for the approval transaction above and multiply by the slippage
424-
let approval_gas_limit = match state
425-
.providers
426-
.simulation_provider
427-
.clone()
428-
.get_cached_gas_estimation(
429-
&approval_transaction.chain_id,
430-
approval_transaction.to,
431-
Some(Erc20FunctionType::Approve),
432-
)
433-
.await?
434-
{
435-
Some(cached_gas) => cached_gas,
436-
None => {
437-
let estimated_gas_used = get_gas_estimate(
438-
&approval_transaction.chain_id,
439-
approval_transaction.from,
440-
approval_transaction.to,
441-
approval_transaction.value,
442-
approval_transaction.input.clone(),
443-
rpc_project_id.as_ref(),
444-
MessageSource::ChainAgnosticCheck,
445-
)
446-
.await?;
447-
// Save the approval tx gas estimation to the cache
448-
{
449-
let state = state.clone();
450-
let approval_tx_chain_id = approval_transaction.chain_id.clone();
451-
tokio::spawn(async move {
452-
state
453-
.providers
454-
.simulation_provider
455-
.clone()
456-
.set_cached_gas_estimation(
457-
&approval_tx_chain_id,
458-
approval_tx.to,
459-
Some(Erc20FunctionType::Approve),
460-
estimated_gas_used,
461-
)
462-
.await
463-
.unwrap_or_else(|e| {
464-
error!(
465-
"Failed to save the approval gas estimation to the cache: {}",
466-
e
467-
)
468-
});
469-
});
470-
}
471-
estimated_gas_used
472-
}
473-
};
474-
475-
// Updating the gas limit for the approval transaction
476-
approval_transaction.gas_limit =
477-
U64::from((approval_gas_limit * (100 + ESTIMATED_GAS_SLIPPAGE as u64)) / 100);
478423
routes.push(approval_transaction);
479424

480425
// Increment the nonce
481426
current_nonce += U64::from(1);
482427
}
483428
}
484429

485-
let mut bridging_transaction = Transaction {
430+
let bridging_transaction = Transaction {
486431
from: from_address,
487432
to: bridge_tx.tx_target,
488433
value: bridge_tx.value,
@@ -491,55 +436,28 @@ async fn handler_internal(
491436
nonce: current_nonce,
492437
chain_id: format!("eip155:{}", bridge_tx.chain_id),
493438
};
439+
routes.push(bridging_transaction);
494440

495-
// Estimate (or get cached) the gas for the bridging transaction above and multiply by the slippage
496-
let bridging_gas_limit = match state
441+
// Estimate the gas usage for the approval (if present) and bridging transactions
442+
// and update gas limits for transactions
443+
let simulation_results = state
497444
.providers
498445
.simulation_provider
499-
.clone()
500-
.get_cached_gas_estimation(&bridge_tx.chain_id.to_string(), bridge_tx.tx_target, None)
501-
.await?
502-
{
503-
Some(cached_gas) => cached_gas,
504-
None => {
505-
let (_, estimated_gas_used) = get_assets_changes_from_simulation(
506-
state.providers.simulation_provider.clone(),
507-
&bridging_transaction,
508-
state.metrics.clone(),
509-
)
510-
.await?;
511-
// Save the bridging tx gas estimation to the cache
512-
{
513-
let state = state.clone();
514-
let bridging_tx_chain_id = bridge_tx.chain_id.to_string().clone();
515-
tokio::spawn(async move {
516-
state
517-
.providers
518-
.simulation_provider
519-
.clone()
520-
.set_cached_gas_estimation(
521-
&bridging_tx_chain_id,
522-
bridge_tx.tx_target,
523-
None,
524-
estimated_gas_used,
525-
)
526-
.await
527-
.unwrap_or_else(|e| {
528-
error!(
529-
"Failed to save the bridging gas estimation to the cache: {}",
530-
e
531-
)
532-
});
533-
});
534-
}
535-
estimated_gas_used
446+
.simulate_bundled_transactions(routes.clone(), HashMap::new(), state.metrics.clone())
447+
.await?;
448+
for (index, simulation_result) in simulation_results.simulation_results.iter().enumerate() {
449+
// Making sure the nonce matches the transaction nonce
450+
if U64::from(simulation_result.transaction.nonce) != routes[index].nonce {
451+
return Err(RpcError::SimulationFailed(
452+
"The nonce for the simulation result does not match the nonce for the transaction"
453+
.into(),
454+
));
536455
}
537-
};
538456

539-
// Updating the gas limit for the bridging transaction
540-
bridging_transaction.gas_limit =
541-
U64::from((bridging_gas_limit * (100 + ESTIMATED_GAS_SLIPPAGE as u64)) / 100);
542-
routes.push(bridging_transaction);
457+
routes[index].gas_limit = U64::from(
458+
(simulation_result.transaction.gas_used * (100 + ESTIMATED_GAS_SLIPPAGE as u64)) / 100,
459+
);
460+
}
543461

544462
// Save the bridging transaction to the IRN
545463
let orchestration_id = Uuid::new_v4().to_string();

src/providers/mod.rs

+8
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ use {
4747
},
4848
tracing::{debug, error, log::warn},
4949
wc::metrics::TaskMetrics,
50+
yttrium::chain_abstraction::api::Transaction,
5051
};
5152

5253
mod arbitrum;
@@ -1028,6 +1029,13 @@ pub trait SimulationProvider: Send + Sync {
10281029
metrics: Arc<Metrics>,
10291030
) -> Result<tenderly::SimulationResponse, RpcError>;
10301031

1032+
async fn simulate_bundled_transactions(
1033+
&self,
1034+
transactions: Vec<Transaction>,
1035+
state_overrides: HashMap<Address, HashMap<B256, B256>>,
1036+
metrics: Arc<Metrics>,
1037+
) -> Result<tenderly::BundledSimulationResponse, RpcError>;
1038+
10311039
/// Get the cached gas estimation
10321040
/// for the token contract and chain_id
10331041
async fn get_cached_gas_estimation(

src/providers/tenderly.rs

+98-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use {
1313
serde::{Deserialize, Serialize},
1414
std::{collections::HashMap, sync::Arc, time::SystemTime},
1515
tracing::error,
16+
yttrium::chain_abstraction::api::Transaction,
1617
};
1718

1819
/// Gas estimation caching TTL paramters
@@ -26,7 +27,12 @@ pub struct SimulationRequest {
2627
pub input: Bytes,
2728
pub estimate_gas: bool,
2829
pub state_objects: HashMap<Address, StateStorage>,
29-
pub save: bool,
30+
pub save: bool, // Save the simulation to the dashboard
31+
}
32+
33+
#[derive(Debug, Deserialize, Serialize, Clone)]
34+
pub struct BundledSimulationRequests {
35+
pub simulations: Vec<SimulationRequest>,
3036
}
3137

3238
#[derive(Debug, Deserialize, Serialize, Clone)]
@@ -39,10 +45,18 @@ pub struct SimulationResponse {
3945
pub transaction: ResponseTransaction,
4046
}
4147

48+
#[derive(Debug, Deserialize, Serialize, Clone)]
49+
pub struct BundledSimulationResponse {
50+
pub simulation_results: Vec<SimulationResponse>,
51+
}
52+
4253
#[derive(Debug, Deserialize, Serialize, Clone)]
4354
pub struct ResponseTransaction {
55+
pub hash: String,
4456
pub gas_used: u64,
4557
pub transaction_info: ResponseTransactionInfo,
58+
pub status: bool, // Was simulating transaction successful
59+
pub nonce: u64,
4660
}
4761

4862
#[derive(Debug, Deserialize, Serialize, Clone)]
@@ -55,7 +69,7 @@ pub struct AssetChange {
5569
#[serde(rename = "type")]
5670
pub asset_type: AssetChangeType,
5771
pub from: Address,
58-
pub to: Address,
72+
pub to: Option<Address>,
5973
pub raw_amount: U256,
6074
pub token_info: TokenInfo,
6175
}
@@ -228,10 +242,91 @@ impl SimulationProvider for TenderlyProvider {
228242
"Failed to get the transaction simulation response from Tenderly with status: {}",
229243
response.status()
230244
);
231-
return Err(RpcError::ConversionProviderError);
245+
return Err(RpcError::SimulationProviderUnavailable);
232246
}
233247
let response = response.json::<SimulationResponse>().await?;
234248

249+
// The transaction failed if the `status` field is false
250+
if !response.transaction.status {
251+
return Err(RpcError::SimulationFailed(format!(
252+
"Failed to simulate the transaction with Tenderly. Transaction hash: {}",
253+
response.transaction.hash
254+
)));
255+
}
256+
257+
Ok(response)
258+
}
259+
260+
#[tracing::instrument(skip(self), fields(provider = "Tenderly"), level = "debug")]
261+
async fn simulate_bundled_transactions(
262+
&self,
263+
transactions: Vec<Transaction>,
264+
state_overrides: HashMap<Address, HashMap<B256, B256>>,
265+
metrics: Arc<Metrics>,
266+
) -> Result<BundledSimulationResponse, RpcError> {
267+
let url = Url::parse(format!("{}/simulate-bundle", &self.base_api_url).as_str())
268+
.map_err(|_| RpcError::ConversionParseURLError)?;
269+
270+
let mut bundled_simulations = BundledSimulationRequests {
271+
simulations: vec![],
272+
};
273+
274+
for transaction in transactions {
275+
let (_, evm_chain_id) = disassemble_caip2(&transaction.chain_id)?;
276+
277+
// fill the state_objects with the state_overrides
278+
let mut state_objects: HashMap<Address, StateStorage> = HashMap::new();
279+
for (address, state) in state_overrides.clone() {
280+
let mut account_state = StateStorage {
281+
storage: HashMap::new(),
282+
};
283+
for (key, value) in state {
284+
account_state.storage.insert(key, value);
285+
}
286+
state_objects.insert(address, account_state);
287+
}
288+
289+
bundled_simulations.simulations.push(SimulationRequest {
290+
network_id: evm_chain_id,
291+
from: transaction.from,
292+
to: transaction.to,
293+
input: transaction.input,
294+
estimate_gas: true,
295+
state_objects,
296+
save: true,
297+
});
298+
}
299+
300+
let latency_start = SystemTime::now();
301+
let response = self.send_post_request(url, &bundled_simulations).await?;
302+
303+
metrics.add_latency_and_status_code_for_provider(
304+
self.provider_kind,
305+
response.status().into(),
306+
latency_start,
307+
None,
308+
Some("simulate_bundled".to_string()),
309+
);
310+
311+
if !response.status().is_success() {
312+
error!(
313+
"Failed to get the transactions bundled simulation response from Tenderly with status: {}",
314+
response.status()
315+
);
316+
return Err(RpcError::SimulationProviderUnavailable);
317+
}
318+
let response = response.json::<BundledSimulationResponse>().await?;
319+
320+
// Check for the status of each transaction
321+
for simulation in response.simulation_results.iter() {
322+
if !simulation.transaction.status {
323+
return Err(RpcError::SimulationFailed(format!(
324+
"Failed to simulate bundled transactions with Tenderly. Failed transaction hash: {}",
325+
simulation.transaction.hash
326+
)));
327+
}
328+
}
329+
235330
Ok(response)
236331
}
237332

0 commit comments

Comments
 (0)