Skip to content

Commit eac7445

Browse files
test: add test for the token balances validation
1 parent 61ad26c commit eac7445

File tree

3 files changed

+156
-60
lines changed

3 files changed

+156
-60
lines changed

protocol-testing/src/rpc.rs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use alloy::{
66
providers::{Provider, ProviderBuilder},
77
transports::http::reqwest::Url,
88
};
9+
use miette::{IntoDiagnostic, WrapErr};
910

1011
const NATIVE_ALIASES: &[Address] = &[
1112
address!("0x0000000000000000000000000000000000000000"),
@@ -24,13 +25,12 @@ impl RPCProvider {
2425
RPCProvider { url }
2526
}
2627

27-
// TODO: Return a Result instead of panicking
2828
pub async fn get_token_balance(
2929
&self,
3030
token_address: Address,
3131
wallet_address: Address,
3232
block_number: u64,
33-
) -> U256 {
33+
) -> miette::Result<U256> {
3434
let provider = ProviderBuilder::new().connect_http(self.url.clone());
3535
let block_id: BlockId = BlockId::from(block_number);
3636

@@ -39,9 +39,12 @@ impl RPCProvider {
3939
.get_balance(wallet_address)
4040
.block_id(block_id)
4141
.await
42-
.expect("Failed to fetch token balance"),
42+
.into_diagnostic()
43+
.wrap_err("Failed to fetch token balance"),
4344
false => {
44-
let abi = serde_json::from_str(ERC_20_ABI).expect("invalid ABI");
45+
let abi = serde_json::from_str(ERC_20_ABI)
46+
.into_diagnostic()
47+
.wrap_err("invalid ABI")?;
4548

4649
let contract = ContractInstance::new(token_address, provider, Interface::new(abi));
4750

@@ -53,14 +56,15 @@ impl RPCProvider {
5356
.block(block_id)
5457
.call()
5558
.await
56-
.expect("Failed to fetch ERC-20 Balance");
59+
.into_diagnostic()
60+
.wrap_err("Failed to fetch ERC-20 Balance")?;
5761
let result: U256 = result_value
5862
.first()
59-
.unwrap()
63+
.ok_or_else(|| miette::miette!("No value returned from contract call"))?
6064
.as_uint()
61-
.unwrap()
65+
.ok_or_else(|| miette::miette!("Returned value is not a uint"))?
6266
.0;
63-
result
67+
Ok(result)
6468
}
6569
}
6670
}
@@ -93,7 +97,8 @@ mod tests {
9397

9498
let balance = rpc_provider
9599
.get_token_balance(token_address, wallet_address, block_number)
96-
.await;
100+
.await
101+
.unwrap();
97102

98103
assert_eq!(
99104
balance,
@@ -112,7 +117,8 @@ mod tests {
112117

113118
let balance = rpc_provider
114119
.get_token_balance(token_address, wallet_address, block_number)
115-
.await;
120+
.await
121+
.unwrap();
116122

117123
assert_eq!(balance, U256::from(717250938432_u64));
118124
}

protocol-testing/src/test_runner.rs

Lines changed: 139 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -254,58 +254,12 @@ fn validate_state(
254254
info!("All expected components were successfully found on Tycho and match the expected state");
255255

256256
// Step 2: Validate Token Balances
257-
// In this step, we validate that the token balances of the components match the values
258-
// on-chain, extracted by querying the token balances using a node.
259-
let rpc_url = env::var("RPC_URL")
260-
.into_diagnostic()
261-
.wrap_err("Missing ETH_RPC_URL in environment")?;
262-
let rpc_provider = RPCProvider::new(rpc_url.to_string());
263-
264-
for (id, component) in components_by_id.iter() {
265-
let component_state = protocol_states_by_id.get(id);
266-
267-
for token in &component.tokens {
268-
let mut balance: U256 = U256::from(0);
269-
270-
if let Some(state) = component_state {
271-
let bal = state.balances.get(token);
272-
if let Some(bal) = bal {
273-
let bal = bal.clone().into();
274-
balance = bytes_to_u256(bal);
275-
}
276-
}
277-
278-
// TODO: Test if balance check works
279-
if !skip_balance_check {
280-
info!(
281-
"Validating token balance for component {} and token {}",
282-
component.id, token
283-
);
284-
let token_address = alloy::primitives::Address::from_slice(&token[..20]);
285-
let component_address = alloy::primitives::Address::from_str(component.id.as_str())
286-
.expect("Failed to parse component address");
287-
let node_balance = rt.block_on(rpc_provider.get_token_balance(
288-
token_address,
289-
component_address,
290-
start_block,
291-
));
292-
if balance != node_balance {
293-
return Err(miette!(
294-
"Token balance mismatch for component {} and token {}",
295-
component.id,
296-
token
297-
))
298-
}
299-
info!(
300-
"Token balance for component {} and token {} matches the expected value",
301-
component.id, token
302-
);
303-
}
304-
}
305-
}
306257
match skip_balance_check {
307258
true => info!("Skipping balance check"),
308-
false => info!("All token balances match the values found onchain"),
259+
false => {
260+
validate_token_balances(&components_by_id, &protocol_states_by_id, start_block, &rt)?;
261+
info!("All token balances match the values found onchain")
262+
}
309263
}
310264

311265
// Step 3: Run Tycho Simulation
@@ -404,9 +358,68 @@ fn validate_state(
404358
Ok(())
405359
}
406360

361+
/// Validate that the token balances of the components match the values
362+
/// on-chain, extracted by querying the token balances using a node.
363+
fn validate_token_balances(
364+
components_by_id: &HashMap<String, ProtocolComponent>,
365+
protocol_states_by_id: &HashMap<String, ResponseProtocolState>,
366+
start_block: u64,
367+
rt: &Runtime,
368+
) -> miette::Result<()> {
369+
let rpc_url = env::var("RPC_URL")
370+
.into_diagnostic()
371+
.wrap_err("Missing RPC_URL in environment")?;
372+
let rpc_provider = RPCProvider::new(rpc_url.to_string());
373+
374+
for (id, component) in components_by_id.iter() {
375+
let component_state = protocol_states_by_id.get(id);
376+
377+
for token in &component.tokens {
378+
let mut balance: U256 = U256::from(0);
379+
380+
if let Some(state) = component_state {
381+
let bal = state.balances.get(token);
382+
if let Some(bal) = bal {
383+
let bal = bal.clone().into();
384+
balance = bytes_to_u256(bal);
385+
}
386+
}
387+
388+
info!("Validating token balance for component {} and token {}", component.id, token);
389+
let token_address = alloy::primitives::Address::from_slice(&token[..20]);
390+
let component_address = alloy::primitives::Address::from_str(component.id.as_str())
391+
.expect("Failed to parse component address");
392+
let node_balance = rt.block_on(rpc_provider.get_token_balance(
393+
token_address,
394+
component_address,
395+
start_block,
396+
))?;
397+
if balance != node_balance {
398+
return Err(miette!(
399+
"Token balance mismatch for component {} and token {}",
400+
component.id,
401+
token
402+
));
403+
}
404+
info!(
405+
"Token balance for component {} and token {} matches the expected value",
406+
component.id, token
407+
);
408+
}
409+
}
410+
Ok(())
411+
}
412+
407413
#[cfg(test)]
408414
mod tests {
415+
use std::collections::HashMap;
416+
417+
use dotenv::dotenv;
409418
use glob::glob;
419+
use tycho_common::{
420+
dto::{ProtocolComponent, ResponseProtocolState},
421+
Bytes,
422+
};
410423

411424
use super::*;
412425

@@ -455,4 +468,80 @@ mod tests {
455468
panic!("One or more config files failed to parse.");
456469
}
457470
}
471+
472+
#[test]
473+
fn test_token_balance_validation() {
474+
// Setup mock data
475+
let block_number = 21998530;
476+
let token_bytes = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap();
477+
let component_id = "0x787B8840100d9BaAdD7463f4a73b5BA73B00C6cA".to_string();
478+
479+
let component = ProtocolComponent {
480+
id: component_id.clone(),
481+
tokens: vec![token_bytes.clone()],
482+
..Default::default()
483+
};
484+
485+
let mut balances = HashMap::new();
486+
let balance_bytes = Bytes::from(
487+
U256::from_str("1070041574684539264153")
488+
.unwrap()
489+
.to_be_bytes::<32>(),
490+
);
491+
balances.insert(token_bytes.clone(), balance_bytes.clone());
492+
let protocol_state = ResponseProtocolState {
493+
component_id: component_id.clone(),
494+
balances,
495+
..Default::default()
496+
};
497+
498+
let mut components_by_id = HashMap::new();
499+
components_by_id.insert(component_id.clone(), component.clone());
500+
let mut protocol_states_by_id = HashMap::new();
501+
protocol_states_by_id.insert(component_id.clone(), protocol_state.clone());
502+
503+
let rt = Runtime::new().unwrap();
504+
dotenv().ok();
505+
let result =
506+
validate_token_balances(&components_by_id, &protocol_states_by_id, block_number, &rt);
507+
assert!(result.is_ok(), "Should pass when balance check is performed and balances match");
508+
}
509+
510+
#[test]
511+
fn test_token_balance_validation_fails_on_mismatch() {
512+
// Setup mock data
513+
let block_number = 21998530;
514+
let token_bytes = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap();
515+
let component_id = "0x787B8840100d9BaAdD7463f4a73b5BA73B00C6cA".to_string();
516+
517+
let component = ProtocolComponent {
518+
id: component_id.clone(),
519+
tokens: vec![token_bytes.clone()],
520+
..Default::default()
521+
};
522+
523+
// Set expected balance to zero
524+
let mut balances = HashMap::new();
525+
let balance_bytes = Bytes::from(U256::from(0).to_be_bytes::<32>());
526+
balances.insert(token_bytes.clone(), balance_bytes.clone());
527+
let protocol_state = ResponseProtocolState {
528+
component_id: component_id.clone(),
529+
balances,
530+
..Default::default()
531+
};
532+
533+
let mut components_by_id = HashMap::new();
534+
components_by_id.insert(component_id.clone(), component.clone());
535+
let mut protocol_states_by_id = HashMap::new();
536+
protocol_states_by_id.insert(component_id.clone(), protocol_state.clone());
537+
538+
let rt = Runtime::new().unwrap();
539+
dotenv().ok();
540+
let result =
541+
validate_token_balances(&components_by_id, &protocol_states_by_id, block_number, &rt);
542+
assert!(
543+
result.is_err(),
544+
"Should fail when balance check is performed and balances do not match"
545+
);
546+
}
458547
}

substreams/ethereum-ekubo-v2/integration_test.tycho.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
substreams_yaml_path: ./substreams.yaml
12
adapter_contract: "EkuboSwapAdapter"
23
adapter_build_signature: "constructor(address)"
34
adapter_build_args: "0x16e186ecdc94083fff53ef2a41d46b92a54f61e2"

0 commit comments

Comments
 (0)