diff --git a/src/evm/simulation.rs b/src/evm/simulation.rs index 34c17830..843c0929 100644 --- a/src/evm/simulation.rs +++ b/src/evm/simulation.rs @@ -29,7 +29,7 @@ use super::{ }; /// An error representing any transaction simulation result other than successful execution -#[derive(Debug, Display, Clone)] +#[derive(Debug, Display, Clone, PartialEq)] pub enum SimulationEngineError { /// Something went wrong while getting storage; might be caused by network issues. /// Retrying may help. @@ -298,6 +298,7 @@ fn interpret_evm_success( gas_used: gas_used - gas_refunded, } } + #[derive(Debug)] /// Data needed to invoke a transaction simulation pub struct SimulationParameters { @@ -635,9 +636,9 @@ mod tests { let caller = Address::from_str("0x0000000000000000000000000000000000000000")?; let router_addr = Address::from_str("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D")?; let router_abi = BaseContract::from( - parse_abi(&[ - "function getAmountsOut(uint amountIn, address[] memory path) public view returns (uint[] memory amounts)", - ])? + parse_abi(&[ + "function getAmountsOut(uint amountIn, address[] memory path) public view returns (uint[] memory amounts)", + ])? ); let weth_addr = Address::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")?; let usdc_addr = Address::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")?; diff --git a/src/protocol/dodo/state.rs b/src/protocol/dodo/state.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/protocol/errors.rs b/src/protocol/errors.rs index 309360d1..0f09807e 100644 --- a/src/protocol/errors.rs +++ b/src/protocol/errors.rs @@ -57,6 +57,7 @@ impl From for InvalidSnapshotError { /// - `InsufficientAmount`: Error indicating that the amount provided for the trade is too low. /// - `ArithmeticOverflow`: Error indicating that an arithmetic operation got an U256 to overflow /// - `Unknown`: Error indicating that an unknown error occurred during the simulation. +/// - `SellAmountTooHigh`: Indicates an error when the sell amount is higher than the sell limit. #[derive(Error, Debug)] pub enum SimulationError { #[error("ABI loading error: {0}")] @@ -83,4 +84,6 @@ pub enum SimulationError { ArithmeticOverflow(), #[error("Unknown error")] Unknown(), + #[error("Sell amount is higher than sell limit")] + SellAmountTooHigh(), // TODO: Make it recoverable } diff --git a/src/protocol/models.rs b/src/protocol/models.rs index 56cbcd2b..0bee30ba 100644 --- a/src/protocol/models.rs +++ b/src/protocol/models.rs @@ -75,12 +75,13 @@ impl PartialEq for Pair { pub struct GetAmountOutResult { pub amount: U256, pub gas: U256, + pub new_state: Box, } impl GetAmountOutResult { /// Constructs a new GetAmountOutResult struct with the given amount and gas - pub fn new(amount: U256, gas: U256) -> Self { - GetAmountOutResult { amount, gas } + pub fn new(amount: U256, gas: U256, new_state: Box) -> Self { + GetAmountOutResult { amount, gas, new_state } } /// Aggregates the given GetAmountOutResult struct to the current one. diff --git a/src/protocol/uniswap_v2/state.rs b/src/protocol/uniswap_v2/state.rs index 85f09cc1..06305412 100644 --- a/src/protocol/uniswap_v2/state.rs +++ b/src/protocol/uniswap_v2/state.rs @@ -9,14 +9,14 @@ use crate::{ state::{ProtocolEvent, ProtocolSim}, BytesConvertible, }, - safe_math::{safe_add_u256, safe_div_u256, safe_mul_u256}, + safe_math::{safe_add_u256, safe_div_u256, safe_mul_u256, safe_sub_u256}, }; use ethers::types::U256; use tycho_core::dto::ProtocolStateDelta; use super::{events::UniswapV2Sync, reserve_price::spot_price_from_reserves}; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct UniswapV2State { pub reserve0: U256, pub reserve1: U256, @@ -110,8 +110,15 @@ impl ProtocolSim for UniswapV2State { safe_add_u256(safe_mul_u256(reserve_sell, U256::from(1000))?, amount_in_with_fee)?; let amount_out = safe_div_u256(numerator, denominator)?; - - Ok(GetAmountOutResult::new(amount_out, U256::from(120_000))) + let mut new_state = self.clone(); + if zero2one { + new_state.reserve0 = safe_add_u256(self.reserve0, amount_in)?; + new_state.reserve1 = safe_sub_u256(self.reserve1, amount_out)?; + } else { + new_state.reserve0 = safe_sub_u256(self.reserve0, amount_out)?; + new_state.reserve1 = safe_add_u256(self.reserve1, amount_in)?; + }; + Ok(GetAmountOutResult::new(amount_out, U256::from(120_000), Box::new(new_state))) } fn delta_transition( @@ -154,7 +161,7 @@ impl ProtocolSim for UniswapV2State { } fn clone_box(&self) -> Box { - Box::new(*self) + Box::new(self.clone()) } fn as_any(&self) -> &dyn Any { @@ -211,20 +218,20 @@ mod tests { fn test_get_amount_out( #[case] r0: U256, #[case] r1: U256, - #[case] t0d: usize, - #[case] t1d: usize, + #[case] token_0_decimals: usize, + #[case] token_1_decimals: usize, #[case] amount_in: U256, #[case] exp: U256, ) { let t0 = ERC20Token::new( "0x0000000000000000000000000000000000000000", - t0d, + token_0_decimals, "T0", U256::from(10_000), ); let t1 = ERC20Token::new( "0x0000000000000000000000000000000000000001", - t1d, + token_1_decimals, "T0", U256::from(10_000), ); @@ -235,6 +242,16 @@ mod tests { .unwrap(); assert_eq!(res.amount, exp); + let new_state = res + .new_state + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(new_state.reserve0, r0 + amount_in); + assert_eq!(new_state.reserve1, r1 - exp); + // Assert that the old state is unchanged + assert_eq!(state.reserve0, r0); + assert_eq!(state.reserve1, r1); } #[test] diff --git a/src/protocol/uniswap_v3/state.rs b/src/protocol/uniswap_v3/state.rs index d852dad8..b5d594df 100644 --- a/src/protocol/uniswap_v3/state.rs +++ b/src/protocol/uniswap_v3/state.rs @@ -152,6 +152,7 @@ impl UniswapV3State { return Err(SimulationError::InsufficientData(GetAmountOutResult::new( state.amount_calculated.abs().into_raw(), gas_used, + self.clone_box(), ))) } _ => return Err(SimulationError::Unknown()), @@ -267,6 +268,10 @@ impl ProtocolSim for UniswapV3State { let result = self.swap(zero_for_one, amount_specified, None)?; trace!(?amount_in, ?token_a, ?token_b, ?zero_for_one, ?result, "V3 SWAP"); + let mut new_state = self.clone(); + new_state.liquidity = result.liquidity; + new_state.tick = result.tick; + new_state.sqrt_price = result.sqrt_price; Ok(GetAmountOutResult::new( result @@ -274,6 +279,7 @@ impl ProtocolSim for UniswapV3State { .abs() .into_raw(), result.gas_used, + Box::new(new_state), )) } diff --git a/src/protocol/vm/adapter_contract.rs b/src/protocol/vm/adapter_contract.rs index 87cdde1c..917967a6 100644 --- a/src/protocol/vm/adapter_contract.rs +++ b/src/protocol/vm/adapter_contract.rs @@ -1,6 +1,14 @@ // TODO: remove skip for clippy dead_code check #![allow(dead_code)] +use std::collections::{HashMap, HashSet}; + +use ethers::{ + abi::{Address, Token}, + types::U256, +}; +use revm::{primitives::Address as rAddress, DatabaseRef}; + use crate::{ evm::account_storage::StateUpdate, protocol::{ @@ -11,18 +19,12 @@ use crate::{ }, }, }; -use ethers::{ - abi::{Address, Token}, - types::U256, -}; -use revm::{primitives::Address as rAddress, DatabaseRef}; -use std::collections::{HashMap, HashSet}; #[derive(Debug)] pub struct Trade { - received_amount: U256, - gas_used: U256, - price: f64, + pub received_amount: U256, + pub gas_used: U256, + pub price: f64, } /// An implementation of `TychoSimulationContract` specific to the `AdapterContract` ABI interface, @@ -42,7 +44,7 @@ impl TychoSimulationContract where D::Error: std::fmt::Debug, { - pub async fn price( + pub fn price( &self, pair_id: String, sell_token: Address, @@ -64,15 +66,14 @@ where ]; let res = self - .call("price", args, block, None, overwrites, None, U256::zero()) - .await? + .call("price", args, block, None, overwrites, None, U256::zero())? .return_value; let price = self.calculate_price(res[0].clone())?; Ok(price) } #[allow(clippy::too_many_arguments)] - pub async fn swap( + pub fn swap( &self, pair_id: String, sell_token: Address, @@ -90,25 +91,37 @@ where Token::Uint(amount), ]; - let res = self - .call("swap", args, block, None, overwrites, None, U256::zero()) - .await?; - let received_amount = res.return_value[0] - .clone() - .into_uint() - .unwrap(); - let gas_used = res.return_value[1] - .clone() - .into_uint() - .unwrap(); - let price = self - .calculate_price(res.return_value[2].clone()) - .unwrap()[0]; + let res = self.call("swap", args, block, None, overwrites, None, U256::zero())?; + + let (received_amount, gas_used, price) = if let Token::Tuple(ref return_value) = + res.return_value[0] + { + match &return_value[..] { + [Token::Uint(amount), Token::Uint(gas), Token::Tuple(price_elements)] => { + let received_amount = *amount; + let gas_used = *gas; + + let price_token = Token::Array(vec![Token::Tuple(price_elements.clone())]); + let price = self + .calculate_price(price_token)? + .first() + .cloned() + .ok_or_else(|| { + SimulationError::DecodingError("There wasn't a calculated price".into()) + })?; + + Ok((received_amount, gas_used, price)) + } + _ => Err(SimulationError::DecodingError("Incorrect types found for price".into())), + } + } else { + Err(SimulationError::DecodingError("return_value is not a Token::Tuple".into())) + }?; Ok((Trade { received_amount, gas_used, price }, res.simulation_result.state_updates)) } - pub async fn get_limits( + pub fn get_limits( &self, pair_id: String, sell_token: Address, @@ -123,8 +136,7 @@ where ]; let res = self - .call("getLimits", args, block, None, overwrites, None, U256::zero()) - .await? + .call("getLimits", args, block, None, overwrites, None, U256::zero())? .return_value; if let Some(Token::Array(inner)) = res.first() { @@ -138,7 +150,7 @@ where Err(SimulationError::DecodingError("Unexpected response format".into())) } - pub async fn get_capabilities( + pub fn get_capabilities( &self, pair_id: String, sell_token: Address, @@ -151,8 +163,7 @@ where ]; let res = self - .call("getCapabilities", args, 1, None, None, None, U256::zero()) - .await? + .call("getCapabilities", args, 1, None, None, None, U256::zero())? .return_value; let capabilities: HashSet = match res.first() { Some(Token::Array(inner_tokens)) => inner_tokens @@ -168,10 +179,9 @@ where Ok(capabilities) } - pub async fn min_gas_usage(&self) -> Result { + pub fn min_gas_usage(&self) -> Result { let res = self - .call("minGasUsage", vec![], 1, None, None, None, U256::zero()) - .await? + .call("minGasUsage", vec![], 1, None, None, None, U256::zero())? .return_value; Ok(res[0] .clone() @@ -213,7 +223,7 @@ where }) .collect() } else { - Err(SimulationError::DecodingError("Expected Token::Array".to_string())) + Err(SimulationError::DecodingError("Price is not a Token::Array".to_string())) } } } diff --git a/src/protocol/vm/assets/ERC20.bin b/src/protocol/vm/assets/ERC20.bin new file mode 100644 index 00000000..d791f43e Binary files /dev/null and b/src/protocol/vm/assets/ERC20.bin differ diff --git a/src/protocol/vm/engine.rs b/src/protocol/vm/engine.rs index d2dab96d..d013ad0f 100644 --- a/src/protocol/vm/engine.rs +++ b/src/protocol/vm/engine.rs @@ -12,11 +12,14 @@ use revm::{ use std::{collections::HashMap, fmt::Debug, sync::Arc}; use tokio::sync::RwLock; -use crate::evm::{ - simulation::SimulationEngine, - simulation_db::BlockHeader, - tycho_db::PreCachedDB, - tycho_models::{AccountUpdate, ChangeType, ResponseAccount}, +use crate::{ + evm::{ + simulation::SimulationEngine, + simulation_db::BlockHeader, + tycho_db::PreCachedDB, + tycho_models::{AccountUpdate, ChangeType, ResponseAccount}, + }, + protocol::{errors::SimulationError, vm::utils::load_erc20_bytecode}, }; lazy_static! { @@ -39,7 +42,7 @@ pub async fn create_engine( db: Arc>, tokens: Vec, trace: bool, -) -> SimulationEngine +) -> Result, SimulationError> where ::Error: Debug, ::Error: Debug, @@ -48,15 +51,19 @@ where let db_read = db.read().await; let engine = SimulationEngine::new(db_read.clone(), trace); + let contract_bytecode = load_erc20_bytecode()?; + for token in tokens { let info = AccountInfo { balance: Default::default(), nonce: 0, code_hash: KECCAK_EMPTY, - code: None, + code: Some(contract_bytecode.clone()), }; engine.state.init_account( - Address::parse_checksummed(token, None).expect("Invalid checksum for token address"), + Address::parse_checksummed(token, None).map_err(|_| { + SimulationError::EncodingError("Checksum for token address must be valid".into()) + })?, info, None, false, @@ -70,7 +77,7 @@ where false, ); - engine + Ok(engine) } pub async fn update_engine( @@ -180,15 +187,11 @@ mod tests { let engine = create_engine(db, tokens.clone(), false).await; - // Verify trace flag is unset - assert!(!engine.trace); - + let state_data = engine.unwrap().state.data; // Verify all tokens are initialized for token in tokens { let token_address = Address::parse_checksummed(token, None).expect("valid checksum"); - let account = engine - .state - .data + let account = state_data .borrow() .get(&token_address) .unwrap() @@ -196,14 +199,12 @@ mod tests { assert_eq!(account.balance, U256::default()); assert_eq!(account.nonce, 0); assert_eq!(account.code_hash, KECCAK_EMPTY); - assert!(account.code.is_none()); + assert!(account.code.is_some()); } // Verify external account initialization let external_account_address = *EXTERNAL_ACCOUNT; - let external_account = engine - .state - .data + let external_account = state_data .borrow() .get(&external_account_address) .unwrap() @@ -222,6 +223,6 @@ mod tests { let engine = create_engine(db, tokens, true).await; // Verify trace flag is set - assert!(engine.trace); + assert!(engine.unwrap().trace); } } diff --git a/src/protocol/vm/state.rs b/src/protocol/vm/state.rs index e9b7cb65..f63cc5e9 100644 --- a/src/protocol/vm/state.rs +++ b/src/protocol/vm/state.rs @@ -1,6 +1,7 @@ // TODO: remove skip for clippy dead_code check #![allow(dead_code)] +use alloy_primitives::Address; use std::{ any::Any, collections::{HashMap, HashSet}, @@ -23,7 +24,6 @@ use revm::{ DatabaseRef, }; use tracing::warn; - use tycho_core::dto::ProtocolStateDelta; use crate::{ @@ -73,7 +73,7 @@ pub struct VMPoolState { /// The supported capabilities of this pool pub capabilities: HashSet, /// Storage overwrites that will be applied to all simulations. They will be cleared - // when ``clear_all_cache`` is called, i.e. usually at each block. Hence, the name. + /// when ``clear_all_cache`` is called, i.e. usually at each block. Hence, the name. pub block_lasting_overwrites: HashMap, /// A set of all contract addresses involved in the simulation of this pool.""" pub involved_contracts: HashSet, @@ -136,7 +136,7 @@ impl VMPoolState { .clone() .ok_or_else(|| SimulationError::NotInitialized("Simulation engine".to_string()))?, )?); - state.set_capabilities().await?; + state.set_capabilities()?; // TODO: add init_token_storage_slots() in 3796 Ok(state) } @@ -149,7 +149,7 @@ impl VMPoolState { .map(|addr| to_checksum(addr, None)) .collect(); let engine: SimulationEngine<_> = - create_engine(SHARED_TYCHO_DB.clone(), token_addresses, self.trace).await; + create_engine(SHARED_TYCHO_DB.clone(), token_addresses, self.trace).await?; engine.state.init_account( "0x0000000000000000000000000000000000000000" .parse() @@ -200,8 +200,7 @@ impl VMPoolState { .to_string(); } let code = get_code_for_contract(&addr_str, None).await?; - let code_hash = B256::from(keccak256(code.clone().bytes())); - (Some(code), code_hash) + (Some(code.clone()), code.hash_slow()) } else { let code = Bytecode::new_raw(Bytes::from(bytecode.clone().ok_or_else(|| { @@ -209,8 +208,7 @@ impl VMPoolState { "Byte code from stateless contracts is None".into(), ) })?)); - let code_hash = B256::from(keccak256(code.clone().bytes())); - (Some(code), code_hash) + (Some(code.clone()), code.hash_slow()) }; engine.state.init_account( address.parse().unwrap(), @@ -312,7 +310,7 @@ impl VMPoolState { Ok(()) } - async fn set_capabilities(&mut self) -> Result<(), SimulationError> { + fn set_capabilities(&mut self) -> Result<(), SimulationError> { let mut capabilities = Vec::new(); // Generate all permutations of tokens and retrieve capabilities @@ -323,8 +321,7 @@ impl VMPoolState { .adapter_contract .clone() .ok_or_else(|| SimulationError::NotInitialized("Adapter contract".to_string()))? - .get_capabilities(self.id.clone()[2..].to_string(), *t0, *t1) - .await?; + .get_capabilities(self.id.clone()[2..].to_string(), *t0, *t1)?; capabilities.push(caps); } } @@ -353,19 +350,21 @@ impl VMPoolState { Ok(()) } - pub async fn set_spot_prices( - &mut self, - tokens: Vec, - ) -> Result<(), SimulationError> { + pub fn set_spot_prices(&mut self, tokens: Vec) -> Result<(), SimulationError> { self.ensure_capability(Capability::PriceFunction)?; for [sell_token, buy_token] in tokens .iter() .permutations(2) .map(|p| [p[0], p[1]]) { - let sell_amount_limit = self - .get_sell_amount_limit(vec![(sell_token.address), (buy_token.address)]) - .await?; + let overwrites = Some(self.get_overwrites( + vec![(*sell_token).clone().address, (*buy_token).clone().address], + U256::from_big_endian(&(*MAX_BALANCE / rU256::from(100)).to_be_bytes::<32>()), + )?); + let sell_amount_limit = self.get_sell_amount_limit( + vec![(sell_token.address), (buy_token.address)], + overwrites.clone(), + )?; let price_result = self .adapter_contract .clone() @@ -376,9 +375,8 @@ impl VMPoolState { buy_token.address, vec![sell_amount_limit / U256::from(100)], self.block.number, - Some(self.block_lasting_overwrites.clone()), - ) - .await?; + overwrites, + )?; let price = if self .capabilities @@ -404,49 +402,41 @@ impl VMPoolState { /// Retrieves the sell amount limit for a given pair of tokens, where the first token is treated /// as the sell token and the second as the buy token. The order of tokens in the input vector /// is significant and determines the direction of the price query. - async fn get_sell_amount_limit(&mut self, tokens: Vec) -> Result { + fn get_sell_amount_limit( + &self, + tokens: Vec, + overwrites: Option>>, + ) -> Result { let binding = self .adapter_contract .clone() .ok_or_else(|| SimulationError::NotInitialized("Adapter contract".to_string()))?; - let limits = binding - .get_limits( - self.id.clone()[2..].to_string(), - tokens[0], - tokens[1], - self.block.number, - Some( - self.get_overwrites( - tokens, - U256::from_big_endian( - &(*MAX_BALANCE / rU256::from(100)).to_be_bytes::<32>(), - ), - ) - .await?, - ), - ) - .await; + let limits = binding.get_limits( + self.id.clone()[2..].to_string(), + tokens[0], + tokens[1], + self.block.number, + overwrites, + ); Ok(limits?.0) } - pub async fn get_overwrites( - &mut self, + fn get_overwrites( + &self, tokens: Vec, max_amount: U256, ) -> Result, SimulationError> { - let token_overwrites = self - .get_token_overwrites(tokens, max_amount) - .await?; + let token_overwrites = self.get_token_overwrites(tokens, max_amount)?; // Merge `block_lasting_overwrites` with `token_overwrites` let merged_overwrites = self.merge(&self.block_lasting_overwrites.clone(), &token_overwrites); - self.block_lasting_overwrites = merged_overwrites.clone(); + Ok(merged_overwrites) } - async fn get_token_overwrites( + fn get_token_overwrites( &self, tokens: Vec, max_amount: U256, @@ -481,10 +471,7 @@ impl VMPoolState { // Merge all overwrites into a single HashMap Ok(res .into_iter() - .fold(HashMap::new(), |acc, overwrite| { - self.merge(&acc, &overwrite); - acc - })) + .fold(HashMap::new(), |acc, overwrite| self.merge(&acc, &overwrite))) } fn get_balance_overwrites( @@ -557,11 +544,94 @@ impl ProtocolSim for VMPoolState { fn get_amount_out( &self, - _amount_in: U256, - _token_in: &ERC20Token, - _token_out: &ERC20Token, + amount_in: U256, + token_in: &ERC20Token, + token_out: &ERC20Token, ) -> Result { - todo!() + let sell_token = token_in.address; + let buy_token = token_out.address; + let sell_amount = amount_in; + let overwrites = self.get_overwrites( + vec![sell_token, buy_token], + U256::from_big_endian(&(*MAX_BALANCE / rU256::from(100)).to_be_bytes::<32>()), + )?; + let sell_amount_limit = + self.get_sell_amount_limit(vec![sell_token, buy_token], Some(overwrites.clone()))?; + let (sell_amount_respecting_limit, sell_amount_exceeds_limit) = if self + .capabilities + .contains(&Capability::HardLimits) && + sell_amount_limit < sell_amount + { + (sell_amount_limit, true) + } else { + (sell_amount, false) + }; + + let overwrites_with_sell_limit = + self.get_overwrites(vec![sell_token, buy_token], sell_amount_limit)?; + let complete_overwrites = self.merge(&overwrites, &overwrites_with_sell_limit); + let pool_id = self.id.clone(); + + let (trade, state_changes) = self + .adapter_contract + .as_ref() + .ok_or_else(|| SimulationError::NotInitialized("Adapter contract".to_string()))? + .swap( + pool_id[2..].to_string(), + sell_token, + buy_token, + false, + sell_amount_respecting_limit, + self.block.number, + Some(complete_overwrites), + )?; + + let mut new_state = self.clone(); + + // Apply state changes to the new state + for (address, state_update) in state_changes { + if let Some(storage) = state_update.storage { + let block_overwrites = new_state + .block_lasting_overwrites + .entry(address) + .or_default(); + for (slot, value) in storage { + let slot = U256::from_dec_str(&slot.to_string()).map_err(|_| { + SimulationError::DecodingError("Failed to decode slot index".to_string()) + })?; + let value = U256::from_dec_str(&value.to_string()).map_err(|_| { + SimulationError::DecodingError( + "Failed to decode slot overwrite".to_string(), + ) + })?; + block_overwrites.insert(slot, value); + } + } + } + + // Update spot prices + let new_price = trade.price; + if new_price != 0.0f64 { + new_state + .spot_prices + .insert((sell_token, buy_token), new_price); + new_state + .spot_prices + .insert((buy_token, sell_token), 1.0f64 / new_price); + } + + let buy_amount = trade.received_amount; + + if sell_amount_exceeds_limit { + return Err(SimulationError::SellAmountTooHigh( + // // Partial buy amount and gas used TODO: make this better + // buy_amount, + // trade.gas_used, + // new_state, + // sell_amount_limit, + )); + } + Ok(GetAmountOutResult::new(buy_amount, trade.gas_used, Box::new(new_state.clone()))) } fn delta_transition( @@ -602,7 +672,6 @@ impl ProtocolSim for VMPoolState { #[cfg(test)] mod tests { use super::*; - use ethers::{ prelude::{H256, U256}, types::Address as EthAddress, @@ -620,6 +689,14 @@ mod tests { protocol::vm::models::Capability, }; + fn dai() -> ERC20Token { + ERC20Token::new("0x6B175474E89094C44Da98b954EedeAC495271d0F", 18, "DAI", U256::from(10_000)) + } + + fn bal() -> ERC20Token { + ERC20Token::new("0xba100000625a3754423978a60c9317c58a424e3D", 18, "BAL", U256::from(10_000)) + } + async fn setup_db(asset_path: &Path) -> Result<(), Box> { let file = File::open(asset_path)?; let data: Value = serde_json::from_reader(file)?; @@ -628,7 +705,12 @@ mod tests { .expect("Expected accounts to match AccountUpdate structure"); let db = SHARED_TYCHO_DB.clone(); - let engine: SimulationEngine<_> = create_engine(db.clone(), vec![], false).await; + let engine: SimulationEngine<_> = create_engine( + db.clone(), + vec![to_checksum(&dai().address, None), to_checksum(&bal().address, None)], + false, + ) + .await?; let block = BlockHeader { number: 20463609, @@ -654,9 +736,8 @@ mod tests { false, ); } - let db_write = db.write(); + let db_write = db.write().await; db_write - .await .update(accounts, Some(block)) .await; @@ -668,8 +749,8 @@ mod tests { .await .expect("Failed to set up database"); - let dai_addr = H160::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); - let bal_addr = H160::from_str("0xba100000625a3754423978a60c9317c58a424e3d").unwrap(); + let dai_addr = dai().address; + let bal_addr = bal().address; let tokens = vec![dai_addr, bal_addr]; let block = BlockHeader { @@ -683,7 +764,7 @@ mod tests { let pool_id: String = "0x4626d81b3a1711beb79f4cecff2413886d461677000200000000000000000011".into(); - + dbg!(&tokens); VMPoolState::::new( pool_id, tokens, @@ -732,7 +813,6 @@ mod tests { pool_state.tokens[0], pool_state.tokens[1], ) - .await .unwrap(); assert_eq!(capabilities_adapter_contract, expected_capabilities.clone()); @@ -754,26 +834,95 @@ mod tests { .is_err()); } - fn dai() -> ERC20Token { - ERC20Token::new("0x6b175474e89094c44da98b954eedeac495271d0f", 18, "DAI", U256::from(10_000)) + #[tokio::test] + async fn test_get_amount_out() -> Result<(), Box> { + setup_db("src/protocol/vm/assets/balancer_contract_storage_block_20463609.json".as_ref()) + .await?; + + let pool_state = setup_pool_state().await; + + let result = pool_state + .get_amount_out(U256::from_dec_str("1000000000000000000").unwrap(), &dai(), &bal()) + .unwrap(); + let new_state = result + .new_state + .as_any() + .downcast_ref::>() + .unwrap(); + assert_eq!(result.amount, U256::from_dec_str("137780051463393923").unwrap()); + assert_eq!(result.gas, U256::from_dec_str("102770").unwrap()); + assert_ne!(new_state.spot_prices, pool_state.spot_prices); + assert!(pool_state + .block_lasting_overwrites + .is_empty()); + Ok(()) } - fn bal() -> ERC20Token { - ERC20Token::new("0xba100000625a3754423978a60c9317c58a424e3d", 18, "BAL", U256::from(10_000)) + #[tokio::test] + async fn test_get_amount_out_dust() { + setup_db("src/protocol/vm/assets/balancer_contract_storage_block_20463609.json".as_ref()) + .await + .unwrap(); + + let pool_state = setup_pool_state().await; + + let result = pool_state + .get_amount_out(U256::from(1), &dai(), &bal()) + .unwrap(); + + let new_state = result + .new_state + .as_any() + .downcast_ref::>() + .unwrap(); + assert_eq!(result.amount, U256::from(0)); + assert_eq!(result.gas, U256::from(68656)); + assert_eq!(new_state.spot_prices, pool_state.spot_prices) + } + + #[tokio::test] + async fn test_get_amount_out_sell_limit() { + setup_db("src/protocol/vm/assets/balancer_contract_storage_block_20463609.json".as_ref()) + .await + .unwrap(); + + let pool_state = setup_pool_state().await; + + let result = pool_state.get_amount_out( + // sell limit is 100279494253364362835 + U256::from_dec_str("100379494253364362835").unwrap(), + &dai(), + &bal(), + ); + + assert!(result.is_err()); + match result { + Err(e) => { + assert!(matches!(e, SimulationError::SellAmountTooHigh())); + } + _ => panic!("Test failed: was expecting an Err value"), + }; } #[tokio::test] async fn test_get_sell_amount_limit() { - let mut pool_state = setup_pool_state().await; + let pool_state = setup_pool_state().await; + let overwrites = pool_state + .get_overwrites( + vec![pool_state.tokens[0], pool_state.tokens[1]], + U256::from_big_endian(&(*MAX_BALANCE / rU256::from(100)).to_be_bytes::<32>()), + ) + .unwrap(); let dai_limit = pool_state - .get_sell_amount_limit(vec![dai().address, bal().address]) - .await + .get_sell_amount_limit(vec![dai().address, bal().address], Some(overwrites.clone())) .unwrap(); assert_eq!(dai_limit, U256::from_dec_str("100279494253364362835").unwrap()); let bal_limit = pool_state - .get_sell_amount_limit(vec![pool_state.tokens[1], pool_state.tokens[0]]) - .await + .get_sell_amount_limit( + vec![pool_state.tokens[1], pool_state.tokens[0]], + Some(overwrites), + ) .unwrap(); assert_eq!(bal_limit, U256::from_dec_str("13997408640689987484").unwrap()); } @@ -784,7 +933,6 @@ mod tests { pool_state .set_spot_prices(vec![bal(), dai()]) - .await .unwrap(); let dai_bal_spot_price = pool_state diff --git a/src/protocol/vm/tycho_simulation_contract.rs b/src/protocol/vm/tycho_simulation_contract.rs index 086ce877..f35a9ba3 100644 --- a/src/protocol/vm/tycho_simulation_contract.rs +++ b/src/protocol/vm/tycho_simulation_contract.rs @@ -1,6 +1,8 @@ // TODO: remove skip for clippy dead_code check #![allow(dead_code)] +use std::collections::HashMap; + use chrono::Utc; use ethers::{ abi::{decode, encode, Abi, ParamType, Token}, @@ -11,7 +13,6 @@ use revm::{ db::DatabaseRef, primitives::{alloy_primitives::Keccak256, Address}, }; -use std::collections::HashMap; use tracing::warn; use crate::{ @@ -139,7 +140,7 @@ where } #[allow(clippy::too_many_arguments)] - pub async fn call( + pub fn call( &self, fname: &str, args: Vec, diff --git a/src/protocol/vm/utils.rs b/src/protocol/vm/utils.rs index d01a16f7..4c6d8217 100644 --- a/src/protocol/vm/utils.rs +++ b/src/protocol/vm/utils.rs @@ -295,20 +295,22 @@ pub fn load_swap_abi() -> Result { Ok(abi) } -pub fn load_erc20_abi() -> Result { - let erc20_abi_path = Path::new(file!()) +pub fn load_erc20_bytecode() -> Result { + let erc20_bin_path = Path::new(file!()) .parent() - .unwrap() + .ok_or_else(|| { + FileError::Structure("Failed to obtain parent directory of current file.".to_string()) + })? .join("assets") - .join("ERC20.abi"); + .join("ERC20.bin"); - let mut file = File::open(&erc20_abi_path).map_err(FileError::Io)?; - let mut contents = String::new(); - file.read_to_string(&mut contents) + let mut erc_20_file = File::open(&erc20_bin_path).map_err(FileError::Io)?; + let mut erc_20_contents = Vec::new(); + erc_20_file + .read_to_end(&mut erc_20_contents) .map_err(FileError::Io)?; - - let abi: Abi = serde_json::from_str(&contents).map_err(FileError::Parse)?; - Ok(abi) + let erc_20_bytecode = Bytecode::new_raw(erc_20_contents.into()); + Ok(erc_20_bytecode) } #[cfg(test)] @@ -484,9 +486,13 @@ mod tests { #[test] fn test_load_erc20_abi() { - let result = load_erc20_abi(); + let result = load_erc20_bytecode(); assert!(result.is_ok()); - let abi: Abi = result.expect("Failed to retrieve ERC20 ABI result"); - assert!(!abi.functions.is_empty(), "The ERC20 ABI should contain functions."); + + let bytecode: Bytecode = result.expect("Failed to retrieve ERC20 ABI result"); + + let expected_bytes = revm::precompile::Bytes::from(ethers::utils::hex::decode("0x608060405234801561000f575f80fd5b50600436106100a6575f3560e01c8063395093511161006e578063395093511461011f57806370a082311461013257806395d89b411461015a578063a457c2d714610162578063a9059cbb14610175578063dd62ed3e14610188575f80fd5b806306fdde03146100aa578063095ea7b3146100c857806318160ddd146100eb57806323b872dd146100fd578063313ce56714610110575b5f80fd5b6100b261019b565b6040516100bf91906105b9565b60405180910390f35b6100db6100d636600461061f565b61022b565b60405190151581526020016100bf565b6002545b6040519081526020016100bf565b6100db61010b366004610647565b610244565b604051601281526020016100bf565b6100db61012d36600461061f565b610267565b6100ef610140366004610680565b6001600160a01b03165f9081526020819052604090205490565b6100b2610288565b6100db61017036600461061f565b610297565b6100db61018336600461061f565b6102f2565b6100ef6101963660046106a0565b6102ff565b6060600380546101aa906106d1565b80601f01602080910402602001604051908101604052809291908181526020018280546101d6906106d1565b80156102215780601f106101f857610100808354040283529160200191610221565b820191905f5260205f20905b81548152906001019060200180831161020457829003601f168201915b5050505050905090565b5f33610238818585610329565b60019150505b92915050565b5f336102518582856103dc565b61025c85858561043e565b506001949350505050565b5f3361023881858561027983836102ff565b6102839190610709565b610329565b6060600480546101aa906106d1565b5f33816102a482866102ff565b9050838110156102e557604051632983c0c360e21b81526001600160a01b038616600482015260248101829052604481018590526064015b60405180910390fd5b61025c8286868403610329565b5f3361023881858561043e565b6001600160a01b039182165f90815260016020908152604080832093909416825291909152205490565b6001600160a01b0383166103525760405163e602df0560e01b81525f60048201526024016102dc565b6001600160a01b03821661037b57604051634a1406b160e11b81525f60048201526024016102dc565b6001600160a01b038381165f8181526001602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591015b60405180910390a3505050565b5f6103e784846102ff565b90505f198114610438578181101561042b57604051637dc7a0d960e11b81526001600160a01b038416600482015260248101829052604481018390526064016102dc565b6104388484848403610329565b50505050565b6001600160a01b03831661046757604051634b637e8f60e11b81525f60048201526024016102dc565b6001600160a01b0382166104905760405163ec442f0560e01b81525f60048201526024016102dc565b61049b8383836104a0565b505050565b6001600160a01b0383166104ca578060025f8282546104bf9190610709565b9091555061053a9050565b6001600160a01b0383165f908152602081905260409020548181101561051c5760405163391434e360e21b81526001600160a01b038516600482015260248101829052604481018390526064016102dc565b6001600160a01b0384165f9081526020819052604090209082900390555b6001600160a01b03821661055657600280548290039055610574565b6001600160a01b0382165f9081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516103cf91815260200190565b5f6020808352835180828501525f5b818110156105e4578581018301518582016040015282016105c8565b505f604082860101526040601f19601f8301168501019250505092915050565b80356001600160a01b038116811461061a575f80fd5b919050565b5f8060408385031215610630575f80fd5b61063983610604565b946020939093013593505050565b5f805f60608486031215610659575f80fd5b61066284610604565b925061067060208501610604565b9150604084013590509250925092565b5f60208284031215610690575f80fd5b61069982610604565b9392505050565b5f80604083850312156106b1575f80fd5b6106ba83610604565b91506106c860208401610604565b90509250929050565b600181811c908216806106e557607f821691505b60208210810361070357634e487b7160e01b5f52602260045260245ffd5b50919050565b8082018082111561023e57634e487b7160e01b5f52601160045260245ffdfea2646970667358221220dfc123d5852c9246ea16b645b377b4436e2f778438195cc6d6c435e8c73a20e764736f6c634300081403000000000000000000000000000000000000000000000000000000000000000000").unwrap()); + let expected_bytecode = Bytecode::new_raw(expected_bytes); + assert_eq!(bytecode, expected_bytecode); } }