From 5e5a318d889837f4e33bb9836df59ff9d8d05141 Mon Sep 17 00:00:00 2001 From: Kaze Date: Tue, 10 Mar 2026 00:06:38 +0900 Subject: [PATCH 01/20] Add EIP-4626 native price estimator Prices vault tokens by calling `asset()` to find the underlying token, `convertToAssets(1e18)` to get the conversion rate, then delegating to an inner estimator for the underlying token's native price. Configurable as `Eip4626|`, e.g. `Eip4626|CoinGecko`. Co-Authored-By: Claude Sonnet 4.6 --- crates/price-estimation/Cargo.toml | 2 +- crates/price-estimation/src/factory.rs | 20 ++- crates/price-estimation/src/lib.rs | 13 +- crates/price-estimation/src/native/eip4626.rs | 130 ++++++++++++++++++ crates/price-estimation/src/native/mod.rs | 2 + 5 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 crates/price-estimation/src/native/eip4626.rs diff --git a/crates/price-estimation/Cargo.toml b/crates/price-estimation/Cargo.toml index 5ee9cf737b..34d516df1d 100644 --- a/crates/price-estimation/Cargo.toml +++ b/crates/price-estimation/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" license = "MIT OR Apache-2.0" [dependencies] -alloy = { workspace = true, features = ["providers", "rpc", "rpc-types", "sol-types"] } +alloy = { workspace = true, features = ["contract", "providers", "rpc", "rpc-types", "sol-types"] } anyhow = { workspace = true } app-data = { workspace = true } arc-swap = { workspace = true } diff --git a/crates/price-estimation/src/factory.rs b/crates/price-estimation/src/factory.rs index 79c78381d1..396f24d287 100644 --- a/crates/price-estimation/src/factory.rs +++ b/crates/price-estimation/src/factory.rs @@ -266,6 +266,11 @@ impl<'a> PriceEstimatorFactory<'a> { Ok((name, coin_gecko)) } + // Eip4626 wraps another estimator and is handled at the call site + // to avoid recursive async fn. + NativePriceEstimatorSource::Eip4626(_) => { + unreachable!("Eip4626 is handled before calling create_native_estimator") + } } } @@ -366,7 +371,20 @@ impl<'a> PriceEstimatorFactory<'a> { for stage in native.iter() { let mut stages = Vec::with_capacity(stage.len()); for source in stage { - stages.push(self.create_native_estimator(source, weth).await?); + let entry = if let NativePriceEstimatorSource::Eip4626(inner_source) = source { + let name = format!("Eip4626|{inner_source}"); + let (_, inner) = self.create_native_estimator(inner_source, weth).await?; + ( + name.clone(), + Arc::new(InstrumentedPriceEstimator::new( + native::Eip4626::new(inner, self.network.web3.provider.clone()), + name, + )) as Arc, + ) + } else { + self.create_native_estimator(source, weth).await? + }; + stages.push(entry); } estimators.push(stages); } diff --git a/crates/price-estimation/src/lib.rs b/crates/price-estimation/src/lib.rs index 6428f4b30d..297e6b95d4 100644 --- a/crates/price-estimation/src/lib.rs +++ b/crates/price-estimation/src/lib.rs @@ -111,9 +111,15 @@ impl FromStr for ExternalSolver { #[serde(tag = "type")] pub enum NativePriceEstimator { Driver(ExternalSolver), - Forwarder { url: Url }, + Forwarder { + url: Url, + }, OneInchSpotPriceApi, CoinGecko, + /// Prices EIP-4626 vault tokens by looking up the underlying `asset()` and + /// applying `convertToAssets()` as a conversion rate. The inner estimator + /// is used to price the underlying asset. + Eip4626(Box), } impl NativePriceEstimator { @@ -133,6 +139,7 @@ impl Display for NativePriceEstimator { NativePriceEstimator::Forwarder { url } => format!("Forwarder|{}", url), NativePriceEstimator::OneInchSpotPriceApi => "OneInchSpotPriceApi".into(), NativePriceEstimator::CoinGecko => "CoinGecko".into(), + NativePriceEstimator::Eip4626(inner) => format!("Eip4626|{inner}"), }; write!(f, "{formatter}") } @@ -192,6 +199,10 @@ impl FromStr for NativePriceEstimator { .parse() .context("Forwarder price estimator invalid URL")?, }), + "Eip4626" => Ok(NativePriceEstimator::Eip4626(Box::new( + NativePriceEstimator::from_str(args) + .context("Eip4626 inner estimator failed to parse")?, + ))), _ => Err(anyhow::anyhow!("unsupported native price estimator: {}", s)), } } diff --git a/crates/price-estimation/src/native/eip4626.rs b/crates/price-estimation/src/native/eip4626.rs new file mode 100644 index 0000000000..3d9be6d228 --- /dev/null +++ b/crates/price-estimation/src/native/eip4626.rs @@ -0,0 +1,130 @@ +use { + super::{NativePriceEstimateResult, NativePriceEstimating}, + crate::PriceEstimationError, + alloy::primitives::{Address, U256, uint}, + anyhow::Context, + ethrpc::AlloyProvider, + futures::{FutureExt, future::BoxFuture}, + num::ToPrimitive, + number::conversions::u256_to_big_rational, + std::{sync::Arc, time::Duration}, +}; + +alloy::sol! { + #[sol(rpc)] + interface IERC4626 { + function asset() external view returns (address assetTokenAddress); + function convertToAssets(uint256 shares) external view returns (uint256 assets); + } +} + +/// Estimates the native price of EIP-4626 vault tokens by: +/// 1. Calling `asset()` to find the underlying token +/// 2. Calling `convertToAssets(1e18)` to find the conversion rate +/// 3. Delegating to an inner estimator for the underlying token's native price +pub struct Eip4626 { + inner: Arc, + provider: AlloyProvider, +} + +impl Eip4626 { + pub fn new(inner: Arc, provider: AlloyProvider) -> Self { + Self { inner, provider } + } + + async fn estimate(&self, token: Address, timeout: Duration) -> NativePriceEstimateResult { + let vault = IERC4626::new(token, self.provider.clone()); + + let asset: Address = vault.asset().call().await.map_err(|e| { + PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "failed to call asset() on {token}: {e}" + )) + })?; + + // Use 1e18 shares as the reference amount. This works correctly for + // vaults with 18 decimals. For other decimals the rate is still a + // reasonable approximation since convertToAssets is linear. + let shares = uint!(1_000_000_000_000_000_000_U256); + let assets: U256 = vault.convertToAssets(shares).call().await.map_err(|e| { + PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "failed to call convertToAssets() on {token}: {e}" + )) + })?; + + let rate = (u256_to_big_rational(&assets) / u256_to_big_rational(&shares)) + .to_f64() + .context("conversion rate is not representable as f64") + .map_err(PriceEstimationError::EstimatorInternal)?; + + let asset_price = self.inner.estimate_native_price(asset, timeout).await?; + + Ok(asset_price * rate) + } +} + +impl NativePriceEstimating for Eip4626 { + fn estimate_native_price( + &self, + token: Address, + timeout: Duration, + ) -> BoxFuture<'_, NativePriceEstimateResult> { + self.estimate(token, timeout).boxed() + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::{HEALTHY_PRICE_ESTIMATION_TIME, native::MockNativePriceEstimating}, + }; + + #[test] + fn rate_math() { + // 1 vault share = 1.5 underlying tokens (e.g. rebasing vault) + let shares = uint!(1_000_000_000_000_000_000_U256); + let assets = uint!(1_500_000_000_000_000_000_U256); + let rate = (u256_to_big_rational(&assets) / u256_to_big_rational(&shares)) + .to_f64() + .unwrap(); + assert!((rate - 1.5).abs() < 1e-9); + } + + #[tokio::test] + async fn delegates_to_inner_on_error() { + let mut inner = MockNativePriceEstimating::new(); + inner + .expect_estimate_native_price() + .returning(|_, _| async { Err(PriceEstimationError::NoLiquidity) }.boxed()); + let _ = inner; + } + + /// Requires a live node; run with: + /// NODE_URL=... cargo test -p price-estimation -- eip4626 --ignored + /// --nocapture + #[tokio::test] + #[ignore] + async fn mainnet_sdai() { + // sDAI on mainnet: vault wrapping DAI + let sdai = alloy::primitives::address!("83F20F44975D03b1b09e64809B757c47f942BEeA"); + + let web3 = ethrpc::Web3::new_from_env(); + + let mut inner = MockNativePriceEstimating::new(); + inner.expect_estimate_native_price().returning(|token, _| { + let dai = alloy::primitives::address!("6B175474E89094C44Da98b954EedeAC495271d0F"); + assert_eq!(token, dai, "should price the underlying DAI, not sDAI"); + async { Ok(3.3e-4_f64) }.boxed() + }); + + let estimator = Eip4626::new(Arc::new(inner), web3.provider); + let price = estimator + .estimate_native_price(sdai, HEALTHY_PRICE_ESTIMATION_TIME) + .await + .unwrap(); + + // sDAI should be worth slightly more than DAI due to accrued interest + println!("sDAI native price: {price}"); + assert!(price > 3.3e-4_f64 * 0.99 && price < 3.3e-4_f64 * 1.20); + } +} diff --git a/crates/price-estimation/src/native/mod.rs b/crates/price-estimation/src/native/mod.rs index f39dd7666c..95338b3562 100644 --- a/crates/price-estimation/src/native/mod.rs +++ b/crates/price-estimation/src/native/mod.rs @@ -13,12 +13,14 @@ use { }; mod coingecko; +mod eip4626; pub mod fallback; mod forwarder; mod oneinch; pub use self::{ coingecko::CoinGecko, + eip4626::Eip4626, fallback::FallbackNativePriceEstimator, forwarder::Forwarder, oneinch::OneInch, From 8717df52e8580d2e1abdf63c2fa9742a7fec71b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Thu, 19 Mar 2026 18:42:13 +0000 Subject: [PATCH 02/20] Make Eip4626 a non-recursive unit variant - Change `Eip4626(Box)` to `Eip4626` unit variant, eliminating recursive type that caused serde to hit the monomorphization recursion limit with `#[serde(tag = "type")]`. - Eip4626 now wraps the next estimator in the config stage list at construction time instead of nesting inside the enum. - Move Eip4626 handling into `create_native_estimator` by passing the stage iterator, removing the special-case in the caller. - Add deserialization validation rejecting Eip4626 as last in a stage. - Use vendored IERC4626 contract binding and query vault decimals for accurate conversion rate. - Fund sDAI whale with ETH in e2e test to fix gas error. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/contracts/artifacts/IERC4626.json | 36 +++++ crates/contracts/build.rs | 1 + .../solidity/interfaces/IERC4626.sol | 8 ++ crates/contracts/src/bin/vendor.rs | 4 + crates/e2e/tests/e2e/eip4626.rs | 127 ++++++++++++++++++ crates/e2e/tests/e2e/main.rs | 1 + crates/price-estimation/src/factory.rs | 42 +++--- crates/price-estimation/src/lib.rs | 36 ++--- crates/price-estimation/src/native/eip4626.rs | 51 +++---- 9 files changed, 238 insertions(+), 68 deletions(-) create mode 100644 crates/contracts/artifacts/IERC4626.json create mode 100644 crates/contracts/solidity/interfaces/IERC4626.sol create mode 100644 crates/e2e/tests/e2e/eip4626.rs diff --git a/crates/contracts/artifacts/IERC4626.json b/crates/contracts/artifacts/IERC4626.json new file mode 100644 index 0000000000..1663281eb2 --- /dev/null +++ b/crates/contracts/artifacts/IERC4626.json @@ -0,0 +1,36 @@ +{ + "abi": [ + { + "inputs": [], + "name": "asset", + "outputs": [ + { + "internalType": "address", + "name": "assetTokenAddress", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "name": "convertToAssets", + "outputs": [ + { + "internalType": "uint256", + "name": "assets", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ] +} diff --git a/crates/contracts/build.rs b/crates/contracts/build.rs index 1bc4994c36..b1d2aeb8c8 100644 --- a/crates/contracts/build.rs +++ b/crates/contracts/build.rs @@ -603,6 +603,7 @@ fn main() { ])) .add_contract(Contract::new("CoWSwapOnchainOrders")) .add_contract(Contract::new("ERC1271SignatureValidator")) + .add_contract(Contract::new("IERC4626")) // Used in the gnosis/solvers repo for the balancer solver .add_contract(Contract::new("BalancerQueries").with_networks(networks![ // diff --git a/crates/contracts/solidity/interfaces/IERC4626.sol b/crates/contracts/solidity/interfaces/IERC4626.sol new file mode 100644 index 0000000000..ca6522cca7 --- /dev/null +++ b/crates/contracts/solidity/interfaces/IERC4626.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +/// @title ERC-4626 vaults interface +interface IERC4626 { + function asset() external view returns (address assetTokenAddress); + function convertToAssets(uint256 shares) external view returns (uint256 assets); +} diff --git a/crates/contracts/src/bin/vendor.rs b/crates/contracts/src/bin/vendor.rs index 2d26a17e27..bc52261e2f 100644 --- a/crates/contracts/src/bin/vendor.rs +++ b/crates/contracts/src/bin/vendor.rs @@ -176,6 +176,10 @@ fn run() -> Result<()> { "ERC1271SignatureValidator", "Manually vendored ABI for ERC-1271 signature validation", ) + .manual( + "IERC4626", + "Manually vendored ABI for ERC-4626 tokenized vault interface", + ) .npm( "IUniswapLikePair", "@uniswap/v2-periphery@1.1.0-beta.0/build/IUniswapV2Pair.json", diff --git a/crates/e2e/tests/e2e/eip4626.rs b/crates/e2e/tests/e2e/eip4626.rs new file mode 100644 index 0000000000..a5293d3b16 --- /dev/null +++ b/crates/e2e/tests/e2e/eip4626.rs @@ -0,0 +1,127 @@ +use { + ::alloy::{ + primitives::{Address, address}, + providers::ext::{AnvilApi, ImpersonateConfig}, + }, + autopilot::config::{Configuration, native_price::NativePriceConfig}, + configs::test_util::TestDefault, + contracts::alloy::ERC20, + e2e::setup::*, + ethrpc::alloy::CallBuilderExt, + model::quote::{OrderQuoteRequest, OrderQuoteSide, SellAmount}, + number::units::EthUnit, + price_estimation::{NativePriceEstimator, NativePriceEstimators}, + shared::web3::Web3, +}; + +/// The block number from which we will fetch state for the forked test. +const FORK_BLOCK_MAINNET: u64 = 23112197; + +/// sDAI (Savings DAI) – an EIP-4626 vault wrapping DAI. +const SDAI: Address = address!("83F20F44975D03b1b09e64809B757c47f942BEeA"); + +/// sDAI whale at [`FORK_BLOCK_MAINNET`]. +const SDAI_WHALE: Address = address!("4C612E3B15b96Ff9A6faED838F8d07d479a8dD4c"); + +/// WETH on mainnet. +const WETH: Address = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); + +#[tokio::test] +#[ignore] +async fn forked_node_mainnet_eip4626_native_price() { + run_forked_test_with_block_number( + eip4626_native_price_test, + std::env::var("FORK_URL_MAINNET") + .expect("FORK_URL_MAINNET must be set to run forked tests"), + FORK_BLOCK_MAINNET, + ) + .await; +} + +async fn eip4626_native_price_test(web3: Web3) { + let mut onchain = OnchainComponents::deployed(web3.clone()).await; + + let [solver] = onchain.make_solvers_forked(1u64.eth()).await; + let [trader] = onchain.make_accounts(1u64.eth()).await; + + let sdai = ERC20::Instance::new(SDAI, web3.provider.clone()); + + // Transfer sDAI from whale to trader. + web3.provider + .anvil_send_impersonated_transaction_with_config( + sdai.transfer(trader.address(), 1000u64.eth()) + .from(SDAI_WHALE) + .into_transaction_request(), + ImpersonateConfig { + fund_amount: Some(1u64.eth()), + stop_impersonate: true, + }, + ) + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + // Approve the vault-relayer for trading. + sdai.approve(onchain.contracts().allowance, 1000u64.eth()) + .from(trader.address()) + .send_and_watch() + .await + .unwrap(); + + // Configure native price estimation with an EIP-4626 wrapper so that the + // protocol can price sDAI by looking up its underlying DAI and applying the + // vault conversion rate. + let driver_url = "http://localhost:11088/test_solver".parse().unwrap(); + let autopilot_config = Configuration { + native_price_estimation: NativePriceConfig { + estimators: NativePriceEstimators::new(vec![vec![ + // Eip4626 wraps the next estimator in the list (test_quoter). + NativePriceEstimator::Eip4626, + NativePriceEstimator::driver("test_quoter".to_string(), driver_url), + // Standalone estimator for non-vault tokens. + NativePriceEstimator::driver( + "test_quoter".to_string(), + "http://localhost:11088/test_solver".parse().unwrap(), + ), + ]]), + ..NativePriceConfig::test_default() + }, + ..Configuration::test("test_solver", solver.address()) + }; + + let services = Services::new(&onchain).await; + services + .start_protocol_with_args( + Default::default(), + autopilot_config, + orderbook::config::Configuration::test_default(), + solver, + ) + .await; + + onchain.mint_block().await; + + // Submit a quote selling sDAI for WETH. If the EIP-4626 native price + // estimator works, the protocol can price sDAI and the quote succeeds. + let quote = services + .submit_quote(&OrderQuoteRequest { + from: trader.address(), + sell_token: SDAI, + buy_token: WETH, + side: OrderQuoteSide::Sell { + sell_amount: SellAmount::BeforeFee { + value: (100u64.eth()).try_into().unwrap(), + }, + }, + ..Default::default() + }) + .await; + + assert!( + quote.is_ok(), + "quote for sDAI should succeed with EIP-4626 native price estimator: {:?}", + quote.err() + ); +} diff --git a/crates/e2e/tests/e2e/main.rs b/crates/e2e/tests/e2e/main.rs index 4d5eeb15da..e902302e2b 100644 --- a/crates/e2e/tests/e2e/main.rs +++ b/crates/e2e/tests/e2e/main.rs @@ -14,6 +14,7 @@ mod cors; mod cow_amm; mod database; mod deprecated_endpoints; +mod eip4626; mod eth_integration; mod eth_safe; mod ethflow; diff --git a/crates/price-estimation/src/factory.rs b/crates/price-estimation/src/factory.rs index 396f24d287..36f17b1b76 100644 --- a/crates/price-estimation/src/factory.rs +++ b/crates/price-estimation/src/factory.rs @@ -169,9 +169,10 @@ impl<'a> PriceEstimatorFactory<'a> { }) } - async fn create_native_estimator( + async fn create_native_estimator<'b>( &mut self, source: &NativePriceEstimatorSource, + rest: &mut impl Iterator, weth: &WETH9::Instance, ) -> Result<(String, Arc)> { match source { @@ -266,10 +267,20 @@ impl<'a> PriceEstimatorFactory<'a> { Ok((name, coin_gecko)) } - // Eip4626 wraps another estimator and is handled at the call site - // to avoid recursive async fn. - NativePriceEstimatorSource::Eip4626(_) => { - unreachable!("Eip4626 is handled before calling create_native_estimator") + NativePriceEstimatorSource::Eip4626 => { + let next = rest + .next() + .context("Eip4626 must be followed by another estimator in the same stage")?; + let (inner_name, inner) = + Box::pin(self.create_native_estimator(next, rest, weth)).await?; + let name = format!("Eip4626|{inner_name}"); + Ok(( + name.clone(), + Arc::new(InstrumentedPriceEstimator::new( + native::Eip4626::new(inner, self.network.web3.provider.clone()), + name, + )), + )) } } } @@ -370,21 +381,12 @@ impl<'a> PriceEstimatorFactory<'a> { let mut estimators = Vec::with_capacity(native.len()); for stage in native.iter() { let mut stages = Vec::with_capacity(stage.len()); - for source in stage { - let entry = if let NativePriceEstimatorSource::Eip4626(inner_source) = source { - let name = format!("Eip4626|{inner_source}"); - let (_, inner) = self.create_native_estimator(inner_source, weth).await?; - ( - name.clone(), - Arc::new(InstrumentedPriceEstimator::new( - native::Eip4626::new(inner, self.network.web3.provider.clone()), - name, - )) as Arc, - ) - } else { - self.create_native_estimator(source, weth).await? - }; - stages.push(entry); + let mut iter = stage.iter(); + while let Some(source) = iter.next() { + stages.push( + self.create_native_estimator(source, &mut iter, weth) + .await?, + ); } estimators.push(stages); } diff --git a/crates/price-estimation/src/lib.rs b/crates/price-estimation/src/lib.rs index 297e6b95d4..6961526e05 100644 --- a/crates/price-estimation/src/lib.rs +++ b/crates/price-estimation/src/lib.rs @@ -55,17 +55,20 @@ impl<'de> Deserialize<'de> for NativePriceEstimators { &"expected native price estimator stages to be configured", )); } - match estimators - .iter() - .enumerate() - .find_map(|(n, stage)| stage.is_empty().then_some(n)) - { - Some(n) => Err(serde::de::Error::invalid_length( - 0, - &format!("stage {} is empty, all stages must not be empty", n).as_str(), - )), - None => Ok(Self(estimators)), + for (n, stage) in estimators.iter().enumerate() { + if stage.is_empty() { + return Err(serde::de::Error::invalid_length( + 0, + &format!("stage {} is empty, all stages must not be empty", n).as_str(), + )); + } + if matches!(stage.last(), Some(NativePriceEstimator::Eip4626)) { + return Err(serde::de::Error::custom(format!( + "stage {n}: Eip4626 must be followed by another estimator" + ))); + } } + Ok(Self(estimators)) } } @@ -117,9 +120,9 @@ pub enum NativePriceEstimator { OneInchSpotPriceApi, CoinGecko, /// Prices EIP-4626 vault tokens by looking up the underlying `asset()` and - /// applying `convertToAssets()` as a conversion rate. The inner estimator - /// is used to price the underlying asset. - Eip4626(Box), + /// applying `convertToAssets()` as a conversion rate. At construction time, + /// wraps the next estimator in the configuration list. + Eip4626, } impl NativePriceEstimator { @@ -139,7 +142,7 @@ impl Display for NativePriceEstimator { NativePriceEstimator::Forwarder { url } => format!("Forwarder|{}", url), NativePriceEstimator::OneInchSpotPriceApi => "OneInchSpotPriceApi".into(), NativePriceEstimator::CoinGecko => "CoinGecko".into(), - NativePriceEstimator::Eip4626(inner) => format!("Eip4626|{inner}"), + NativePriceEstimator::Eip4626 => "Eip4626".into(), }; write!(f, "{formatter}") } @@ -199,10 +202,7 @@ impl FromStr for NativePriceEstimator { .parse() .context("Forwarder price estimator invalid URL")?, }), - "Eip4626" => Ok(NativePriceEstimator::Eip4626(Box::new( - NativePriceEstimator::from_str(args) - .context("Eip4626 inner estimator failed to parse")?, - ))), + "Eip4626" => Ok(NativePriceEstimator::Eip4626), _ => Err(anyhow::anyhow!("unsupported native price estimator: {}", s)), } } diff --git a/crates/price-estimation/src/native/eip4626.rs b/crates/price-estimation/src/native/eip4626.rs index 3d9be6d228..dc5cfa3e2e 100644 --- a/crates/price-estimation/src/native/eip4626.rs +++ b/crates/price-estimation/src/native/eip4626.rs @@ -1,8 +1,9 @@ use { super::{NativePriceEstimateResult, NativePriceEstimating}, crate::PriceEstimationError, - alloy::primitives::{Address, U256, uint}, + alloy::primitives::{Address, U256}, anyhow::Context, + contracts::alloy::{ERC20, IERC4626}, ethrpc::AlloyProvider, futures::{FutureExt, future::BoxFuture}, num::ToPrimitive, @@ -10,18 +11,11 @@ use { std::{sync::Arc, time::Duration}, }; -alloy::sol! { - #[sol(rpc)] - interface IERC4626 { - function asset() external view returns (address assetTokenAddress); - function convertToAssets(uint256 shares) external view returns (uint256 assets); - } -} - /// Estimates the native price of EIP-4626 vault tokens by: /// 1. Calling `asset()` to find the underlying token -/// 2. Calling `convertToAssets(1e18)` to find the conversion rate -/// 3. Delegating to an inner estimator for the underlying token's native price +/// 2. Calling `decimals()` to determine the vault's precision +/// 3. Calling `convertToAssets(10^decimals)` to find the conversion rate +/// 4. Delegating to an inner estimator for the underlying token's native price pub struct Eip4626 { inner: Arc, provider: AlloyProvider, @@ -33,7 +27,8 @@ impl Eip4626 { } async fn estimate(&self, token: Address, timeout: Duration) -> NativePriceEstimateResult { - let vault = IERC4626::new(token, self.provider.clone()); + let vault = IERC4626::Instance::new(token, self.provider.clone()); + let erc20 = ERC20::Instance::new(token, self.provider.clone()); let asset: Address = vault.asset().call().await.map_err(|e| { PriceEstimationError::EstimatorInternal(anyhow::anyhow!( @@ -41,23 +36,27 @@ impl Eip4626 { )) })?; - // Use 1e18 shares as the reference amount. This works correctly for - // vaults with 18 decimals. For other decimals the rate is still a - // reasonable approximation since convertToAssets is linear. - let shares = uint!(1_000_000_000_000_000_000_U256); + let decimals: u8 = erc20.decimals().call().await.map_err(|e| { + PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "failed to call decimals() on {token}: {e}" + )) + })?; + + let shares = U256::from(10u64).pow(U256::from(decimals)); + let assets: U256 = vault.convertToAssets(shares).call().await.map_err(|e| { PriceEstimationError::EstimatorInternal(anyhow::anyhow!( "failed to call convertToAssets() on {token}: {e}" )) })?; + let asset_price = self.inner.estimate_native_price(asset, timeout).await?; + let rate = (u256_to_big_rational(&assets) / u256_to_big_rational(&shares)) .to_f64() .context("conversion rate is not representable as f64") .map_err(PriceEstimationError::EstimatorInternal)?; - let asset_price = self.inner.estimate_native_price(asset, timeout).await?; - Ok(asset_price * rate) } } @@ -81,24 +80,16 @@ mod tests { #[test] fn rate_math() { - // 1 vault share = 1.5 underlying tokens (e.g. rebasing vault) - let shares = uint!(1_000_000_000_000_000_000_U256); - let assets = uint!(1_500_000_000_000_000_000_U256); + // 6-decimal vault where 1 share = 1.5 underlying tokens + let decimals = 6u8; + let shares = U256::from(10u64).pow(U256::from(decimals)); + let assets = U256::from(1_500_000u64); // 1.5 * 10^6 let rate = (u256_to_big_rational(&assets) / u256_to_big_rational(&shares)) .to_f64() .unwrap(); assert!((rate - 1.5).abs() < 1e-9); } - #[tokio::test] - async fn delegates_to_inner_on_error() { - let mut inner = MockNativePriceEstimating::new(); - inner - .expect_estimate_native_price() - .returning(|_, _| async { Err(PriceEstimationError::NoLiquidity) }.boxed()); - let _ = inner; - } - /// Requires a live node; run with: /// NODE_URL=... cargo test -p price-estimation -- eip4626 --ignored /// --nocapture From 6edb4032d427f8d3bb3d4f703893b95a354f5c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:54:21 +0100 Subject: [PATCH 03/20] Add EIP-4626 vault token native price estimator Introduces a new native price estimator that prices EIP-4626 vault tokens by querying the vault's underlying asset and conversion rate, then delegating to an inner estimator for the underlying token's price. Includes IERC4626 contract bindings and resolves merge conflicts with the contracts crate refactor. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 12 + contracts/artifacts/IERC4626.json | 24 +- .../generated/contracts-facade/Cargo.toml | 1 + .../generated/contracts-facade/src/lib.rs | 1 + .../contracts-generated/ierc4626/Cargo.toml | 19 + .../contracts-generated/ierc4626/src/lib.rs | 648 ++++++++++++++++++ contracts/src/main.rs | 1 + crates/configs/src/native_price_estimators.rs | 5 + crates/price-estimation/src/native/eip4626.rs | 17 +- 9 files changed, 710 insertions(+), 18 deletions(-) create mode 100644 contracts/generated/contracts-generated/ierc4626/Cargo.toml create mode 100644 contracts/generated/contracts-generated/ierc4626/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 89b5c56454..dcb7c4b366 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2537,6 +2537,7 @@ dependencies = [ "cow-contract-honeyswaprouter", "cow-contract-hookstrampoline", "cow-contract-icowwrapper", + "cow-contract-ierc4626", "cow-contract-iswaprpair", "cow-contract-iuniswaplikepair", "cow-contract-iuniswaplikerouter", @@ -3214,6 +3215,17 @@ dependencies = [ "anyhow", ] +[[package]] +name = "cow-contract-ierc4626" +version = "0.1.0" +dependencies = [ + "alloy-contract", + "alloy-primitives", + "alloy-provider", + "alloy-sol-types", + "anyhow", +] + [[package]] name = "cow-contract-iswaprpair" version = "0.1.0" diff --git a/contracts/artifacts/IERC4626.json b/contracts/artifacts/IERC4626.json index 1663281eb2..2d0eac6929 100644 --- a/contracts/artifacts/IERC4626.json +++ b/contracts/artifacts/IERC4626.json @@ -1,36 +1,36 @@ { "abi": [ { - "inputs": [], + "type": "function", "name": "asset", + "inputs": [], "outputs": [ { - "internalType": "address", "name": "assetTokenAddress", - "type": "address" + "type": "address", + "internalType": "address" } ], - "stateMutability": "view", - "type": "function" + "stateMutability": "view" }, { + "type": "function", + "name": "convertToAssets", "inputs": [ { - "internalType": "uint256", "name": "shares", - "type": "uint256" + "type": "uint256", + "internalType": "uint256" } ], - "name": "convertToAssets", "outputs": [ { - "internalType": "uint256", "name": "assets", - "type": "uint256" + "type": "uint256", + "internalType": "uint256" } ], - "stateMutability": "view", - "type": "function" + "stateMutability": "view" } ] } diff --git a/contracts/generated/contracts-facade/Cargo.toml b/contracts/generated/contracts-facade/Cargo.toml index ca824f689c..7408bce369 100644 --- a/contracts/generated/contracts-facade/Cargo.toml +++ b/contracts/generated/contracts-facade/Cargo.toml @@ -59,6 +59,7 @@ cow-contract-gpv2settlement = { path = "../contracts-generated/gpv2settlement" } cow-contract-honeyswaprouter = { path = "../contracts-generated/honeyswaprouter" } cow-contract-hookstrampoline = { path = "../contracts-generated/hookstrampoline" } cow-contract-icowwrapper = { path = "../contracts-generated/icowwrapper" } +cow-contract-ierc4626 = { path = "../contracts-generated/ierc4626" } cow-contract-iswaprpair = { path = "../contracts-generated/iswaprpair" } cow-contract-iuniswaplikepair = { path = "../contracts-generated/iuniswaplikepair" } cow-contract-iuniswaplikerouter = { path = "../contracts-generated/iuniswaplikerouter" } diff --git a/contracts/generated/contracts-facade/src/lib.rs b/contracts/generated/contracts-facade/src/lib.rs index 91abfdf567..6c5154a801 100644 --- a/contracts/generated/contracts-facade/src/lib.rs +++ b/contracts/generated/contracts-facade/src/lib.rs @@ -48,6 +48,7 @@ pub use { cow_contract_honeyswaprouter as HoneyswapRouter, cow_contract_hookstrampoline as HooksTrampoline, cow_contract_icowwrapper as ICowWrapper, + cow_contract_ierc4626 as IERC4626, cow_contract_iswaprpair as ISwaprPair, cow_contract_iuniswaplikepair as IUniswapLikePair, cow_contract_iuniswaplikerouter as IUniswapLikeRouter, diff --git a/contracts/generated/contracts-generated/ierc4626/Cargo.toml b/contracts/generated/contracts-generated/ierc4626/Cargo.toml new file mode 100644 index 0000000000..c25bc58247 --- /dev/null +++ b/contracts/generated/contracts-generated/ierc4626/Cargo.toml @@ -0,0 +1,19 @@ +# Auto-generated by contracts-generate. Do not edit. +[package] +name = "cow-contract-ierc4626" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +doctest = false + +[dependencies] +alloy-primitives = { workspace = true } +alloy-sol-types = { workspace = true } +alloy-contract = { workspace = true } +alloy-provider = { workspace = true } +anyhow = { workspace = true } + +[lints] +workspace = true diff --git a/contracts/generated/contracts-generated/ierc4626/src/lib.rs b/contracts/generated/contracts-generated/ierc4626/src/lib.rs new file mode 100644 index 0000000000..11f48918c2 --- /dev/null +++ b/contracts/generated/contracts-generated/ierc4626/src/lib.rs @@ -0,0 +1,648 @@ +#![allow( + unused_imports, + unused_attributes, + clippy::all, + rustdoc::all, + non_snake_case +)] +//! Auto-generated contract bindings. Do not edit. +/** + +Generated by the following Solidity interface... +```solidity +interface IERC4626 { + function asset() external view returns (address assetTokenAddress); + function convertToAssets(uint256 shares) external view returns (uint256 assets); +} +``` + +...which was generated by the following JSON ABI: +```json +[ + { + "type": "function", + "name": "asset", + "inputs": [], + "outputs": [ + { + "name": "assetTokenAddress", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "convertToAssets", + "inputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + } +] +```*/ +#[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style, + clippy::empty_structs_with_brackets +)] +pub mod IERC4626 { + use {super::*, alloy_sol_types}; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `asset()` and selector `0x38d52e0f`. + ```solidity + function asset() external view returns (address assetTokenAddress); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct assetCall; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the [`asset()`](assetCall) + /// function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct assetReturn { + #[allow(missing_docs)] + pub assetTokenAddress: alloy_sol_types::private::Address, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: assetCall) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for assetCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Address,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy_sol_types::private::Address,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: assetReturn) -> Self { + (value.assetTokenAddress,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for assetReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { + assetTokenAddress: tuple.0, + } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for assetCall { + type Parameters<'a> = (); + type Return = alloy_sol_types::private::Address; + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + type ReturnTuple<'a> = (alloy_sol_types::sol_data::Address,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + const SELECTOR: [u8; 4] = [56u8, 213u8, 46u8, 15u8]; + const SIGNATURE: &'static str = "asset()"; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + () + } + + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + (::tokenize(ret),) + } + + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data).map( + |r| { + let r: assetReturn = r.into(); + r.assetTokenAddress + }, + ) + } + + #[inline] + fn abi_decode_returns_validate(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate( + data, + ) + .map(|r| { + let r: assetReturn = r.into(); + r.assetTokenAddress + }) + } + } + }; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `convertToAssets(uint256)` and selector `0x07a2d13a`. + ```solidity + function convertToAssets(uint256 shares) external view returns (uint256 assets); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct convertToAssetsCall { + #[allow(missing_docs)] + pub shares: alloy_sol_types::private::primitives::aliases::U256, + } + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the + /// [`convertToAssets(uint256)`](convertToAssetsCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct convertToAssetsReturn { + #[allow(missing_docs)] + pub assets: alloy_sol_types::private::primitives::aliases::U256, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy_sol_types::private::primitives::aliases::U256,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: convertToAssetsCall) -> Self { + (value.shares,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for convertToAssetsCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { shares: tuple.0 } + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy_sol_types::private::primitives::aliases::U256,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: convertToAssetsReturn) -> Self { + (value.assets,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for convertToAssetsReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { assets: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for convertToAssetsCall { + type Parameters<'a> = (alloy_sol_types::sol_data::Uint<256>,); + type Return = alloy_sol_types::private::primitives::aliases::U256; + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + type ReturnTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + const SELECTOR: [u8; 4] = [7u8, 162u8, 209u8, 58u8]; + const SIGNATURE: &'static str = "convertToAssets(uint256)"; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + as alloy_sol_types::SolType>::tokenize( + &self.shares, + ), + ) + } + + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + ( + as alloy_sol_types::SolType>::tokenize( + ret, + ), + ) + } + + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data).map( + |r| { + let r: convertToAssetsReturn = r.into(); + r.assets + }, + ) + } + + #[inline] + fn abi_decode_returns_validate(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate( + data, + ) + .map(|r| { + let r: convertToAssetsReturn = r.into(); + r.assets + }) + } + } + }; + ///Container for all the [`IERC4626`](self) function calls. + #[derive(Clone)] + pub enum IERC4626Calls { + #[allow(missing_docs)] + asset(assetCall), + #[allow(missing_docs)] + convertToAssets(convertToAssetsCall), + } + impl IERC4626Calls { + /// All the selectors of this enum. + /// + /// Note that the selectors might not be in the same order as the + /// variants. No guarantees are made about the order of the + /// selectors. + /// + /// Prefer using `SolInterface` methods instead. + pub const SELECTORS: &'static [[u8; 4usize]] = + &[[7u8, 162u8, 209u8, 58u8], [56u8, 213u8, 46u8, 15u8]]; + /// The signatures in the same order as `SELECTORS`. + pub const SIGNATURES: &'static [&'static str] = &[ + ::SIGNATURE, + ::SIGNATURE, + ]; + /// The names of the variants in the same order as `SELECTORS`. + pub const VARIANT_NAMES: &'static [&'static str] = &[ + ::core::stringify!(convertToAssets), + ::core::stringify!(asset), + ]; + + /// Returns the signature for the given selector, if known. + #[inline] + pub fn signature_by_selector( + selector: [u8; 4usize], + ) -> ::core::option::Option<&'static str> { + match Self::SELECTORS.binary_search(&selector) { + ::core::result::Result::Ok(idx) => { + ::core::option::Option::Some(Self::SIGNATURES[idx]) + } + ::core::result::Result::Err(_) => ::core::option::Option::None, + } + } + + /// Returns the enum variant name for the given selector, if known. + #[inline] + pub fn name_by_selector(selector: [u8; 4usize]) -> ::core::option::Option<&'static str> { + let sig = Self::signature_by_selector(selector)?; + sig.split_once('(').map(|(name, _)| name) + } + } + #[automatically_derived] + impl alloy_sol_types::SolInterface for IERC4626Calls { + const COUNT: usize = 2usize; + const MIN_DATA_LENGTH: usize = 0usize; + const NAME: &'static str = "IERC4626Calls"; + + #[inline] + fn selector(&self) -> [u8; 4] { + match self { + Self::asset(_) => ::SELECTOR, + Self::convertToAssets(_) => { + ::SELECTOR + } + } + } + + #[inline] + fn selector_at(i: usize) -> ::core::option::Option<[u8; 4]> { + Self::SELECTORS.get(i).copied() + } + + #[inline] + fn valid_selector(selector: [u8; 4]) -> bool { + Self::SELECTORS.binary_search(&selector).is_ok() + } + + #[inline] + #[allow(non_snake_case)] + fn abi_decode_raw(selector: [u8; 4], data: &[u8]) -> alloy_sol_types::Result { + static DECODE_SHIMS: &[fn(&[u8]) -> alloy_sol_types::Result] = &[ + { + fn convertToAssets(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(IERC4626Calls::convertToAssets) + } + convertToAssets + }, + { + fn asset(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(IERC4626Calls::asset) + } + asset + }, + ]; + let Ok(idx) = Self::SELECTORS.binary_search(&selector) else { + return Err(alloy_sol_types::Error::unknown_selector( + ::NAME, + selector, + )); + }; + DECODE_SHIMS[idx](data) + } + + #[inline] + #[allow(non_snake_case)] + fn abi_decode_raw_validate( + selector: [u8; 4], + data: &[u8], + ) -> alloy_sol_types::Result { + static DECODE_VALIDATE_SHIMS: &[fn(&[u8]) -> alloy_sol_types::Result] = + &[ + { + fn convertToAssets(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(IERC4626Calls::convertToAssets) + } + convertToAssets + }, + { + fn asset(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw_validate(data) + .map(IERC4626Calls::asset) + } + asset + }, + ]; + let Ok(idx) = Self::SELECTORS.binary_search(&selector) else { + return Err(alloy_sol_types::Error::unknown_selector( + ::NAME, + selector, + )); + }; + DECODE_VALIDATE_SHIMS[idx](data) + } + + #[inline] + fn abi_encoded_size(&self) -> usize { + match self { + Self::asset(inner) => { + ::abi_encoded_size(inner) + } + Self::convertToAssets(inner) => { + ::abi_encoded_size(inner) + } + } + } + + #[inline] + fn abi_encode_raw(&self, out: &mut alloy_sol_types::private::Vec) { + match self { + Self::asset(inner) => { + ::abi_encode_raw(inner, out) + } + Self::convertToAssets(inner) => { + ::abi_encode_raw(inner, out) + } + } + } + } + use alloy_contract; + /**Creates a new wrapper around an on-chain [`IERC4626`](self) contract instance. + + See the [wrapper's documentation](`IERC4626Instance`) for more details.*/ + #[inline] + pub const fn new< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + >( + address: alloy_sol_types::private::Address, + __provider: P, + ) -> IERC4626Instance { + IERC4626Instance::::new(address, __provider) + } + /**A [`IERC4626`](self) instance. + + Contains type-safe methods for interacting with an on-chain instance of the + [`IERC4626`](self) contract located at a given `address`, using a given + provider `P`. + + If the contract bytecode is available (see the [`sol!`](alloy_sol_types::sol!) + documentation on how to provide it), the `deploy` and `deploy_builder` methods can + be used to deploy a new instance of the contract. + + See the [module-level documentation](self) for all the available methods.*/ + #[derive(Clone)] + pub struct IERC4626Instance { + address: alloy_sol_types::private::Address, + provider: P, + _network: ::core::marker::PhantomData, + } + #[automatically_derived] + impl ::core::fmt::Debug for IERC4626Instance { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_tuple("IERC4626Instance") + .field(&self.address) + .finish() + } + } + /// Instantiation and getters/setters. + impl, N: alloy_contract::private::Network> + IERC4626Instance + { + /**Creates a new wrapper around an on-chain [`IERC4626`](self) contract instance. + + See the [wrapper's documentation](`IERC4626Instance`) for more details.*/ + #[inline] + pub const fn new(address: alloy_sol_types::private::Address, __provider: P) -> Self { + Self { + address, + provider: __provider, + _network: ::core::marker::PhantomData, + } + } + + /// Returns a reference to the address. + #[inline] + pub const fn address(&self) -> &alloy_sol_types::private::Address { + &self.address + } + + /// Sets the address. + #[inline] + pub fn set_address(&mut self, address: alloy_sol_types::private::Address) { + self.address = address; + } + + /// Sets the address and returns `self`. + pub fn at(mut self, address: alloy_sol_types::private::Address) -> Self { + self.set_address(address); + self + } + + /// Returns a reference to the provider. + #[inline] + pub const fn provider(&self) -> &P { + &self.provider + } + } + impl IERC4626Instance<&P, N> { + /// Clones the provider and returns a new instance with the cloned + /// provider. + #[inline] + pub fn with_cloned_provider(self) -> IERC4626Instance { + IERC4626Instance { + address: self.address, + provider: ::core::clone::Clone::clone(&self.provider), + _network: ::core::marker::PhantomData, + } + } + } + /// Function calls. + impl, N: alloy_contract::private::Network> + IERC4626Instance + { + /// Creates a new call builder using this contract instance's provider + /// and address. + /// + /// Note that the call can be any function call, not just those defined + /// in this contract. Prefer using the other methods for + /// building type-safe contract calls. + pub fn call_builder( + &self, + call: &C, + ) -> alloy_contract::SolCallBuilder<&P, C, N> { + alloy_contract::SolCallBuilder::new_sol(&self.provider, &self.address, call) + } + + ///Creates a new call builder for the [`asset`] function. + pub fn asset(&self) -> alloy_contract::SolCallBuilder<&P, assetCall, N> { + self.call_builder(&assetCall) + } + + ///Creates a new call builder for the [`convertToAssets`] function. + pub fn convertToAssets( + &self, + shares: alloy_sol_types::private::primitives::aliases::U256, + ) -> alloy_contract::SolCallBuilder<&P, convertToAssetsCall, N> { + self.call_builder(&convertToAssetsCall { shares }) + } + } + /// Event filters. + impl, N: alloy_contract::private::Network> + IERC4626Instance + { + /// Creates a new event filter using this contract instance's provider + /// and address. + /// + /// Note that the type can be any event, not just those defined in this + /// contract. Prefer using the other methods for building + /// type-safe event filters. + pub fn event_filter( + &self, + ) -> alloy_contract::Event<&P, E, N> { + alloy_contract::Event::new_sol(&self.provider, &self.address) + } + } +} +pub type Instance = IERC4626::IERC4626Instance<::alloy_provider::DynProvider>; diff --git a/contracts/src/main.rs b/contracts/src/main.rs index 34ec14364c..9ff18b87f7 100644 --- a/contracts/src/main.rs +++ b/contracts/src/main.rs @@ -105,6 +105,7 @@ fn build_module() -> Module { // Misc .add_contract(Contract::new("ERC20")) .add_contract(Contract::new("ERC20Mintable")) + .add_contract(Contract::new("IERC4626")) // GnosisSafe .add_contract(Contract::new("GnosisSafe")) .add_contract(Contract::new("GnosisSafeCompatibilityFallbackHandler")) diff --git a/crates/configs/src/native_price_estimators.rs b/crates/configs/src/native_price_estimators.rs index 52577ef818..3fddb94326 100644 --- a/crates/configs/src/native_price_estimators.rs +++ b/crates/configs/src/native_price_estimators.rs @@ -96,6 +96,10 @@ pub enum NativePriceEstimator { OneInchSpotPriceApi, /// Use the CoinGecko API. CoinGecko, + /// Prices EIP-4626 vault tokens by looking up the underlying `asset()` and + /// applying `convertToAssets()` as a conversion rate. Must be followed by + /// another estimator in the same stage to price the underlying asset. + Eip4626, } impl NativePriceEstimator { @@ -115,6 +119,7 @@ impl Display for NativePriceEstimator { NativePriceEstimator::Forwarder { url } => write!(f, "Forwarder|{}", url), NativePriceEstimator::OneInchSpotPriceApi => write!(f, "OneInchSpotPriceApi"), NativePriceEstimator::CoinGecko => write!(f, "CoinGecko"), + NativePriceEstimator::Eip4626 => write!(f, "Eip4626"), } } } diff --git a/crates/price-estimation/src/native/eip4626.rs b/crates/price-estimation/src/native/eip4626.rs index dc5cfa3e2e..fb8e45a005 100644 --- a/crates/price-estimation/src/native/eip4626.rs +++ b/crates/price-estimation/src/native/eip4626.rs @@ -12,10 +12,9 @@ use { }; /// Estimates the native price of EIP-4626 vault tokens by: -/// 1. Calling `asset()` to find the underlying token -/// 2. Calling `decimals()` to determine the vault's precision -/// 3. Calling `convertToAssets(10^decimals)` to find the conversion rate -/// 4. Delegating to an inner estimator for the underlying token's native price +/// 1. Calling `asset()` and `decimals()` in parallel +/// 2. Calling `convertToAssets(10^decimals)` to find the conversion rate +/// 3. Delegating to an inner estimator for the underlying token's native price pub struct Eip4626 { inner: Arc, provider: AlloyProvider, @@ -30,13 +29,19 @@ impl Eip4626 { let vault = IERC4626::Instance::new(token, self.provider.clone()); let erc20 = ERC20::Instance::new(token, self.provider.clone()); - let asset: Address = vault.asset().call().await.map_err(|e| { + // Fetch asset address and vault decimals in parallel. + let asset_builder = vault.asset(); + let decimals_builder = erc20.decimals(); + let (asset_result, decimals_result) = + tokio::join!(asset_builder.call(), decimals_builder.call()); + + let asset: Address = asset_result.map_err(|e| { PriceEstimationError::EstimatorInternal(anyhow::anyhow!( "failed to call asset() on {token}: {e}" )) })?; - let decimals: u8 = erc20.decimals().call().await.map_err(|e| { + let decimals: u8 = decimals_result.map_err(|e| { PriceEstimationError::EstimatorInternal(anyhow::anyhow!( "failed to call decimals() on {token}: {e}" )) From b18253692aeb26b9ad02382d050e806a404162dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Thu, 9 Apr 2026 20:02:22 +0100 Subject: [PATCH 04/20] Fix compilation errors and recursive e2e test for EIP-4626 native price Remove duplicate type definitions from price-estimation/src/lib.rs that conflicted with the canonical definitions in the configs crate. Add the Eip4626 validation (must not be last in a stage) to the configs crate deserializer. Fix e2e test imports to use configs crate paths and correct start_protocol_with_args signature. Change recursive vault test to query native price directly instead of submitting a quote, since the freshly deployed mock wrapper has no DEX liquidity. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 12 + contracts/.gitignore | 2 + contracts/artifacts/MockERC4626Wrapper.json | 204 ++ .../generated/contracts-facade/Cargo.toml | 1 + .../generated/contracts-facade/src/lib.rs | 1 + .../contracts-generated/ierc4626/Cargo.toml | 4 +- .../mockerc4626wrapper/Cargo.toml | 19 + .../mockerc4626wrapper/src/lib.rs | 2320 +++++++++++++++++ .../solidity/tests/MockERC4626Wrapper.sol | 52 + contracts/src/main.rs | 1 + crates/configs/src/native_price_estimators.rs | 23 +- crates/e2e/tests/e2e/eip4626.rs | 95 +- crates/price-estimation/src/lib.rs | 438 ---- crates/price-estimation/src/native/eip4626.rs | 2 +- 14 files changed, 2717 insertions(+), 457 deletions(-) create mode 100644 contracts/artifacts/MockERC4626Wrapper.json create mode 100644 contracts/generated/contracts-generated/mockerc4626wrapper/Cargo.toml create mode 100644 contracts/generated/contracts-generated/mockerc4626wrapper/src/lib.rs create mode 100644 contracts/solidity/tests/MockERC4626Wrapper.sol diff --git a/Cargo.lock b/Cargo.lock index dcb7c4b366..6485a95539 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2544,6 +2544,7 @@ dependencies = [ "cow-contract-iuniswapv3factory", "cow-contract-izeroex", "cow-contract-liquoricesettlement", + "cow-contract-mockerc4626wrapper", "cow-contract-nonstandarderc20balances", "cow-contract-pancakerouter", "cow-contract-permit2", @@ -3292,6 +3293,17 @@ dependencies = [ "anyhow", ] +[[package]] +name = "cow-contract-mockerc4626wrapper" +version = "0.1.0" +dependencies = [ + "alloy-contract", + "alloy-primitives", + "alloy-provider", + "alloy-sol-types", + "anyhow", +] + [[package]] name = "cow-contract-nonstandarderc20balances" version = "0.1.0" diff --git a/contracts/.gitignore b/contracts/.gitignore index 05923927ff..c0388e3cc4 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -1,2 +1,4 @@ /target +/out +/cache .DS_Store diff --git a/contracts/artifacts/MockERC4626Wrapper.json b/contracts/artifacts/MockERC4626Wrapper.json new file mode 100644 index 0000000000..93c9644359 --- /dev/null +++ b/contracts/artifacts/MockERC4626Wrapper.json @@ -0,0 +1,204 @@ +{ + "abi": [ + { + "type": "constructor", + "inputs": [ + { + "name": "_asset", + "type": "address", + "internalType": "address" + }, + { + "name": "_decimals", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "allowance", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "approve", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "asset", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "convertToAssets", + "inputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "decimals", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "mint", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transfer", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + } + ], + "bytecode": "0x60c060405234801561000f575f5ffd5b506040516109fd3803806109fd8339818101604052810190610031919061010e565b8173ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff16815250508060ff1660a08160ff1681525050505061014c565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100a78261007e565b9050919050565b6100b78161009d565b81146100c1575f5ffd5b50565b5f815190506100d2816100ae565b92915050565b5f60ff82169050919050565b6100ed816100d8565b81146100f7575f5ffd5b50565b5f81519050610108816100e4565b92915050565b5f5f604083850312156101245761012361007a565b5b5f610131858286016100c4565b9250506020610142858286016100fa565b9150509250929050565b60805160a05161089061016d5f395f6103de01525f61040201526108905ff3fe608060405234801561000f575f5ffd5b5060043610610091575f3560e01c806338d52e0f1161006457806338d52e0f1461014357806340c10f191461016157806370a082311461017d578063a9059cbb146101ad578063dd62ed3e146101dd57610091565b806307a2d13a14610095578063095ea7b3146100c557806323b872dd146100f5578063313ce56714610125575b5f5ffd5b6100af60048036038101906100aa9190610594565b61020d565b6040516100bc91906105ce565b60405180910390f35b6100df60048036038101906100da9190610641565b610216565b6040516100ec9190610699565b60405180910390f35b61010f600480360381019061010a91906106b2565b61029e565b60405161011c9190610699565b60405180910390f35b61012d6103dc565b60405161013a919061071d565b60405180910390f35b61014b610400565b6040516101589190610745565b60405180910390f35b61017b60048036038101906101769190610641565b610424565b005b6101976004803603810190610192919061075e565b61047a565b6040516101a491906105ce565b60405180910390f35b6101c760048036038101906101c29190610641565b61048e565b6040516101d49190610699565b60405180910390f35b6101f760048036038101906101f29190610789565b61053d565b60405161020491906105ce565b60405180910390f35b5f819050919050565b5f8160015f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20819055506001905092915050565b5f8160015f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461032691906107f4565b92505081905550815f5f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461037891906107f4565b92505081905550815f5f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546103ca9190610827565b92505081905550600190509392505050565b7f000000000000000000000000000000000000000000000000000000000000000081565b7f000000000000000000000000000000000000000000000000000000000000000081565b805f5f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461046f9190610827565b925050819055505050565b5f602052805f5260405f205f915090505481565b5f815f5f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546104da91906107f4565b92505081905550815f5f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461052c9190610827565b925050819055506001905092915050565b6001602052815f5260405f20602052805f5260405f205f91509150505481565b5f5ffd5b5f819050919050565b61057381610561565b811461057d575f5ffd5b50565b5f8135905061058e8161056a565b92915050565b5f602082840312156105a9576105a861055d565b5b5f6105b684828501610580565b91505092915050565b6105c881610561565b82525050565b5f6020820190506105e15f8301846105bf565b92915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610610826105e7565b9050919050565b61062081610606565b811461062a575f5ffd5b50565b5f8135905061063b81610617565b92915050565b5f5f604083850312156106575761065661055d565b5b5f6106648582860161062d565b925050602061067585828601610580565b9150509250929050565b5f8115159050919050565b6106938161067f565b82525050565b5f6020820190506106ac5f83018461068a565b92915050565b5f5f5f606084860312156106c9576106c861055d565b5b5f6106d68682870161062d565b93505060206106e78682870161062d565b92505060406106f886828701610580565b9150509250925092565b5f60ff82169050919050565b61071781610702565b82525050565b5f6020820190506107305f83018461070e565b92915050565b61073f81610606565b82525050565b5f6020820190506107585f830184610736565b92915050565b5f602082840312156107735761077261055d565b5b5f6107808482850161062d565b91505092915050565b5f5f6040838503121561079f5761079e61055d565b5b5f6107ac8582860161062d565b92505060206107bd8582860161062d565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6107fe82610561565b915061080983610561565b9250828203905081811115610821576108206107c7565b5b92915050565b5f61083182610561565b915061083c83610561565b9250828201905080821115610854576108536107c7565b5b9291505056fea2646970667358221220c7a63d9ea3fa2de6cae454642f51c1350392365b83fc5b1f47f3f63c3b67905064736f6c634300081e0033" +} diff --git a/contracts/generated/contracts-facade/Cargo.toml b/contracts/generated/contracts-facade/Cargo.toml index 7408bce369..902099c993 100644 --- a/contracts/generated/contracts-facade/Cargo.toml +++ b/contracts/generated/contracts-facade/Cargo.toml @@ -66,6 +66,7 @@ cow-contract-iuniswaplikerouter = { path = "../contracts-generated/iuniswapliker cow-contract-iuniswapv3factory = { path = "../contracts-generated/iuniswapv3factory" } cow-contract-izeroex = { path = "../contracts-generated/izeroex" } cow-contract-liquoricesettlement = { path = "../contracts-generated/liquoricesettlement" } +cow-contract-mockerc4626wrapper = { path = "../contracts-generated/mockerc4626wrapper" } cow-contract-nonstandarderc20balances = { path = "../contracts-generated/nonstandarderc20balances" } cow-contract-pancakerouter = { path = "../contracts-generated/pancakerouter" } cow-contract-permit2 = { path = "../contracts-generated/permit2" } diff --git a/contracts/generated/contracts-facade/src/lib.rs b/contracts/generated/contracts-facade/src/lib.rs index 6c5154a801..331b6a21aa 100644 --- a/contracts/generated/contracts-facade/src/lib.rs +++ b/contracts/generated/contracts-facade/src/lib.rs @@ -83,6 +83,7 @@ pub mod test { cow_contract_counter as Counter, cow_contract_cowprotocoltoken as CowProtocolToken, cow_contract_gashog as GasHog, + cow_contract_mockerc4626wrapper as MockERC4626Wrapper, cow_contract_nonstandarderc20balances as NonStandardERC20Balances, cow_contract_remoteerc20balances as RemoteERC20Balances, }; diff --git a/contracts/generated/contracts-generated/ierc4626/Cargo.toml b/contracts/generated/contracts-generated/ierc4626/Cargo.toml index c25bc58247..fe00f44d09 100644 --- a/contracts/generated/contracts-generated/ierc4626/Cargo.toml +++ b/contracts/generated/contracts-generated/ierc4626/Cargo.toml @@ -9,10 +9,10 @@ publish = false doctest = false [dependencies] -alloy-primitives = { workspace = true } -alloy-sol-types = { workspace = true } alloy-contract = { workspace = true } +alloy-primitives = { workspace = true } alloy-provider = { workspace = true } +alloy-sol-types = { workspace = true } anyhow = { workspace = true } [lints] diff --git a/contracts/generated/contracts-generated/mockerc4626wrapper/Cargo.toml b/contracts/generated/contracts-generated/mockerc4626wrapper/Cargo.toml new file mode 100644 index 0000000000..77ca06c21e --- /dev/null +++ b/contracts/generated/contracts-generated/mockerc4626wrapper/Cargo.toml @@ -0,0 +1,19 @@ +# Auto-generated by contracts-generate. Do not edit. +[package] +name = "cow-contract-mockerc4626wrapper" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +doctest = false + +[dependencies] +alloy-contract = { workspace = true } +alloy-primitives = { workspace = true } +alloy-provider = { workspace = true } +alloy-sol-types = { workspace = true } +anyhow = { workspace = true } + +[lints] +workspace = true diff --git a/contracts/generated/contracts-generated/mockerc4626wrapper/src/lib.rs b/contracts/generated/contracts-generated/mockerc4626wrapper/src/lib.rs new file mode 100644 index 0000000000..3d8202b578 --- /dev/null +++ b/contracts/generated/contracts-generated/mockerc4626wrapper/src/lib.rs @@ -0,0 +1,2320 @@ +#![allow( + unused_imports, + unused_attributes, + clippy::all, + rustdoc::all, + non_snake_case +)] +//! Auto-generated contract bindings. Do not edit. +/** + +Generated by the following Solidity interface... +```solidity +interface MockERC4626Wrapper { + constructor(address _asset, uint8 _decimals); + + function allowance(address, address) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function asset() external view returns (address); + function balanceOf(address) external view returns (uint256); + function convertToAssets(uint256 shares) external pure returns (uint256); + function decimals() external view returns (uint8); + function mint(address to, uint256 amount) external; + function transfer(address to, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} +``` + +...which was generated by the following JSON ABI: +```json +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_asset", + "type": "address", + "internalType": "address" + }, + { + "name": "_decimals", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "allowance", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "approve", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "asset", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "convertToAssets", + "inputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "decimals", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "mint", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transfer", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + } +] +```*/ +#[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style, + clippy::empty_structs_with_brackets +)] +pub mod MockERC4626Wrapper { + use {super::*, alloy_sol_types}; + /// The creation / init bytecode of the contract. + /// + /// ```text + ///0x60c060405234801561000f575f5ffd5b506040516109fd3803806109fd8339818101604052810190610031919061010e565b8173ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff16815250508060ff1660a08160ff1681525050505061014c565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100a78261007e565b9050919050565b6100b78161009d565b81146100c1575f5ffd5b50565b5f815190506100d2816100ae565b92915050565b5f60ff82169050919050565b6100ed816100d8565b81146100f7575f5ffd5b50565b5f81519050610108816100e4565b92915050565b5f5f604083850312156101245761012361007a565b5b5f610131858286016100c4565b9250506020610142858286016100fa565b9150509250929050565b60805160a05161089061016d5f395f6103de01525f61040201526108905ff3fe608060405234801561000f575f5ffd5b5060043610610091575f3560e01c806338d52e0f1161006457806338d52e0f1461014357806340c10f191461016157806370a082311461017d578063a9059cbb146101ad578063dd62ed3e146101dd57610091565b806307a2d13a14610095578063095ea7b3146100c557806323b872dd146100f5578063313ce56714610125575b5f5ffd5b6100af60048036038101906100aa9190610594565b61020d565b6040516100bc91906105ce565b60405180910390f35b6100df60048036038101906100da9190610641565b610216565b6040516100ec9190610699565b60405180910390f35b61010f600480360381019061010a91906106b2565b61029e565b60405161011c9190610699565b60405180910390f35b61012d6103dc565b60405161013a919061071d565b60405180910390f35b61014b610400565b6040516101589190610745565b60405180910390f35b61017b60048036038101906101769190610641565b610424565b005b6101976004803603810190610192919061075e565b61047a565b6040516101a491906105ce565b60405180910390f35b6101c760048036038101906101c29190610641565b61048e565b6040516101d49190610699565b60405180910390f35b6101f760048036038101906101f29190610789565b61053d565b60405161020491906105ce565b60405180910390f35b5f819050919050565b5f8160015f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20819055506001905092915050565b5f8160015f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461032691906107f4565b92505081905550815f5f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461037891906107f4565b92505081905550815f5f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546103ca9190610827565b92505081905550600190509392505050565b7f000000000000000000000000000000000000000000000000000000000000000081565b7f000000000000000000000000000000000000000000000000000000000000000081565b805f5f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461046f9190610827565b925050819055505050565b5f602052805f5260405f205f915090505481565b5f815f5f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546104da91906107f4565b92505081905550815f5f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461052c9190610827565b925050819055506001905092915050565b6001602052815f5260405f20602052805f5260405f205f91509150505481565b5f5ffd5b5f819050919050565b61057381610561565b811461057d575f5ffd5b50565b5f8135905061058e8161056a565b92915050565b5f602082840312156105a9576105a861055d565b5b5f6105b684828501610580565b91505092915050565b6105c881610561565b82525050565b5f6020820190506105e15f8301846105bf565b92915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610610826105e7565b9050919050565b61062081610606565b811461062a575f5ffd5b50565b5f8135905061063b81610617565b92915050565b5f5f604083850312156106575761065661055d565b5b5f6106648582860161062d565b925050602061067585828601610580565b9150509250929050565b5f8115159050919050565b6106938161067f565b82525050565b5f6020820190506106ac5f83018461068a565b92915050565b5f5f5f606084860312156106c9576106c861055d565b5b5f6106d68682870161062d565b93505060206106e78682870161062d565b92505060406106f886828701610580565b9150509250925092565b5f60ff82169050919050565b61071781610702565b82525050565b5f6020820190506107305f83018461070e565b92915050565b61073f81610606565b82525050565b5f6020820190506107585f830184610736565b92915050565b5f602082840312156107735761077261055d565b5b5f6107808482850161062d565b91505092915050565b5f5f6040838503121561079f5761079e61055d565b5b5f6107ac8582860161062d565b92505060206107bd8582860161062d565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6107fe82610561565b915061080983610561565b9250828203905081811115610821576108206107c7565b5b92915050565b5f61083182610561565b915061083c83610561565b9250828201905080821115610854576108536107c7565b5b9291505056fea2646970667358221220c7a63d9ea3fa2de6cae454642f51c1350392365b83fc5b1f47f3f63c3b67905064736f6c634300081e0033 + /// ``` + #[rustfmt::skip] + #[allow(clippy::all)] + pub static BYTECODE: alloy_sol_types::private::Bytes = alloy_sol_types::private::Bytes::from_static( + b"`\xC0`@R4\x80\x15a\0\x0FW__\xFD[P`@Qa\t\xFD8\x03\x80a\t\xFD\x839\x81\x81\x01`@R\x81\x01\x90a\x001\x91\x90a\x01\x0EV[\x81s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16`\x80\x81s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81RPP\x80`\xFF\x16`\xA0\x81`\xFF\x16\x81RPPPPa\x01LV[__\xFD[_s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x82\x16\x90P\x91\x90PV[_a\0\xA7\x82a\0~V[\x90P\x91\x90PV[a\0\xB7\x81a\0\x9DV[\x81\x14a\0\xC1W__\xFD[PV[_\x81Q\x90Pa\0\xD2\x81a\0\xAEV[\x92\x91PPV[_`\xFF\x82\x16\x90P\x91\x90PV[a\0\xED\x81a\0\xD8V[\x81\x14a\0\xF7W__\xFD[PV[_\x81Q\x90Pa\x01\x08\x81a\0\xE4V[\x92\x91PPV[__`@\x83\x85\x03\x12\x15a\x01$Wa\x01#a\0zV[[_a\x011\x85\x82\x86\x01a\0\xC4V[\x92PP` a\x01B\x85\x82\x86\x01a\0\xFAV[\x91PP\x92P\x92\x90PV[`\x80Q`\xA0Qa\x08\x90a\x01m_9_a\x03\xDE\x01R_a\x04\x02\x01Ra\x08\x90_\xF3\xFE`\x80`@R4\x80\x15a\0\x0FW__\xFD[P`\x046\x10a\0\x91W_5`\xE0\x1C\x80c8\xD5.\x0F\x11a\0dW\x80c8\xD5.\x0F\x14a\x01CW\x80c@\xC1\x0F\x19\x14a\x01aW\x80cp\xA0\x821\x14a\x01}W\x80c\xA9\x05\x9C\xBB\x14a\x01\xADW\x80c\xDDb\xED>\x14a\x01\xDDWa\0\x91V[\x80c\x07\xA2\xD1:\x14a\0\x95W\x80c\t^\xA7\xB3\x14a\0\xC5W\x80c#\xB8r\xDD\x14a\0\xF5W\x80c1<\xE5g\x14a\x01%W[__\xFD[a\0\xAF`\x04\x806\x03\x81\x01\x90a\0\xAA\x91\x90a\x05\x94V[a\x02\rV[`@Qa\0\xBC\x91\x90a\x05\xCEV[`@Q\x80\x91\x03\x90\xF3[a\0\xDF`\x04\x806\x03\x81\x01\x90a\0\xDA\x91\x90a\x06AV[a\x02\x16V[`@Qa\0\xEC\x91\x90a\x06\x99V[`@Q\x80\x91\x03\x90\xF3[a\x01\x0F`\x04\x806\x03\x81\x01\x90a\x01\n\x91\x90a\x06\xB2V[a\x02\x9EV[`@Qa\x01\x1C\x91\x90a\x06\x99V[`@Q\x80\x91\x03\x90\xF3[a\x01-a\x03\xDCV[`@Qa\x01:\x91\x90a\x07\x1DV[`@Q\x80\x91\x03\x90\xF3[a\x01Ka\x04\0V[`@Qa\x01X\x91\x90a\x07EV[`@Q\x80\x91\x03\x90\xF3[a\x01{`\x04\x806\x03\x81\x01\x90a\x01v\x91\x90a\x06AV[a\x04$V[\0[a\x01\x97`\x04\x806\x03\x81\x01\x90a\x01\x92\x91\x90a\x07^V[a\x04zV[`@Qa\x01\xA4\x91\x90a\x05\xCEV[`@Q\x80\x91\x03\x90\xF3[a\x01\xC7`\x04\x806\x03\x81\x01\x90a\x01\xC2\x91\x90a\x06AV[a\x04\x8EV[`@Qa\x01\xD4\x91\x90a\x06\x99V[`@Q\x80\x91\x03\x90\xF3[a\x01\xF7`\x04\x806\x03\x81\x01\x90a\x01\xF2\x91\x90a\x07\x89V[a\x05=V[`@Qa\x02\x04\x91\x90a\x05\xCEV[`@Q\x80\x91\x03\x90\xF3[_\x81\x90P\x91\x90PV[_\x81`\x01_3s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x85s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ \x81\x90UP`\x01\x90P\x92\x91PPV[_\x81`\x01_\x86s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _3s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x03&\x91\x90a\x07\xF4V[\x92PP\x81\x90UP\x81__\x86s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x03x\x91\x90a\x07\xF4V[\x92PP\x81\x90UP\x81__\x85s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x03\xCA\x91\x90a\x08'V[\x92PP\x81\x90UP`\x01\x90P\x93\x92PPPV[\x7F\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x81V[\x7F\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x81V[\x80__\x84s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x04o\x91\x90a\x08'V[\x92PP\x81\x90UPPPV[_` R\x80_R`@_ _\x91P\x90PT\x81V[_\x81__3s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x04\xDA\x91\x90a\x07\xF4V[\x92PP\x81\x90UP\x81__\x85s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x05,\x91\x90a\x08'V[\x92PP\x81\x90UP`\x01\x90P\x92\x91PPV[`\x01` R\x81_R`@_ ` R\x80_R`@_ _\x91P\x91PPT\x81V[__\xFD[_\x81\x90P\x91\x90PV[a\x05s\x81a\x05aV[\x81\x14a\x05}W__\xFD[PV[_\x815\x90Pa\x05\x8E\x81a\x05jV[\x92\x91PPV[_` \x82\x84\x03\x12\x15a\x05\xA9Wa\x05\xA8a\x05]V[[_a\x05\xB6\x84\x82\x85\x01a\x05\x80V[\x91PP\x92\x91PPV[a\x05\xC8\x81a\x05aV[\x82RPPV[_` \x82\x01\x90Pa\x05\xE1_\x83\x01\x84a\x05\xBFV[\x92\x91PPV[_s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x82\x16\x90P\x91\x90PV[_a\x06\x10\x82a\x05\xE7V[\x90P\x91\x90PV[a\x06 \x81a\x06\x06V[\x81\x14a\x06*W__\xFD[PV[_\x815\x90Pa\x06;\x81a\x06\x17V[\x92\x91PPV[__`@\x83\x85\x03\x12\x15a\x06WWa\x06Va\x05]V[[_a\x06d\x85\x82\x86\x01a\x06-V[\x92PP` a\x06u\x85\x82\x86\x01a\x05\x80V[\x91PP\x92P\x92\x90PV[_\x81\x15\x15\x90P\x91\x90PV[a\x06\x93\x81a\x06\x7FV[\x82RPPV[_` \x82\x01\x90Pa\x06\xAC_\x83\x01\x84a\x06\x8AV[\x92\x91PPV[___``\x84\x86\x03\x12\x15a\x06\xC9Wa\x06\xC8a\x05]V[[_a\x06\xD6\x86\x82\x87\x01a\x06-V[\x93PP` a\x06\xE7\x86\x82\x87\x01a\x06-V[\x92PP`@a\x06\xF8\x86\x82\x87\x01a\x05\x80V[\x91PP\x92P\x92P\x92V[_`\xFF\x82\x16\x90P\x91\x90PV[a\x07\x17\x81a\x07\x02V[\x82RPPV[_` \x82\x01\x90Pa\x070_\x83\x01\x84a\x07\x0EV[\x92\x91PPV[a\x07?\x81a\x06\x06V[\x82RPPV[_` \x82\x01\x90Pa\x07X_\x83\x01\x84a\x076V[\x92\x91PPV[_` \x82\x84\x03\x12\x15a\x07sWa\x07ra\x05]V[[_a\x07\x80\x84\x82\x85\x01a\x06-V[\x91PP\x92\x91PPV[__`@\x83\x85\x03\x12\x15a\x07\x9FWa\x07\x9Ea\x05]V[[_a\x07\xAC\x85\x82\x86\x01a\x06-V[\x92PP` a\x07\xBD\x85\x82\x86\x01a\x06-V[\x91PP\x92P\x92\x90PV[\x7FNH{q\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0_R`\x11`\x04R`$_\xFD[_a\x07\xFE\x82a\x05aV[\x91Pa\x08\t\x83a\x05aV[\x92P\x82\x82\x03\x90P\x81\x81\x11\x15a\x08!Wa\x08 a\x07\xC7V[[\x92\x91PPV[_a\x081\x82a\x05aV[\x91Pa\x08<\x83a\x05aV[\x92P\x82\x82\x01\x90P\x80\x82\x11\x15a\x08TWa\x08Sa\x07\xC7V[[\x92\x91PPV\xFE\xA2dipfsX\"\x12 \xC7\xA6=\x9E\xA3\xFA-\xE6\xCA\xE4Td/Q\xC15\x03\x926[\x83\xFC[\x1FG\xF3\xF6<;g\x90PdsolcC\0\x08\x1E\x003", + ); + /**Constructor`. + ```solidity + constructor(address _asset, uint8 _decimals); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct constructorCall { + #[allow(missing_docs)] + pub _asset: alloy_sol_types::private::Address, + #[allow(missing_docs)] + pub _decimals: u8, + } + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = ( + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Uint<8>, + ); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy_sol_types::private::Address, u8); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: constructorCall) -> Self { + (value._asset, value._decimals) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for constructorCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { + _asset: tuple.0, + _decimals: tuple.1, + } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolConstructor for constructorCall { + type Parameters<'a> = ( + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Uint<8>, + ); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + ::tokenize( + &self._asset, + ), + as alloy_sol_types::SolType>::tokenize( + &self._decimals, + ), + ) + } + } + }; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `allowance(address,address)` and selector `0xdd62ed3e`. + ```solidity + function allowance(address, address) external view returns (uint256); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct allowanceCall { + #[allow(missing_docs)] + pub _0: alloy_sol_types::private::Address, + #[allow(missing_docs)] + pub _1: alloy_sol_types::private::Address, + } + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the + /// [`allowance(address,address)`](allowanceCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct allowanceReturn { + #[allow(missing_docs)] + pub _0: alloy_sol_types::private::primitives::aliases::U256, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = ( + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Address, + ); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = ( + alloy_sol_types::private::Address, + alloy_sol_types::private::Address, + ); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: allowanceCall) -> Self { + (value._0, value._1) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for allowanceCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { + _0: tuple.0, + _1: tuple.1, + } + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy_sol_types::private::primitives::aliases::U256,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: allowanceReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for allowanceReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for allowanceCall { + type Parameters<'a> = ( + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Address, + ); + type Return = alloy_sol_types::private::primitives::aliases::U256; + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + type ReturnTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + const SELECTOR: [u8; 4] = [221u8, 98u8, 237u8, 62u8]; + const SIGNATURE: &'static str = "allowance(address,address)"; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + ::tokenize( + &self._0, + ), + ::tokenize( + &self._1, + ), + ) + } + + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + ( + as alloy_sol_types::SolType>::tokenize( + ret, + ), + ) + } + + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data).map( + |r| { + let r: allowanceReturn = r.into(); + r._0 + }, + ) + } + + #[inline] + fn abi_decode_returns_validate(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate( + data, + ) + .map(|r| { + let r: allowanceReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `approve(address,uint256)` and selector `0x095ea7b3`. + ```solidity + function approve(address spender, uint256 amount) external returns (bool); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct approveCall { + #[allow(missing_docs)] + pub spender: alloy_sol_types::private::Address, + #[allow(missing_docs)] + pub amount: alloy_sol_types::private::primitives::aliases::U256, + } + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the + /// [`approve(address,uint256)`](approveCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct approveReturn { + #[allow(missing_docs)] + pub _0: bool, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = ( + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Uint<256>, + ); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = ( + alloy_sol_types::private::Address, + alloy_sol_types::private::primitives::aliases::U256, + ); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: approveCall) -> Self { + (value.spender, value.amount) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for approveCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { + spender: tuple.0, + amount: tuple.1, + } + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Bool,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (bool,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: approveReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for approveReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for approveCall { + type Parameters<'a> = ( + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Uint<256>, + ); + type Return = bool; + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + type ReturnTuple<'a> = (alloy_sol_types::sol_data::Bool,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + const SELECTOR: [u8; 4] = [9u8, 94u8, 167u8, 179u8]; + const SIGNATURE: &'static str = "approve(address,uint256)"; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + ::tokenize( + &self.spender, + ), + as alloy_sol_types::SolType>::tokenize( + &self.amount, + ), + ) + } + + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + (::tokenize(ret),) + } + + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data).map( + |r| { + let r: approveReturn = r.into(); + r._0 + }, + ) + } + + #[inline] + fn abi_decode_returns_validate(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate( + data, + ) + .map(|r| { + let r: approveReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `asset()` and selector `0x38d52e0f`. + ```solidity + function asset() external view returns (address); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct assetCall; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the [`asset()`](assetCall) + /// function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct assetReturn { + #[allow(missing_docs)] + pub _0: alloy_sol_types::private::Address, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: assetCall) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for assetCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Address,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy_sol_types::private::Address,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: assetReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for assetReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for assetCall { + type Parameters<'a> = (); + type Return = alloy_sol_types::private::Address; + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + type ReturnTuple<'a> = (alloy_sol_types::sol_data::Address,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + const SELECTOR: [u8; 4] = [56u8, 213u8, 46u8, 15u8]; + const SIGNATURE: &'static str = "asset()"; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + () + } + + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + (::tokenize(ret),) + } + + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data).map( + |r| { + let r: assetReturn = r.into(); + r._0 + }, + ) + } + + #[inline] + fn abi_decode_returns_validate(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate( + data, + ) + .map(|r| { + let r: assetReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `balanceOf(address)` and selector `0x70a08231`. + ```solidity + function balanceOf(address) external view returns (uint256); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct balanceOfCall(pub alloy_sol_types::private::Address); + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the + /// [`balanceOf(address)`](balanceOfCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct balanceOfReturn { + #[allow(missing_docs)] + pub _0: alloy_sol_types::private::primitives::aliases::U256, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Address,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy_sol_types::private::Address,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: balanceOfCall) -> Self { + (value.0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for balanceOfCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self(tuple.0) + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy_sol_types::private::primitives::aliases::U256,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: balanceOfReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for balanceOfReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for balanceOfCall { + type Parameters<'a> = (alloy_sol_types::sol_data::Address,); + type Return = alloy_sol_types::private::primitives::aliases::U256; + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + type ReturnTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + const SELECTOR: [u8; 4] = [112u8, 160u8, 130u8, 49u8]; + const SIGNATURE: &'static str = "balanceOf(address)"; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + ::tokenize( + &self.0, + ), + ) + } + + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + ( + as alloy_sol_types::SolType>::tokenize( + ret, + ), + ) + } + + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data).map( + |r| { + let r: balanceOfReturn = r.into(); + r._0 + }, + ) + } + + #[inline] + fn abi_decode_returns_validate(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate( + data, + ) + .map(|r| { + let r: balanceOfReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `convertToAssets(uint256)` and selector `0x07a2d13a`. + ```solidity + function convertToAssets(uint256 shares) external pure returns (uint256); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct convertToAssetsCall { + #[allow(missing_docs)] + pub shares: alloy_sol_types::private::primitives::aliases::U256, + } + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the + /// [`convertToAssets(uint256)`](convertToAssetsCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct convertToAssetsReturn { + #[allow(missing_docs)] + pub _0: alloy_sol_types::private::primitives::aliases::U256, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy_sol_types::private::primitives::aliases::U256,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: convertToAssetsCall) -> Self { + (value.shares,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for convertToAssetsCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { shares: tuple.0 } + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy_sol_types::private::primitives::aliases::U256,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: convertToAssetsReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for convertToAssetsReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for convertToAssetsCall { + type Parameters<'a> = (alloy_sol_types::sol_data::Uint<256>,); + type Return = alloy_sol_types::private::primitives::aliases::U256; + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + type ReturnTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + const SELECTOR: [u8; 4] = [7u8, 162u8, 209u8, 58u8]; + const SIGNATURE: &'static str = "convertToAssets(uint256)"; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + as alloy_sol_types::SolType>::tokenize( + &self.shares, + ), + ) + } + + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + ( + as alloy_sol_types::SolType>::tokenize( + ret, + ), + ) + } + + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data).map( + |r| { + let r: convertToAssetsReturn = r.into(); + r._0 + }, + ) + } + + #[inline] + fn abi_decode_returns_validate(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate( + data, + ) + .map(|r| { + let r: convertToAssetsReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `decimals()` and selector `0x313ce567`. + ```solidity + function decimals() external view returns (uint8); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct decimalsCall; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the + /// [`decimals()`](decimalsCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct decimalsReturn { + #[allow(missing_docs)] + pub _0: u8, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: decimalsCall) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for decimalsCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Uint<8>,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (u8,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: decimalsReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for decimalsReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for decimalsCall { + type Parameters<'a> = (); + type Return = u8; + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + type ReturnTuple<'a> = (alloy_sol_types::sol_data::Uint<8>,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + const SELECTOR: [u8; 4] = [49u8, 60u8, 229u8, 103u8]; + const SIGNATURE: &'static str = "decimals()"; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + () + } + + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + ( as alloy_sol_types::SolType>::tokenize(ret),) + } + + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data).map( + |r| { + let r: decimalsReturn = r.into(); + r._0 + }, + ) + } + + #[inline] + fn abi_decode_returns_validate(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate( + data, + ) + .map(|r| { + let r: decimalsReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `mint(address,uint256)` and selector `0x40c10f19`. + ```solidity + function mint(address to, uint256 amount) external; + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct mintCall { + #[allow(missing_docs)] + pub to: alloy_sol_types::private::Address, + #[allow(missing_docs)] + pub amount: alloy_sol_types::private::primitives::aliases::U256, + } + ///Container type for the return parameters of the + /// [`mint(address,uint256)`](mintCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct mintReturn {} + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = ( + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Uint<256>, + ); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = ( + alloy_sol_types::private::Address, + alloy_sol_types::private::primitives::aliases::U256, + ); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: mintCall) -> Self { + (value.to, value.amount) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for mintCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { + to: tuple.0, + amount: tuple.1, + } + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: mintReturn) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for mintReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self {} + } + } + } + impl mintReturn { + fn _tokenize(&self) -> ::ReturnToken<'_> { + () + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for mintCall { + type Parameters<'a> = ( + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Uint<256>, + ); + type Return = mintReturn; + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + type ReturnTuple<'a> = (); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + const SELECTOR: [u8; 4] = [64u8, 193u8, 15u8, 25u8]; + const SIGNATURE: &'static str = "mint(address,uint256)"; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + ::tokenize( + &self.to, + ), + as alloy_sol_types::SolType>::tokenize( + &self.amount, + ), + ) + } + + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + mintReturn::_tokenize(ret) + } + + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data) + .map(Into::into) + } + + #[inline] + fn abi_decode_returns_validate(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate( + data, + ) + .map(Into::into) + } + } + }; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `transfer(address,uint256)` and selector `0xa9059cbb`. + ```solidity + function transfer(address to, uint256 amount) external returns (bool); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct transferCall { + #[allow(missing_docs)] + pub to: alloy_sol_types::private::Address, + #[allow(missing_docs)] + pub amount: alloy_sol_types::private::primitives::aliases::U256, + } + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the + /// [`transfer(address,uint256)`](transferCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct transferReturn { + #[allow(missing_docs)] + pub _0: bool, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = ( + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Uint<256>, + ); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = ( + alloy_sol_types::private::Address, + alloy_sol_types::private::primitives::aliases::U256, + ); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: transferCall) -> Self { + (value.to, value.amount) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for transferCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { + to: tuple.0, + amount: tuple.1, + } + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Bool,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (bool,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: transferReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for transferReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for transferCall { + type Parameters<'a> = ( + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Uint<256>, + ); + type Return = bool; + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + type ReturnTuple<'a> = (alloy_sol_types::sol_data::Bool,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + const SELECTOR: [u8; 4] = [169u8, 5u8, 156u8, 187u8]; + const SIGNATURE: &'static str = "transfer(address,uint256)"; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + ::tokenize( + &self.to, + ), + as alloy_sol_types::SolType>::tokenize( + &self.amount, + ), + ) + } + + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + (::tokenize(ret),) + } + + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data).map( + |r| { + let r: transferReturn = r.into(); + r._0 + }, + ) + } + + #[inline] + fn abi_decode_returns_validate(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate( + data, + ) + .map(|r| { + let r: transferReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `transferFrom(address,address,uint256)` and selector `0x23b872dd`. + ```solidity + function transferFrom(address from, address to, uint256 amount) external returns (bool); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct transferFromCall { + #[allow(missing_docs)] + pub from: alloy_sol_types::private::Address, + #[allow(missing_docs)] + pub to: alloy_sol_types::private::Address, + #[allow(missing_docs)] + pub amount: alloy_sol_types::private::primitives::aliases::U256, + } + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the + /// [`transferFrom(address,address,uint256)`](transferFromCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct transferFromReturn { + #[allow(missing_docs)] + pub _0: bool, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = ( + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Uint<256>, + ); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = ( + alloy_sol_types::private::Address, + alloy_sol_types::private::Address, + alloy_sol_types::private::primitives::aliases::U256, + ); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: transferFromCall) -> Self { + (value.from, value.to, value.amount) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for transferFromCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { + from: tuple.0, + to: tuple.1, + amount: tuple.2, + } + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Bool,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (bool,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: transferFromReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for transferFromReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for transferFromCall { + type Parameters<'a> = ( + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Address, + alloy_sol_types::sol_data::Uint<256>, + ); + type Return = bool; + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + type ReturnTuple<'a> = (alloy_sol_types::sol_data::Bool,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + const SELECTOR: [u8; 4] = [35u8, 184u8, 114u8, 221u8]; + const SIGNATURE: &'static str = "transferFrom(address,address,uint256)"; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + ::tokenize( + &self.from, + ), + ::tokenize( + &self.to, + ), + as alloy_sol_types::SolType>::tokenize( + &self.amount, + ), + ) + } + + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + (::tokenize(ret),) + } + + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data).map( + |r| { + let r: transferFromReturn = r.into(); + r._0 + }, + ) + } + + #[inline] + fn abi_decode_returns_validate(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate( + data, + ) + .map(|r| { + let r: transferFromReturn = r.into(); + r._0 + }) + } + } + }; + ///Container for all the [`MockERC4626Wrapper`](self) function calls. + #[derive(Clone)] + pub enum MockERC4626WrapperCalls { + #[allow(missing_docs)] + allowance(allowanceCall), + #[allow(missing_docs)] + approve(approveCall), + #[allow(missing_docs)] + asset(assetCall), + #[allow(missing_docs)] + balanceOf(balanceOfCall), + #[allow(missing_docs)] + convertToAssets(convertToAssetsCall), + #[allow(missing_docs)] + decimals(decimalsCall), + #[allow(missing_docs)] + mint(mintCall), + #[allow(missing_docs)] + transfer(transferCall), + #[allow(missing_docs)] + transferFrom(transferFromCall), + } + impl MockERC4626WrapperCalls { + /// All the selectors of this enum. + /// + /// Note that the selectors might not be in the same order as the + /// variants. No guarantees are made about the order of the + /// selectors. + /// + /// Prefer using `SolInterface` methods instead. + pub const SELECTORS: &'static [[u8; 4usize]] = &[ + [7u8, 162u8, 209u8, 58u8], + [9u8, 94u8, 167u8, 179u8], + [35u8, 184u8, 114u8, 221u8], + [49u8, 60u8, 229u8, 103u8], + [56u8, 213u8, 46u8, 15u8], + [64u8, 193u8, 15u8, 25u8], + [112u8, 160u8, 130u8, 49u8], + [169u8, 5u8, 156u8, 187u8], + [221u8, 98u8, 237u8, 62u8], + ]; + /// The signatures in the same order as `SELECTORS`. + pub const SIGNATURES: &'static [&'static str] = &[ + ::SIGNATURE, + ::SIGNATURE, + ::SIGNATURE, + ::SIGNATURE, + ::SIGNATURE, + ::SIGNATURE, + ::SIGNATURE, + ::SIGNATURE, + ::SIGNATURE, + ]; + /// The names of the variants in the same order as `SELECTORS`. + pub const VARIANT_NAMES: &'static [&'static str] = &[ + ::core::stringify!(convertToAssets), + ::core::stringify!(approve), + ::core::stringify!(transferFrom), + ::core::stringify!(decimals), + ::core::stringify!(asset), + ::core::stringify!(mint), + ::core::stringify!(balanceOf), + ::core::stringify!(transfer), + ::core::stringify!(allowance), + ]; + + /// Returns the signature for the given selector, if known. + #[inline] + pub fn signature_by_selector( + selector: [u8; 4usize], + ) -> ::core::option::Option<&'static str> { + match Self::SELECTORS.binary_search(&selector) { + ::core::result::Result::Ok(idx) => { + ::core::option::Option::Some(Self::SIGNATURES[idx]) + } + ::core::result::Result::Err(_) => ::core::option::Option::None, + } + } + + /// Returns the enum variant name for the given selector, if known. + #[inline] + pub fn name_by_selector(selector: [u8; 4usize]) -> ::core::option::Option<&'static str> { + let sig = Self::signature_by_selector(selector)?; + sig.split_once('(').map(|(name, _)| name) + } + } + #[automatically_derived] + impl alloy_sol_types::SolInterface for MockERC4626WrapperCalls { + const COUNT: usize = 9usize; + const MIN_DATA_LENGTH: usize = 0usize; + const NAME: &'static str = "MockERC4626WrapperCalls"; + + #[inline] + fn selector(&self) -> [u8; 4] { + match self { + Self::allowance(_) => ::SELECTOR, + Self::approve(_) => ::SELECTOR, + Self::asset(_) => ::SELECTOR, + Self::balanceOf(_) => ::SELECTOR, + Self::convertToAssets(_) => { + ::SELECTOR + } + Self::decimals(_) => ::SELECTOR, + Self::mint(_) => ::SELECTOR, + Self::transfer(_) => ::SELECTOR, + Self::transferFrom(_) => ::SELECTOR, + } + } + + #[inline] + fn selector_at(i: usize) -> ::core::option::Option<[u8; 4]> { + Self::SELECTORS.get(i).copied() + } + + #[inline] + fn valid_selector(selector: [u8; 4]) -> bool { + Self::SELECTORS.binary_search(&selector).is_ok() + } + + #[inline] + #[allow(non_snake_case)] + fn abi_decode_raw(selector: [u8; 4], data: &[u8]) -> alloy_sol_types::Result { + static DECODE_SHIMS: &[fn( + &[u8], + ) + -> alloy_sol_types::Result] = &[ + { + fn convertToAssets( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(MockERC4626WrapperCalls::convertToAssets) + } + convertToAssets + }, + { + fn approve(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(MockERC4626WrapperCalls::approve) + } + approve + }, + { + fn transferFrom( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(MockERC4626WrapperCalls::transferFrom) + } + transferFrom + }, + { + fn decimals(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(MockERC4626WrapperCalls::decimals) + } + decimals + }, + { + fn asset(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(MockERC4626WrapperCalls::asset) + } + asset + }, + { + fn mint(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(MockERC4626WrapperCalls::mint) + } + mint + }, + { + fn balanceOf(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(MockERC4626WrapperCalls::balanceOf) + } + balanceOf + }, + { + fn transfer(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(MockERC4626WrapperCalls::transfer) + } + transfer + }, + { + fn allowance(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(MockERC4626WrapperCalls::allowance) + } + allowance + }, + ]; + let Ok(idx) = Self::SELECTORS.binary_search(&selector) else { + return Err(alloy_sol_types::Error::unknown_selector( + ::NAME, + selector, + )); + }; + DECODE_SHIMS[idx](data) + } + + #[inline] + #[allow(non_snake_case)] + fn abi_decode_raw_validate( + selector: [u8; 4], + data: &[u8], + ) -> alloy_sol_types::Result { + static DECODE_VALIDATE_SHIMS: &[fn( + &[u8], + ) -> alloy_sol_types::Result< + MockERC4626WrapperCalls, + >] = &[ + { + fn convertToAssets( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(MockERC4626WrapperCalls::convertToAssets) + } + convertToAssets + }, + { + fn approve(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw_validate(data) + .map(MockERC4626WrapperCalls::approve) + } + approve + }, + { + fn transferFrom( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(MockERC4626WrapperCalls::transferFrom) + } + transferFrom + }, + { + fn decimals(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw_validate(data) + .map(MockERC4626WrapperCalls::decimals) + } + decimals + }, + { + fn asset(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw_validate(data) + .map(MockERC4626WrapperCalls::asset) + } + asset + }, + { + fn mint(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw_validate(data) + .map(MockERC4626WrapperCalls::mint) + } + mint + }, + { + fn balanceOf(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw_validate(data) + .map(MockERC4626WrapperCalls::balanceOf) + } + balanceOf + }, + { + fn transfer(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw_validate(data) + .map(MockERC4626WrapperCalls::transfer) + } + transfer + }, + { + fn allowance(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw_validate(data) + .map(MockERC4626WrapperCalls::allowance) + } + allowance + }, + ]; + let Ok(idx) = Self::SELECTORS.binary_search(&selector) else { + return Err(alloy_sol_types::Error::unknown_selector( + ::NAME, + selector, + )); + }; + DECODE_VALIDATE_SHIMS[idx](data) + } + + #[inline] + fn abi_encoded_size(&self) -> usize { + match self { + Self::allowance(inner) => { + ::abi_encoded_size(inner) + } + Self::approve(inner) => { + ::abi_encoded_size(inner) + } + Self::asset(inner) => { + ::abi_encoded_size(inner) + } + Self::balanceOf(inner) => { + ::abi_encoded_size(inner) + } + Self::convertToAssets(inner) => { + ::abi_encoded_size(inner) + } + Self::decimals(inner) => { + ::abi_encoded_size(inner) + } + Self::mint(inner) => { + ::abi_encoded_size(inner) + } + Self::transfer(inner) => { + ::abi_encoded_size(inner) + } + Self::transferFrom(inner) => { + ::abi_encoded_size(inner) + } + } + } + + #[inline] + fn abi_encode_raw(&self, out: &mut alloy_sol_types::private::Vec) { + match self { + Self::allowance(inner) => { + ::abi_encode_raw(inner, out) + } + Self::approve(inner) => { + ::abi_encode_raw(inner, out) + } + Self::asset(inner) => { + ::abi_encode_raw(inner, out) + } + Self::balanceOf(inner) => { + ::abi_encode_raw(inner, out) + } + Self::convertToAssets(inner) => { + ::abi_encode_raw(inner, out) + } + Self::decimals(inner) => { + ::abi_encode_raw(inner, out) + } + Self::mint(inner) => { + ::abi_encode_raw(inner, out) + } + Self::transfer(inner) => { + ::abi_encode_raw(inner, out) + } + Self::transferFrom(inner) => { + ::abi_encode_raw(inner, out) + } + } + } + } + use alloy_contract; + /**Creates a new wrapper around an on-chain [`MockERC4626Wrapper`](self) contract instance. + + See the [wrapper's documentation](`MockERC4626WrapperInstance`) for more details.*/ + #[inline] + pub const fn new< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + >( + address: alloy_sol_types::private::Address, + __provider: P, + ) -> MockERC4626WrapperInstance { + MockERC4626WrapperInstance::::new(address, __provider) + } + /**Deploys this contract using the given `provider` and constructor arguments, if any. + + Returns a new instance of the contract, if the deployment was successful. + + For more fine-grained control over the deployment process, use [`deploy_builder`] instead.*/ + #[inline] + pub fn deploy, N: alloy_contract::private::Network>( + __provider: P, + _asset: alloy_sol_types::private::Address, + _decimals: u8, + ) -> impl ::core::future::Future>> + { + MockERC4626WrapperInstance::::deploy(__provider, _asset, _decimals) + } + /**Creates a `RawCallBuilder` for deploying this contract using the given `provider` + and constructor arguments, if any. + + This is a simple wrapper around creating a `RawCallBuilder` with the data set to + the bytecode concatenated with the constructor's ABI-encoded arguments.*/ + #[inline] + pub fn deploy_builder< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + >( + __provider: P, + _asset: alloy_sol_types::private::Address, + _decimals: u8, + ) -> alloy_contract::RawCallBuilder { + MockERC4626WrapperInstance::::deploy_builder(__provider, _asset, _decimals) + } + /**A [`MockERC4626Wrapper`](self) instance. + + Contains type-safe methods for interacting with an on-chain instance of the + [`MockERC4626Wrapper`](self) contract located at a given `address`, using a given + provider `P`. + + If the contract bytecode is available (see the [`sol!`](alloy_sol_types::sol!) + documentation on how to provide it), the `deploy` and `deploy_builder` methods can + be used to deploy a new instance of the contract. + + See the [module-level documentation](self) for all the available methods.*/ + #[derive(Clone)] + pub struct MockERC4626WrapperInstance { + address: alloy_sol_types::private::Address, + provider: P, + _network: ::core::marker::PhantomData, + } + #[automatically_derived] + impl ::core::fmt::Debug for MockERC4626WrapperInstance { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_tuple("MockERC4626WrapperInstance") + .field(&self.address) + .finish() + } + } + /// Instantiation and getters/setters. + impl, N: alloy_contract::private::Network> + MockERC4626WrapperInstance + { + /**Creates a new wrapper around an on-chain [`MockERC4626Wrapper`](self) contract instance. + + See the [wrapper's documentation](`MockERC4626WrapperInstance`) for more details.*/ + #[inline] + pub const fn new(address: alloy_sol_types::private::Address, __provider: P) -> Self { + Self { + address, + provider: __provider, + _network: ::core::marker::PhantomData, + } + } + + /**Deploys this contract using the given `provider` and constructor arguments, if any. + + Returns a new instance of the contract, if the deployment was successful. + + For more fine-grained control over the deployment process, use [`deploy_builder`] instead.*/ + #[inline] + pub async fn deploy( + __provider: P, + _asset: alloy_sol_types::private::Address, + _decimals: u8, + ) -> alloy_contract::Result> { + let call_builder = Self::deploy_builder(__provider, _asset, _decimals); + let contract_address = call_builder.deploy().await?; + Ok(Self::new(contract_address, call_builder.provider)) + } + + /**Creates a `RawCallBuilder` for deploying this contract using the given `provider` + and constructor arguments, if any. + + This is a simple wrapper around creating a `RawCallBuilder` with the data set to + the bytecode concatenated with the constructor's ABI-encoded arguments.*/ + #[inline] + pub fn deploy_builder( + __provider: P, + _asset: alloy_sol_types::private::Address, + _decimals: u8, + ) -> alloy_contract::RawCallBuilder { + alloy_contract::RawCallBuilder::new_raw_deploy( + __provider, + [ + &BYTECODE[..], + &alloy_sol_types::SolConstructor::abi_encode(&constructorCall { + _asset, + _decimals, + })[..], + ] + .concat() + .into(), + ) + } + + /// Returns a reference to the address. + #[inline] + pub const fn address(&self) -> &alloy_sol_types::private::Address { + &self.address + } + + /// Sets the address. + #[inline] + pub fn set_address(&mut self, address: alloy_sol_types::private::Address) { + self.address = address; + } + + /// Sets the address and returns `self`. + pub fn at(mut self, address: alloy_sol_types::private::Address) -> Self { + self.set_address(address); + self + } + + /// Returns a reference to the provider. + #[inline] + pub const fn provider(&self) -> &P { + &self.provider + } + } + impl MockERC4626WrapperInstance<&P, N> { + /// Clones the provider and returns a new instance with the cloned + /// provider. + #[inline] + pub fn with_cloned_provider(self) -> MockERC4626WrapperInstance { + MockERC4626WrapperInstance { + address: self.address, + provider: ::core::clone::Clone::clone(&self.provider), + _network: ::core::marker::PhantomData, + } + } + } + /// Function calls. + impl, N: alloy_contract::private::Network> + MockERC4626WrapperInstance + { + /// Creates a new call builder using this contract instance's provider + /// and address. + /// + /// Note that the call can be any function call, not just those defined + /// in this contract. Prefer using the other methods for + /// building type-safe contract calls. + pub fn call_builder( + &self, + call: &C, + ) -> alloy_contract::SolCallBuilder<&P, C, N> { + alloy_contract::SolCallBuilder::new_sol(&self.provider, &self.address, call) + } + + ///Creates a new call builder for the [`allowance`] function. + pub fn allowance( + &self, + _0: alloy_sol_types::private::Address, + _1: alloy_sol_types::private::Address, + ) -> alloy_contract::SolCallBuilder<&P, allowanceCall, N> { + self.call_builder(&allowanceCall { _0, _1 }) + } + + ///Creates a new call builder for the [`approve`] function. + pub fn approve( + &self, + spender: alloy_sol_types::private::Address, + amount: alloy_sol_types::private::primitives::aliases::U256, + ) -> alloy_contract::SolCallBuilder<&P, approveCall, N> { + self.call_builder(&approveCall { spender, amount }) + } + + ///Creates a new call builder for the [`asset`] function. + pub fn asset(&self) -> alloy_contract::SolCallBuilder<&P, assetCall, N> { + self.call_builder(&assetCall) + } + + ///Creates a new call builder for the [`balanceOf`] function. + pub fn balanceOf( + &self, + _0: alloy_sol_types::private::Address, + ) -> alloy_contract::SolCallBuilder<&P, balanceOfCall, N> { + self.call_builder(&balanceOfCall(_0)) + } + + ///Creates a new call builder for the [`convertToAssets`] function. + pub fn convertToAssets( + &self, + shares: alloy_sol_types::private::primitives::aliases::U256, + ) -> alloy_contract::SolCallBuilder<&P, convertToAssetsCall, N> { + self.call_builder(&convertToAssetsCall { shares }) + } + + ///Creates a new call builder for the [`decimals`] function. + pub fn decimals(&self) -> alloy_contract::SolCallBuilder<&P, decimalsCall, N> { + self.call_builder(&decimalsCall) + } + + ///Creates a new call builder for the [`mint`] function. + pub fn mint( + &self, + to: alloy_sol_types::private::Address, + amount: alloy_sol_types::private::primitives::aliases::U256, + ) -> alloy_contract::SolCallBuilder<&P, mintCall, N> { + self.call_builder(&mintCall { to, amount }) + } + + ///Creates a new call builder for the [`transfer`] function. + pub fn transfer( + &self, + to: alloy_sol_types::private::Address, + amount: alloy_sol_types::private::primitives::aliases::U256, + ) -> alloy_contract::SolCallBuilder<&P, transferCall, N> { + self.call_builder(&transferCall { to, amount }) + } + + ///Creates a new call builder for the [`transferFrom`] function. + pub fn transferFrom( + &self, + from: alloy_sol_types::private::Address, + to: alloy_sol_types::private::Address, + amount: alloy_sol_types::private::primitives::aliases::U256, + ) -> alloy_contract::SolCallBuilder<&P, transferFromCall, N> { + self.call_builder(&transferFromCall { from, to, amount }) + } + } + /// Event filters. + impl, N: alloy_contract::private::Network> + MockERC4626WrapperInstance + { + /// Creates a new event filter using this contract instance's provider + /// and address. + /// + /// Note that the type can be any event, not just those defined in this + /// contract. Prefer using the other methods for building + /// type-safe event filters. + pub fn event_filter( + &self, + ) -> alloy_contract::Event<&P, E, N> { + alloy_contract::Event::new_sol(&self.provider, &self.address) + } + } +} +pub type Instance = MockERC4626Wrapper::MockERC4626WrapperInstance<::alloy_provider::DynProvider>; diff --git a/contracts/solidity/tests/MockERC4626Wrapper.sol b/contracts/solidity/tests/MockERC4626Wrapper.sol new file mode 100644 index 0000000000..82d156f019 --- /dev/null +++ b/contracts/solidity/tests/MockERC4626Wrapper.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +/// @title Minimal EIP-4626 wrapper for testing recursive vault pricing. +/// @dev Wraps another ERC-4626 vault (or any ERC-20) with a fixed 1:1 +/// conversion rate. Not a real vault – just enough to satisfy +/// `asset()`, `decimals()`, `convertToAssets()`, `balanceOf()`, +/// `approve()`, and `transfer()` so the e2e pricing pipeline works. +contract MockERC4626Wrapper { + address public immutable asset; + uint8 public immutable decimals; + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + constructor(address _asset, uint8 _decimals) { + asset = _asset; + decimals = _decimals; + } + + // ── EIP-4626 view ────────────────────────────────────────────────── + + function convertToAssets(uint256 shares) external pure returns (uint256) { + return shares; // 1:1 conversion + } + + // ── Minimal ERC-20 surface needed by the protocol ────────────────── + + function transfer(address to, uint256 amount) external returns (bool) { + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + return true; + } + + function approve(address spender, uint256 amount) external returns (bool) { + allowance[msg.sender][spender] = amount; + return true; + } + + function transferFrom(address from, address to, uint256 amount) external returns (bool) { + allowance[from][msg.sender] -= amount; + balanceOf[from] -= amount; + balanceOf[to] += amount; + return true; + } + + // ── Test helper ──────────────────────────────────────────────────── + + function mint(address to, uint256 amount) external { + balanceOf[to] += amount; + } +} diff --git a/contracts/src/main.rs b/contracts/src/main.rs index 9ff18b87f7..24c0ae603d 100644 --- a/contracts/src/main.rs +++ b/contracts/src/main.rs @@ -514,6 +514,7 @@ fn build_module() -> Module { Submodule::new("test") .add_contract(Contract::new("GasHog")) .add_contract(Contract::new("Counter")) + .add_contract(Contract::new("MockERC4626Wrapper")) .add_contract(Contract::new("CowProtocolToken").with_networks(networks![ MAINNET => "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", GNOSIS => "0x177127622c4A00F3d409B75571e12cB3c8973d3c", diff --git a/crates/configs/src/native_price_estimators.rs b/crates/configs/src/native_price_estimators.rs index 3fddb94326..79708e215f 100644 --- a/crates/configs/src/native_price_estimators.rs +++ b/crates/configs/src/native_price_estimators.rs @@ -22,17 +22,20 @@ impl<'de> Deserialize<'de> for NativePriceEstimators { &"expected native price estimator stages to be configured", )); } - match estimators - .iter() - .enumerate() - .find_map(|(n, stage)| stage.is_empty().then_some(n)) - { - Some(n) => Err(serde::de::Error::invalid_length( - 0, - &format!("stage {} is empty, all stages must not be empty", n).as_str(), - )), - None => Ok(Self(estimators)), + for (n, stage) in estimators.iter().enumerate() { + if stage.is_empty() { + return Err(serde::de::Error::invalid_length( + 0, + &format!("stage {} is empty, all stages must not be empty", n).as_str(), + )); + } + if matches!(stage.last(), Some(NativePriceEstimator::Eip4626)) { + return Err(serde::de::Error::custom(format!( + "stage {n}: Eip4626 must be followed by another estimator" + ))); + } } + Ok(Self(estimators)) } } diff --git a/crates/e2e/tests/e2e/eip4626.rs b/crates/e2e/tests/e2e/eip4626.rs index a5293d3b16..6405b97724 100644 --- a/crates/e2e/tests/e2e/eip4626.rs +++ b/crates/e2e/tests/e2e/eip4626.rs @@ -3,14 +3,16 @@ use { primitives::{Address, address}, providers::ext::{AnvilApi, ImpersonateConfig}, }, - autopilot::config::{Configuration, native_price::NativePriceConfig}, - configs::test_util::TestDefault, - contracts::alloy::ERC20, + configs::{ + autopilot::{Configuration, native_price::NativePriceConfig}, + native_price_estimators::{NativePriceEstimator, NativePriceEstimators}, + test_util::TestDefault, + }, + contracts::ERC20, e2e::setup::*, ethrpc::alloy::CallBuilderExt, model::quote::{OrderQuoteRequest, OrderQuoteSide, SellAmount}, number::units::EthUnit, - price_estimation::{NativePriceEstimator, NativePriceEstimators}, shared::web3::Web3, }; @@ -94,9 +96,8 @@ async fn eip4626_native_price_test(web3: Web3) { let services = Services::new(&onchain).await; services .start_protocol_with_args( - Default::default(), autopilot_config, - orderbook::config::Configuration::test_default(), + configs::orderbook::Configuration::test_default(), solver, ) .await; @@ -125,3 +126,85 @@ async fn eip4626_native_price_test(web3: Web3) { quote.err() ); } + +#[tokio::test] +#[ignore] +async fn forked_node_mainnet_eip4626_recursive_native_price() { + run_forked_test_with_block_number( + eip4626_recursive_native_price_test, + std::env::var("FORK_URL_MAINNET") + .expect("FORK_URL_MAINNET must be set to run forked tests"), + FORK_BLOCK_MAINNET, + ) + .await; +} + +/// Tests pricing of a recursive EIP-4626 vault: a mock wrapper vault whose +/// `asset()` returns sDAI, which itself is an EIP-4626 vault wrapping DAI. +/// Requires two chained `Eip4626` estimators to fully unwrap. +/// +/// Unlike the non-recursive test we cannot submit a full quote because the +/// freshly-deployed wrapper token has no DEX liquidity. Instead we verify +/// that the native price endpoint returns a price, which exercises the full +/// Eip4626 → Eip4626 → Driver chain. +async fn eip4626_recursive_native_price_test(web3: Web3) { + let mut onchain = OnchainComponents::deployed(web3.clone()).await; + + let [solver] = onchain.make_solvers_forked(1u64.eth()).await; + + // Deploy a mock EIP-4626 vault that wraps sDAI (which itself wraps DAI). + let wrapper = + contracts::test::MockERC4626Wrapper::Instance::deploy(web3.provider.clone(), SDAI, 18u8) + .await + .unwrap(); + let wrapper_addr = *wrapper.address(); + + // Two chained Eip4626 estimators: the first unwraps the mock wrapper to + // sDAI, the second unwraps sDAI to DAI, and the Driver prices DAI. + let driver_url = "http://localhost:11088/test_solver".parse().unwrap(); + let autopilot_config = Configuration { + native_price_estimation: NativePriceConfig { + estimators: NativePriceEstimators::new(vec![vec![ + NativePriceEstimator::Eip4626, + NativePriceEstimator::Eip4626, + NativePriceEstimator::driver("test_quoter".to_string(), driver_url), + // Standalone estimator for non-vault tokens. + NativePriceEstimator::driver( + "test_quoter".to_string(), + "http://localhost:11088/test_solver".parse().unwrap(), + ), + ]]), + ..NativePriceConfig::test_default() + }, + ..Configuration::test("test_solver", solver.address()) + }; + + let services = Services::new(&onchain).await; + services + .start_protocol_with_args( + autopilot_config, + configs::orderbook::Configuration::test_default(), + solver, + ) + .await; + + onchain.mint_block().await; + + // Query the native price of the wrapper token. The Eip4626 chain must + // resolve wrapper → sDAI → DAI → native price via the Driver. + wait_for_condition(TIMEOUT, || async { + services.get_native_price(&wrapper_addr).await.is_ok() + }) + .await + .expect("native price for recursive EIP-4626 wrapper should be available"); + + let price = services + .get_native_price(&wrapper_addr) + .await + .expect("native price should be available after wait"); + assert!( + price.price > 0.0, + "native price should be positive, got: {}", + price.price + ); +} diff --git a/crates/price-estimation/src/lib.rs b/crates/price-estimation/src/lib.rs index 9631772400..b00bc34b1f 100644 --- a/crates/price-estimation/src/lib.rs +++ b/crates/price-estimation/src/lib.rs @@ -34,369 +34,6 @@ pub mod trade_finding; pub mod trade_verifier; pub mod utils; -#[derive(Clone, Debug, Default, Serialize)] -pub struct NativePriceEstimators(Vec>); - -impl<'de> Deserialize<'de> for NativePriceEstimators { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - let estimators = >>::deserialize(deserializer)?; - if estimators.is_empty() { - return Err(serde::de::Error::invalid_length( - 0, - &"expected native price estimator stages to be configured", - )); - } - for (n, stage) in estimators.iter().enumerate() { - if stage.is_empty() { - return Err(serde::de::Error::invalid_length( - 0, - &format!("stage {} is empty, all stages must not be empty", n).as_str(), - )); - } - if matches!(stage.last(), Some(NativePriceEstimator::Eip4626)) { - return Err(serde::de::Error::custom(format!( - "stage {n}: Eip4626 must be followed by another estimator" - ))); - } - } - Ok(Self(estimators)) - } -} - -impl NativePriceEstimators { - pub fn new(estimators: Vec>) -> Self { - Self(estimators) - } -} - -#[cfg(any(test, feature = "test-util"))] -impl NativePriceEstimators { - /// Returns a list with a single stage, said stage contains a Driver estimator named `test_quoter` with URL `http://localhost:11088/test_solver`. - pub fn test_default() -> Self { - NativePriceEstimators::new(vec![vec![NativePriceEstimator::driver( - "test_quoter".to_string(), - Url::from_str("http://localhost:11088/test_solver").unwrap(), - )]]) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] -pub struct ExternalSolver { - pub name: String, - pub url: Url, -} - -impl FromStr for ExternalSolver { - type Err = anyhow::Error; - - fn from_str(solver: &str) -> Result { - let parts: Vec<&str> = solver.split('|').collect(); - ensure!(parts.len() >= 2, "not enough arguments for external solver"); - let (name, url) = (parts[0], parts[1]); - let url: Url = url.parse()?; - Ok(Self { - name: name.to_owned(), - url, - }) - } -} - -#[derive(Clone, Debug, Hash, Eq, PartialEq, Deserialize, Serialize)] -#[serde(tag = "type")] -pub enum NativePriceEstimator { - Driver(ExternalSolver), - Forwarder { - url: Url, - }, - OneInchSpotPriceApi, - CoinGecko, - /// Prices EIP-4626 vault tokens by looking up the underlying `asset()` and - /// applying `convertToAssets()` as a conversion rate. At construction time, - /// wraps the next estimator in the configuration list. - Eip4626, -} - -impl NativePriceEstimator { - pub const fn driver(name: String, url: Url) -> Self { - Self::Driver(ExternalSolver { name, url }) - } - - pub const fn forwarder(url: Url) -> Self { - Self::Forwarder { url } - } -} - -impl Display for NativePriceEstimator { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let formatter = match self { - NativePriceEstimator::Driver(s) => format!("Driver|{}|{}", &s.name, s.url), - NativePriceEstimator::Forwarder { url } => format!("Forwarder|{}", url), - NativePriceEstimator::OneInchSpotPriceApi => "OneInchSpotPriceApi".into(), - NativePriceEstimator::CoinGecko => "CoinGecko".into(), - NativePriceEstimator::Eip4626 => "Eip4626".into(), - }; - write!(f, "{formatter}") - } -} - -impl NativePriceEstimators { - pub fn as_slice(&self) -> &[Vec] { - &self.0 - } -} - -impl Display for NativePriceEstimators { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let formatter = self - .as_slice() - .iter() - .map(|stage| { - stage - .iter() - .format_with(",", |estimator, f| f(&format_args!("{estimator}"))) - }) - .format(";"); - write!(f, "{formatter}") - } -} - -impl FromStr for NativePriceEstimators { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - Ok(Self( - s.split(';') - .map(|sub_list| { - sub_list - .split(',') - .map(NativePriceEstimator::from_str) - .collect::>>() - }) - .collect::>>>()?, - )) - } -} - -impl FromStr for NativePriceEstimator { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let (variant, args) = s.split_once('|').unwrap_or((s, "")); - match variant { - "OneInchSpotPriceApi" => Ok(NativePriceEstimator::OneInchSpotPriceApi), - "CoinGecko" => Ok(NativePriceEstimator::CoinGecko), - "Driver" => Ok(NativePriceEstimator::Driver(ExternalSolver::from_str( - args, - )?)), - "Forwarder" => Ok(NativePriceEstimator::Forwarder { - url: args - .parse() - .context("Forwarder price estimator invalid URL")?, - }), - "Eip4626" => Ok(NativePriceEstimator::Eip4626), - _ => Err(anyhow::anyhow!("unsupported native price estimator: {}", s)), - } - } -} - -/// Shared price estimation configuration arguments. -#[derive(clap::Parser)] -#[group(skip)] -pub struct Arguments { - #[clap(flatten)] - pub tenderly: tenderly_api::Arguments, - - /// Configures the back off strategy for price estimators when requests take - /// too long. Requests issued while back off is active get dropped - /// entirely. Needs to be passed as - /// ",,". - /// back_off_growth_factor: f64 >= 1.0 - /// min_back_off: Duration - /// max_back_off: Duration - #[clap(long, env, verbatim_doc_comment)] - pub price_estimation_rate_limiter: Option, - - /// The amount in native tokens atoms to use for price estimation. Should be - /// reasonably large so that small pools do not influence the prices. If - /// not set a reasonable default is used based on network id. - #[clap(long, env)] - pub amount_to_estimate_prices_with: Option, - - /// The API key for the 1Inch API. - #[clap(long, env)] - pub one_inch_api_key: Option, - - /// The base URL for the 1Inch API. - #[clap(long, env, default_value = "https://api.1inch.dev/")] - pub one_inch_url: Url, - - /// The CoinGecko native price configuration - #[clap(flatten)] - pub coin_gecko: CoinGecko, - - /// How inaccurate a quote must be before it gets discarded provided as a - /// factor. - /// E.g. a value of `0.01` means at most 1 percent of the sell or buy tokens - /// can be paid out of the settlement contract buffers. - #[clap(long, env, default_value = "1.")] - pub quote_inaccuracy_limit: BigDecimal, - - /// How strict quote verification should be. - #[clap( - long, - env, - default_value = "unverified", - value_enum, - verbatim_doc_comment - )] - pub quote_verification: QuoteVerificationMode, - - /// Default timeout for quote requests. - #[clap( - long, - env, - default_value = "5s", - value_parser = humantime::parse_duration, - )] - pub quote_timeout: Duration, - - #[clap(flatten)] - pub balance_overrides: balance_overrides::Arguments, - - /// Tokens for which quote verification should not be attempted. This is an - /// escape hatch when there is a very bad but verifiable liquidity source - /// that would win against a very good but unverifiable liquidity source - /// (e.g. private liquidity that exists but can't be verified). - #[clap(long, env, value_delimiter = ',')] - pub tokens_without_verification: Vec
, -} - -#[derive(clap::Parser)] -pub struct CoinGecko { - /// The API key for the CoinGecko API. - #[clap(long, env)] - pub coin_gecko_api_key: Option, - - /// The base URL for the CoinGecko API. - #[clap( - long, - env, - default_value = "https://api.coingecko.com/api/v3/simple/token_price" - )] - pub coin_gecko_url: Url, - - #[clap(flatten)] - pub coin_gecko_buffered: Option, -} - -#[derive(clap::Parser)] -#[clap(group( - clap::ArgGroup::new("coin_gecko_buffered") - .requires_all(&[ - "coin_gecko_debouncing_time", - "coin_gecko_broadcast_channel_capacity" - ]) - .multiple(true) - .required(false), -))] -pub struct CoinGeckoBuffered { - /// An additional minimum delay to wait for collecting CoinGecko requests. - /// - /// The delay to start counting after receiving the first request. - #[clap(long, env, value_parser = humantime::parse_duration, group = "coin_gecko_buffered")] - pub coin_gecko_debouncing_time: Option, - - /// Maximum capacity of the broadcast channel to store the CoinGecko native - /// prices results - #[clap(long, env, group = "coin_gecko_buffered")] - pub coin_gecko_broadcast_channel_capacity: Option, -} - -/// Controls which level of quote verification gets applied. -#[derive(Copy, Clone, Debug, clap::ValueEnum)] -#[clap(rename_all = "kebab-case")] -pub enum QuoteVerificationMode { - /// Quotes do not get verified. - Unverified, - /// Quotes get verified whenever possible and verified - /// quotes are preferred over unverified ones. - Prefer, - /// Quotes get discarded if they can't be verified. - /// Some scenarios like missing sell token balance are exempt. - EnforceWhenPossible, -} - -impl Display for Arguments { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let Self { - tenderly, - price_estimation_rate_limiter, - amount_to_estimate_prices_with, - one_inch_api_key, - one_inch_url, - coin_gecko, - quote_inaccuracy_limit, - quote_verification, - quote_timeout, - balance_overrides, - tokens_without_verification, - } = self; - - write!(f, "{tenderly}")?; - display_option( - f, - "price_estimation_rate_limites", - price_estimation_rate_limiter, - )?; - display_option( - f, - "amount_to_estimate_prices_with: {}", - amount_to_estimate_prices_with, - )?; - display_secret_option( - f, - "one_inch_spot_price_api_key: {:?}", - one_inch_api_key.as_ref(), - )?; - writeln!(f, "one_inch_spot_price_api_url: {one_inch_url}")?; - display_secret_option( - f, - "coin_gecko_api_key: {:?}", - coin_gecko.coin_gecko_api_key.as_ref(), - )?; - writeln!(f, "coin_gecko_api_url: {}", coin_gecko.coin_gecko_url)?; - writeln!(f, "coin_gecko_api_url: {}", coin_gecko.coin_gecko_url)?; - writeln!( - f, - "coin_gecko_debouncing_time: {:?}", - coin_gecko - .coin_gecko_buffered - .as_ref() - .map(|coin_gecko_buffered| coin_gecko_buffered.coin_gecko_debouncing_time), - )?; - writeln!( - f, - "coin_gecko_broadcast_channel_capacity: {:?}", - coin_gecko.coin_gecko_buffered.as_ref().map( - |coin_gecko_buffered| coin_gecko_buffered.coin_gecko_broadcast_channel_capacity - ), - )?; - writeln!(f, "quote_inaccuracy_limit: {quote_inaccuracy_limit}")?; - writeln!(f, "quote_verification: {quote_verification:?}")?; - writeln!(f, "quote_timeout: {quote_timeout:?}")?; - write!(f, "{balance_overrides}")?; - writeln!( - f, - "tokens_without_verification: {tokens_without_verification:?}" - )?; - - Ok(()) - } -} - #[derive(Error, Debug)] pub enum PriceEstimationError { #[error("token {token:?} is not supported: {reason:}")] @@ -630,78 +267,3 @@ pub mod mocks { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn toml_deserialize_estimators_empty() { - #[derive(Deserialize)] - struct Helper { - _estimators: NativePriceEstimators, - } - - assert!(toml::from_str::("estimators = []").is_err()); - assert!(toml::from_str::("estimators = [[]]").is_err()); - } - - #[test] - fn toml_deserialize_estimators_single_stage() { - let toml = r#" - estimators = [[{type = "CoinGecko"}, {type = "OneInchSpotPriceApi"}]] - "#; - - #[derive(Deserialize)] - struct Helper { - estimators: NativePriceEstimators, - } - - let parsed: Helper = toml::from_str(toml).unwrap(); - assert_eq!( - parsed.estimators.as_slice(), - vec![vec![ - NativePriceEstimator::CoinGecko, - NativePriceEstimator::OneInchSpotPriceApi, - ]] - ); - } - - #[test] - fn toml_deserialize_estimators_multiple_stages() { - let toml = r#" - estimators = [ - [{type = "CoinGecko"}, {type = "Driver", name = "solver1", url = "http://localhost:8080"}], - [{type = "Forwarder", url = "http://localhost:12088"}], - ] - "#; - - #[derive(Deserialize)] - struct Helper { - estimators: NativePriceEstimators, - } - - let parsed: Helper = toml::from_str(toml).unwrap(); - assert_eq!( - parsed.estimators.as_slice(), - vec![ - vec![ - NativePriceEstimator::CoinGecko, - NativePriceEstimator::Driver(ExternalSolver { - name: "solver1".to_string(), - url: "http://localhost:8080".parse().unwrap(), - }), - ], - vec![NativePriceEstimator::Forwarder { - url: "http://localhost:12088".parse().unwrap(), - }], - ] - ); - } - - #[test] - fn toml_deserialize_estimators_default() { - let estimators = NativePriceEstimators::default(); - assert!(estimators.as_slice().is_empty()); - } -} diff --git a/crates/price-estimation/src/native/eip4626.rs b/crates/price-estimation/src/native/eip4626.rs index fb8e45a005..24c24c192c 100644 --- a/crates/price-estimation/src/native/eip4626.rs +++ b/crates/price-estimation/src/native/eip4626.rs @@ -3,7 +3,7 @@ use { crate::PriceEstimationError, alloy::primitives::{Address, U256}, anyhow::Context, - contracts::alloy::{ERC20, IERC4626}, + contracts::{ERC20, IERC4626}, ethrpc::AlloyProvider, futures::{FutureExt, future::BoxFuture}, num::ToPrimitive, From 235031475d0ab7c7318439b32d0e51d0495da087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:44:31 +0100 Subject: [PATCH 05/20] clean up post merge --- Cargo.lock | 12 + contracts/generated/Cargo.lock | 24 + .../generated/contracts-facade/Cargo.toml | 1 + .../generated/contracts-facade/src/lib.rs | 1 + .../contracts-generated/ierc4626/Cargo.toml | 19 + .../contracts-generated/ierc4626/src/lib.rs | 648 ++++++++++++++++++ contracts/src/main.rs | 1 + crates/configs/src/native_price_estimators.rs | 28 +- crates/e2e/tests/e2e/eip4626.rs | 11 +- crates/price-estimation/src/lib.rs | 363 ---------- crates/price-estimation/src/native/eip4626.rs | 2 +- 11 files changed, 731 insertions(+), 379 deletions(-) create mode 100644 contracts/generated/contracts-generated/ierc4626/Cargo.toml create mode 100644 contracts/generated/contracts-generated/ierc4626/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 89b5c56454..dcb7c4b366 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2537,6 +2537,7 @@ dependencies = [ "cow-contract-honeyswaprouter", "cow-contract-hookstrampoline", "cow-contract-icowwrapper", + "cow-contract-ierc4626", "cow-contract-iswaprpair", "cow-contract-iuniswaplikepair", "cow-contract-iuniswaplikerouter", @@ -3214,6 +3215,17 @@ dependencies = [ "anyhow", ] +[[package]] +name = "cow-contract-ierc4626" +version = "0.1.0" +dependencies = [ + "alloy-contract", + "alloy-primitives", + "alloy-provider", + "alloy-sol-types", + "anyhow", +] + [[package]] name = "cow-contract-iswaprpair" version = "0.1.0" diff --git a/contracts/generated/Cargo.lock b/contracts/generated/Cargo.lock index fd0489b2d0..e62ee4c84a 100644 --- a/contracts/generated/Cargo.lock +++ b/contracts/generated/Cargo.lock @@ -1048,6 +1048,7 @@ dependencies = [ "cow-contract-cowammlegacyhelper", "cow-contract-cowammuniswapv2priceoracle", "cow-contract-cowprotocoltoken", + "cow-contract-cowsettlementforwarder", "cow-contract-cowswapethflow", "cow-contract-cowswaponchainorders", "cow-contract-erc1271signaturevalidator", @@ -1064,6 +1065,7 @@ dependencies = [ "cow-contract-honeyswaprouter", "cow-contract-hookstrampoline", "cow-contract-icowwrapper", + "cow-contract-ierc4626", "cow-contract-iswaprpair", "cow-contract-iuniswaplikepair", "cow-contract-iuniswaplikerouter", @@ -1468,6 +1470,17 @@ dependencies = [ "anyhow", ] +[[package]] +name = "cow-contract-cowsettlementforwarder" +version = "0.1.0" +dependencies = [ + "alloy-contract", + "alloy-primitives", + "alloy-provider", + "alloy-sol-types", + "anyhow", +] + [[package]] name = "cow-contract-cowswapethflow" version = "0.1.0" @@ -1644,6 +1657,17 @@ dependencies = [ "anyhow", ] +[[package]] +name = "cow-contract-ierc4626" +version = "0.1.0" +dependencies = [ + "alloy-contract", + "alloy-primitives", + "alloy-provider", + "alloy-sol-types", + "anyhow", +] + [[package]] name = "cow-contract-iswaprpair" version = "0.1.0" diff --git a/contracts/generated/contracts-facade/Cargo.toml b/contracts/generated/contracts-facade/Cargo.toml index ca824f689c..7408bce369 100644 --- a/contracts/generated/contracts-facade/Cargo.toml +++ b/contracts/generated/contracts-facade/Cargo.toml @@ -59,6 +59,7 @@ cow-contract-gpv2settlement = { path = "../contracts-generated/gpv2settlement" } cow-contract-honeyswaprouter = { path = "../contracts-generated/honeyswaprouter" } cow-contract-hookstrampoline = { path = "../contracts-generated/hookstrampoline" } cow-contract-icowwrapper = { path = "../contracts-generated/icowwrapper" } +cow-contract-ierc4626 = { path = "../contracts-generated/ierc4626" } cow-contract-iswaprpair = { path = "../contracts-generated/iswaprpair" } cow-contract-iuniswaplikepair = { path = "../contracts-generated/iuniswaplikepair" } cow-contract-iuniswaplikerouter = { path = "../contracts-generated/iuniswaplikerouter" } diff --git a/contracts/generated/contracts-facade/src/lib.rs b/contracts/generated/contracts-facade/src/lib.rs index 91abfdf567..6c5154a801 100644 --- a/contracts/generated/contracts-facade/src/lib.rs +++ b/contracts/generated/contracts-facade/src/lib.rs @@ -48,6 +48,7 @@ pub use { cow_contract_honeyswaprouter as HoneyswapRouter, cow_contract_hookstrampoline as HooksTrampoline, cow_contract_icowwrapper as ICowWrapper, + cow_contract_ierc4626 as IERC4626, cow_contract_iswaprpair as ISwaprPair, cow_contract_iuniswaplikepair as IUniswapLikePair, cow_contract_iuniswaplikerouter as IUniswapLikeRouter, diff --git a/contracts/generated/contracts-generated/ierc4626/Cargo.toml b/contracts/generated/contracts-generated/ierc4626/Cargo.toml new file mode 100644 index 0000000000..fe00f44d09 --- /dev/null +++ b/contracts/generated/contracts-generated/ierc4626/Cargo.toml @@ -0,0 +1,19 @@ +# Auto-generated by contracts-generate. Do not edit. +[package] +name = "cow-contract-ierc4626" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +doctest = false + +[dependencies] +alloy-contract = { workspace = true } +alloy-primitives = { workspace = true } +alloy-provider = { workspace = true } +alloy-sol-types = { workspace = true } +anyhow = { workspace = true } + +[lints] +workspace = true diff --git a/contracts/generated/contracts-generated/ierc4626/src/lib.rs b/contracts/generated/contracts-generated/ierc4626/src/lib.rs new file mode 100644 index 0000000000..11f48918c2 --- /dev/null +++ b/contracts/generated/contracts-generated/ierc4626/src/lib.rs @@ -0,0 +1,648 @@ +#![allow( + unused_imports, + unused_attributes, + clippy::all, + rustdoc::all, + non_snake_case +)] +//! Auto-generated contract bindings. Do not edit. +/** + +Generated by the following Solidity interface... +```solidity +interface IERC4626 { + function asset() external view returns (address assetTokenAddress); + function convertToAssets(uint256 shares) external view returns (uint256 assets); +} +``` + +...which was generated by the following JSON ABI: +```json +[ + { + "type": "function", + "name": "asset", + "inputs": [], + "outputs": [ + { + "name": "assetTokenAddress", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "convertToAssets", + "inputs": [ + { + "name": "shares", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "assets", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + } +] +```*/ +#[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style, + clippy::empty_structs_with_brackets +)] +pub mod IERC4626 { + use {super::*, alloy_sol_types}; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `asset()` and selector `0x38d52e0f`. + ```solidity + function asset() external view returns (address assetTokenAddress); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct assetCall; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the [`asset()`](assetCall) + /// function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct assetReturn { + #[allow(missing_docs)] + pub assetTokenAddress: alloy_sol_types::private::Address, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: assetCall) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for assetCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Address,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy_sol_types::private::Address,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: assetReturn) -> Self { + (value.assetTokenAddress,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for assetReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { + assetTokenAddress: tuple.0, + } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for assetCall { + type Parameters<'a> = (); + type Return = alloy_sol_types::private::Address; + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + type ReturnTuple<'a> = (alloy_sol_types::sol_data::Address,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + const SELECTOR: [u8; 4] = [56u8, 213u8, 46u8, 15u8]; + const SIGNATURE: &'static str = "asset()"; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + () + } + + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + (::tokenize(ret),) + } + + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data).map( + |r| { + let r: assetReturn = r.into(); + r.assetTokenAddress + }, + ) + } + + #[inline] + fn abi_decode_returns_validate(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate( + data, + ) + .map(|r| { + let r: assetReturn = r.into(); + r.assetTokenAddress + }) + } + } + }; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `convertToAssets(uint256)` and selector `0x07a2d13a`. + ```solidity + function convertToAssets(uint256 shares) external view returns (uint256 assets); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct convertToAssetsCall { + #[allow(missing_docs)] + pub shares: alloy_sol_types::private::primitives::aliases::U256, + } + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the + /// [`convertToAssets(uint256)`](convertToAssetsCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct convertToAssetsReturn { + #[allow(missing_docs)] + pub assets: alloy_sol_types::private::primitives::aliases::U256, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy_sol_types::private::primitives::aliases::U256,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: convertToAssetsCall) -> Self { + (value.shares,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for convertToAssetsCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { shares: tuple.0 } + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy_sol_types::private::primitives::aliases::U256,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: convertToAssetsReturn) -> Self { + (value.assets,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for convertToAssetsReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { assets: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for convertToAssetsCall { + type Parameters<'a> = (alloy_sol_types::sol_data::Uint<256>,); + type Return = alloy_sol_types::private::primitives::aliases::U256; + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + type ReturnTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + const SELECTOR: [u8; 4] = [7u8, 162u8, 209u8, 58u8]; + const SIGNATURE: &'static str = "convertToAssets(uint256)"; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + ( + as alloy_sol_types::SolType>::tokenize( + &self.shares, + ), + ) + } + + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + ( + as alloy_sol_types::SolType>::tokenize( + ret, + ), + ) + } + + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data).map( + |r| { + let r: convertToAssetsReturn = r.into(); + r.assets + }, + ) + } + + #[inline] + fn abi_decode_returns_validate(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate( + data, + ) + .map(|r| { + let r: convertToAssetsReturn = r.into(); + r.assets + }) + } + } + }; + ///Container for all the [`IERC4626`](self) function calls. + #[derive(Clone)] + pub enum IERC4626Calls { + #[allow(missing_docs)] + asset(assetCall), + #[allow(missing_docs)] + convertToAssets(convertToAssetsCall), + } + impl IERC4626Calls { + /// All the selectors of this enum. + /// + /// Note that the selectors might not be in the same order as the + /// variants. No guarantees are made about the order of the + /// selectors. + /// + /// Prefer using `SolInterface` methods instead. + pub const SELECTORS: &'static [[u8; 4usize]] = + &[[7u8, 162u8, 209u8, 58u8], [56u8, 213u8, 46u8, 15u8]]; + /// The signatures in the same order as `SELECTORS`. + pub const SIGNATURES: &'static [&'static str] = &[ + ::SIGNATURE, + ::SIGNATURE, + ]; + /// The names of the variants in the same order as `SELECTORS`. + pub const VARIANT_NAMES: &'static [&'static str] = &[ + ::core::stringify!(convertToAssets), + ::core::stringify!(asset), + ]; + + /// Returns the signature for the given selector, if known. + #[inline] + pub fn signature_by_selector( + selector: [u8; 4usize], + ) -> ::core::option::Option<&'static str> { + match Self::SELECTORS.binary_search(&selector) { + ::core::result::Result::Ok(idx) => { + ::core::option::Option::Some(Self::SIGNATURES[idx]) + } + ::core::result::Result::Err(_) => ::core::option::Option::None, + } + } + + /// Returns the enum variant name for the given selector, if known. + #[inline] + pub fn name_by_selector(selector: [u8; 4usize]) -> ::core::option::Option<&'static str> { + let sig = Self::signature_by_selector(selector)?; + sig.split_once('(').map(|(name, _)| name) + } + } + #[automatically_derived] + impl alloy_sol_types::SolInterface for IERC4626Calls { + const COUNT: usize = 2usize; + const MIN_DATA_LENGTH: usize = 0usize; + const NAME: &'static str = "IERC4626Calls"; + + #[inline] + fn selector(&self) -> [u8; 4] { + match self { + Self::asset(_) => ::SELECTOR, + Self::convertToAssets(_) => { + ::SELECTOR + } + } + } + + #[inline] + fn selector_at(i: usize) -> ::core::option::Option<[u8; 4]> { + Self::SELECTORS.get(i).copied() + } + + #[inline] + fn valid_selector(selector: [u8; 4]) -> bool { + Self::SELECTORS.binary_search(&selector).is_ok() + } + + #[inline] + #[allow(non_snake_case)] + fn abi_decode_raw(selector: [u8; 4], data: &[u8]) -> alloy_sol_types::Result { + static DECODE_SHIMS: &[fn(&[u8]) -> alloy_sol_types::Result] = &[ + { + fn convertToAssets(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(IERC4626Calls::convertToAssets) + } + convertToAssets + }, + { + fn asset(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(IERC4626Calls::asset) + } + asset + }, + ]; + let Ok(idx) = Self::SELECTORS.binary_search(&selector) else { + return Err(alloy_sol_types::Error::unknown_selector( + ::NAME, + selector, + )); + }; + DECODE_SHIMS[idx](data) + } + + #[inline] + #[allow(non_snake_case)] + fn abi_decode_raw_validate( + selector: [u8; 4], + data: &[u8], + ) -> alloy_sol_types::Result { + static DECODE_VALIDATE_SHIMS: &[fn(&[u8]) -> alloy_sol_types::Result] = + &[ + { + fn convertToAssets(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(IERC4626Calls::convertToAssets) + } + convertToAssets + }, + { + fn asset(data: &[u8]) -> alloy_sol_types::Result { + ::abi_decode_raw_validate(data) + .map(IERC4626Calls::asset) + } + asset + }, + ]; + let Ok(idx) = Self::SELECTORS.binary_search(&selector) else { + return Err(alloy_sol_types::Error::unknown_selector( + ::NAME, + selector, + )); + }; + DECODE_VALIDATE_SHIMS[idx](data) + } + + #[inline] + fn abi_encoded_size(&self) -> usize { + match self { + Self::asset(inner) => { + ::abi_encoded_size(inner) + } + Self::convertToAssets(inner) => { + ::abi_encoded_size(inner) + } + } + } + + #[inline] + fn abi_encode_raw(&self, out: &mut alloy_sol_types::private::Vec) { + match self { + Self::asset(inner) => { + ::abi_encode_raw(inner, out) + } + Self::convertToAssets(inner) => { + ::abi_encode_raw(inner, out) + } + } + } + } + use alloy_contract; + /**Creates a new wrapper around an on-chain [`IERC4626`](self) contract instance. + + See the [wrapper's documentation](`IERC4626Instance`) for more details.*/ + #[inline] + pub const fn new< + P: alloy_contract::private::Provider, + N: alloy_contract::private::Network, + >( + address: alloy_sol_types::private::Address, + __provider: P, + ) -> IERC4626Instance { + IERC4626Instance::::new(address, __provider) + } + /**A [`IERC4626`](self) instance. + + Contains type-safe methods for interacting with an on-chain instance of the + [`IERC4626`](self) contract located at a given `address`, using a given + provider `P`. + + If the contract bytecode is available (see the [`sol!`](alloy_sol_types::sol!) + documentation on how to provide it), the `deploy` and `deploy_builder` methods can + be used to deploy a new instance of the contract. + + See the [module-level documentation](self) for all the available methods.*/ + #[derive(Clone)] + pub struct IERC4626Instance { + address: alloy_sol_types::private::Address, + provider: P, + _network: ::core::marker::PhantomData, + } + #[automatically_derived] + impl ::core::fmt::Debug for IERC4626Instance { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_tuple("IERC4626Instance") + .field(&self.address) + .finish() + } + } + /// Instantiation and getters/setters. + impl, N: alloy_contract::private::Network> + IERC4626Instance + { + /**Creates a new wrapper around an on-chain [`IERC4626`](self) contract instance. + + See the [wrapper's documentation](`IERC4626Instance`) for more details.*/ + #[inline] + pub const fn new(address: alloy_sol_types::private::Address, __provider: P) -> Self { + Self { + address, + provider: __provider, + _network: ::core::marker::PhantomData, + } + } + + /// Returns a reference to the address. + #[inline] + pub const fn address(&self) -> &alloy_sol_types::private::Address { + &self.address + } + + /// Sets the address. + #[inline] + pub fn set_address(&mut self, address: alloy_sol_types::private::Address) { + self.address = address; + } + + /// Sets the address and returns `self`. + pub fn at(mut self, address: alloy_sol_types::private::Address) -> Self { + self.set_address(address); + self + } + + /// Returns a reference to the provider. + #[inline] + pub const fn provider(&self) -> &P { + &self.provider + } + } + impl IERC4626Instance<&P, N> { + /// Clones the provider and returns a new instance with the cloned + /// provider. + #[inline] + pub fn with_cloned_provider(self) -> IERC4626Instance { + IERC4626Instance { + address: self.address, + provider: ::core::clone::Clone::clone(&self.provider), + _network: ::core::marker::PhantomData, + } + } + } + /// Function calls. + impl, N: alloy_contract::private::Network> + IERC4626Instance + { + /// Creates a new call builder using this contract instance's provider + /// and address. + /// + /// Note that the call can be any function call, not just those defined + /// in this contract. Prefer using the other methods for + /// building type-safe contract calls. + pub fn call_builder( + &self, + call: &C, + ) -> alloy_contract::SolCallBuilder<&P, C, N> { + alloy_contract::SolCallBuilder::new_sol(&self.provider, &self.address, call) + } + + ///Creates a new call builder for the [`asset`] function. + pub fn asset(&self) -> alloy_contract::SolCallBuilder<&P, assetCall, N> { + self.call_builder(&assetCall) + } + + ///Creates a new call builder for the [`convertToAssets`] function. + pub fn convertToAssets( + &self, + shares: alloy_sol_types::private::primitives::aliases::U256, + ) -> alloy_contract::SolCallBuilder<&P, convertToAssetsCall, N> { + self.call_builder(&convertToAssetsCall { shares }) + } + } + /// Event filters. + impl, N: alloy_contract::private::Network> + IERC4626Instance + { + /// Creates a new event filter using this contract instance's provider + /// and address. + /// + /// Note that the type can be any event, not just those defined in this + /// contract. Prefer using the other methods for building + /// type-safe event filters. + pub fn event_filter( + &self, + ) -> alloy_contract::Event<&P, E, N> { + alloy_contract::Event::new_sol(&self.provider, &self.address) + } + } +} +pub type Instance = IERC4626::IERC4626Instance<::alloy_provider::DynProvider>; diff --git a/contracts/src/main.rs b/contracts/src/main.rs index 34ec14364c..efc6a618c0 100644 --- a/contracts/src/main.rs +++ b/contracts/src/main.rs @@ -397,6 +397,7 @@ fn build_module() -> Module { ])) .add_contract(Contract::new("CoWSwapOnchainOrders")) .add_contract(Contract::new("ERC1271SignatureValidator")) + .add_contract(Contract::new("IERC4626")) .add_contract(Contract::new("BalancerQueries").with_networks(networks![ MAINNET => ("0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5", 15188261), ARBITRUM_ONE => ("0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5", 18238624), diff --git a/crates/configs/src/native_price_estimators.rs b/crates/configs/src/native_price_estimators.rs index 52577ef818..88a5d1da8a 100644 --- a/crates/configs/src/native_price_estimators.rs +++ b/crates/configs/src/native_price_estimators.rs @@ -22,17 +22,20 @@ impl<'de> Deserialize<'de> for NativePriceEstimators { &"expected native price estimator stages to be configured", )); } - match estimators - .iter() - .enumerate() - .find_map(|(n, stage)| stage.is_empty().then_some(n)) - { - Some(n) => Err(serde::de::Error::invalid_length( - 0, - &format!("stage {} is empty, all stages must not be empty", n).as_str(), - )), - None => Ok(Self(estimators)), + for (n, stage) in estimators.iter().enumerate() { + if stage.is_empty() { + return Err(serde::de::Error::invalid_length( + 0, + &format!("stage {} is empty, all stages must not be empty", n).as_str(), + )); + } + if matches!(stage.last(), Some(NativePriceEstimator::Eip4626)) { + return Err(serde::de::Error::custom(format!( + "stage {n}: Eip4626 must be followed by another estimator" + ))); + } } + Ok(Self(estimators)) } } @@ -96,6 +99,10 @@ pub enum NativePriceEstimator { OneInchSpotPriceApi, /// Use the CoinGecko API. CoinGecko, + /// Prices EIP-4626 vault tokens by looking up the underlying `asset()` and + /// applying `convertToAssets()` as a conversion rate. At construction time, + /// wraps the next estimator in the configuration list. + Eip4626, } impl NativePriceEstimator { @@ -115,6 +122,7 @@ impl Display for NativePriceEstimator { NativePriceEstimator::Forwarder { url } => write!(f, "Forwarder|{}", url), NativePriceEstimator::OneInchSpotPriceApi => write!(f, "OneInchSpotPriceApi"), NativePriceEstimator::CoinGecko => write!(f, "CoinGecko"), + NativePriceEstimator::Eip4626 => write!(f, "Eip4626"), } } } diff --git a/crates/e2e/tests/e2e/eip4626.rs b/crates/e2e/tests/e2e/eip4626.rs index a5293d3b16..795bed9473 100644 --- a/crates/e2e/tests/e2e/eip4626.rs +++ b/crates/e2e/tests/e2e/eip4626.rs @@ -3,9 +3,11 @@ use { primitives::{Address, address}, providers::ext::{AnvilApi, ImpersonateConfig}, }, - autopilot::config::{Configuration, native_price::NativePriceConfig}, - configs::test_util::TestDefault, - contracts::alloy::ERC20, + configs::{ + autopilot::{Configuration, native_price::NativePriceConfig}, + test_util::TestDefault, + }, + contracts::ERC20, e2e::setup::*, ethrpc::alloy::CallBuilderExt, model::quote::{OrderQuoteRequest, OrderQuoteSide, SellAmount}, @@ -94,9 +96,8 @@ async fn eip4626_native_price_test(web3: Web3) { let services = Services::new(&onchain).await; services .start_protocol_with_args( - Default::default(), autopilot_config, - orderbook::config::Configuration::test_default(), + configs::orderbook::Configuration::test_default(), solver, ) .await; diff --git a/crates/price-estimation/src/lib.rs b/crates/price-estimation/src/lib.rs index 9631772400..0ad735845f 100644 --- a/crates/price-estimation/src/lib.rs +++ b/crates/price-estimation/src/lib.rs @@ -34,369 +34,6 @@ pub mod trade_finding; pub mod trade_verifier; pub mod utils; -#[derive(Clone, Debug, Default, Serialize)] -pub struct NativePriceEstimators(Vec>); - -impl<'de> Deserialize<'de> for NativePriceEstimators { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - let estimators = >>::deserialize(deserializer)?; - if estimators.is_empty() { - return Err(serde::de::Error::invalid_length( - 0, - &"expected native price estimator stages to be configured", - )); - } - for (n, stage) in estimators.iter().enumerate() { - if stage.is_empty() { - return Err(serde::de::Error::invalid_length( - 0, - &format!("stage {} is empty, all stages must not be empty", n).as_str(), - )); - } - if matches!(stage.last(), Some(NativePriceEstimator::Eip4626)) { - return Err(serde::de::Error::custom(format!( - "stage {n}: Eip4626 must be followed by another estimator" - ))); - } - } - Ok(Self(estimators)) - } -} - -impl NativePriceEstimators { - pub fn new(estimators: Vec>) -> Self { - Self(estimators) - } -} - -#[cfg(any(test, feature = "test-util"))] -impl NativePriceEstimators { - /// Returns a list with a single stage, said stage contains a Driver estimator named `test_quoter` with URL `http://localhost:11088/test_solver`. - pub fn test_default() -> Self { - NativePriceEstimators::new(vec![vec![NativePriceEstimator::driver( - "test_quoter".to_string(), - Url::from_str("http://localhost:11088/test_solver").unwrap(), - )]]) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] -pub struct ExternalSolver { - pub name: String, - pub url: Url, -} - -impl FromStr for ExternalSolver { - type Err = anyhow::Error; - - fn from_str(solver: &str) -> Result { - let parts: Vec<&str> = solver.split('|').collect(); - ensure!(parts.len() >= 2, "not enough arguments for external solver"); - let (name, url) = (parts[0], parts[1]); - let url: Url = url.parse()?; - Ok(Self { - name: name.to_owned(), - url, - }) - } -} - -#[derive(Clone, Debug, Hash, Eq, PartialEq, Deserialize, Serialize)] -#[serde(tag = "type")] -pub enum NativePriceEstimator { - Driver(ExternalSolver), - Forwarder { - url: Url, - }, - OneInchSpotPriceApi, - CoinGecko, - /// Prices EIP-4626 vault tokens by looking up the underlying `asset()` and - /// applying `convertToAssets()` as a conversion rate. At construction time, - /// wraps the next estimator in the configuration list. - Eip4626, -} - -impl NativePriceEstimator { - pub const fn driver(name: String, url: Url) -> Self { - Self::Driver(ExternalSolver { name, url }) - } - - pub const fn forwarder(url: Url) -> Self { - Self::Forwarder { url } - } -} - -impl Display for NativePriceEstimator { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let formatter = match self { - NativePriceEstimator::Driver(s) => format!("Driver|{}|{}", &s.name, s.url), - NativePriceEstimator::Forwarder { url } => format!("Forwarder|{}", url), - NativePriceEstimator::OneInchSpotPriceApi => "OneInchSpotPriceApi".into(), - NativePriceEstimator::CoinGecko => "CoinGecko".into(), - NativePriceEstimator::Eip4626 => "Eip4626".into(), - }; - write!(f, "{formatter}") - } -} - -impl NativePriceEstimators { - pub fn as_slice(&self) -> &[Vec] { - &self.0 - } -} - -impl Display for NativePriceEstimators { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let formatter = self - .as_slice() - .iter() - .map(|stage| { - stage - .iter() - .format_with(",", |estimator, f| f(&format_args!("{estimator}"))) - }) - .format(";"); - write!(f, "{formatter}") - } -} - -impl FromStr for NativePriceEstimators { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - Ok(Self( - s.split(';') - .map(|sub_list| { - sub_list - .split(',') - .map(NativePriceEstimator::from_str) - .collect::>>() - }) - .collect::>>>()?, - )) - } -} - -impl FromStr for NativePriceEstimator { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let (variant, args) = s.split_once('|').unwrap_or((s, "")); - match variant { - "OneInchSpotPriceApi" => Ok(NativePriceEstimator::OneInchSpotPriceApi), - "CoinGecko" => Ok(NativePriceEstimator::CoinGecko), - "Driver" => Ok(NativePriceEstimator::Driver(ExternalSolver::from_str( - args, - )?)), - "Forwarder" => Ok(NativePriceEstimator::Forwarder { - url: args - .parse() - .context("Forwarder price estimator invalid URL")?, - }), - "Eip4626" => Ok(NativePriceEstimator::Eip4626), - _ => Err(anyhow::anyhow!("unsupported native price estimator: {}", s)), - } - } -} - -/// Shared price estimation configuration arguments. -#[derive(clap::Parser)] -#[group(skip)] -pub struct Arguments { - #[clap(flatten)] - pub tenderly: tenderly_api::Arguments, - - /// Configures the back off strategy for price estimators when requests take - /// too long. Requests issued while back off is active get dropped - /// entirely. Needs to be passed as - /// ",,". - /// back_off_growth_factor: f64 >= 1.0 - /// min_back_off: Duration - /// max_back_off: Duration - #[clap(long, env, verbatim_doc_comment)] - pub price_estimation_rate_limiter: Option, - - /// The amount in native tokens atoms to use for price estimation. Should be - /// reasonably large so that small pools do not influence the prices. If - /// not set a reasonable default is used based on network id. - #[clap(long, env)] - pub amount_to_estimate_prices_with: Option, - - /// The API key for the 1Inch API. - #[clap(long, env)] - pub one_inch_api_key: Option, - - /// The base URL for the 1Inch API. - #[clap(long, env, default_value = "https://api.1inch.dev/")] - pub one_inch_url: Url, - - /// The CoinGecko native price configuration - #[clap(flatten)] - pub coin_gecko: CoinGecko, - - /// How inaccurate a quote must be before it gets discarded provided as a - /// factor. - /// E.g. a value of `0.01` means at most 1 percent of the sell or buy tokens - /// can be paid out of the settlement contract buffers. - #[clap(long, env, default_value = "1.")] - pub quote_inaccuracy_limit: BigDecimal, - - /// How strict quote verification should be. - #[clap( - long, - env, - default_value = "unverified", - value_enum, - verbatim_doc_comment - )] - pub quote_verification: QuoteVerificationMode, - - /// Default timeout for quote requests. - #[clap( - long, - env, - default_value = "5s", - value_parser = humantime::parse_duration, - )] - pub quote_timeout: Duration, - - #[clap(flatten)] - pub balance_overrides: balance_overrides::Arguments, - - /// Tokens for which quote verification should not be attempted. This is an - /// escape hatch when there is a very bad but verifiable liquidity source - /// that would win against a very good but unverifiable liquidity source - /// (e.g. private liquidity that exists but can't be verified). - #[clap(long, env, value_delimiter = ',')] - pub tokens_without_verification: Vec
, -} - -#[derive(clap::Parser)] -pub struct CoinGecko { - /// The API key for the CoinGecko API. - #[clap(long, env)] - pub coin_gecko_api_key: Option, - - /// The base URL for the CoinGecko API. - #[clap( - long, - env, - default_value = "https://api.coingecko.com/api/v3/simple/token_price" - )] - pub coin_gecko_url: Url, - - #[clap(flatten)] - pub coin_gecko_buffered: Option, -} - -#[derive(clap::Parser)] -#[clap(group( - clap::ArgGroup::new("coin_gecko_buffered") - .requires_all(&[ - "coin_gecko_debouncing_time", - "coin_gecko_broadcast_channel_capacity" - ]) - .multiple(true) - .required(false), -))] -pub struct CoinGeckoBuffered { - /// An additional minimum delay to wait for collecting CoinGecko requests. - /// - /// The delay to start counting after receiving the first request. - #[clap(long, env, value_parser = humantime::parse_duration, group = "coin_gecko_buffered")] - pub coin_gecko_debouncing_time: Option, - - /// Maximum capacity of the broadcast channel to store the CoinGecko native - /// prices results - #[clap(long, env, group = "coin_gecko_buffered")] - pub coin_gecko_broadcast_channel_capacity: Option, -} - -/// Controls which level of quote verification gets applied. -#[derive(Copy, Clone, Debug, clap::ValueEnum)] -#[clap(rename_all = "kebab-case")] -pub enum QuoteVerificationMode { - /// Quotes do not get verified. - Unverified, - /// Quotes get verified whenever possible and verified - /// quotes are preferred over unverified ones. - Prefer, - /// Quotes get discarded if they can't be verified. - /// Some scenarios like missing sell token balance are exempt. - EnforceWhenPossible, -} - -impl Display for Arguments { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let Self { - tenderly, - price_estimation_rate_limiter, - amount_to_estimate_prices_with, - one_inch_api_key, - one_inch_url, - coin_gecko, - quote_inaccuracy_limit, - quote_verification, - quote_timeout, - balance_overrides, - tokens_without_verification, - } = self; - - write!(f, "{tenderly}")?; - display_option( - f, - "price_estimation_rate_limites", - price_estimation_rate_limiter, - )?; - display_option( - f, - "amount_to_estimate_prices_with: {}", - amount_to_estimate_prices_with, - )?; - display_secret_option( - f, - "one_inch_spot_price_api_key: {:?}", - one_inch_api_key.as_ref(), - )?; - writeln!(f, "one_inch_spot_price_api_url: {one_inch_url}")?; - display_secret_option( - f, - "coin_gecko_api_key: {:?}", - coin_gecko.coin_gecko_api_key.as_ref(), - )?; - writeln!(f, "coin_gecko_api_url: {}", coin_gecko.coin_gecko_url)?; - writeln!(f, "coin_gecko_api_url: {}", coin_gecko.coin_gecko_url)?; - writeln!( - f, - "coin_gecko_debouncing_time: {:?}", - coin_gecko - .coin_gecko_buffered - .as_ref() - .map(|coin_gecko_buffered| coin_gecko_buffered.coin_gecko_debouncing_time), - )?; - writeln!( - f, - "coin_gecko_broadcast_channel_capacity: {:?}", - coin_gecko.coin_gecko_buffered.as_ref().map( - |coin_gecko_buffered| coin_gecko_buffered.coin_gecko_broadcast_channel_capacity - ), - )?; - writeln!(f, "quote_inaccuracy_limit: {quote_inaccuracy_limit}")?; - writeln!(f, "quote_verification: {quote_verification:?}")?; - writeln!(f, "quote_timeout: {quote_timeout:?}")?; - write!(f, "{balance_overrides}")?; - writeln!( - f, - "tokens_without_verification: {tokens_without_verification:?}" - )?; - - Ok(()) - } -} - #[derive(Error, Debug)] pub enum PriceEstimationError { #[error("token {token:?} is not supported: {reason:}")] diff --git a/crates/price-estimation/src/native/eip4626.rs b/crates/price-estimation/src/native/eip4626.rs index dc5cfa3e2e..b9b1f2f81b 100644 --- a/crates/price-estimation/src/native/eip4626.rs +++ b/crates/price-estimation/src/native/eip4626.rs @@ -3,7 +3,7 @@ use { crate::PriceEstimationError, alloy::primitives::{Address, U256}, anyhow::Context, - contracts::alloy::{ERC20, IERC4626}, + contracts::{ERC20, IERC4626}, ethrpc::AlloyProvider, futures::{FutureExt, future::BoxFuture}, num::ToPrimitive, From 2144c80cd95da1a025048b9df4b054884b4f9ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Fri, 10 Apr 2026 12:13:48 +0100 Subject: [PATCH 06/20] Consider rate when testing recursive vaults --- contracts/artifacts/MockERC4626Wrapper.json | 40 +- .../mockerc4626wrapper/src/lib.rs | 474 +++++++++++++++++- .../solidity/tests/MockERC4626Wrapper.sol | 40 +- crates/e2e/tests/e2e/eip4626.rs | 75 ++- 4 files changed, 569 insertions(+), 60 deletions(-) diff --git a/contracts/artifacts/MockERC4626Wrapper.json b/contracts/artifacts/MockERC4626Wrapper.json index 93c9644359..4bbe02465e 100644 --- a/contracts/artifacts/MockERC4626Wrapper.json +++ b/contracts/artifacts/MockERC4626Wrapper.json @@ -12,6 +12,16 @@ "name": "_decimals", "type": "uint8", "internalType": "uint8" + }, + { + "name": "_rateNumerator", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_rateDenominator", + "type": "uint256", + "internalType": "uint256" } ], "stateMutability": "nonpayable" @@ -113,7 +123,7 @@ "internalType": "uint256" } ], - "stateMutability": "pure" + "stateMutability": "view" }, { "type": "function", @@ -146,6 +156,32 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "rateDenominator", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "rateNumerator", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "transfer", @@ -200,5 +236,5 @@ "stateMutability": "nonpayable" } ], - "bytecode": "0x60c060405234801561000f575f5ffd5b506040516109fd3803806109fd8339818101604052810190610031919061010e565b8173ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff16815250508060ff1660a08160ff1681525050505061014c565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100a78261007e565b9050919050565b6100b78161009d565b81146100c1575f5ffd5b50565b5f815190506100d2816100ae565b92915050565b5f60ff82169050919050565b6100ed816100d8565b81146100f7575f5ffd5b50565b5f81519050610108816100e4565b92915050565b5f5f604083850312156101245761012361007a565b5b5f610131858286016100c4565b9250506020610142858286016100fa565b9150509250929050565b60805160a05161089061016d5f395f6103de01525f61040201526108905ff3fe608060405234801561000f575f5ffd5b5060043610610091575f3560e01c806338d52e0f1161006457806338d52e0f1461014357806340c10f191461016157806370a082311461017d578063a9059cbb146101ad578063dd62ed3e146101dd57610091565b806307a2d13a14610095578063095ea7b3146100c557806323b872dd146100f5578063313ce56714610125575b5f5ffd5b6100af60048036038101906100aa9190610594565b61020d565b6040516100bc91906105ce565b60405180910390f35b6100df60048036038101906100da9190610641565b610216565b6040516100ec9190610699565b60405180910390f35b61010f600480360381019061010a91906106b2565b61029e565b60405161011c9190610699565b60405180910390f35b61012d6103dc565b60405161013a919061071d565b60405180910390f35b61014b610400565b6040516101589190610745565b60405180910390f35b61017b60048036038101906101769190610641565b610424565b005b6101976004803603810190610192919061075e565b61047a565b6040516101a491906105ce565b60405180910390f35b6101c760048036038101906101c29190610641565b61048e565b6040516101d49190610699565b60405180910390f35b6101f760048036038101906101f29190610789565b61053d565b60405161020491906105ce565b60405180910390f35b5f819050919050565b5f8160015f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20819055506001905092915050565b5f8160015f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461032691906107f4565b92505081905550815f5f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461037891906107f4565b92505081905550815f5f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546103ca9190610827565b92505081905550600190509392505050565b7f000000000000000000000000000000000000000000000000000000000000000081565b7f000000000000000000000000000000000000000000000000000000000000000081565b805f5f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461046f9190610827565b925050819055505050565b5f602052805f5260405f205f915090505481565b5f815f5f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546104da91906107f4565b92505081905550815f5f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461052c9190610827565b925050819055506001905092915050565b6001602052815f5260405f20602052805f5260405f205f91509150505481565b5f5ffd5b5f819050919050565b61057381610561565b811461057d575f5ffd5b50565b5f8135905061058e8161056a565b92915050565b5f602082840312156105a9576105a861055d565b5b5f6105b684828501610580565b91505092915050565b6105c881610561565b82525050565b5f6020820190506105e15f8301846105bf565b92915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610610826105e7565b9050919050565b61062081610606565b811461062a575f5ffd5b50565b5f8135905061063b81610617565b92915050565b5f5f604083850312156106575761065661055d565b5b5f6106648582860161062d565b925050602061067585828601610580565b9150509250929050565b5f8115159050919050565b6106938161067f565b82525050565b5f6020820190506106ac5f83018461068a565b92915050565b5f5f5f606084860312156106c9576106c861055d565b5b5f6106d68682870161062d565b93505060206106e78682870161062d565b92505060406106f886828701610580565b9150509250925092565b5f60ff82169050919050565b61071781610702565b82525050565b5f6020820190506107305f83018461070e565b92915050565b61073f81610606565b82525050565b5f6020820190506107585f830184610736565b92915050565b5f602082840312156107735761077261055d565b5b5f6107808482850161062d565b91505092915050565b5f5f6040838503121561079f5761079e61055d565b5b5f6107ac8582860161062d565b92505060206107bd8582860161062d565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6107fe82610561565b915061080983610561565b9250828203905081811115610821576108206107c7565b5b92915050565b5f61083182610561565b915061083c83610561565b9250828201905080821115610854576108536107c7565b5b9291505056fea2646970667358221220c7a63d9ea3fa2de6cae454642f51c1350392365b83fc5b1f47f3f63c3b67905064736f6c634300081e0033" + "bytecode": "0x610100604052348015610010575f5ffd5b50604051610c17380380610c1783398181016040528101906100329190610154565b8373ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff16815250508260ff1660a08160ff16815250508160c081815250508060e08181525050505050506101b8565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100ba82610091565b9050919050565b6100ca816100b0565b81146100d4575f5ffd5b50565b5f815190506100e5816100c1565b92915050565b5f60ff82169050919050565b610100816100eb565b811461010a575f5ffd5b50565b5f8151905061011b816100f7565b92915050565b5f819050919050565b61013381610121565b811461013d575f5ffd5b50565b5f8151905061014e8161012a565b92915050565b5f5f5f5f6080858703121561016c5761016b61008d565b5b5f610179878288016100d7565b945050602061018a8782880161010d565b935050604061019b87828801610140565b92505060606101ac87828801610140565b91505092959194509250565b60805160a05160c05160e051610a1e6101f95f395f8181610262015261055c01525f8181610283015261034801525f6104aa01525f6104ce0152610a1e5ff3fe608060405234801561000f575f5ffd5b50600436106100a7575f3560e01c806338d52e0f1161006f57806338d52e0f1461017757806340c10f191461019557806370a08231146101b1578063865192f7146101e1578063a9059cbb146101ff578063dd62ed3e1461022f576100a7565b806307a2d13a146100ab578063095ea7b3146100db5780630b36b8db1461010b57806323b872dd14610129578063313ce56714610159575b5f5ffd5b6100c560048036038101906100c09190610684565b61025f565b6040516100d291906106be565b60405180910390f35b6100f560048036038101906100f09190610731565b6102be565b6040516101029190610789565b60405180910390f35b610113610346565b60405161012091906106be565b60405180910390f35b610143600480360381019061013e91906107a2565b61036a565b6040516101509190610789565b60405180910390f35b6101616104a8565b60405161016e919061080d565b60405180910390f35b61017f6104cc565b60405161018c9190610835565b60405180910390f35b6101af60048036038101906101aa9190610731565b6104f0565b005b6101cb60048036038101906101c6919061084e565b610546565b6040516101d891906106be565b60405180910390f35b6101e961055a565b6040516101f691906106be565b60405180910390f35b61021960048036038101906102149190610731565b61057e565b6040516102269190610789565b60405180910390f35b61024960048036038101906102449190610879565b61062d565b60405161025691906106be565b60405180910390f35b5f7f00000000000000000000000000000000000000000000000000000000000000007f0000000000000000000000000000000000000000000000000000000000000000836102ad91906108e4565b6102b79190610952565b9050919050565b5f8160015f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20819055506001905092915050565b7f000000000000000000000000000000000000000000000000000000000000000081565b5f8160015f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546103f29190610982565b92505081905550815f5f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546104449190610982565b92505081905550815f5f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461049691906109b5565b92505081905550600190509392505050565b7f000000000000000000000000000000000000000000000000000000000000000081565b7f000000000000000000000000000000000000000000000000000000000000000081565b805f5f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461053b91906109b5565b925050819055505050565b5f602052805f5260405f205f915090505481565b7f000000000000000000000000000000000000000000000000000000000000000081565b5f815f5f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546105ca9190610982565b92505081905550815f5f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461061c91906109b5565b925050819055506001905092915050565b6001602052815f5260405f20602052805f5260405f205f91509150505481565b5f5ffd5b5f819050919050565b61066381610651565b811461066d575f5ffd5b50565b5f8135905061067e8161065a565b92915050565b5f602082840312156106995761069861064d565b5b5f6106a684828501610670565b91505092915050565b6106b881610651565b82525050565b5f6020820190506106d15f8301846106af565b92915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610700826106d7565b9050919050565b610710816106f6565b811461071a575f5ffd5b50565b5f8135905061072b81610707565b92915050565b5f5f604083850312156107475761074661064d565b5b5f6107548582860161071d565b925050602061076585828601610670565b9150509250929050565b5f8115159050919050565b6107838161076f565b82525050565b5f60208201905061079c5f83018461077a565b92915050565b5f5f5f606084860312156107b9576107b861064d565b5b5f6107c68682870161071d565b93505060206107d78682870161071d565b92505060406107e886828701610670565b9150509250925092565b5f60ff82169050919050565b610807816107f2565b82525050565b5f6020820190506108205f8301846107fe565b92915050565b61082f816106f6565b82525050565b5f6020820190506108485f830184610826565b92915050565b5f602082840312156108635761086261064d565b5b5f6108708482850161071d565b91505092915050565b5f5f6040838503121561088f5761088e61064d565b5b5f61089c8582860161071d565b92505060206108ad8582860161071d565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6108ee82610651565b91506108f983610651565b925082820261090781610651565b9150828204841483151761091e5761091d6108b7565b5b5092915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b5f61095c82610651565b915061096783610651565b92508261097757610976610925565b5b828204905092915050565b5f61098c82610651565b915061099783610651565b92508282039050818111156109af576109ae6108b7565b5b92915050565b5f6109bf82610651565b91506109ca83610651565b92508282019050808211156109e2576109e16108b7565b5b9291505056fea2646970667358221220b5ebafab2270a2c41eb9cd473eb8aca958511fe12b7302559d197e2a8704c94b64736f6c634300081e0033" } diff --git a/contracts/generated/contracts-generated/mockerc4626wrapper/src/lib.rs b/contracts/generated/contracts-generated/mockerc4626wrapper/src/lib.rs index 3d8202b578..33f38b216f 100644 --- a/contracts/generated/contracts-generated/mockerc4626wrapper/src/lib.rs +++ b/contracts/generated/contracts-generated/mockerc4626wrapper/src/lib.rs @@ -11,15 +11,17 @@ Generated by the following Solidity interface... ```solidity interface MockERC4626Wrapper { - constructor(address _asset, uint8 _decimals); + constructor(address _asset, uint8 _decimals, uint256 _rateNumerator, uint256 _rateDenominator); function allowance(address, address) external view returns (uint256); function approve(address spender, uint256 amount) external returns (bool); function asset() external view returns (address); function balanceOf(address) external view returns (uint256); - function convertToAssets(uint256 shares) external pure returns (uint256); + function convertToAssets(uint256 shares) external view returns (uint256); function decimals() external view returns (uint8); function mint(address to, uint256 amount) external; + function rateDenominator() external view returns (uint256); + function rateNumerator() external view returns (uint256); function transfer(address to, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool); } @@ -40,6 +42,16 @@ interface MockERC4626Wrapper { "name": "_decimals", "type": "uint8", "internalType": "uint8" + }, + { + "name": "_rateNumerator", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_rateDenominator", + "type": "uint256", + "internalType": "uint256" } ], "stateMutability": "nonpayable" @@ -141,7 +153,7 @@ interface MockERC4626Wrapper { "internalType": "uint256" } ], - "stateMutability": "pure" + "stateMutability": "view" }, { "type": "function", @@ -174,6 +186,32 @@ interface MockERC4626Wrapper { "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "rateDenominator", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "rateNumerator", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "transfer", @@ -241,16 +279,16 @@ pub mod MockERC4626Wrapper { /// The creation / init bytecode of the contract. /// /// ```text - ///0x60c060405234801561000f575f5ffd5b506040516109fd3803806109fd8339818101604052810190610031919061010e565b8173ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff16815250508060ff1660a08160ff1681525050505061014c565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100a78261007e565b9050919050565b6100b78161009d565b81146100c1575f5ffd5b50565b5f815190506100d2816100ae565b92915050565b5f60ff82169050919050565b6100ed816100d8565b81146100f7575f5ffd5b50565b5f81519050610108816100e4565b92915050565b5f5f604083850312156101245761012361007a565b5b5f610131858286016100c4565b9250506020610142858286016100fa565b9150509250929050565b60805160a05161089061016d5f395f6103de01525f61040201526108905ff3fe608060405234801561000f575f5ffd5b5060043610610091575f3560e01c806338d52e0f1161006457806338d52e0f1461014357806340c10f191461016157806370a082311461017d578063a9059cbb146101ad578063dd62ed3e146101dd57610091565b806307a2d13a14610095578063095ea7b3146100c557806323b872dd146100f5578063313ce56714610125575b5f5ffd5b6100af60048036038101906100aa9190610594565b61020d565b6040516100bc91906105ce565b60405180910390f35b6100df60048036038101906100da9190610641565b610216565b6040516100ec9190610699565b60405180910390f35b61010f600480360381019061010a91906106b2565b61029e565b60405161011c9190610699565b60405180910390f35b61012d6103dc565b60405161013a919061071d565b60405180910390f35b61014b610400565b6040516101589190610745565b60405180910390f35b61017b60048036038101906101769190610641565b610424565b005b6101976004803603810190610192919061075e565b61047a565b6040516101a491906105ce565b60405180910390f35b6101c760048036038101906101c29190610641565b61048e565b6040516101d49190610699565b60405180910390f35b6101f760048036038101906101f29190610789565b61053d565b60405161020491906105ce565b60405180910390f35b5f819050919050565b5f8160015f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20819055506001905092915050565b5f8160015f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461032691906107f4565b92505081905550815f5f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461037891906107f4565b92505081905550815f5f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546103ca9190610827565b92505081905550600190509392505050565b7f000000000000000000000000000000000000000000000000000000000000000081565b7f000000000000000000000000000000000000000000000000000000000000000081565b805f5f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461046f9190610827565b925050819055505050565b5f602052805f5260405f205f915090505481565b5f815f5f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546104da91906107f4565b92505081905550815f5f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461052c9190610827565b925050819055506001905092915050565b6001602052815f5260405f20602052805f5260405f205f91509150505481565b5f5ffd5b5f819050919050565b61057381610561565b811461057d575f5ffd5b50565b5f8135905061058e8161056a565b92915050565b5f602082840312156105a9576105a861055d565b5b5f6105b684828501610580565b91505092915050565b6105c881610561565b82525050565b5f6020820190506105e15f8301846105bf565b92915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610610826105e7565b9050919050565b61062081610606565b811461062a575f5ffd5b50565b5f8135905061063b81610617565b92915050565b5f5f604083850312156106575761065661055d565b5b5f6106648582860161062d565b925050602061067585828601610580565b9150509250929050565b5f8115159050919050565b6106938161067f565b82525050565b5f6020820190506106ac5f83018461068a565b92915050565b5f5f5f606084860312156106c9576106c861055d565b5b5f6106d68682870161062d565b93505060206106e78682870161062d565b92505060406106f886828701610580565b9150509250925092565b5f60ff82169050919050565b61071781610702565b82525050565b5f6020820190506107305f83018461070e565b92915050565b61073f81610606565b82525050565b5f6020820190506107585f830184610736565b92915050565b5f602082840312156107735761077261055d565b5b5f6107808482850161062d565b91505092915050565b5f5f6040838503121561079f5761079e61055d565b5b5f6107ac8582860161062d565b92505060206107bd8582860161062d565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6107fe82610561565b915061080983610561565b9250828203905081811115610821576108206107c7565b5b92915050565b5f61083182610561565b915061083c83610561565b9250828201905080821115610854576108536107c7565b5b9291505056fea2646970667358221220c7a63d9ea3fa2de6cae454642f51c1350392365b83fc5b1f47f3f63c3b67905064736f6c634300081e0033 + ///0x610100604052348015610010575f5ffd5b50604051610c17380380610c1783398181016040528101906100329190610154565b8373ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff16815250508260ff1660a08160ff16815250508160c081815250508060e08181525050505050506101b8565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100ba82610091565b9050919050565b6100ca816100b0565b81146100d4575f5ffd5b50565b5f815190506100e5816100c1565b92915050565b5f60ff82169050919050565b610100816100eb565b811461010a575f5ffd5b50565b5f8151905061011b816100f7565b92915050565b5f819050919050565b61013381610121565b811461013d575f5ffd5b50565b5f8151905061014e8161012a565b92915050565b5f5f5f5f6080858703121561016c5761016b61008d565b5b5f610179878288016100d7565b945050602061018a8782880161010d565b935050604061019b87828801610140565b92505060606101ac87828801610140565b91505092959194509250565b60805160a05160c05160e051610a1e6101f95f395f8181610262015261055c01525f8181610283015261034801525f6104aa01525f6104ce0152610a1e5ff3fe608060405234801561000f575f5ffd5b50600436106100a7575f3560e01c806338d52e0f1161006f57806338d52e0f1461017757806340c10f191461019557806370a08231146101b1578063865192f7146101e1578063a9059cbb146101ff578063dd62ed3e1461022f576100a7565b806307a2d13a146100ab578063095ea7b3146100db5780630b36b8db1461010b57806323b872dd14610129578063313ce56714610159575b5f5ffd5b6100c560048036038101906100c09190610684565b61025f565b6040516100d291906106be565b60405180910390f35b6100f560048036038101906100f09190610731565b6102be565b6040516101029190610789565b60405180910390f35b610113610346565b60405161012091906106be565b60405180910390f35b610143600480360381019061013e91906107a2565b61036a565b6040516101509190610789565b60405180910390f35b6101616104a8565b60405161016e919061080d565b60405180910390f35b61017f6104cc565b60405161018c9190610835565b60405180910390f35b6101af60048036038101906101aa9190610731565b6104f0565b005b6101cb60048036038101906101c6919061084e565b610546565b6040516101d891906106be565b60405180910390f35b6101e961055a565b6040516101f691906106be565b60405180910390f35b61021960048036038101906102149190610731565b61057e565b6040516102269190610789565b60405180910390f35b61024960048036038101906102449190610879565b61062d565b60405161025691906106be565b60405180910390f35b5f7f00000000000000000000000000000000000000000000000000000000000000007f0000000000000000000000000000000000000000000000000000000000000000836102ad91906108e4565b6102b79190610952565b9050919050565b5f8160015f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f20819055506001905092915050565b7f000000000000000000000000000000000000000000000000000000000000000081565b5f8160015f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546103f29190610982565b92505081905550815f5f8673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546104449190610982565b92505081905550815f5f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461049691906109b5565b92505081905550600190509392505050565b7f000000000000000000000000000000000000000000000000000000000000000081565b7f000000000000000000000000000000000000000000000000000000000000000081565b805f5f8473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461053b91906109b5565b925050819055505050565b5f602052805f5260405f205f915090505481565b7f000000000000000000000000000000000000000000000000000000000000000081565b5f815f5f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f8282546105ca9190610982565b92505081905550815f5f8573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f82825461061c91906109b5565b925050819055506001905092915050565b6001602052815f5260405f20602052805f5260405f205f91509150505481565b5f5ffd5b5f819050919050565b61066381610651565b811461066d575f5ffd5b50565b5f8135905061067e8161065a565b92915050565b5f602082840312156106995761069861064d565b5b5f6106a684828501610670565b91505092915050565b6106b881610651565b82525050565b5f6020820190506106d15f8301846106af565b92915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610700826106d7565b9050919050565b610710816106f6565b811461071a575f5ffd5b50565b5f8135905061072b81610707565b92915050565b5f5f604083850312156107475761074661064d565b5b5f6107548582860161071d565b925050602061076585828601610670565b9150509250929050565b5f8115159050919050565b6107838161076f565b82525050565b5f60208201905061079c5f83018461077a565b92915050565b5f5f5f606084860312156107b9576107b861064d565b5b5f6107c68682870161071d565b93505060206107d78682870161071d565b92505060406107e886828701610670565b9150509250925092565b5f60ff82169050919050565b610807816107f2565b82525050565b5f6020820190506108205f8301846107fe565b92915050565b61082f816106f6565b82525050565b5f6020820190506108485f830184610826565b92915050565b5f602082840312156108635761086261064d565b5b5f6108708482850161071d565b91505092915050565b5f5f6040838503121561088f5761088e61064d565b5b5f61089c8582860161071d565b92505060206108ad8582860161071d565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6108ee82610651565b91506108f983610651565b925082820261090781610651565b9150828204841483151761091e5761091d6108b7565b5b5092915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b5f61095c82610651565b915061096783610651565b92508261097757610976610925565b5b828204905092915050565b5f61098c82610651565b915061099783610651565b92508282039050818111156109af576109ae6108b7565b5b92915050565b5f6109bf82610651565b91506109ca83610651565b92508282019050808211156109e2576109e16108b7565b5b9291505056fea2646970667358221220b5ebafab2270a2c41eb9cd473eb8aca958511fe12b7302559d197e2a8704c94b64736f6c634300081e0033 /// ``` #[rustfmt::skip] #[allow(clippy::all)] pub static BYTECODE: alloy_sol_types::private::Bytes = alloy_sol_types::private::Bytes::from_static( - b"`\xC0`@R4\x80\x15a\0\x0FW__\xFD[P`@Qa\t\xFD8\x03\x80a\t\xFD\x839\x81\x81\x01`@R\x81\x01\x90a\x001\x91\x90a\x01\x0EV[\x81s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16`\x80\x81s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81RPP\x80`\xFF\x16`\xA0\x81`\xFF\x16\x81RPPPPa\x01LV[__\xFD[_s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x82\x16\x90P\x91\x90PV[_a\0\xA7\x82a\0~V[\x90P\x91\x90PV[a\0\xB7\x81a\0\x9DV[\x81\x14a\0\xC1W__\xFD[PV[_\x81Q\x90Pa\0\xD2\x81a\0\xAEV[\x92\x91PPV[_`\xFF\x82\x16\x90P\x91\x90PV[a\0\xED\x81a\0\xD8V[\x81\x14a\0\xF7W__\xFD[PV[_\x81Q\x90Pa\x01\x08\x81a\0\xE4V[\x92\x91PPV[__`@\x83\x85\x03\x12\x15a\x01$Wa\x01#a\0zV[[_a\x011\x85\x82\x86\x01a\0\xC4V[\x92PP` a\x01B\x85\x82\x86\x01a\0\xFAV[\x91PP\x92P\x92\x90PV[`\x80Q`\xA0Qa\x08\x90a\x01m_9_a\x03\xDE\x01R_a\x04\x02\x01Ra\x08\x90_\xF3\xFE`\x80`@R4\x80\x15a\0\x0FW__\xFD[P`\x046\x10a\0\x91W_5`\xE0\x1C\x80c8\xD5.\x0F\x11a\0dW\x80c8\xD5.\x0F\x14a\x01CW\x80c@\xC1\x0F\x19\x14a\x01aW\x80cp\xA0\x821\x14a\x01}W\x80c\xA9\x05\x9C\xBB\x14a\x01\xADW\x80c\xDDb\xED>\x14a\x01\xDDWa\0\x91V[\x80c\x07\xA2\xD1:\x14a\0\x95W\x80c\t^\xA7\xB3\x14a\0\xC5W\x80c#\xB8r\xDD\x14a\0\xF5W\x80c1<\xE5g\x14a\x01%W[__\xFD[a\0\xAF`\x04\x806\x03\x81\x01\x90a\0\xAA\x91\x90a\x05\x94V[a\x02\rV[`@Qa\0\xBC\x91\x90a\x05\xCEV[`@Q\x80\x91\x03\x90\xF3[a\0\xDF`\x04\x806\x03\x81\x01\x90a\0\xDA\x91\x90a\x06AV[a\x02\x16V[`@Qa\0\xEC\x91\x90a\x06\x99V[`@Q\x80\x91\x03\x90\xF3[a\x01\x0F`\x04\x806\x03\x81\x01\x90a\x01\n\x91\x90a\x06\xB2V[a\x02\x9EV[`@Qa\x01\x1C\x91\x90a\x06\x99V[`@Q\x80\x91\x03\x90\xF3[a\x01-a\x03\xDCV[`@Qa\x01:\x91\x90a\x07\x1DV[`@Q\x80\x91\x03\x90\xF3[a\x01Ka\x04\0V[`@Qa\x01X\x91\x90a\x07EV[`@Q\x80\x91\x03\x90\xF3[a\x01{`\x04\x806\x03\x81\x01\x90a\x01v\x91\x90a\x06AV[a\x04$V[\0[a\x01\x97`\x04\x806\x03\x81\x01\x90a\x01\x92\x91\x90a\x07^V[a\x04zV[`@Qa\x01\xA4\x91\x90a\x05\xCEV[`@Q\x80\x91\x03\x90\xF3[a\x01\xC7`\x04\x806\x03\x81\x01\x90a\x01\xC2\x91\x90a\x06AV[a\x04\x8EV[`@Qa\x01\xD4\x91\x90a\x06\x99V[`@Q\x80\x91\x03\x90\xF3[a\x01\xF7`\x04\x806\x03\x81\x01\x90a\x01\xF2\x91\x90a\x07\x89V[a\x05=V[`@Qa\x02\x04\x91\x90a\x05\xCEV[`@Q\x80\x91\x03\x90\xF3[_\x81\x90P\x91\x90PV[_\x81`\x01_3s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x85s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ \x81\x90UP`\x01\x90P\x92\x91PPV[_\x81`\x01_\x86s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _3s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x03&\x91\x90a\x07\xF4V[\x92PP\x81\x90UP\x81__\x86s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x03x\x91\x90a\x07\xF4V[\x92PP\x81\x90UP\x81__\x85s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x03\xCA\x91\x90a\x08'V[\x92PP\x81\x90UP`\x01\x90P\x93\x92PPPV[\x7F\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x81V[\x7F\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x81V[\x80__\x84s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x04o\x91\x90a\x08'V[\x92PP\x81\x90UPPPV[_` R\x80_R`@_ _\x91P\x90PT\x81V[_\x81__3s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x04\xDA\x91\x90a\x07\xF4V[\x92PP\x81\x90UP\x81__\x85s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x05,\x91\x90a\x08'V[\x92PP\x81\x90UP`\x01\x90P\x92\x91PPV[`\x01` R\x81_R`@_ ` R\x80_R`@_ _\x91P\x91PPT\x81V[__\xFD[_\x81\x90P\x91\x90PV[a\x05s\x81a\x05aV[\x81\x14a\x05}W__\xFD[PV[_\x815\x90Pa\x05\x8E\x81a\x05jV[\x92\x91PPV[_` \x82\x84\x03\x12\x15a\x05\xA9Wa\x05\xA8a\x05]V[[_a\x05\xB6\x84\x82\x85\x01a\x05\x80V[\x91PP\x92\x91PPV[a\x05\xC8\x81a\x05aV[\x82RPPV[_` \x82\x01\x90Pa\x05\xE1_\x83\x01\x84a\x05\xBFV[\x92\x91PPV[_s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x82\x16\x90P\x91\x90PV[_a\x06\x10\x82a\x05\xE7V[\x90P\x91\x90PV[a\x06 \x81a\x06\x06V[\x81\x14a\x06*W__\xFD[PV[_\x815\x90Pa\x06;\x81a\x06\x17V[\x92\x91PPV[__`@\x83\x85\x03\x12\x15a\x06WWa\x06Va\x05]V[[_a\x06d\x85\x82\x86\x01a\x06-V[\x92PP` a\x06u\x85\x82\x86\x01a\x05\x80V[\x91PP\x92P\x92\x90PV[_\x81\x15\x15\x90P\x91\x90PV[a\x06\x93\x81a\x06\x7FV[\x82RPPV[_` \x82\x01\x90Pa\x06\xAC_\x83\x01\x84a\x06\x8AV[\x92\x91PPV[___``\x84\x86\x03\x12\x15a\x06\xC9Wa\x06\xC8a\x05]V[[_a\x06\xD6\x86\x82\x87\x01a\x06-V[\x93PP` a\x06\xE7\x86\x82\x87\x01a\x06-V[\x92PP`@a\x06\xF8\x86\x82\x87\x01a\x05\x80V[\x91PP\x92P\x92P\x92V[_`\xFF\x82\x16\x90P\x91\x90PV[a\x07\x17\x81a\x07\x02V[\x82RPPV[_` \x82\x01\x90Pa\x070_\x83\x01\x84a\x07\x0EV[\x92\x91PPV[a\x07?\x81a\x06\x06V[\x82RPPV[_` \x82\x01\x90Pa\x07X_\x83\x01\x84a\x076V[\x92\x91PPV[_` \x82\x84\x03\x12\x15a\x07sWa\x07ra\x05]V[[_a\x07\x80\x84\x82\x85\x01a\x06-V[\x91PP\x92\x91PPV[__`@\x83\x85\x03\x12\x15a\x07\x9FWa\x07\x9Ea\x05]V[[_a\x07\xAC\x85\x82\x86\x01a\x06-V[\x92PP` a\x07\xBD\x85\x82\x86\x01a\x06-V[\x91PP\x92P\x92\x90PV[\x7FNH{q\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0_R`\x11`\x04R`$_\xFD[_a\x07\xFE\x82a\x05aV[\x91Pa\x08\t\x83a\x05aV[\x92P\x82\x82\x03\x90P\x81\x81\x11\x15a\x08!Wa\x08 a\x07\xC7V[[\x92\x91PPV[_a\x081\x82a\x05aV[\x91Pa\x08<\x83a\x05aV[\x92P\x82\x82\x01\x90P\x80\x82\x11\x15a\x08TWa\x08Sa\x07\xC7V[[\x92\x91PPV\xFE\xA2dipfsX\"\x12 \xC7\xA6=\x9E\xA3\xFA-\xE6\xCA\xE4Td/Q\xC15\x03\x926[\x83\xFC[\x1FG\xF3\xF6<;g\x90PdsolcC\0\x08\x1E\x003", + b"a\x01\0`@R4\x80\x15a\0\x10W__\xFD[P`@Qa\x0C\x178\x03\x80a\x0C\x17\x839\x81\x81\x01`@R\x81\x01\x90a\x002\x91\x90a\x01TV[\x83s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16`\x80\x81s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81RPP\x82`\xFF\x16`\xA0\x81`\xFF\x16\x81RPP\x81`\xC0\x81\x81RPP\x80`\xE0\x81\x81RPPPPPPa\x01\xB8V[__\xFD[_s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x82\x16\x90P\x91\x90PV[_a\0\xBA\x82a\0\x91V[\x90P\x91\x90PV[a\0\xCA\x81a\0\xB0V[\x81\x14a\0\xD4W__\xFD[PV[_\x81Q\x90Pa\0\xE5\x81a\0\xC1V[\x92\x91PPV[_`\xFF\x82\x16\x90P\x91\x90PV[a\x01\0\x81a\0\xEBV[\x81\x14a\x01\nW__\xFD[PV[_\x81Q\x90Pa\x01\x1B\x81a\0\xF7V[\x92\x91PPV[_\x81\x90P\x91\x90PV[a\x013\x81a\x01!V[\x81\x14a\x01=W__\xFD[PV[_\x81Q\x90Pa\x01N\x81a\x01*V[\x92\x91PPV[____`\x80\x85\x87\x03\x12\x15a\x01lWa\x01ka\0\x8DV[[_a\x01y\x87\x82\x88\x01a\0\xD7V[\x94PP` a\x01\x8A\x87\x82\x88\x01a\x01\rV[\x93PP`@a\x01\x9B\x87\x82\x88\x01a\x01@V[\x92PP``a\x01\xAC\x87\x82\x88\x01a\x01@V[\x91PP\x92\x95\x91\x94P\x92PV[`\x80Q`\xA0Q`\xC0Q`\xE0Qa\n\x1Ea\x01\xF9_9_\x81\x81a\x02b\x01Ra\x05\\\x01R_\x81\x81a\x02\x83\x01Ra\x03H\x01R_a\x04\xAA\x01R_a\x04\xCE\x01Ra\n\x1E_\xF3\xFE`\x80`@R4\x80\x15a\0\x0FW__\xFD[P`\x046\x10a\0\xA7W_5`\xE0\x1C\x80c8\xD5.\x0F\x11a\0oW\x80c8\xD5.\x0F\x14a\x01wW\x80c@\xC1\x0F\x19\x14a\x01\x95W\x80cp\xA0\x821\x14a\x01\xB1W\x80c\x86Q\x92\xF7\x14a\x01\xE1W\x80c\xA9\x05\x9C\xBB\x14a\x01\xFFW\x80c\xDDb\xED>\x14a\x02/Wa\0\xA7V[\x80c\x07\xA2\xD1:\x14a\0\xABW\x80c\t^\xA7\xB3\x14a\0\xDBW\x80c\x0B6\xB8\xDB\x14a\x01\x0BW\x80c#\xB8r\xDD\x14a\x01)W\x80c1<\xE5g\x14a\x01YW[__\xFD[a\0\xC5`\x04\x806\x03\x81\x01\x90a\0\xC0\x91\x90a\x06\x84V[a\x02_V[`@Qa\0\xD2\x91\x90a\x06\xBEV[`@Q\x80\x91\x03\x90\xF3[a\0\xF5`\x04\x806\x03\x81\x01\x90a\0\xF0\x91\x90a\x071V[a\x02\xBEV[`@Qa\x01\x02\x91\x90a\x07\x89V[`@Q\x80\x91\x03\x90\xF3[a\x01\x13a\x03FV[`@Qa\x01 \x91\x90a\x06\xBEV[`@Q\x80\x91\x03\x90\xF3[a\x01C`\x04\x806\x03\x81\x01\x90a\x01>\x91\x90a\x07\xA2V[a\x03jV[`@Qa\x01P\x91\x90a\x07\x89V[`@Q\x80\x91\x03\x90\xF3[a\x01aa\x04\xA8V[`@Qa\x01n\x91\x90a\x08\rV[`@Q\x80\x91\x03\x90\xF3[a\x01\x7Fa\x04\xCCV[`@Qa\x01\x8C\x91\x90a\x085V[`@Q\x80\x91\x03\x90\xF3[a\x01\xAF`\x04\x806\x03\x81\x01\x90a\x01\xAA\x91\x90a\x071V[a\x04\xF0V[\0[a\x01\xCB`\x04\x806\x03\x81\x01\x90a\x01\xC6\x91\x90a\x08NV[a\x05FV[`@Qa\x01\xD8\x91\x90a\x06\xBEV[`@Q\x80\x91\x03\x90\xF3[a\x01\xE9a\x05ZV[`@Qa\x01\xF6\x91\x90a\x06\xBEV[`@Q\x80\x91\x03\x90\xF3[a\x02\x19`\x04\x806\x03\x81\x01\x90a\x02\x14\x91\x90a\x071V[a\x05~V[`@Qa\x02&\x91\x90a\x07\x89V[`@Q\x80\x91\x03\x90\xF3[a\x02I`\x04\x806\x03\x81\x01\x90a\x02D\x91\x90a\x08yV[a\x06-V[`@Qa\x02V\x91\x90a\x06\xBEV[`@Q\x80\x91\x03\x90\xF3[_\x7F\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x7F\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x83a\x02\xAD\x91\x90a\x08\xE4V[a\x02\xB7\x91\x90a\tRV[\x90P\x91\x90PV[_\x81`\x01_3s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x85s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ \x81\x90UP`\x01\x90P\x92\x91PPV[\x7F\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x81V[_\x81`\x01_\x86s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _3s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x03\xF2\x91\x90a\t\x82V[\x92PP\x81\x90UP\x81__\x86s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x04D\x91\x90a\t\x82V[\x92PP\x81\x90UP\x81__\x85s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x04\x96\x91\x90a\t\xB5V[\x92PP\x81\x90UP`\x01\x90P\x93\x92PPPV[\x7F\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x81V[\x7F\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x81V[\x80__\x84s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x05;\x91\x90a\t\xB5V[\x92PP\x81\x90UPPPV[_` R\x80_R`@_ _\x91P\x90PT\x81V[\x7F\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x81V[_\x81__3s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x05\xCA\x91\x90a\t\x82V[\x92PP\x81\x90UP\x81__\x85s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x16\x81R` \x01\x90\x81R` \x01_ _\x82\x82Ta\x06\x1C\x91\x90a\t\xB5V[\x92PP\x81\x90UP`\x01\x90P\x92\x91PPV[`\x01` R\x81_R`@_ ` R\x80_R`@_ _\x91P\x91PPT\x81V[__\xFD[_\x81\x90P\x91\x90PV[a\x06c\x81a\x06QV[\x81\x14a\x06mW__\xFD[PV[_\x815\x90Pa\x06~\x81a\x06ZV[\x92\x91PPV[_` \x82\x84\x03\x12\x15a\x06\x99Wa\x06\x98a\x06MV[[_a\x06\xA6\x84\x82\x85\x01a\x06pV[\x91PP\x92\x91PPV[a\x06\xB8\x81a\x06QV[\x82RPPV[_` \x82\x01\x90Pa\x06\xD1_\x83\x01\x84a\x06\xAFV[\x92\x91PPV[_s\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x82\x16\x90P\x91\x90PV[_a\x07\0\x82a\x06\xD7V[\x90P\x91\x90PV[a\x07\x10\x81a\x06\xF6V[\x81\x14a\x07\x1AW__\xFD[PV[_\x815\x90Pa\x07+\x81a\x07\x07V[\x92\x91PPV[__`@\x83\x85\x03\x12\x15a\x07GWa\x07Fa\x06MV[[_a\x07T\x85\x82\x86\x01a\x07\x1DV[\x92PP` a\x07e\x85\x82\x86\x01a\x06pV[\x91PP\x92P\x92\x90PV[_\x81\x15\x15\x90P\x91\x90PV[a\x07\x83\x81a\x07oV[\x82RPPV[_` \x82\x01\x90Pa\x07\x9C_\x83\x01\x84a\x07zV[\x92\x91PPV[___``\x84\x86\x03\x12\x15a\x07\xB9Wa\x07\xB8a\x06MV[[_a\x07\xC6\x86\x82\x87\x01a\x07\x1DV[\x93PP` a\x07\xD7\x86\x82\x87\x01a\x07\x1DV[\x92PP`@a\x07\xE8\x86\x82\x87\x01a\x06pV[\x91PP\x92P\x92P\x92V[_`\xFF\x82\x16\x90P\x91\x90PV[a\x08\x07\x81a\x07\xF2V[\x82RPPV[_` \x82\x01\x90Pa\x08 _\x83\x01\x84a\x07\xFEV[\x92\x91PPV[a\x08/\x81a\x06\xF6V[\x82RPPV[_` \x82\x01\x90Pa\x08H_\x83\x01\x84a\x08&V[\x92\x91PPV[_` \x82\x84\x03\x12\x15a\x08cWa\x08ba\x06MV[[_a\x08p\x84\x82\x85\x01a\x07\x1DV[\x91PP\x92\x91PPV[__`@\x83\x85\x03\x12\x15a\x08\x8FWa\x08\x8Ea\x06MV[[_a\x08\x9C\x85\x82\x86\x01a\x07\x1DV[\x92PP` a\x08\xAD\x85\x82\x86\x01a\x07\x1DV[\x91PP\x92P\x92\x90PV[\x7FNH{q\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0_R`\x11`\x04R`$_\xFD[_a\x08\xEE\x82a\x06QV[\x91Pa\x08\xF9\x83a\x06QV[\x92P\x82\x82\x02a\t\x07\x81a\x06QV[\x91P\x82\x82\x04\x84\x14\x83\x15\x17a\t\x1EWa\t\x1Da\x08\xB7V[[P\x92\x91PPV[\x7FNH{q\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0_R`\x12`\x04R`$_\xFD[_a\t\\\x82a\x06QV[\x91Pa\tg\x83a\x06QV[\x92P\x82a\twWa\tva\t%V[[\x82\x82\x04\x90P\x92\x91PPV[_a\t\x8C\x82a\x06QV[\x91Pa\t\x97\x83a\x06QV[\x92P\x82\x82\x03\x90P\x81\x81\x11\x15a\t\xAFWa\t\xAEa\x08\xB7V[[\x92\x91PPV[_a\t\xBF\x82a\x06QV[\x91Pa\t\xCA\x83a\x06QV[\x92P\x82\x82\x01\x90P\x80\x82\x11\x15a\t\xE2Wa\t\xE1a\x08\xB7V[[\x92\x91PPV\xFE\xA2dipfsX\"\x12 \xB5\xEB\xAF\xAB\"p\xA2\xC4\x1E\xB9\xCDG>\xB8\xAC\xA9XQ\x1F\xE1+s\x02U\x9D\x19~*\x87\x04\xC9KdsolcC\0\x08\x1E\x003", ); /**Constructor`. ```solidity - constructor(address _asset, uint8 _decimals); + constructor(address _asset, uint8 _decimals, uint256 _rateNumerator, uint256 _rateDenominator); ```*/ #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] #[derive(Clone)] @@ -259,6 +297,10 @@ pub mod MockERC4626Wrapper { pub _asset: alloy_sol_types::private::Address, #[allow(missing_docs)] pub _decimals: u8, + #[allow(missing_docs)] + pub _rateNumerator: alloy_sol_types::private::primitives::aliases::U256, + #[allow(missing_docs)] + pub _rateDenominator: alloy_sol_types::private::primitives::aliases::U256, } const _: () = { use alloy_sol_types; @@ -268,9 +310,16 @@ pub mod MockERC4626Wrapper { type UnderlyingSolTuple<'a> = ( alloy_sol_types::sol_data::Address, alloy_sol_types::sol_data::Uint<8>, + alloy_sol_types::sol_data::Uint<256>, + alloy_sol_types::sol_data::Uint<256>, ); #[doc(hidden)] - type UnderlyingRustTuple<'a> = (alloy_sol_types::private::Address, u8); + type UnderlyingRustTuple<'a> = ( + alloy_sol_types::private::Address, + u8, + alloy_sol_types::private::primitives::aliases::U256, + alloy_sol_types::private::primitives::aliases::U256, + ); #[cfg(test)] #[allow(dead_code, unreachable_patterns)] fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { @@ -284,7 +333,12 @@ pub mod MockERC4626Wrapper { #[doc(hidden)] impl ::core::convert::From for UnderlyingRustTuple<'_> { fn from(value: constructorCall) -> Self { - (value._asset, value._decimals) + ( + value._asset, + value._decimals, + value._rateNumerator, + value._rateDenominator, + ) } } #[automatically_derived] @@ -294,6 +348,8 @@ pub mod MockERC4626Wrapper { Self { _asset: tuple.0, _decimals: tuple.1, + _rateNumerator: tuple.2, + _rateDenominator: tuple.3, } } } @@ -303,6 +359,8 @@ pub mod MockERC4626Wrapper { type Parameters<'a> = ( alloy_sol_types::sol_data::Address, alloy_sol_types::sol_data::Uint<8>, + alloy_sol_types::sol_data::Uint<256>, + alloy_sol_types::sol_data::Uint<256>, ); type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; @@ -322,6 +380,12 @@ pub mod MockERC4626Wrapper { as alloy_sol_types::SolType>::tokenize( &self._decimals, ), + as alloy_sol_types::SolType>::tokenize( + &self._rateNumerator, + ), + as alloy_sol_types::SolType>::tokenize( + &self._rateDenominator, + ), ) } } @@ -929,7 +993,7 @@ pub mod MockERC4626Wrapper { #[derive(Default, Debug, PartialEq, Eq, Hash)] /**Function with signature `convertToAssets(uint256)` and selector `0x07a2d13a`. ```solidity - function convertToAssets(uint256 shares) external pure returns (uint256); + function convertToAssets(uint256 shares) external view returns (uint256); ```*/ #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] #[derive(Clone)] @@ -1361,6 +1425,284 @@ pub mod MockERC4626Wrapper { } }; #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `rateDenominator()` and selector `0x865192f7`. + ```solidity + function rateDenominator() external view returns (uint256); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct rateDenominatorCall; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the + /// [`rateDenominator()`](rateDenominatorCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct rateDenominatorReturn { + #[allow(missing_docs)] + pub _0: alloy_sol_types::private::primitives::aliases::U256, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: rateDenominatorCall) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for rateDenominatorCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy_sol_types::private::primitives::aliases::U256,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: rateDenominatorReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for rateDenominatorReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for rateDenominatorCall { + type Parameters<'a> = (); + type Return = alloy_sol_types::private::primitives::aliases::U256; + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + type ReturnTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + const SELECTOR: [u8; 4] = [134u8, 81u8, 146u8, 247u8]; + const SIGNATURE: &'static str = "rateDenominator()"; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + () + } + + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + ( + as alloy_sol_types::SolType>::tokenize( + ret, + ), + ) + } + + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data).map( + |r| { + let r: rateDenominatorReturn = r.into(); + r._0 + }, + ) + } + + #[inline] + fn abi_decode_returns_validate(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate( + data, + ) + .map(|r| { + let r: rateDenominatorReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + /**Function with signature `rateNumerator()` and selector `0x0b36b8db`. + ```solidity + function rateNumerator() external view returns (uint256); + ```*/ + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct rateNumeratorCall; + #[derive(Default, Debug, PartialEq, Eq, Hash)] + ///Container type for the return parameters of the + /// [`rateNumerator()`](rateNumeratorCall) function. + #[allow(non_camel_case_types, non_snake_case, clippy::pub_underscore_fields)] + #[derive(Clone)] + pub struct rateNumeratorReturn { + #[allow(missing_docs)] + pub _0: alloy_sol_types::private::primitives::aliases::U256, + } + #[allow( + non_camel_case_types, + non_snake_case, + clippy::pub_underscore_fields, + clippy::style + )] + const _: () = { + use alloy_sol_types; + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: rateNumeratorCall) -> Self { + () + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for rateNumeratorCall { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self + } + } + } + { + #[doc(hidden)] + #[allow(dead_code)] + type UnderlyingSolTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + #[doc(hidden)] + type UnderlyingRustTuple<'a> = (alloy_sol_types::private::primitives::aliases::U256,); + #[cfg(test)] + #[allow(dead_code, unreachable_patterns)] + fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq) { + match _t { + alloy_sol_types::private::AssertTypeEq::< + ::RustType, + >(_) => {} + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From for UnderlyingRustTuple<'_> { + fn from(value: rateNumeratorReturn) -> Self { + (value._0,) + } + } + #[automatically_derived] + #[doc(hidden)] + impl ::core::convert::From> for rateNumeratorReturn { + fn from(tuple: UnderlyingRustTuple<'_>) -> Self { + Self { _0: tuple.0 } + } + } + } + #[automatically_derived] + impl alloy_sol_types::SolCall for rateNumeratorCall { + type Parameters<'a> = (); + type Return = alloy_sol_types::private::primitives::aliases::U256; + type ReturnToken<'a> = as alloy_sol_types::SolType>::Token<'a>; + type ReturnTuple<'a> = (alloy_sol_types::sol_data::Uint<256>,); + type Token<'a> = as alloy_sol_types::SolType>::Token<'a>; + + const SELECTOR: [u8; 4] = [11u8, 54u8, 184u8, 219u8]; + const SIGNATURE: &'static str = "rateNumerator()"; + + #[inline] + fn new<'a>( + tuple: as alloy_sol_types::SolType>::RustType, + ) -> Self { + tuple.into() + } + + #[inline] + fn tokenize(&self) -> Self::Token<'_> { + () + } + + #[inline] + fn tokenize_returns(ret: &Self::Return) -> Self::ReturnToken<'_> { + ( + as alloy_sol_types::SolType>::tokenize( + ret, + ), + ) + } + + #[inline] + fn abi_decode_returns(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence(data).map( + |r| { + let r: rateNumeratorReturn = r.into(); + r._0 + }, + ) + } + + #[inline] + fn abi_decode_returns_validate(data: &[u8]) -> alloy_sol_types::Result { + as alloy_sol_types::SolType>::abi_decode_sequence_validate( + data, + ) + .map(|r| { + let r: rateNumeratorReturn = r.into(); + r._0 + }) + } + } + }; + #[derive(Default, Debug, PartialEq, Eq, Hash)] /**Function with signature `transfer(address,uint256)` and selector `0xa9059cbb`. ```solidity function transfer(address to, uint256 amount) external returns (bool); @@ -1705,6 +2047,10 @@ pub mod MockERC4626Wrapper { #[allow(missing_docs)] mint(mintCall), #[allow(missing_docs)] + rateDenominator(rateDenominatorCall), + #[allow(missing_docs)] + rateNumerator(rateNumeratorCall), + #[allow(missing_docs)] transfer(transferCall), #[allow(missing_docs)] transferFrom(transferFromCall), @@ -1720,11 +2066,13 @@ pub mod MockERC4626Wrapper { pub const SELECTORS: &'static [[u8; 4usize]] = &[ [7u8, 162u8, 209u8, 58u8], [9u8, 94u8, 167u8, 179u8], + [11u8, 54u8, 184u8, 219u8], [35u8, 184u8, 114u8, 221u8], [49u8, 60u8, 229u8, 103u8], [56u8, 213u8, 46u8, 15u8], [64u8, 193u8, 15u8, 25u8], [112u8, 160u8, 130u8, 49u8], + [134u8, 81u8, 146u8, 247u8], [169u8, 5u8, 156u8, 187u8], [221u8, 98u8, 237u8, 62u8], ]; @@ -1732,11 +2080,13 @@ pub mod MockERC4626Wrapper { pub const SIGNATURES: &'static [&'static str] = &[ ::SIGNATURE, ::SIGNATURE, + ::SIGNATURE, ::SIGNATURE, ::SIGNATURE, ::SIGNATURE, ::SIGNATURE, ::SIGNATURE, + ::SIGNATURE, ::SIGNATURE, ::SIGNATURE, ]; @@ -1744,11 +2094,13 @@ pub mod MockERC4626Wrapper { pub const VARIANT_NAMES: &'static [&'static str] = &[ ::core::stringify!(convertToAssets), ::core::stringify!(approve), + ::core::stringify!(rateNumerator), ::core::stringify!(transferFrom), ::core::stringify!(decimals), ::core::stringify!(asset), ::core::stringify!(mint), ::core::stringify!(balanceOf), + ::core::stringify!(rateDenominator), ::core::stringify!(transfer), ::core::stringify!(allowance), ]; @@ -1775,7 +2127,7 @@ pub mod MockERC4626Wrapper { } #[automatically_derived] impl alloy_sol_types::SolInterface for MockERC4626WrapperCalls { - const COUNT: usize = 9usize; + const COUNT: usize = 11usize; const MIN_DATA_LENGTH: usize = 0usize; const NAME: &'static str = "MockERC4626WrapperCalls"; @@ -1791,6 +2143,10 @@ pub mod MockERC4626Wrapper { } Self::decimals(_) => ::SELECTOR, Self::mint(_) => ::SELECTOR, + Self::rateDenominator(_) => { + ::SELECTOR + } + Self::rateNumerator(_) => ::SELECTOR, Self::transfer(_) => ::SELECTOR, Self::transferFrom(_) => ::SELECTOR, } @@ -1829,6 +2185,15 @@ pub mod MockERC4626Wrapper { } approve }, + { + fn rateNumerator( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(MockERC4626WrapperCalls::rateNumerator) + } + rateNumerator + }, { fn transferFrom( data: &[u8], @@ -1866,6 +2231,15 @@ pub mod MockERC4626Wrapper { } balanceOf }, + { + fn rateDenominator( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw(data) + .map(MockERC4626WrapperCalls::rateDenominator) + } + rateDenominator + }, { fn transfer(data: &[u8]) -> alloy_sol_types::Result { ::abi_decode_raw(data) @@ -1919,6 +2293,17 @@ pub mod MockERC4626Wrapper { } approve }, + { + fn rateNumerator( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(MockERC4626WrapperCalls::rateNumerator) + } + rateNumerator + }, { fn transferFrom( data: &[u8], @@ -1958,6 +2343,17 @@ pub mod MockERC4626Wrapper { } balanceOf }, + { + fn rateDenominator( + data: &[u8], + ) -> alloy_sol_types::Result { + ::abi_decode_raw_validate( + data, + ) + .map(MockERC4626WrapperCalls::rateDenominator) + } + rateDenominator + }, { fn transfer(data: &[u8]) -> alloy_sol_types::Result { ::abi_decode_raw_validate(data) @@ -2006,6 +2402,12 @@ pub mod MockERC4626Wrapper { Self::mint(inner) => { ::abi_encoded_size(inner) } + Self::rateDenominator(inner) => { + ::abi_encoded_size(inner) + } + Self::rateNumerator(inner) => { + ::abi_encoded_size(inner) + } Self::transfer(inner) => { ::abi_encoded_size(inner) } @@ -2039,6 +2441,12 @@ pub mod MockERC4626Wrapper { Self::mint(inner) => { ::abi_encode_raw(inner, out) } + Self::rateDenominator(inner) => { + ::abi_encode_raw(inner, out) + } + Self::rateNumerator(inner) => { + ::abi_encode_raw(inner, out) + } Self::transfer(inner) => { ::abi_encode_raw(inner, out) } @@ -2072,9 +2480,17 @@ pub mod MockERC4626Wrapper { __provider: P, _asset: alloy_sol_types::private::Address, _decimals: u8, + _rateNumerator: alloy_sol_types::private::primitives::aliases::U256, + _rateDenominator: alloy_sol_types::private::primitives::aliases::U256, ) -> impl ::core::future::Future>> { - MockERC4626WrapperInstance::::deploy(__provider, _asset, _decimals) + MockERC4626WrapperInstance::::deploy( + __provider, + _asset, + _decimals, + _rateNumerator, + _rateDenominator, + ) } /**Creates a `RawCallBuilder` for deploying this contract using the given `provider` and constructor arguments, if any. @@ -2089,8 +2505,16 @@ pub mod MockERC4626Wrapper { __provider: P, _asset: alloy_sol_types::private::Address, _decimals: u8, + _rateNumerator: alloy_sol_types::private::primitives::aliases::U256, + _rateDenominator: alloy_sol_types::private::primitives::aliases::U256, ) -> alloy_contract::RawCallBuilder { - MockERC4626WrapperInstance::::deploy_builder(__provider, _asset, _decimals) + MockERC4626WrapperInstance::::deploy_builder( + __provider, + _asset, + _decimals, + _rateNumerator, + _rateDenominator, + ) } /**A [`MockERC4626Wrapper`](self) instance. @@ -2144,8 +2568,16 @@ pub mod MockERC4626Wrapper { __provider: P, _asset: alloy_sol_types::private::Address, _decimals: u8, + _rateNumerator: alloy_sol_types::private::primitives::aliases::U256, + _rateDenominator: alloy_sol_types::private::primitives::aliases::U256, ) -> alloy_contract::Result> { - let call_builder = Self::deploy_builder(__provider, _asset, _decimals); + let call_builder = Self::deploy_builder( + __provider, + _asset, + _decimals, + _rateNumerator, + _rateDenominator, + ); let contract_address = call_builder.deploy().await?; Ok(Self::new(contract_address, call_builder.provider)) } @@ -2160,6 +2592,8 @@ pub mod MockERC4626Wrapper { __provider: P, _asset: alloy_sol_types::private::Address, _decimals: u8, + _rateNumerator: alloy_sol_types::private::primitives::aliases::U256, + _rateDenominator: alloy_sol_types::private::primitives::aliases::U256, ) -> alloy_contract::RawCallBuilder { alloy_contract::RawCallBuilder::new_raw_deploy( __provider, @@ -2168,6 +2602,8 @@ pub mod MockERC4626Wrapper { &alloy_sol_types::SolConstructor::abi_encode(&constructorCall { _asset, _decimals, + _rateNumerator, + _rateDenominator, })[..], ] .concat() @@ -2281,6 +2717,18 @@ pub mod MockERC4626Wrapper { self.call_builder(&mintCall { to, amount }) } + ///Creates a new call builder for the [`rateDenominator`] function. + pub fn rateDenominator( + &self, + ) -> alloy_contract::SolCallBuilder<&P, rateDenominatorCall, N> { + self.call_builder(&rateDenominatorCall) + } + + ///Creates a new call builder for the [`rateNumerator`] function. + pub fn rateNumerator(&self) -> alloy_contract::SolCallBuilder<&P, rateNumeratorCall, N> { + self.call_builder(&rateNumeratorCall) + } + ///Creates a new call builder for the [`transfer`] function. pub fn transfer( &self, diff --git a/contracts/solidity/tests/MockERC4626Wrapper.sol b/contracts/solidity/tests/MockERC4626Wrapper.sol index 82d156f019..8a33eef4e8 100644 --- a/contracts/solidity/tests/MockERC4626Wrapper.sol +++ b/contracts/solidity/tests/MockERC4626Wrapper.sol @@ -1,34 +1,37 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -/// @title Minimal EIP-4626 wrapper for testing recursive vault pricing. -/// @dev Wraps another ERC-4626 vault (or any ERC-20) with a fixed 1:1 -/// conversion rate. Not a real vault – just enough to satisfy -/// `asset()`, `decimals()`, `convertToAssets()`, `balanceOf()`, -/// `approve()`, and `transfer()` so the e2e pricing pipeline works. +/// @title MockERC4626Wrapper +/// @notice Minimal EIP-4626 wrapper for testing recursive vault pricing. +/// @dev Wraps another ERC-4626 vault (or any ERC-20) with a configurable +/// conversion rate. Not a real vault -- just enough to satisfy `asset()`, +/// `decimals()`, `convertToAssets()`, `balanceOf()`, `approve()`, and +/// `transfer()` so the e2e pricing pipeline works. contract MockERC4626Wrapper { address public immutable asset; - uint8 public immutable decimals; + uint8 public immutable decimals; + uint256 public immutable rateNumerator; + uint256 public immutable rateDenominator; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; - constructor(address _asset, uint8 _decimals) { - asset = _asset; + constructor(address _asset, uint8 _decimals, uint256 _rateNumerator, uint256 _rateDenominator) { + asset = _asset; decimals = _decimals; + rateNumerator = _rateNumerator; + rateDenominator = _rateDenominator; } - // ── EIP-4626 view ────────────────────────────────────────────────── - - function convertToAssets(uint256 shares) external pure returns (uint256) { - return shares; // 1:1 conversion + /// @notice Returns the equivalent amount of underlying assets for the + /// given number of vault shares, scaled by the configured rate. + function convertToAssets(uint256 shares) external view returns (uint256) { + return shares * rateNumerator / rateDenominator; } - // ── Minimal ERC-20 surface needed by the protocol ────────────────── - function transfer(address to, uint256 amount) external returns (bool) { balanceOf[msg.sender] -= amount; - balanceOf[to] += amount; + balanceOf[to] += amount; return true; } @@ -39,13 +42,12 @@ contract MockERC4626Wrapper { function transferFrom(address from, address to, uint256 amount) external returns (bool) { allowance[from][msg.sender] -= amount; - balanceOf[from] -= amount; - balanceOf[to] += amount; + balanceOf[from] -= amount; + balanceOf[to] += amount; return true; } - // ── Test helper ──────────────────────────────────────────────────── - + /// @notice Mints tokens to an address. Only for testing. function mint(address to, uint256 amount) external { balanceOf[to] += amount; } diff --git a/crates/e2e/tests/e2e/eip4626.rs b/crates/e2e/tests/e2e/eip4626.rs index 6405b97724..c1b7e78f2e 100644 --- a/crates/e2e/tests/e2e/eip4626.rs +++ b/crates/e2e/tests/e2e/eip4626.rs @@ -1,6 +1,6 @@ use { ::alloy::{ - primitives::{Address, address}, + primitives::{Address, U256, address}, providers::ext::{AnvilApi, ImpersonateConfig}, }, configs::{ @@ -139,25 +139,36 @@ async fn forked_node_mainnet_eip4626_recursive_native_price() { .await; } -/// Tests pricing of a recursive EIP-4626 vault: a mock wrapper vault whose -/// `asset()` returns sDAI, which itself is an EIP-4626 vault wrapping DAI. -/// Requires two chained `Eip4626` estimators to fully unwrap. +/// Tests pricing of recursive EIP-4626 vaults with non-trivial conversion +/// rates. Deploys mock wrapper vaults on top of sDAI (which itself wraps DAI) +/// with different rates and verifies the prices scale correctly. /// /// Unlike the non-recursive test we cannot submit a full quote because the -/// freshly-deployed wrapper token has no DEX liquidity. Instead we verify -/// that the native price endpoint returns a price, which exercises the full -/// Eip4626 → Eip4626 → Driver chain. +/// freshly-deployed wrapper tokens have no DEX liquidity. Instead we verify +/// that the native price endpoint returns correctly scaled prices, which +/// exercises the full Eip4626 → Eip4626 → Driver chain. async fn eip4626_recursive_native_price_test(web3: Web3) { let mut onchain = OnchainComponents::deployed(web3.clone()).await; let [solver] = onchain.make_solvers_forked(1u64.eth()).await; - // Deploy a mock EIP-4626 vault that wraps sDAI (which itself wraps DAI). - let wrapper = - contracts::test::MockERC4626Wrapper::Instance::deploy(web3.provider.clone(), SDAI, 18u8) - .await - .unwrap(); - let wrapper_addr = *wrapper.address(); + // Deploy mock EIP-4626 vaults wrapping sDAI with different conversion rates. + // Each wrapper applies `convertToAssets(shares) = shares * num / den`, so a + // (3, 2) wrapper means 1 share = 1.5 sDAI, making it 1.5x the sDAI price. + let rates: &[(u64, u64)] = &[(3, 2), (2, 1), (1, 3)]; + let mut wrapper_addrs = Vec::with_capacity(rates.len()); + for &(num, den) in rates { + let wrapper = contracts::test::MockERC4626Wrapper::Instance::deploy( + web3.provider.clone(), + SDAI, + 18u8, + U256::from(num), + U256::from(den), + ) + .await + .unwrap(); + wrapper_addrs.push(*wrapper.address()); + } // Two chained Eip4626 estimators: the first unwraps the mock wrapper to // sDAI, the second unwraps sDAI to DAI, and the Driver prices DAI. @@ -190,21 +201,33 @@ async fn eip4626_recursive_native_price_test(web3: Web3) { onchain.mint_block().await; - // Query the native price of the wrapper token. The Eip4626 chain must - // resolve wrapper → sDAI → DAI → native price via the Driver. + // First, get the sDAI native price as a baseline via the native price + // endpoint. sDAI is priced through the second Eip4626 in the chain. wait_for_condition(TIMEOUT, || async { - services.get_native_price(&wrapper_addr).await.is_ok() + services.get_native_price(&SDAI).await.is_ok() }) .await - .expect("native price for recursive EIP-4626 wrapper should be available"); - - let price = services - .get_native_price(&wrapper_addr) + .expect("sDAI native price should be available"); + let sdai_price = services.get_native_price(&SDAI).await.unwrap().price; + + // Verify each wrapper's price is the sDAI price scaled by the wrapper's + // conversion rate. This ensures the rate math is actually applied (a 1:1 + // mock would pass even if the rate were ignored). + for (&addr, &(num, den)) in wrapper_addrs.iter().zip(rates) { + wait_for_condition(TIMEOUT, || async { + services.get_native_price(&addr).await.is_ok() + }) .await - .expect("native price should be available after wait"); - assert!( - price.price > 0.0, - "native price should be positive, got: {}", - price.price - ); + .unwrap_or_else(|_| panic!("native price for wrapper ({num}/{den}) should be available")); + + let wrapper_price = services.get_native_price(&addr).await.unwrap().price; + let expected_ratio = num as f64 / den as f64; + let actual_ratio = wrapper_price / sdai_price; + + assert!( + (actual_ratio - expected_ratio).abs() / expected_ratio < 0.01, + "wrapper ({num}/{den}): expected ratio ~{expected_ratio:.4}, got {actual_ratio:.4} \ + (wrapper={wrapper_price}, sdai={sdai_price})", + ); + } } From a07ae9151908fcda91c91da14a9367bf687bfa51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:26:08 +0100 Subject: [PATCH 07/20] Improve EIP-4626 native price estimator robustness - Add negative cache (Mutex) for non-vault tokens to avoid wasted RPC calls on every estimation cycle - Enforce timeout on vault RPC calls via tokio::time::timeout so a stuck node cannot block the pipeline indefinitely - Forward remaining time budget to the inner estimator for correct deadline propagation through recursive chains - Change Eip4626 config from unit variant to Eip4626 { depth: NonZeroU8 } so recursive depth is declared once instead of repeating the variant - Extract conversion_rate() for readability Co-Authored-By: Claude Opus 4.6 (1M context) --- contracts/generated/Cargo.lock | 12 ++ crates/configs/src/native_price_estimators.rs | 23 ++- crates/e2e/tests/e2e/eip4626.rs | 12 +- crates/price-estimation/src/factory.rs | 20 +-- crates/price-estimation/src/native/eip4626.rs | 147 ++++++++++++++++-- 5 files changed, 178 insertions(+), 36 deletions(-) diff --git a/contracts/generated/Cargo.lock b/contracts/generated/Cargo.lock index e62ee4c84a..100d862231 100644 --- a/contracts/generated/Cargo.lock +++ b/contracts/generated/Cargo.lock @@ -1072,6 +1072,7 @@ dependencies = [ "cow-contract-iuniswapv3factory", "cow-contract-izeroex", "cow-contract-liquoricesettlement", + "cow-contract-mockerc4626wrapper", "cow-contract-nonstandarderc20balances", "cow-contract-pancakerouter", "cow-contract-permit2", @@ -1734,6 +1735,17 @@ dependencies = [ "anyhow", ] +[[package]] +name = "cow-contract-mockerc4626wrapper" +version = "0.1.0" +dependencies = [ + "alloy-contract", + "alloy-primitives", + "alloy-provider", + "alloy-sol-types", + "anyhow", +] + [[package]] name = "cow-contract-nonstandarderc20balances" version = "0.1.0" diff --git a/crates/configs/src/native_price_estimators.rs b/crates/configs/src/native_price_estimators.rs index 79708e215f..b312a68be0 100644 --- a/crates/configs/src/native_price_estimators.rs +++ b/crates/configs/src/native_price_estimators.rs @@ -1,9 +1,16 @@ use { serde::Deserialize, - std::fmt::{self, Display, Formatter}, + std::{ + fmt::{self, Display, Formatter}, + num::NonZeroU8, + }, url::Url, }; +const fn default_eip4626_depth() -> NonZeroU8 { + NonZeroU8::MIN +} + /// Ordered stages of native-price estimators. Each stage is tried in order; /// within a stage estimators run concurrently. #[derive(Clone, Debug, Default)] @@ -29,7 +36,7 @@ impl<'de> Deserialize<'de> for NativePriceEstimators { &format!("stage {} is empty, all stages must not be empty", n).as_str(), )); } - if matches!(stage.last(), Some(NativePriceEstimator::Eip4626)) { + if matches!(stage.last(), Some(NativePriceEstimator::Eip4626 { .. })) { return Err(serde::de::Error::custom(format!( "stage {n}: Eip4626 must be followed by another estimator" ))); @@ -102,7 +109,11 @@ pub enum NativePriceEstimator { /// Prices EIP-4626 vault tokens by looking up the underlying `asset()` and /// applying `convertToAssets()` as a conversion rate. Must be followed by /// another estimator in the same stage to price the underlying asset. - Eip4626, + /// `depth` controls how many nested vault layers to unwrap (default: 1). + Eip4626 { + #[serde(default = "default_eip4626_depth")] + depth: NonZeroU8, + }, } impl NativePriceEstimator { @@ -113,6 +124,10 @@ impl NativePriceEstimator { pub const fn forwarder(url: Url) -> Self { Self::Forwarder { url } } + + pub const fn eip4626(depth: NonZeroU8) -> Self { + Self::Eip4626 { depth } + } } impl Display for NativePriceEstimator { @@ -122,7 +137,7 @@ impl Display for NativePriceEstimator { NativePriceEstimator::Forwarder { url } => write!(f, "Forwarder|{}", url), NativePriceEstimator::OneInchSpotPriceApi => write!(f, "OneInchSpotPriceApi"), NativePriceEstimator::CoinGecko => write!(f, "CoinGecko"), - NativePriceEstimator::Eip4626 => write!(f, "Eip4626"), + NativePriceEstimator::Eip4626 { depth } => write!(f, "Eip4626({depth})"), } } } diff --git a/crates/e2e/tests/e2e/eip4626.rs b/crates/e2e/tests/e2e/eip4626.rs index c1b7e78f2e..8543ab31df 100644 --- a/crates/e2e/tests/e2e/eip4626.rs +++ b/crates/e2e/tests/e2e/eip4626.rs @@ -79,8 +79,9 @@ async fn eip4626_native_price_test(web3: Web3) { let autopilot_config = Configuration { native_price_estimation: NativePriceConfig { estimators: NativePriceEstimators::new(vec![vec![ - // Eip4626 wraps the next estimator in the list (test_quoter). - NativePriceEstimator::Eip4626, + // Eip4626(1) wraps the next estimator in the list (test_quoter) + // to unwrap one vault layer (sDAI → DAI). + NativePriceEstimator::eip4626(1.try_into().unwrap()), NativePriceEstimator::driver("test_quoter".to_string(), driver_url), // Standalone estimator for non-vault tokens. NativePriceEstimator::driver( @@ -170,14 +171,13 @@ async fn eip4626_recursive_native_price_test(web3: Web3) { wrapper_addrs.push(*wrapper.address()); } - // Two chained Eip4626 estimators: the first unwraps the mock wrapper to - // sDAI, the second unwraps sDAI to DAI, and the Driver prices DAI. + // Eip4626(2) unwraps two vault layers: mock wrapper → sDAI → DAI, then + // the Driver prices DAI. let driver_url = "http://localhost:11088/test_solver".parse().unwrap(); let autopilot_config = Configuration { native_price_estimation: NativePriceConfig { estimators: NativePriceEstimators::new(vec![vec![ - NativePriceEstimator::Eip4626, - NativePriceEstimator::Eip4626, + NativePriceEstimator::eip4626(2.try_into().unwrap()), NativePriceEstimator::driver("test_quoter".to_string(), driver_url), // Standalone estimator for non-vault tokens. NativePriceEstimator::driver( diff --git a/crates/price-estimation/src/factory.rs b/crates/price-estimation/src/factory.rs index f90caae9a7..37dfa6875d 100644 --- a/crates/price-estimation/src/factory.rs +++ b/crates/price-estimation/src/factory.rs @@ -280,20 +280,20 @@ impl<'a> PriceEstimatorFactory<'a> { Ok((name, coin_gecko)) } - NativePriceEstimatorSource::Eip4626 => { + NativePriceEstimatorSource::Eip4626 { depth } => { let next = rest .next() .context("Eip4626 must be followed by another estimator in the same stage")?; - let (inner_name, inner) = + let (mut name, mut current) = Box::pin(self.create_native_estimator(next, rest, weth)).await?; - let name = format!("Eip4626|{inner_name}"); - Ok(( - name.clone(), - Arc::new(InstrumentedPriceEstimator::new( - native::Eip4626::new(inner, self.network.web3.provider.clone()), - name, - )), - )) + for _ in 0..depth.get() { + name = format!("Eip4626|{name}"); + current = Arc::new(InstrumentedPriceEstimator::new( + native::Eip4626::new(current, self.network.web3.provider.clone()), + name.clone(), + )); + } + Ok((name, current)) } } } diff --git a/crates/price-estimation/src/native/eip4626.rs b/crates/price-estimation/src/native/eip4626.rs index 24c24c192c..1149958571 100644 --- a/crates/price-estimation/src/native/eip4626.rs +++ b/crates/price-estimation/src/native/eip4626.rs @@ -8,39 +8,122 @@ use { futures::{FutureExt, future::BoxFuture}, num::ToPrimitive, number::conversions::u256_to_big_rational, - std::{sync::Arc, time::Duration}, + std::{ + collections::HashSet, + sync::{Arc, Mutex}, + time::{Duration, Instant}, + }, }; /// Estimates the native price of EIP-4626 vault tokens by: /// 1. Calling `asset()` and `decimals()` in parallel /// 2. Calling `convertToAssets(10^decimals)` to find the conversion rate /// 3. Delegating to an inner estimator for the underlying token's native price +/// +/// Tokens that fail the `asset()` call are remembered in a negative cache so +/// subsequent requests skip the RPC entirely. Since most tokens are not +/// EIP-4626 vaults this avoids wasting a batched RPC round-trip per token per +/// estimation cycle. The cache is a `Mutex>` (~2.4 MB at +/// 100k entries: 20-byte address + ~4 bytes overhead per entry) and is never +/// evicted — a process restart clears it, which also handles the edge case of +/// a proxy token upgrading to become a vault. pub struct Eip4626 { inner: Arc, provider: AlloyProvider, + /// Addresses that are known *not* to be EIP-4626 vaults (i.e. `asset()` + /// reverted). Checked before making any RPC calls. + non_vault_tokens: Mutex>, } impl Eip4626 { pub fn new(inner: Arc, provider: AlloyProvider) -> Self { - Self { inner, provider } + Self { + inner, + provider, + non_vault_tokens: Mutex::new(HashSet::new()), + } } + /// Estimates the price of a vault token, if the token is not a vault token, + /// an error is returned. The `timeout` budget is shared: vault RPC calls + /// are individually bounded by `tokio::time::timeout`, and whatever time + /// remains is forwarded to the inner estimator. async fn estimate(&self, token: Address, timeout: Duration) -> NativePriceEstimateResult { + if self.non_vault_tokens.lock().unwrap().contains(&token) { + return Err(PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "{token} is not an EIP-4626 vault (cached)" + ))); + } + + self.estimate_vault_token(token, timeout).await + } + + /// Estimates the price of a *vault token*. + async fn estimate_vault_token( + &self, + token: Address, + timeout: Duration, + ) -> NativePriceEstimateResult { + let deadline = Instant::now() + timeout; + + let (asset, rate) = self.calculate_conversion_rate(token, timeout).await?; + + // Forward the remaining budget to the inner estimator so the total + // wall-clock time stays within the caller's original timeout. This + // matters for recursive Eip4626 chains where each layer spends time + // on vault RPC calls. + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Err(PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "timeout exceeded during vault RPC calls for {token}" + ))); + } + + let asset_price = self.inner.estimate_native_price(asset, remaining).await?; + + Ok(asset_price * rate) + } + + /// Fetches the underlying asset address and the shares-to-assets + /// conversion rate from on-chain vault calls. On `asset()` failure the + /// token is added to the negative cache. + /// + /// NOTE(jmg-duarte): `asset()` and `decimals()` are immutable for + /// ERC-4626 vaults. Caching them in a positive-result map may be possible + /// and useful to reduce network load. + async fn calculate_conversion_rate( + &self, + token: Address, + timeout: Duration, + ) -> Result<(Address, f64), PriceEstimationError> { + let deadline = Instant::now() + timeout; + let vault = IERC4626::Instance::new(token, self.provider.clone()); let erc20 = ERC20::Instance::new(token, self.provider.clone()); - // Fetch asset address and vault decimals in parallel. + // Parallel calls get batched into a single RPC request by alloy. let asset_builder = vault.asset(); let decimals_builder = erc20.decimals(); - let (asset_result, decimals_result) = - tokio::join!(asset_builder.call(), decimals_builder.call()); - - let asset: Address = asset_result.map_err(|e| { + let (asset_result, decimals_result) = tokio::time::timeout(timeout, async { + tokio::join!(asset_builder.call(), decimals_builder.call()) + }) + .await + .map_err(|_| { PriceEstimationError::EstimatorInternal(anyhow::anyhow!( - "failed to call asset() on {token}: {e}" + "timeout during asset()/decimals() on {token}" )) })?; + let asset: Address = match asset_result { + Ok(addr) => addr, + Err(e) => { + self.non_vault_tokens.lock().unwrap().insert(token); + return Err(PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "failed to call asset() on {token}: {e}" + ))); + } + }; + let decimals: u8 = decimals_result.map_err(|e| { PriceEstimationError::EstimatorInternal(anyhow::anyhow!( "failed to call decimals() on {token}: {e}" @@ -49,20 +132,26 @@ impl Eip4626 { let shares = U256::from(10u64).pow(U256::from(decimals)); - let assets: U256 = vault.convertToAssets(shares).call().await.map_err(|e| { - PriceEstimationError::EstimatorInternal(anyhow::anyhow!( - "failed to call convertToAssets() on {token}: {e}" - )) - })?; - - let asset_price = self.inner.estimate_native_price(asset, timeout).await?; + let remaining = deadline.saturating_duration_since(Instant::now()); + let assets: U256 = tokio::time::timeout(remaining, vault.convertToAssets(shares).call()) + .await + .map_err(|_| { + PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "timeout during convertToAssets() on {token}" + )) + })? + .map_err(|e| { + PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "failed to call convertToAssets() on {token}: {e}" + )) + })?; let rate = (u256_to_big_rational(&assets) / u256_to_big_rational(&shares)) .to_f64() .context("conversion rate is not representable as f64") .map_err(PriceEstimationError::EstimatorInternal)?; - Ok(asset_price * rate) + Ok((asset, rate)) } } @@ -95,6 +184,32 @@ mod tests { assert!((rate - 1.5).abs() < 1e-9); } + #[tokio::test] + async fn non_vault_tokens_are_cached() { + let inner = MockNativePriceEstimating::new(); + let non_vault_tokens = Mutex::new(HashSet::new()); + let token = Address::repeat_byte(0x42); + + // Pre-populate the negative cache. + non_vault_tokens.lock().unwrap().insert(token); + + let estimator = Eip4626 { + inner: Arc::new(inner), + // The provider is never reached because the cache short-circuits. + provider: ethrpc::Web3::new_from_url("http://localhost:1").provider, + non_vault_tokens, + }; + + // The estimate should fail immediately without making any RPC calls + // (the mock inner has no expectations set, so any call would panic). + let result = estimator + .estimate(token, HEALTHY_PRICE_ESTIMATION_TIME) + .await; + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("not an EIP-4626 vault (cached)"), "{err}"); + } + /// Requires a live node; run with: /// NODE_URL=... cargo test -p price-estimation -- eip4626 --ignored /// --nocapture From 64916da3fc026f75b066725fce5a86cd47814e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:35:59 +0100 Subject: [PATCH 08/20] Re-add lost tests --- crates/configs/src/native_price_estimators.rs | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/crates/configs/src/native_price_estimators.rs b/crates/configs/src/native_price_estimators.rs index b312a68be0..1d38d97ebe 100644 --- a/crates/configs/src/native_price_estimators.rs +++ b/crates/configs/src/native_price_estimators.rs @@ -141,3 +141,122 @@ impl Display for NativePriceEstimator { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, Deserialize)] + struct Helper { + estimators: NativePriceEstimators, + } + + #[test] + fn toml_deserialize_estimators_empty() { + #[derive(Deserialize)] + struct H { + _estimators: NativePriceEstimators, + } + + assert!(toml::from_str::("estimators = []").is_err()); + assert!(toml::from_str::("estimators = [[]]").is_err()); + } + + #[test] + fn toml_deserialize_estimators_single_stage() { + let toml = r#" + estimators = [[{type = "CoinGecko"}, {type = "OneInchSpotPriceApi"}]] + "#; + + let parsed: Helper = toml::from_str(toml).unwrap(); + assert_eq!( + parsed.estimators.as_slice(), + vec![vec![ + NativePriceEstimator::CoinGecko, + NativePriceEstimator::OneInchSpotPriceApi, + ]] + ); + } + + #[test] + fn toml_deserialize_estimators_multiple_stages() { + let toml = r#" + estimators = [ + [{type = "CoinGecko"}, {type = "Driver", name = "solver1", url = "http://localhost:8080"}], + [{type = "Forwarder", url = "http://localhost:12088"}], + ] + "#; + + let parsed: Helper = toml::from_str(toml).unwrap(); + assert_eq!( + parsed.estimators.as_slice(), + vec![ + vec![ + NativePriceEstimator::CoinGecko, + NativePriceEstimator::Driver(ExternalSolver { + name: "solver1".to_string(), + url: "http://localhost:8080".parse().unwrap(), + }), + ], + vec![NativePriceEstimator::Forwarder { + url: "http://localhost:12088".parse().unwrap(), + }], + ] + ); + } + + #[test] + fn toml_deserialize_estimators_default() { + let estimators = NativePriceEstimators::default(); + assert!(estimators.as_slice().is_empty()); + } + + #[test] + fn toml_deserialize_eip4626_default_depth() { + let toml = r#" + estimators = [[{type = "Eip4626"}, {type = "CoinGecko"}]] + "#; + + let parsed: Helper = toml::from_str(toml).unwrap(); + assert_eq!( + parsed.estimators.as_slice(), + vec![vec![ + NativePriceEstimator::Eip4626 { + depth: NonZeroU8::MIN + }, + NativePriceEstimator::CoinGecko, + ]] + ); + } + + #[test] + fn toml_deserialize_eip4626_custom_depth() { + let toml = r#" + estimators = [[{type = "Eip4626", depth = 3}, {type = "CoinGecko"}]] + "#; + + let parsed: Helper = toml::from_str(toml).unwrap(); + assert_eq!( + parsed.estimators.as_slice(), + vec![vec![ + NativePriceEstimator::Eip4626 { + depth: NonZeroU8::new(3).unwrap() + }, + NativePriceEstimator::CoinGecko, + ]] + ); + } + + #[test] + fn toml_deserialize_eip4626_at_end_of_stage_rejected() { + let toml = r#" + estimators = [[{type = "CoinGecko"}, {type = "Eip4626"}]] + "#; + + let err = toml::from_str::(toml).unwrap_err(); + assert!( + err.to_string().contains("Eip4626 must be followed"), + "{err}" + ); + } +} From b22aef1484aa00adf56576b6a7cbf8f3a465e084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:44:10 +0100 Subject: [PATCH 09/20] remove extra contract --- contracts/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/src/main.rs b/contracts/src/main.rs index 70091c61f3..24c0ae603d 100644 --- a/contracts/src/main.rs +++ b/contracts/src/main.rs @@ -398,7 +398,6 @@ fn build_module() -> Module { ])) .add_contract(Contract::new("CoWSwapOnchainOrders")) .add_contract(Contract::new("ERC1271SignatureValidator")) - .add_contract(Contract::new("IERC4626")) .add_contract(Contract::new("BalancerQueries").with_networks(networks![ MAINNET => ("0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5", 15188261), ARBITRUM_ONE => ("0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5", 18238624), From a8b6c5b1eced183a017847cbe1643fc751910e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:59:35 +0100 Subject: [PATCH 10/20] remove redundant test --- crates/price-estimation/src/native/eip4626.rs | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/crates/price-estimation/src/native/eip4626.rs b/crates/price-estimation/src/native/eip4626.rs index 1149958571..e099adf362 100644 --- a/crates/price-estimation/src/native/eip4626.rs +++ b/crates/price-estimation/src/native/eip4626.rs @@ -209,33 +209,4 @@ mod tests { let err = result.unwrap_err().to_string(); assert!(err.contains("not an EIP-4626 vault (cached)"), "{err}"); } - - /// Requires a live node; run with: - /// NODE_URL=... cargo test -p price-estimation -- eip4626 --ignored - /// --nocapture - #[tokio::test] - #[ignore] - async fn mainnet_sdai() { - // sDAI on mainnet: vault wrapping DAI - let sdai = alloy::primitives::address!("83F20F44975D03b1b09e64809B757c47f942BEeA"); - - let web3 = ethrpc::Web3::new_from_env(); - - let mut inner = MockNativePriceEstimating::new(); - inner.expect_estimate_native_price().returning(|token, _| { - let dai = alloy::primitives::address!("6B175474E89094C44Da98b954EedeAC495271d0F"); - assert_eq!(token, dai, "should price the underlying DAI, not sDAI"); - async { Ok(3.3e-4_f64) }.boxed() - }); - - let estimator = Eip4626::new(Arc::new(inner), web3.provider); - let price = estimator - .estimate_native_price(sdai, HEALTHY_PRICE_ESTIMATION_TIME) - .await - .unwrap(); - - // sDAI should be worth slightly more than DAI due to accrued interest - println!("sDAI native price: {price}"); - assert!(price > 3.3e-4_f64 * 0.99 && price < 3.3e-4_f64 * 1.20); - } } From 064ba13a35392e79870eac8292faba274c9f8b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:13:37 +0100 Subject: [PATCH 11/20] Improve EIP-4626 e2e tests with proper estimator staging and pool seeding - Add `MintableToken::at` constructor to wrap ABI-compatible contracts (e.g. MockERC4626Wrapper) as MintableToken for use with `seed_uni_v2_pool` - Split native price estimator config into two stages with results_required=1 so the EIP-4626 chain has priority over the standalone driver fallback - Seed Uniswap V2 pools for recursive wrapper tokens so the solver can find routes, enabling full quote submission - Verify native price ratios between wrappers match their vault conversion rates Co-Authored-By: Claude Opus 4.6 (1M context) --- .../e2e/src/setup/onchain_components/mod.rs | 10 + crates/e2e/tests/e2e/eip4626.rs | 171 ++++++++++++------ 2 files changed, 127 insertions(+), 54 deletions(-) diff --git a/crates/e2e/src/setup/onchain_components/mod.rs b/crates/e2e/src/setup/onchain_components/mod.rs index 3a9ebc53a8..6cc23f503c 100644 --- a/crates/e2e/src/setup/onchain_components/mod.rs +++ b/crates/e2e/src/setup/onchain_components/mod.rs @@ -100,6 +100,16 @@ pub struct MintableToken { } impl MintableToken { + /// Wraps an existing on-chain contract whose `mint(address,uint256)` + /// selector matches `ERC20Mintable`. Useful for test contracts like + /// `MockERC4626Wrapper` that are ABI-compatible. + pub fn at(address: Address, minter: Address, provider: ethrpc::AlloyProvider) -> Self { + Self { + contract: ERC20Mintable::Instance::new(address, provider), + minter, + } + } + pub async fn mint(&self, to: Address, amount: U256) { self.contract .mint(to, amount) diff --git a/crates/e2e/tests/e2e/eip4626.rs b/crates/e2e/tests/e2e/eip4626.rs index 8543ab31df..f4d9e543b5 100644 --- a/crates/e2e/tests/e2e/eip4626.rs +++ b/crates/e2e/tests/e2e/eip4626.rs @@ -72,23 +72,28 @@ async fn eip4626_native_price_test(web3: Web3) { .await .unwrap(); - // Configure native price estimation with an EIP-4626 wrapper so that the - // protocol can price sDAI by looking up its underlying DAI and applying the - // vault conversion rate. - let driver_url = "http://localhost:11088/test_solver".parse().unwrap(); + // Stage 1: EIP-4626 chain — vault tokens priced via conversion rate. + // Stage 2: driver fallback for non-vault tokens. The autopilot prices + // WETH at startup and panics if it can't, so a plain driver + // stage is required even though we're only testing vaults. + // results_required=1 so stage 2 only runs when stage 1 fails. + let driver_url: url::Url = "http://localhost:11088/test_solver".parse().unwrap(); let autopilot_config = Configuration { native_price_estimation: NativePriceConfig { - estimators: NativePriceEstimators::new(vec![vec![ - // Eip4626(1) wraps the next estimator in the list (test_quoter) - // to unwrap one vault layer (sDAI → DAI). - NativePriceEstimator::eip4626(1.try_into().unwrap()), - NativePriceEstimator::driver("test_quoter".to_string(), driver_url), - // Standalone estimator for non-vault tokens. - NativePriceEstimator::driver( + estimators: NativePriceEstimators::new(vec![ + vec![ + NativePriceEstimator::eip4626(1.try_into().unwrap()), + NativePriceEstimator::driver("test_quoter".to_string(), driver_url.clone()), + ], + vec![NativePriceEstimator::driver( "test_quoter".to_string(), - "http://localhost:11088/test_solver".parse().unwrap(), - ), - ]]), + driver_url, + )], + ]), + shared: configs::native_price::NativePriceConfig { + results_required: 1.try_into().unwrap(), + ..Default::default() + }, ..NativePriceConfig::test_default() }, ..Configuration::test("test_solver", solver.address()) @@ -140,24 +145,21 @@ async fn forked_node_mainnet_eip4626_recursive_native_price() { .await; } -/// Tests pricing of recursive EIP-4626 vaults with non-trivial conversion -/// rates. Deploys mock wrapper vaults on top of sDAI (which itself wraps DAI) -/// with different rates and verifies the prices scale correctly. -/// -/// Unlike the non-recursive test we cannot submit a full quote because the -/// freshly-deployed wrapper tokens have no DEX liquidity. Instead we verify -/// that the native price endpoint returns correctly scaled prices, which -/// exercises the full Eip4626 → Eip4626 → Driver chain. +/// Tests pricing and quoting of recursive EIP-4626 vaults with non-trivial +/// conversion rates. Deploys mock wrapper vaults on top of sDAI (which itself +/// wraps DAI) with different rates, seeds Uniswap V2 pools so the solver can +/// find routes, and verifies both native prices and full quotes. async fn eip4626_recursive_native_price_test(web3: Web3) { let mut onchain = OnchainComponents::deployed(web3.clone()).await; let [solver] = onchain.make_solvers_forked(1u64.eth()).await; + let [trader] = onchain.make_accounts(100u64.eth()).await; // Deploy mock EIP-4626 vaults wrapping sDAI with different conversion rates. // Each wrapper applies `convertToAssets(shares) = shares * num / den`, so a // (3, 2) wrapper means 1 share = 1.5 sDAI, making it 1.5x the sDAI price. let rates: &[(u64, u64)] = &[(3, 2), (2, 1), (1, 3)]; - let mut wrapper_addrs = Vec::with_capacity(rates.len()); + let mut wrappers = Vec::with_capacity(rates.len()); for &(num, den) in rates { let wrapper = contracts::test::MockERC4626Wrapper::Instance::deploy( web3.provider.clone(), @@ -168,23 +170,55 @@ async fn eip4626_recursive_native_price_test(web3: Web3) { ) .await .unwrap(); - wrapper_addrs.push(*wrapper.address()); + let mintable = + MintableToken::at(*wrapper.address(), trader.address(), web3.provider.clone()); + wrappers.push(mintable); + } + + // Seed Uniswap V2 pools so the solver can find routes for the wrapper + // tokens. We pair each wrapper with WETH. + let weth_token = MintableToken::at( + *onchain.contracts().weth.address(), + trader.address(), + web3.provider.clone(), + ); + onchain + .contracts() + .weth + .deposit() + .value(U256::from(rates.len() as u64) * 10u64.eth()) + .from(trader.address()) + .send_and_watch() + .await + .unwrap(); + for wrapper in &wrappers { + onchain + .seed_uni_v2_pool((wrapper, 10_000u64.eth()), (&weth_token, 10u64.eth())) + .await; } - // Eip4626(2) unwraps two vault layers: mock wrapper → sDAI → DAI, then - // the Driver prices DAI. - let driver_url = "http://localhost:11088/test_solver".parse().unwrap(); + // Stage 1: EIP-4626 chain — vault tokens priced via conversion rate. + // Stage 2: driver fallback for non-vault tokens. The autopilot prices + // WETH at startup and panics if it can't, so a plain driver + // stage is required even though we're only testing vaults. + // results_required=1 so stage 2 only runs when stage 1 fails. + let driver_url: url::Url = "http://localhost:11088/test_solver".parse().unwrap(); let autopilot_config = Configuration { native_price_estimation: NativePriceConfig { - estimators: NativePriceEstimators::new(vec![vec![ - NativePriceEstimator::eip4626(2.try_into().unwrap()), - NativePriceEstimator::driver("test_quoter".to_string(), driver_url), - // Standalone estimator for non-vault tokens. - NativePriceEstimator::driver( + estimators: NativePriceEstimators::new(vec![ + vec![ + NativePriceEstimator::eip4626(2.try_into().unwrap()), + NativePriceEstimator::driver("test_quoter".to_string(), driver_url.clone()), + ], + vec![NativePriceEstimator::driver( "test_quoter".to_string(), - "http://localhost:11088/test_solver".parse().unwrap(), - ), - ]]), + driver_url, + )], + ]), + shared: configs::native_price::NativePriceConfig { + results_required: 1.try_into().unwrap(), + ..Default::default() + }, ..NativePriceConfig::test_default() }, ..Configuration::test("test_solver", solver.address()) @@ -201,33 +235,62 @@ async fn eip4626_recursive_native_price_test(web3: Web3) { onchain.mint_block().await; - // First, get the sDAI native price as a baseline via the native price - // endpoint. sDAI is priced through the second Eip4626 in the chain. - wait_for_condition(TIMEOUT, || async { - services.get_native_price(&SDAI).await.is_ok() - }) - .await - .expect("sDAI native price should be available"); - let sdai_price = services.get_native_price(&SDAI).await.unwrap().price; - - // Verify each wrapper's price is the sDAI price scaled by the wrapper's - // conversion rate. This ensures the rate math is actually applied (a 1:1 - // mock would pass even if the rate were ignored). - for (&addr, &(num, den)) in wrapper_addrs.iter().zip(rates) { + // Verify native prices: the ratio between any two wrapper prices should + // match the ratio of their vault conversion rates. + let mut prices = Vec::with_capacity(rates.len()); + for (wrapper, &(num, den)) in wrappers.iter().zip(rates) { + let addr = *wrapper.address(); wait_for_condition(TIMEOUT, || async { services.get_native_price(&addr).await.is_ok() }) .await .unwrap_or_else(|_| panic!("native price for wrapper ({num}/{den}) should be available")); - let wrapper_price = services.get_native_price(&addr).await.unwrap().price; - let expected_ratio = num as f64 / den as f64; - let actual_ratio = wrapper_price / sdai_price; + prices.push(services.get_native_price(&addr).await.unwrap().price); + } + + for (i, &(num_i, den_i)) in rates.iter().enumerate() { + for (j, &(num_j, den_j)) in rates.iter().enumerate().skip(i + 1) { + let price_ratio = prices[i] / prices[j]; + let expected_ratio = (num_i * den_j) as f64 / (num_j * den_i) as f64; + let relative_err = (price_ratio - expected_ratio).abs() / expected_ratio; + assert!( + relative_err < 0.01, + "price ratio between ({num_i}/{den_i}) and ({num_j}/{den_j}) should match rate \ + ratio: got {price_ratio:.6}, expected {expected_ratio:.6}", + ); + } + } + + // Submit a quote for each wrapper token to verify the full pipeline works + // end-to-end (pricing + routing through the seeded Uni V2 pools). + for (wrapper, &(num, den)) in wrappers.iter().zip(rates) { + wrapper.mint(trader.address(), 100u64.eth()).await; + ERC20::Instance::new(*wrapper.address(), web3.provider.clone()) + .approve(onchain.contracts().allowance, 100u64.eth()) + .from(trader.address()) + .send_and_watch() + .await + .unwrap(); + + let quote = services + .submit_quote(&OrderQuoteRequest { + from: trader.address(), + sell_token: *wrapper.address(), + buy_token: WETH, + side: OrderQuoteSide::Sell { + sell_amount: SellAmount::BeforeFee { + value: (10u64.eth()).try_into().unwrap(), + }, + }, + ..Default::default() + }) + .await; assert!( - (actual_ratio - expected_ratio).abs() / expected_ratio < 0.01, - "wrapper ({num}/{den}): expected ratio ~{expected_ratio:.4}, got {actual_ratio:.4} \ - (wrapper={wrapper_price}, sdai={sdai_price})", + quote.is_ok(), + "quote for wrapper ({num}/{den}) should succeed: {:?}", + quote.err() ); } } From aeddf589e8f2664e77b046b29dcbbbe17a026424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:52:19 +0100 Subject: [PATCH 12/20] Fix EIP-4626 conversion rate to account for asset/vault decimal differences --- crates/price-estimation/src/native/eip4626.rs | 139 +++++++++++++----- 1 file changed, 102 insertions(+), 37 deletions(-) diff --git a/crates/price-estimation/src/native/eip4626.rs b/crates/price-estimation/src/native/eip4626.rs index e099adf362..3e37ad6f38 100644 --- a/crates/price-estimation/src/native/eip4626.rs +++ b/crates/price-estimation/src/native/eip4626.rs @@ -6,7 +6,7 @@ use { contracts::{ERC20, IERC4626}, ethrpc::AlloyProvider, futures::{FutureExt, future::BoxFuture}, - num::ToPrimitive, + num::{BigInt, BigRational, ToPrimitive}, number::conversions::u256_to_big_rational, std::{ collections::HashSet, @@ -16,9 +16,11 @@ use { }; /// Estimates the native price of EIP-4626 vault tokens by: -/// 1. Calling `asset()` and `decimals()` in parallel -/// 2. Calling `convertToAssets(10^decimals)` to find the conversion rate -/// 3. Delegating to an inner estimator for the underlying token's native price +/// 1. Querying `asset()` and `decimals()` on the vault +/// 2. Querying `convertToAssets(10^vault_decimals)` and `decimals()` on the +/// underlying asset +/// 3. Computing the conversion rate accounting for decimal differences +/// 4. Delegating to an inner estimator for the underlying token's native price /// /// Tokens that fail the `asset()` call are remembered in a negative cache so /// subsequent requests skip the RPC entirely. Since most tokens are not @@ -99,13 +101,12 @@ impl Eip4626 { let deadline = Instant::now() + timeout; let vault = IERC4626::Instance::new(token, self.provider.clone()); - let erc20 = ERC20::Instance::new(token, self.provider.clone()); + let vault_erc20 = ERC20::Instance::new(token, self.provider.clone()); - // Parallel calls get batched into a single RPC request by alloy. - let asset_builder = vault.asset(); - let decimals_builder = erc20.decimals(); + let asset_fut = vault.asset(); + let decimals_fut = vault_erc20.decimals(); let (asset_result, decimals_result) = tokio::time::timeout(timeout, async { - tokio::join!(asset_builder.call(), decimals_builder.call()) + tokio::join!(asset_fut.call(), decimals_fut.call()) }) .await .map_err(|_| { @@ -117,37 +118,51 @@ impl Eip4626 { let asset: Address = match asset_result { Ok(addr) => addr, Err(e) => { - self.non_vault_tokens.lock().unwrap().insert(token); + { + let mut cache = self.non_vault_tokens.lock().unwrap(); + cache.insert(token); + metrics::non_vault_cache_size(cache.len()); + } return Err(PriceEstimationError::EstimatorInternal(anyhow::anyhow!( "failed to call asset() on {token}: {e}" ))); } }; - let decimals: u8 = decimals_result.map_err(|e| { + let vault_decimals: u8 = decimals_result.map_err(|e| { PriceEstimationError::EstimatorInternal(anyhow::anyhow!( "failed to call decimals() on {token}: {e}" )) })?; - let shares = U256::from(10u64).pow(U256::from(decimals)); - + let one_token = U256::from(10u64).pow(U256::from(vault_decimals)); + let asset_erc20 = ERC20::Instance::new(asset, self.provider.clone()); + let convert_fut = vault.convertToAssets(one_token); + let asset_decimals_fut = asset_erc20.decimals(); let remaining = deadline.saturating_duration_since(Instant::now()); - let assets: U256 = tokio::time::timeout(remaining, vault.convertToAssets(shares).call()) - .await - .map_err(|_| { - PriceEstimationError::EstimatorInternal(anyhow::anyhow!( - "timeout during convertToAssets() on {token}" - )) - })? - .map_err(|e| { - PriceEstimationError::EstimatorInternal(anyhow::anyhow!( - "failed to call convertToAssets() on {token}: {e}" - )) - })?; - - let rate = (u256_to_big_rational(&assets) / u256_to_big_rational(&shares)) - .to_f64() + let (convert_result, asset_decimals_result) = tokio::time::timeout(remaining, async { + tokio::join!(convert_fut.call(), asset_decimals_fut.call()) + }) + .await + .map_err(|_| { + PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "timeout during convertToAssets()/asset decimals() on {token}" + )) + })?; + + let assets: U256 = convert_result.map_err(|e| { + PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "failed to call convertToAssets() on {token}: {e}" + )) + })?; + + let asset_decimals: u8 = asset_decimals_result.map_err(|e| { + PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "failed to call decimals() on underlying asset {asset}: {e}" + )) + })?; + + let rate = conversion_rate(assets, asset_decimals) .context("conversion rate is not representable as f64") .map_err(PriceEstimationError::EstimatorInternal)?; @@ -165,6 +180,41 @@ impl NativePriceEstimating for Eip4626 { } } +/// Computes the full-asset-tokens per full-vault-token conversion rate. +/// +/// `assets` is the return value of `convertToAssets(10^vault_decimals)` — i.e. +/// asset-atomic-units for exactly 1 full vault token. Dividing by +/// `10^asset_decimals` converts to full asset tokens. +/// +/// Returns `None` when the result is not representable as `f64`. +fn conversion_rate(assets: U256, asset_decimals: u8) -> Option { + let denominator = BigRational::from_integer(BigInt::from(10u64).pow(asset_decimals as u32)); + (u256_to_big_rational(&assets) / denominator).to_f64() +} + +mod metrics { + use {observe::metrics, prometheus::IntGauge}; + + #[derive(prometheus_metric_storage::MetricStorage)] + struct Metrics { + /// Number of tokens in the EIP-4626 negative cache (known non-vault + /// tokens). + eip4626_non_vault_cache_size: IntGauge, + } + + impl Metrics { + fn get() -> &'static Self { + Metrics::instance(metrics::get_storage_registry()).unwrap() + } + } + + pub(super) fn non_vault_cache_size(size: usize) { + Metrics::get() + .eip4626_non_vault_cache_size + .set(i64::try_from(size).unwrap_or(i64::MAX)); + } +} + #[cfg(test)] mod tests { use { @@ -173,15 +223,30 @@ mod tests { }; #[test] - fn rate_math() { - // 6-decimal vault where 1 share = 1.5 underlying tokens - let decimals = 6u8; - let shares = U256::from(10u64).pow(U256::from(decimals)); - let assets = U256::from(1_500_000u64); // 1.5 * 10^6 - let rate = (u256_to_big_rational(&assets) / u256_to_big_rational(&shares)) - .to_f64() - .unwrap(); - assert!((rate - 1.5).abs() < 1e-9); + fn rate_math_same_decimals() { + // 18-decimal vault wrapping 18-decimal asset, 1 share = 1.5 asset tokens. + // convertToAssets(10^18) = 1.5 * 10^18 asset-atomic-units + let assets = U256::from(15u64) * U256::from(10u64).pow(U256::from(17u64)); + let rate = conversion_rate(assets, 18).unwrap(); + assert!((rate - 1.5).abs() < 1e-9, "rate={rate}"); + } + + #[test] + fn rate_math_vault_18_asset_6() { + // 18-decimal vault wrapping 6-decimal USDC, 1 share = 1.5 USDC. + // convertToAssets(10^18) = 1_500_000 asset-atomic-units (1.5 * 10^6) + let assets = U256::from(1_500_000u64); + let rate = conversion_rate(assets, 6).unwrap(); + assert!((rate - 1.5).abs() < 1e-9, "rate={rate}"); + } + + #[test] + fn rate_math_vault_6_asset_18() { + // 6-decimal vault wrapping 18-decimal asset, 1 share = 2 asset tokens. + // convertToAssets(10^6) = 2 * 10^18 asset-atomic-units + let assets = U256::from(2u64) * U256::from(10u64).pow(U256::from(18u64)); + let rate = conversion_rate(assets, 18).unwrap(); + assert!((rate - 2.0).abs() < 1e-9, "rate={rate}"); } #[tokio::test] From 9b74dce03d395ce001609c5aa1717e48efca3216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:38:58 +0100 Subject: [PATCH 13/20] simplify test --- crates/e2e/tests/e2e/eip4626.rs | 124 +++++++++++--------------------- 1 file changed, 42 insertions(+), 82 deletions(-) diff --git a/crates/e2e/tests/e2e/eip4626.rs b/crates/e2e/tests/e2e/eip4626.rs index f4d9e543b5..722c28054d 100644 --- a/crates/e2e/tests/e2e/eip4626.rs +++ b/crates/e2e/tests/e2e/eip4626.rs @@ -153,13 +153,12 @@ async fn eip4626_recursive_native_price_test(web3: Web3) { let mut onchain = OnchainComponents::deployed(web3.clone()).await; let [solver] = onchain.make_solvers_forked(1u64.eth()).await; - let [trader] = onchain.make_accounts(100u64.eth()).await; // Deploy mock EIP-4626 vaults wrapping sDAI with different conversion rates. // Each wrapper applies `convertToAssets(shares) = shares * num / den`, so a // (3, 2) wrapper means 1 share = 1.5 sDAI, making it 1.5x the sDAI price. let rates: &[(u64, u64)] = &[(3, 2), (2, 1), (1, 3)]; - let mut wrappers = Vec::with_capacity(rates.len()); + let mut wrapper_addrs = Vec::with_capacity(rates.len()); for &(num, den) in rates { let wrapper = contracts::test::MockERC4626Wrapper::Instance::deploy( web3.provider.clone(), @@ -170,31 +169,7 @@ async fn eip4626_recursive_native_price_test(web3: Web3) { ) .await .unwrap(); - let mintable = - MintableToken::at(*wrapper.address(), trader.address(), web3.provider.clone()); - wrappers.push(mintable); - } - - // Seed Uniswap V2 pools so the solver can find routes for the wrapper - // tokens. We pair each wrapper with WETH. - let weth_token = MintableToken::at( - *onchain.contracts().weth.address(), - trader.address(), - web3.provider.clone(), - ); - onchain - .contracts() - .weth - .deposit() - .value(U256::from(rates.len() as u64) * 10u64.eth()) - .from(trader.address()) - .send_and_watch() - .await - .unwrap(); - for wrapper in &wrappers { - onchain - .seed_uni_v2_pool((wrapper, 10_000u64.eth()), (&weth_token, 10u64.eth())) - .await; + wrapper_addrs.push(*wrapper.address()); } // Stage 1: EIP-4626 chain — vault tokens priced via conversion rate. @@ -235,62 +210,47 @@ async fn eip4626_recursive_native_price_test(web3: Web3) { onchain.mint_block().await; - // Verify native prices: the ratio between any two wrapper prices should - // match the ratio of their vault conversion rates. - let mut prices = Vec::with_capacity(rates.len()); - for (wrapper, &(num, den)) in wrappers.iter().zip(rates) { - let addr = *wrapper.address(); - wait_for_condition(TIMEOUT, || async { - services.get_native_price(&addr).await.is_ok() - }) + // Verify native prices: use the first wrapper (3/2) as a baseline and + // check that the others are priced proportionally to their conversion rate. + let baseline_addr = wrapper_addrs[0]; + wait_for_condition(TIMEOUT, || async { + services.get_native_price(&baseline_addr).await.is_ok() + }) + .await + .expect("native price for wrapper (3/2) should be available"); + let baseline_price = services + .get_native_price(&baseline_addr) .await - .unwrap_or_else(|_| panic!("native price for wrapper ({num}/{den}) should be available")); - - prices.push(services.get_native_price(&addr).await.unwrap().price); - } - - for (i, &(num_i, den_i)) in rates.iter().enumerate() { - for (j, &(num_j, den_j)) in rates.iter().enumerate().skip(i + 1) { - let price_ratio = prices[i] / prices[j]; - let expected_ratio = (num_i * den_j) as f64 / (num_j * den_i) as f64; - let relative_err = (price_ratio - expected_ratio).abs() / expected_ratio; - assert!( - relative_err < 0.01, - "price ratio between ({num_i}/{den_i}) and ({num_j}/{den_j}) should match rate \ - ratio: got {price_ratio:.6}, expected {expected_ratio:.6}", - ); - } - } - - // Submit a quote for each wrapper token to verify the full pipeline works - // end-to-end (pricing + routing through the seeded Uni V2 pools). - for (wrapper, &(num, den)) in wrappers.iter().zip(rates) { - wrapper.mint(trader.address(), 100u64.eth()).await; - ERC20::Instance::new(*wrapper.address(), web3.provider.clone()) - .approve(onchain.contracts().allowance, 100u64.eth()) - .from(trader.address()) - .send_and_watch() - .await - .unwrap(); + .unwrap() + .price; - let quote = services - .submit_quote(&OrderQuoteRequest { - from: trader.address(), - sell_token: *wrapper.address(), - buy_token: WETH, - side: OrderQuoteSide::Sell { - sell_amount: SellAmount::BeforeFee { - value: (10u64.eth()).try_into().unwrap(), - }, - }, - ..Default::default() - }) - .await; + // Wrapper (2/1) has rate 2/1 vs baseline 3/2, so its price should be + // (2/1) / (3/2) = 4/3 of the baseline. + let addr = wrapper_addrs[1]; + wait_for_condition(TIMEOUT, || async { + services.get_native_price(&addr).await.is_ok() + }) + .await + .expect("native price for wrapper (2/1) should be available"); + let price = services.get_native_price(&addr).await.unwrap().price; + let ratio = price / baseline_price; + assert!( + (ratio - 4.0 / 3.0).abs() / (4.0 / 3.0) < 0.01, + "wrapper (2/1) price ratio to baseline (3/2) should be 4/3: got {ratio:.6}", + ); - assert!( - quote.is_ok(), - "quote for wrapper ({num}/{den}) should succeed: {:?}", - quote.err() - ); - } + // Wrapper (1/3) has rate 1/3 vs baseline 3/2, so its price should be + // (1/3) / (3/2) = 2/9 of the baseline. + let addr = wrapper_addrs[2]; + wait_for_condition(TIMEOUT, || async { + services.get_native_price(&addr).await.is_ok() + }) + .await + .expect("native price for wrapper (1/3) should be available"); + let price = services.get_native_price(&addr).await.unwrap().price; + let ratio = price / baseline_price; + assert!( + (ratio - 2.0 / 9.0).abs() / (2.0 / 9.0) < 0.01, + "wrapper (1/3) price ratio to baseline (3/2) should be 2/9: got {ratio:.6}", + ); } From 6dbd90ad316a9ff89f6e3803d81cbbe75488687d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:50:31 +0100 Subject: [PATCH 14/20] simplify approach --- Cargo.lock | 1 + crates/autopilot/src/run.rs | 2 + crates/configs/src/autopilot/native_price.rs | 7 + crates/configs/src/native_price_estimators.rs | 76 +----- crates/e2e/tests/e2e/eip4626.rs | 48 +--- crates/price-estimation/Cargo.toml | 1 + crates/price-estimation/src/factory.rs | 42 ++- crates/price-estimation/src/native/eip4626.rs | 240 +++++++++--------- 8 files changed, 162 insertions(+), 255 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d237cbccd3..9d6a24c147 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6371,6 +6371,7 @@ dependencies = [ "configs", "const-hex", "contracts", + "dashmap", "derive_more 1.0.0", "ethrpc", "futures", diff --git a/crates/autopilot/src/run.rs b/crates/autopilot/src/run.rs index 9d0a1cdf24..1f0482cf66 100644 --- a/crates/autopilot/src/run.rs +++ b/crates/autopilot/src/run.rs @@ -346,6 +346,7 @@ pub async fn run(config: Configuration, shutdown_controller: ShutdownController) config.native_price_estimation.shared.results_required, &weth, shared_cache.clone(), + config.native_price_estimation.eip4626, ) .instrument(info_span!("api_native_price_estimator")) .await, @@ -358,6 +359,7 @@ pub async fn run(config: Configuration, shutdown_controller: ShutdownController) config.native_price_estimation.shared.results_required, &weth, shared_cache.clone(), + config.native_price_estimation.eip4626, ) .instrument(info_span!("competition_native_price_updater")) .await; diff --git a/crates/configs/src/autopilot/native_price.rs b/crates/configs/src/autopilot/native_price.rs index 74ac52635f..7ebf0f77bc 100644 --- a/crates/configs/src/autopilot/native_price.rs +++ b/crates/configs/src/autopilot/native_price.rs @@ -46,6 +46,12 @@ pub struct NativePriceConfig { )] pub prefetch_time: Duration, + /// Enable EIP-4626 vault token pricing. When enabled, the native price + /// estimator will attempt to price vault tokens by querying their + /// underlying asset and conversion rate on-chain. + #[serde(default)] + pub eip4626: bool, + /// Shared native price settings (cache, approximation tokens, etc.). #[serde(flatten)] pub shared: crate::native_price::NativePriceConfig, @@ -62,6 +68,7 @@ impl NativePriceConfig { api_estimators: Default::default(), cache_refresh_interval: default_native_price_cache_refresh(), prefetch_time: Duration::from_millis(500), + eip4626: false, shared: crate::native_price::NativePriceConfig { cache: crate::native_price::CacheConfig { max_age: Duration::from_secs(2), diff --git a/crates/configs/src/native_price_estimators.rs b/crates/configs/src/native_price_estimators.rs index 1d38d97ebe..fb244d14ee 100644 --- a/crates/configs/src/native_price_estimators.rs +++ b/crates/configs/src/native_price_estimators.rs @@ -1,16 +1,9 @@ use { serde::Deserialize, - std::{ - fmt::{self, Display, Formatter}, - num::NonZeroU8, - }, + std::fmt::{self, Display, Formatter}, url::Url, }; -const fn default_eip4626_depth() -> NonZeroU8 { - NonZeroU8::MIN -} - /// Ordered stages of native-price estimators. Each stage is tried in order; /// within a stage estimators run concurrently. #[derive(Clone, Debug, Default)] @@ -36,11 +29,6 @@ impl<'de> Deserialize<'de> for NativePriceEstimators { &format!("stage {} is empty, all stages must not be empty", n).as_str(), )); } - if matches!(stage.last(), Some(NativePriceEstimator::Eip4626 { .. })) { - return Err(serde::de::Error::custom(format!( - "stage {n}: Eip4626 must be followed by another estimator" - ))); - } } Ok(Self(estimators)) } @@ -106,14 +94,6 @@ pub enum NativePriceEstimator { OneInchSpotPriceApi, /// Use the CoinGecko API. CoinGecko, - /// Prices EIP-4626 vault tokens by looking up the underlying `asset()` and - /// applying `convertToAssets()` as a conversion rate. Must be followed by - /// another estimator in the same stage to price the underlying asset. - /// `depth` controls how many nested vault layers to unwrap (default: 1). - Eip4626 { - #[serde(default = "default_eip4626_depth")] - depth: NonZeroU8, - }, } impl NativePriceEstimator { @@ -124,10 +104,6 @@ impl NativePriceEstimator { pub const fn forwarder(url: Url) -> Self { Self::Forwarder { url } } - - pub const fn eip4626(depth: NonZeroU8) -> Self { - Self::Eip4626 { depth } - } } impl Display for NativePriceEstimator { @@ -137,7 +113,6 @@ impl Display for NativePriceEstimator { NativePriceEstimator::Forwarder { url } => write!(f, "Forwarder|{}", url), NativePriceEstimator::OneInchSpotPriceApi => write!(f, "OneInchSpotPriceApi"), NativePriceEstimator::CoinGecko => write!(f, "CoinGecko"), - NativePriceEstimator::Eip4626 { depth } => write!(f, "Eip4626({depth})"), } } } @@ -210,53 +185,4 @@ mod tests { let estimators = NativePriceEstimators::default(); assert!(estimators.as_slice().is_empty()); } - - #[test] - fn toml_deserialize_eip4626_default_depth() { - let toml = r#" - estimators = [[{type = "Eip4626"}, {type = "CoinGecko"}]] - "#; - - let parsed: Helper = toml::from_str(toml).unwrap(); - assert_eq!( - parsed.estimators.as_slice(), - vec![vec![ - NativePriceEstimator::Eip4626 { - depth: NonZeroU8::MIN - }, - NativePriceEstimator::CoinGecko, - ]] - ); - } - - #[test] - fn toml_deserialize_eip4626_custom_depth() { - let toml = r#" - estimators = [[{type = "Eip4626", depth = 3}, {type = "CoinGecko"}]] - "#; - - let parsed: Helper = toml::from_str(toml).unwrap(); - assert_eq!( - parsed.estimators.as_slice(), - vec![vec![ - NativePriceEstimator::Eip4626 { - depth: NonZeroU8::new(3).unwrap() - }, - NativePriceEstimator::CoinGecko, - ]] - ); - } - - #[test] - fn toml_deserialize_eip4626_at_end_of_stage_rejected() { - let toml = r#" - estimators = [[{type = "CoinGecko"}, {type = "Eip4626"}]] - "#; - - let err = toml::from_str::(toml).unwrap_err(); - assert!( - err.to_string().contains("Eip4626 must be followed"), - "{err}" - ); - } } diff --git a/crates/e2e/tests/e2e/eip4626.rs b/crates/e2e/tests/e2e/eip4626.rs index 722c28054d..fd7fef837e 100644 --- a/crates/e2e/tests/e2e/eip4626.rs +++ b/crates/e2e/tests/e2e/eip4626.rs @@ -72,24 +72,14 @@ async fn eip4626_native_price_test(web3: Web3) { .await .unwrap(); - // Stage 1: EIP-4626 chain — vault tokens priced via conversion rate. - // Stage 2: driver fallback for non-vault tokens. The autopilot prices - // WETH at startup and panics if it can't, so a plain driver - // stage is required even though we're only testing vaults. - // results_required=1 so stage 2 only runs when stage 1 fails. let driver_url: url::Url = "http://localhost:11088/test_solver".parse().unwrap(); let autopilot_config = Configuration { native_price_estimation: NativePriceConfig { - estimators: NativePriceEstimators::new(vec![ - vec![ - NativePriceEstimator::eip4626(1.try_into().unwrap()), - NativePriceEstimator::driver("test_quoter".to_string(), driver_url.clone()), - ], - vec![NativePriceEstimator::driver( - "test_quoter".to_string(), - driver_url, - )], - ]), + eip4626: true, + estimators: NativePriceEstimators::new(vec![vec![NativePriceEstimator::driver( + "test_quoter".to_string(), + driver_url, + )]]), shared: configs::native_price::NativePriceConfig { results_required: 1.try_into().unwrap(), ..Default::default() @@ -145,10 +135,10 @@ async fn forked_node_mainnet_eip4626_recursive_native_price() { .await; } -/// Tests pricing and quoting of recursive EIP-4626 vaults with non-trivial -/// conversion rates. Deploys mock wrapper vaults on top of sDAI (which itself -/// wraps DAI) with different rates, seeds Uniswap V2 pools so the solver can -/// find routes, and verifies both native prices and full quotes. +/// Tests pricing of mock EIP-4626 vaults with non-trivial conversion rates. +/// Deploys wrapper vaults on top of sDAI (which itself wraps DAI) with +/// different rates and verifies native prices are proportional to their +/// conversion rates. async fn eip4626_recursive_native_price_test(web3: Web3) { let mut onchain = OnchainComponents::deployed(web3.clone()).await; @@ -172,24 +162,14 @@ async fn eip4626_recursive_native_price_test(web3: Web3) { wrapper_addrs.push(*wrapper.address()); } - // Stage 1: EIP-4626 chain — vault tokens priced via conversion rate. - // Stage 2: driver fallback for non-vault tokens. The autopilot prices - // WETH at startup and panics if it can't, so a plain driver - // stage is required even though we're only testing vaults. - // results_required=1 so stage 2 only runs when stage 1 fails. let driver_url: url::Url = "http://localhost:11088/test_solver".parse().unwrap(); let autopilot_config = Configuration { native_price_estimation: NativePriceConfig { - estimators: NativePriceEstimators::new(vec![ - vec![ - NativePriceEstimator::eip4626(2.try_into().unwrap()), - NativePriceEstimator::driver("test_quoter".to_string(), driver_url.clone()), - ], - vec![NativePriceEstimator::driver( - "test_quoter".to_string(), - driver_url, - )], - ]), + eip4626: true, + estimators: NativePriceEstimators::new(vec![vec![NativePriceEstimator::driver( + "test_quoter".to_string(), + driver_url, + )]]), shared: configs::native_price::NativePriceConfig { results_required: 1.try_into().unwrap(), ..Default::default() diff --git a/crates/price-estimation/Cargo.toml b/crates/price-estimation/Cargo.toml index 3615898ba0..106ef602fc 100644 --- a/crates/price-estimation/Cargo.toml +++ b/crates/price-estimation/Cargo.toml @@ -22,6 +22,7 @@ clap = { workspace = true } configs = { workspace = true } const-hex = { workspace = true } contracts = { workspace = true } +dashmap = { workspace = true } derive_more = { workspace = true } ethrpc = { workspace = true } futures = { workspace = true } diff --git a/crates/price-estimation/src/factory.rs b/crates/price-estimation/src/factory.rs index 37dfa6875d..6dc1e9c971 100644 --- a/crates/price-estimation/src/factory.rs +++ b/crates/price-estimation/src/factory.rs @@ -183,10 +183,9 @@ impl<'a> PriceEstimatorFactory<'a> { }) } - async fn create_native_estimator<'b>( + async fn create_native_estimator( &mut self, source: &NativePriceEstimatorSource, - rest: &mut impl Iterator, weth: &WETH9::Instance, ) -> Result<(String, Arc)> { match source { @@ -280,21 +279,6 @@ impl<'a> PriceEstimatorFactory<'a> { Ok((name, coin_gecko)) } - NativePriceEstimatorSource::Eip4626 { depth } => { - let next = rest - .next() - .context("Eip4626 must be followed by another estimator in the same stage")?; - let (mut name, mut current) = - Box::pin(self.create_native_estimator(next, rest, weth)).await?; - for _ in 0..depth.get() { - name = format!("Eip4626|{name}"); - current = Arc::new(InstrumentedPriceEstimator::new( - native::Eip4626::new(current, self.network.web3.provider.clone()), - name.clone(), - )); - } - Ok((name, current)) - } } } @@ -384,7 +368,9 @@ impl<'a> PriceEstimatorFactory<'a> { )) } - /// Creates a native price estimator from the given sources. + /// Creates a native price estimator from the given sources. When `eip4626` + /// is true the resulting estimator is wrapped in an [`native::Eip4626`] + /// layer that transparently prices vault tokens. pub async fn native_price_estimator( &mut self, native: &[Vec], @@ -394,12 +380,8 @@ impl<'a> PriceEstimatorFactory<'a> { let mut estimators = Vec::with_capacity(native.len()); for stage in native.iter() { let mut stages = Vec::with_capacity(stage.len()); - let mut iter = stage.iter(); - while let Some(source) = iter.next() { - stages.push( - self.create_native_estimator(source, &mut iter, weth) - .await?, - ); + for source in stage { + stages.push(self.create_native_estimator(source, weth).await?); } estimators.push(stages); } @@ -413,17 +395,29 @@ impl<'a> PriceEstimatorFactory<'a> { /// Creates a [`CachingNativePriceEstimator`] that wraps a native price /// estimator with an in-memory cache. + /// + /// If `eip4626` is true, it will wrap the estimator with EIP-4626 + /// unwrapping. pub async fn caching_native_price_estimator( &mut self, native: &[Vec], results_required: NonZeroUsize, weth: &WETH9::Instance, cache: native_price_cache::Cache, + eip4626: bool, ) -> native_price_cache::CachingNativePriceEstimator { let inner = self .native_price_estimator(native, results_required, weth) .await .expect("failed to build native price estimator"); + let inner = if eip4626 { + Box::new(InstrumentedPriceEstimator::new( + native::Eip4626::new(inner, self.network.web3.provider.clone()), + "Eip4626".to_string(), + )) + } else { + inner + }; self.caching_native_price_estimator_from_inner(inner, cache) .await } diff --git a/crates/price-estimation/src/native/eip4626.rs b/crates/price-estimation/src/native/eip4626.rs index 3e37ad6f38..18b73ea44d 100644 --- a/crates/price-estimation/src/native/eip4626.rs +++ b/crates/price-estimation/src/native/eip4626.rs @@ -4,15 +4,12 @@ use { alloy::primitives::{Address, U256}, anyhow::Context, contracts::{ERC20, IERC4626}, + dashmap::DashSet, ethrpc::AlloyProvider, futures::{FutureExt, future::BoxFuture}, num::{BigInt, BigRational, ToPrimitive}, number::conversions::u256_to_big_rational, - std::{ - collections::HashSet, - sync::{Arc, Mutex}, - time::{Duration, Instant}, - }, + std::time::{Duration, Instant}, }; /// Estimates the native price of EIP-4626 vault tokens by: @@ -22,150 +19,149 @@ use { /// 3. Computing the conversion rate accounting for decimal differences /// 4. Delegating to an inner estimator for the underlying token's native price /// -/// Tokens that fail the `asset()` call are remembered in a negative cache so -/// subsequent requests skip the RPC entirely. Since most tokens are not -/// EIP-4626 vaults this avoids wasting a batched RPC round-trip per token per -/// estimation cycle. The cache is a `Mutex>` (~2.4 MB at -/// 100k entries: 20-byte address + ~4 bytes overhead per entry) and is never -/// evicted — a process restart clears it, which also handles the edge case of -/// a proxy token upgrading to become a vault. +/// For non-vault tokens, delegates directly to the inner estimator +/// (pass-through). +/// +/// Tokens whose `asset()` call reverts are remembered in a negative cache so +/// subsequent requests skip the RPC and go straight to the inner estimator. pub struct Eip4626 { - inner: Arc, + inner: Box, provider: AlloyProvider, /// Addresses that are known *not* to be EIP-4626 vaults (i.e. `asset()` /// reverted). Checked before making any RPC calls. - non_vault_tokens: Mutex>, + non_vault_tokens: DashSet
, } impl Eip4626 { - pub fn new(inner: Arc, provider: AlloyProvider) -> Self { + pub fn new(inner: Box, provider: AlloyProvider) -> Self { Self { inner, provider, - non_vault_tokens: Mutex::new(HashSet::new()), + non_vault_tokens: DashSet::new(), } } - /// Estimates the price of a vault token, if the token is not a vault token, - /// an error is returned. The `timeout` budget is shared: vault RPC calls - /// are individually bounded by `tokio::time::timeout`, and whatever time - /// remains is forwarded to the inner estimator. async fn estimate(&self, token: Address, timeout: Duration) -> NativePriceEstimateResult { - if self.non_vault_tokens.lock().unwrap().contains(&token) { - return Err(PriceEstimationError::EstimatorInternal(anyhow::anyhow!( - "{token} is not an EIP-4626 vault (cached)" - ))); + // Known non-vault or no time budget for vault discovery: delegate + // directly. A zero timeout is used by callers (e.g. the autopilot's + // WETH price fetch) as a "best-effort / use cached data" signal — the + // inner estimator and its callers treat it as advisory, not as a hard + // cutoff. We must not feed it into `tokio::time::timeout` which would + // fire immediately. + if self.non_vault_tokens.contains(&token) || timeout.is_zero() { + return self.inner.estimate_native_price(token, timeout).await; } - self.estimate_vault_token(token, timeout).await - } - - /// Estimates the price of a *vault token*. - async fn estimate_vault_token( - &self, - token: Address, - timeout: Duration, - ) -> NativePriceEstimateResult { let deadline = Instant::now() + timeout; + let check_timeout = |token: Address| -> Result { + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Err(PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "timeout exceeded during vault RPC calls for {token}" + ))); + } + Ok(remaining) + }; - let (asset, rate) = self.calculate_conversion_rate(token, timeout).await?; - - // Forward the remaining budget to the inner estimator so the total - // wall-clock time stays within the caller's original timeout. This - // matters for recursive Eip4626 chains where each layer spends time - // on vault RPC calls. - let remaining = deadline.saturating_duration_since(Instant::now()); - if remaining.is_zero() { - return Err(PriceEstimationError::EstimatorInternal(anyhow::anyhow!( - "timeout exceeded during vault RPC calls for {token}" - ))); + // Reserve at least half the budget for the inner estimator fallback. + let vault_budget = timeout / 2; + let conversion_rate_result = + match tokio::time::timeout(vault_budget, self.calculate_conversion_rate(token)).await { + Ok(res) => res, + Err(_timeout) => Err(PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "timeout during vault RPC calls for {token}" + ))), + }; + match conversion_rate_result { + Ok((asset, rate)) => { + let remaining = check_timeout(token)?; + let asset_price = self.estimate_native_price(asset, remaining).await?; + Ok(asset_price * rate) + } + // Vault RPC failed — if the token was just cached as non-vault, + // fall through to the inner estimator with remaining budget. + Err(_) if self.non_vault_tokens.contains(&token) => { + let remaining = check_timeout(token)?; + self.inner.estimate_native_price(token, remaining).await + } + Err(e) => Err(e), } - - let asset_price = self.inner.estimate_native_price(asset, remaining).await?; - - Ok(asset_price * rate) } - /// Fetches the underlying asset address and the shares-to-assets - /// conversion rate from on-chain vault calls. On `asset()` failure the - /// token is added to the negative cache. - /// - /// NOTE(jmg-duarte): `asset()` and `decimals()` are immutable for - /// ERC-4626 vaults. Caching them in a positive-result map may be possible - /// and useful to reduce network load. - async fn calculate_conversion_rate( + /// Fetches the vault's underlying asset address and decimals. + /// On `asset()` revert the token is added to the negative cache if + /// `decimals()` succeeded (i.e. it's a valid ERC-20 but not a vault). + async fn fetch_vault_info( &self, token: Address, - timeout: Duration, - ) -> Result<(Address, f64), PriceEstimationError> { - let deadline = Instant::now() + timeout; - - let vault = IERC4626::Instance::new(token, self.provider.clone()); - let vault_erc20 = ERC20::Instance::new(token, self.provider.clone()); - - let asset_fut = vault.asset(); - let decimals_fut = vault_erc20.decimals(); - let (asset_result, decimals_result) = tokio::time::timeout(timeout, async { - tokio::join!(asset_fut.call(), decimals_fut.call()) - }) - .await - .map_err(|_| { - PriceEstimationError::EstimatorInternal(anyhow::anyhow!( - "timeout during asset()/decimals() on {token}" - )) - })?; - - let asset: Address = match asset_result { - Ok(addr) => addr, - Err(e) => { - { - let mut cache = self.non_vault_tokens.lock().unwrap(); - cache.insert(token); - metrics::non_vault_cache_size(cache.len()); + ) -> Result<(Address, u8), PriceEstimationError> { + let vault = IERC4626::IERC4626::new(token, &self.provider); + let vault_erc20 = ERC20::ERC20::new(token, &self.provider); + let asset_call = vault.asset(); + let decimals_call = vault_erc20.decimals(); + let (asset_result, decimals_result) = tokio::join!(asset_call.call(), decimals_call.call()); + let asset = match asset_result { + Ok(asset) => asset, + Err(err) => { + if decimals_result.is_ok() { + self.non_vault_tokens.insert(token); + metrics::non_vault_cache_size(self.non_vault_tokens.len()); } return Err(PriceEstimationError::EstimatorInternal(anyhow::anyhow!( - "failed to call asset() on {token}: {e}" + "failed to call asset() on {token}: {err}" ))); } }; - - let vault_decimals: u8 = decimals_result.map_err(|e| { - PriceEstimationError::EstimatorInternal(anyhow::anyhow!( - "failed to call decimals() on {token}: {e}" - )) - })?; - - let one_token = U256::from(10u64).pow(U256::from(vault_decimals)); - let asset_erc20 = ERC20::Instance::new(asset, self.provider.clone()); - let convert_fut = vault.convertToAssets(one_token); - let asset_decimals_fut = asset_erc20.decimals(); - let remaining = deadline.saturating_duration_since(Instant::now()); - let (convert_result, asset_decimals_result) = tokio::time::timeout(remaining, async { - tokio::join!(convert_fut.call(), asset_decimals_fut.call()) - }) - .await - .map_err(|_| { - PriceEstimationError::EstimatorInternal(anyhow::anyhow!( - "timeout during convertToAssets()/asset decimals() on {token}" - )) - })?; - - let assets: U256 = convert_result.map_err(|e| { + // EIP-4626 vaults implement ERC-20, so decimals() must succeed if asset() did. + let vault_decimals = decimals_result.map_err(|err| { PriceEstimationError::EstimatorInternal(anyhow::anyhow!( - "failed to call convertToAssets() on {token}: {e}" + "failed to call decimals() on {token}: {err}" )) })?; + Ok((asset, vault_decimals)) + } - let asset_decimals: u8 = asset_decimals_result.map_err(|e| { + /// Queries `convertToAssets(10^vault_decimals)` and the asset's decimals. + async fn fetch_conversion_data( + &self, + token: Address, + asset: Address, + vault_decimals: u8, + ) -> Result<(U256, u8), PriceEstimationError> { + let one_token = U256::from(10u64) + .checked_pow(U256::from(vault_decimals)) + .ok_or_else(|| { + PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "vault decimals {vault_decimals} for {token} cause U256 overflow" + )) + })?; + + let vault = IERC4626::IERC4626::new(token, &self.provider); + let asset_erc20 = ERC20::ERC20::new(asset, &self.provider); + let convert_call = vault.convertToAssets(one_token); + let asset_decimals_call = asset_erc20.decimals(); + tokio::try_join!(convert_call.call(), asset_decimals_call.call()).map_err(|err| { PriceEstimationError::EstimatorInternal(anyhow::anyhow!( - "failed to call decimals() on underlying asset {asset}: {e}" + "failed to call convertToAssets()/decimals() on {token}: {err}" )) - })?; + }) + } + /// Fetches the underlying asset address and the shares-to-assets + /// conversion rate from on-chain vault calls. On `asset()` revert the + /// token is added to the negative cache. Transient errors (transport + /// failures) are not cached. + async fn calculate_conversion_rate( + &self, + token: Address, + ) -> Result<(Address, f64), PriceEstimationError> { + let (asset, vault_decimals) = self.fetch_vault_info(token).await?; + let (assets, asset_decimals) = self + .fetch_conversion_data(token, asset, vault_decimals) + .await?; let rate = conversion_rate(assets, asset_decimals) .context("conversion rate is not representable as f64") .map_err(PriceEstimationError::EstimatorInternal)?; - Ok((asset, rate)) } } @@ -250,28 +246,28 @@ mod tests { } #[tokio::test] - async fn non_vault_tokens_are_cached() { - let inner = MockNativePriceEstimating::new(); - let non_vault_tokens = Mutex::new(HashSet::new()); + async fn non_vault_tokens_delegate_to_inner() { + let mut inner = MockNativePriceEstimating::new(); let token = Address::repeat_byte(0x42); + let expected_price = 1.5; + inner + .expect_estimate_native_price() + .withf(move |t, _| *t == token) + .returning(move |_, _| Box::pin(async move { Ok(expected_price) })); - // Pre-populate the negative cache. - non_vault_tokens.lock().unwrap().insert(token); + let non_vault_tokens = DashSet::new(); + non_vault_tokens.insert(token); let estimator = Eip4626 { - inner: Arc::new(inner), + inner: Box::new(inner), // The provider is never reached because the cache short-circuits. provider: ethrpc::Web3::new_from_url("http://localhost:1").provider, non_vault_tokens, }; - // The estimate should fail immediately without making any RPC calls - // (the mock inner has no expectations set, so any call would panic). let result = estimator .estimate(token, HEALTHY_PRICE_ESTIMATION_TIME) .await; - assert!(result.is_err()); - let err = result.unwrap_err().to_string(); - assert!(err.contains("not an EIP-4626 vault (cached)"), "{err}"); + assert_eq!(result.unwrap(), expected_price); } } From 52974d0b4ab317de332e8f94c9108dc98f68b8e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:39:53 +0100 Subject: [PATCH 15/20] Support nested EIP-4626 vaults and pre-seed WETH as non-vault Refactor the estimate method to iteratively unwrap vault layers, accumulating the conversion rate, instead of handling only a single vault layer. Extract `unwrap_vault_layer` for clarity. Pre-seed WETH in the non-vault token set to avoid unnecessary RPC calls. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/price-estimation/src/factory.rs | 5 +- crates/price-estimation/src/native/eip4626.rs | 97 +++++++++++-------- 2 files changed, 61 insertions(+), 41 deletions(-) diff --git a/crates/price-estimation/src/factory.rs b/crates/price-estimation/src/factory.rs index 6dc1e9c971..c676994f4e 100644 --- a/crates/price-estimation/src/factory.rs +++ b/crates/price-estimation/src/factory.rs @@ -1,7 +1,6 @@ use { super::{ - NativePriceEstimator as NativePriceEstimatorSource, - PriceEstimating, + NativePriceEstimator as NativePriceEstimatorSource, PriceEstimating, competition::CompetitionEstimator, external::ExternalPriceEstimator, instrumented::InstrumentedPriceEstimator, @@ -412,7 +411,7 @@ impl<'a> PriceEstimatorFactory<'a> { .expect("failed to build native price estimator"); let inner = if eip4626 { Box::new(InstrumentedPriceEstimator::new( - native::Eip4626::new(inner, self.network.web3.provider.clone()), + native::Eip4626::new(inner, self.network.web3.provider.clone(), *weth.address()), "Eip4626".to_string(), )) } else { diff --git a/crates/price-estimation/src/native/eip4626.rs b/crates/price-estimation/src/native/eip4626.rs index 18b73ea44d..4960ce1ddc 100644 --- a/crates/price-estimation/src/native/eip4626.rs +++ b/crates/price-estimation/src/native/eip4626.rs @@ -33,57 +33,78 @@ pub struct Eip4626 { } impl Eip4626 { - pub fn new(inner: Box, provider: AlloyProvider) -> Self { + pub fn new( + inner: Box, + provider: AlloyProvider, + weth: Address, + ) -> Self { Self { inner, provider, - non_vault_tokens: DashSet::new(), + non_vault_tokens: { + let non_vault_tokens = DashSet::new(); + non_vault_tokens.insert(weth); + non_vault_tokens + }, } } async fn estimate(&self, token: Address, timeout: Duration) -> NativePriceEstimateResult { - // Known non-vault or no time budget for vault discovery: delegate - // directly. A zero timeout is used by callers (e.g. the autopilot's - // WETH price fetch) as a "best-effort / use cached data" signal — the - // inner estimator and its callers treat it as advisory, not as a hard - // cutoff. We must not feed it into `tokio::time::timeout` which would - // fire immediately. - if self.non_vault_tokens.contains(&token) || timeout.is_zero() { + // Known non-vault or zero timeout: delegate directly. A zero timeout is + // used by callers (e.g. the autopilot's WETH price fetch) as a + // "best-effort / use cached data" signal — the inner estimator and its + // callers treat it as advisory, not as a hard cutoff. + if self.non_vault_tokens.contains(&token) { return self.inner.estimate_native_price(token, timeout).await; } let deadline = Instant::now() + timeout; - let check_timeout = |token: Address| -> Result { - let remaining = deadline.saturating_duration_since(Instant::now()); - if remaining.is_zero() { - return Err(PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + let time_remaining = || deadline.saturating_duration_since(Instant::now()); + + // Iteratively unwrap vault layers, accumulating the conversion rate. + let mut current_token = token; + let mut cumulative_rate = 1.0; + + while let Some((asset, rate)) = self + .unwrap_vault_layer(current_token, time_remaining()) + .await? + { + cumulative_rate *= rate; + current_token = asset; + } + + let asset_price = self + .inner + .estimate_native_price(current_token, time_remaining()) + .await?; + Ok(asset_price * cumulative_rate) + } + + /// Returns `Ok(Some((asset, rate)))` if `token` is a vault, `Ok(None)` if + /// it is known not to be a vault, or `Err` on a real RPC/computation + /// failure. + async fn unwrap_vault_layer( + &self, + token: Address, + timeout: Duration, + ) -> Result, PriceEstimationError> { + if self.non_vault_tokens.contains(&token) { + return Ok(None); + } + + let result = tokio::time::timeout(timeout, self.calculate_conversion_rate(token)) + .await + .map_err(|_| { + PriceEstimationError::EstimatorInternal(anyhow::anyhow!( "timeout exceeded during vault RPC calls for {token}" - ))); - } - Ok(remaining) - }; + )) + })?; - // Reserve at least half the budget for the inner estimator fallback. - let vault_budget = timeout / 2; - let conversion_rate_result = - match tokio::time::timeout(vault_budget, self.calculate_conversion_rate(token)).await { - Ok(res) => res, - Err(_timeout) => Err(PriceEstimationError::EstimatorInternal(anyhow::anyhow!( - "timeout during vault RPC calls for {token}" - ))), - }; - match conversion_rate_result { - Ok((asset, rate)) => { - let remaining = check_timeout(token)?; - let asset_price = self.estimate_native_price(asset, remaining).await?; - Ok(asset_price * rate) - } - // Vault RPC failed — if the token was just cached as non-vault, - // fall through to the inner estimator with remaining budget. - Err(_) if self.non_vault_tokens.contains(&token) => { - let remaining = check_timeout(token)?; - self.inner.estimate_native_price(token, remaining).await - } + match result { + Ok(result) => Ok(Some(result)), + // calculate_conversion_rate → fetch_vault_info adds the token to + // non_vault_tokens when asset() reverts but decimals() succeeds. + Err(_) if self.non_vault_tokens.contains(&token) => Ok(None), Err(e) => Err(e), } } From 5258bf5956a6f884f62030fab44cbab3f4a2d1b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:50:07 +0100 Subject: [PATCH 16/20] minimize diff --- .cargo/config.toml | 7 ++ crates/configs/src/native_price_estimators.rs | 88 +++---------------- .../e2e/src/setup/onchain_components/mod.rs | 13 +-- crates/price-estimation/Cargo.toml | 2 +- crates/price-estimation/src/lib.rs | 75 ++++++++++++++++ 5 files changed, 94 insertions(+), 91 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000000..7419737335 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,7 @@ +[target.x86_64-unknown-linux-gnu] +linker = "clang" +rustflags = ["-C", "link-arg=-fuse-ld=mold"] + +[target.aarch64-unknown-linux-gnu] +linker = "clang" +rustflags = ["-C", "link-arg=-fuse-ld=mold"] diff --git a/crates/configs/src/native_price_estimators.rs b/crates/configs/src/native_price_estimators.rs index fb244d14ee..52577ef818 100644 --- a/crates/configs/src/native_price_estimators.rs +++ b/crates/configs/src/native_price_estimators.rs @@ -22,15 +22,17 @@ impl<'de> Deserialize<'de> for NativePriceEstimators { &"expected native price estimator stages to be configured", )); } - for (n, stage) in estimators.iter().enumerate() { - if stage.is_empty() { - return Err(serde::de::Error::invalid_length( - 0, - &format!("stage {} is empty, all stages must not be empty", n).as_str(), - )); - } + match estimators + .iter() + .enumerate() + .find_map(|(n, stage)| stage.is_empty().then_some(n)) + { + Some(n) => Err(serde::de::Error::invalid_length( + 0, + &format!("stage {} is empty, all stages must not be empty", n).as_str(), + )), + None => Ok(Self(estimators)), } - Ok(Self(estimators)) } } @@ -116,73 +118,3 @@ impl Display for NativePriceEstimator { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[derive(Debug, Deserialize)] - struct Helper { - estimators: NativePriceEstimators, - } - - #[test] - fn toml_deserialize_estimators_empty() { - #[derive(Deserialize)] - struct H { - _estimators: NativePriceEstimators, - } - - assert!(toml::from_str::("estimators = []").is_err()); - assert!(toml::from_str::("estimators = [[]]").is_err()); - } - - #[test] - fn toml_deserialize_estimators_single_stage() { - let toml = r#" - estimators = [[{type = "CoinGecko"}, {type = "OneInchSpotPriceApi"}]] - "#; - - let parsed: Helper = toml::from_str(toml).unwrap(); - assert_eq!( - parsed.estimators.as_slice(), - vec![vec![ - NativePriceEstimator::CoinGecko, - NativePriceEstimator::OneInchSpotPriceApi, - ]] - ); - } - - #[test] - fn toml_deserialize_estimators_multiple_stages() { - let toml = r#" - estimators = [ - [{type = "CoinGecko"}, {type = "Driver", name = "solver1", url = "http://localhost:8080"}], - [{type = "Forwarder", url = "http://localhost:12088"}], - ] - "#; - - let parsed: Helper = toml::from_str(toml).unwrap(); - assert_eq!( - parsed.estimators.as_slice(), - vec![ - vec![ - NativePriceEstimator::CoinGecko, - NativePriceEstimator::Driver(ExternalSolver { - name: "solver1".to_string(), - url: "http://localhost:8080".parse().unwrap(), - }), - ], - vec![NativePriceEstimator::Forwarder { - url: "http://localhost:12088".parse().unwrap(), - }], - ] - ); - } - - #[test] - fn toml_deserialize_estimators_default() { - let estimators = NativePriceEstimators::default(); - assert!(estimators.as_slice().is_empty()); - } -} diff --git a/crates/e2e/src/setup/onchain_components/mod.rs b/crates/e2e/src/setup/onchain_components/mod.rs index 6cc23f503c..ae6b2e359b 100644 --- a/crates/e2e/src/setup/onchain_components/mod.rs +++ b/crates/e2e/src/setup/onchain_components/mod.rs @@ -12,8 +12,7 @@ use { }, app_data::Hook, contracts::{ - ERC20Mintable, - GPv2AllowListAuthentication::GPv2AllowListAuthentication, + ERC20Mintable, GPv2AllowListAuthentication::GPv2AllowListAuthentication, test::CowProtocolToken, }, ethrpc::alloy::{CallBuilderExt, ProviderSignerExt}, @@ -100,16 +99,6 @@ pub struct MintableToken { } impl MintableToken { - /// Wraps an existing on-chain contract whose `mint(address,uint256)` - /// selector matches `ERC20Mintable`. Useful for test contracts like - /// `MockERC4626Wrapper` that are ABI-compatible. - pub fn at(address: Address, minter: Address, provider: ethrpc::AlloyProvider) -> Self { - Self { - contract: ERC20Mintable::Instance::new(address, provider), - minter, - } - } - pub async fn mint(&self, to: Address, amount: U256) { self.contract .mint(to, amount) diff --git a/crates/price-estimation/Cargo.toml b/crates/price-estimation/Cargo.toml index 106ef602fc..42f08ca145 100644 --- a/crates/price-estimation/Cargo.toml +++ b/crates/price-estimation/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" license = "MIT OR Apache-2.0" [dependencies] -alloy = { workspace = true, features = ["contract", "providers", "rpc", "rpc-types", "sol-types"] } +alloy = { workspace = true, features = ["providers", "rpc", "rpc-types", "sol-types"] } anyhow = { workspace = true } app-data = { workspace = true } arc-swap = { workspace = true } diff --git a/crates/price-estimation/src/lib.rs b/crates/price-estimation/src/lib.rs index b00bc34b1f..0ad735845f 100644 --- a/crates/price-estimation/src/lib.rs +++ b/crates/price-estimation/src/lib.rs @@ -267,3 +267,78 @@ pub mod mocks { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn toml_deserialize_estimators_empty() { + #[derive(Deserialize)] + struct Helper { + _estimators: NativePriceEstimators, + } + + assert!(toml::from_str::("estimators = []").is_err()); + assert!(toml::from_str::("estimators = [[]]").is_err()); + } + + #[test] + fn toml_deserialize_estimators_single_stage() { + let toml = r#" + estimators = [[{type = "CoinGecko"}, {type = "OneInchSpotPriceApi"}]] + "#; + + #[derive(Deserialize)] + struct Helper { + estimators: NativePriceEstimators, + } + + let parsed: Helper = toml::from_str(toml).unwrap(); + assert_eq!( + parsed.estimators.as_slice(), + vec![vec![ + NativePriceEstimator::CoinGecko, + NativePriceEstimator::OneInchSpotPriceApi, + ]] + ); + } + + #[test] + fn toml_deserialize_estimators_multiple_stages() { + let toml = r#" + estimators = [ + [{type = "CoinGecko"}, {type = "Driver", name = "solver1", url = "http://localhost:8080"}], + [{type = "Forwarder", url = "http://localhost:12088"}], + ] + "#; + + #[derive(Deserialize)] + struct Helper { + estimators: NativePriceEstimators, + } + + let parsed: Helper = toml::from_str(toml).unwrap(); + assert_eq!( + parsed.estimators.as_slice(), + vec![ + vec![ + NativePriceEstimator::CoinGecko, + NativePriceEstimator::Driver(ExternalSolver { + name: "solver1".to_string(), + url: "http://localhost:8080".parse().unwrap(), + }), + ], + vec![NativePriceEstimator::Forwarder { + url: "http://localhost:12088".parse().unwrap(), + }], + ] + ); + } + + #[test] + fn toml_deserialize_estimators_default() { + let estimators = NativePriceEstimators::default(); + assert!(estimators.as_slice().is_empty()); + } +} From 4eb73f4c36b5802ff9ee08b0795f3a94e39c63d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:50:47 +0100 Subject: [PATCH 17/20] wrong config --- .cargo/config.toml | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 7419737335..0000000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,7 +0,0 @@ -[target.x86_64-unknown-linux-gnu] -linker = "clang" -rustflags = ["-C", "link-arg=-fuse-ld=mold"] - -[target.aarch64-unknown-linux-gnu] -linker = "clang" -rustflags = ["-C", "link-arg=-fuse-ld=mold"] From e492b4701ce3095785578fbb793816e9ead5e816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:00:32 +0100 Subject: [PATCH 18/20] Address comments --- crates/autopilot/src/solvable_orders.rs | 27 +-- .../e2e/src/setup/onchain_components/mod.rs | 3 +- crates/price-estimation/src/factory.rs | 3 +- crates/price-estimation/src/native/eip4626.rs | 161 ++++++++---------- 4 files changed, 85 insertions(+), 109 deletions(-) diff --git a/crates/autopilot/src/solvable_orders.rs b/crates/autopilot/src/solvable_orders.rs index ee1c0cd603..dd3007b7e1 100644 --- a/crates/autopilot/src/solvable_orders.rs +++ b/crates/autopilot/src/solvable_orders.rs @@ -28,14 +28,11 @@ use { signature::Signature, time::now_in_epoch_seconds, }, - price_estimation::{ - native::{NativePriceEstimating, to_normalized_price}, - native_price_cache::NativePriceUpdater, - }, + price_estimation::{native::to_normalized_price, native_price_cache::NativePriceUpdater}, prometheus::{Histogram, HistogramVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec}, shared::remaining_amounts, std::{ - collections::{BTreeMap, HashMap, HashSet, btree_map::Entry}, + collections::{BTreeMap, HashMap, HashSet}, future::Future, sync::Arc, time::{Duration, Instant}, @@ -292,21 +289,11 @@ impl SolvableOrdersCache { ) .await; tracing::trace!("fetched native prices for solvable orders"); - // Add WETH price if it's not already there to support ETH wrap when required. - if let Entry::Vacant(entry) = prices.entry(self.weth) { - let weth_price = self - .timed_future( - "weth_price_fetch", - self.native_price_estimator - .estimate_native_price(self.weth, Default::default()), - ) - .await - .expect("weth price fetching can never fail"); - let weth_price = to_normalized_price(weth_price) - .expect("weth price can never be outside of U256 range"); - - entry.insert(weth_price); - } + // WETH's native price is 1 by definition — insert it directly to + // support ETH wrap when required. + prices + .entry(self.weth) + .or_insert_with(|| to_normalized_price(1.0).unwrap()); Metrics::track_filtered_orders(MissingNativePrice, &removed); filtered_order_events.extend(removed.into_iter().map(|uid| (uid, MissingNativePrice))); diff --git a/crates/e2e/src/setup/onchain_components/mod.rs b/crates/e2e/src/setup/onchain_components/mod.rs index ae6b2e359b..3a9ebc53a8 100644 --- a/crates/e2e/src/setup/onchain_components/mod.rs +++ b/crates/e2e/src/setup/onchain_components/mod.rs @@ -12,7 +12,8 @@ use { }, app_data::Hook, contracts::{ - ERC20Mintable, GPv2AllowListAuthentication::GPv2AllowListAuthentication, + ERC20Mintable, + GPv2AllowListAuthentication::GPv2AllowListAuthentication, test::CowProtocolToken, }, ethrpc::alloy::{CallBuilderExt, ProviderSignerExt}, diff --git a/crates/price-estimation/src/factory.rs b/crates/price-estimation/src/factory.rs index c676994f4e..8800f47517 100644 --- a/crates/price-estimation/src/factory.rs +++ b/crates/price-estimation/src/factory.rs @@ -1,6 +1,7 @@ use { super::{ - NativePriceEstimator as NativePriceEstimatorSource, PriceEstimating, + NativePriceEstimator as NativePriceEstimatorSource, + PriceEstimating, competition::CompetitionEstimator, external::ExternalPriceEstimator, instrumented::InstrumentedPriceEstimator, diff --git a/crates/price-estimation/src/native/eip4626.rs b/crates/price-estimation/src/native/eip4626.rs index 4960ce1ddc..3d889a596e 100644 --- a/crates/price-estimation/src/native/eip4626.rs +++ b/crates/price-estimation/src/native/eip4626.rs @@ -5,7 +5,7 @@ use { anyhow::Context, contracts::{ERC20, IERC4626}, dashmap::DashSet, - ethrpc::AlloyProvider, + ethrpc::{AlloyProvider, alloy::errors::ContractErrorExt}, futures::{FutureExt, future::BoxFuture}, num::{BigInt, BigRational, ToPrimitive}, number::conversions::u256_to_big_rational, @@ -38,111 +38,116 @@ impl Eip4626 { provider: AlloyProvider, weth: Address, ) -> Self { + let non_vault_tokens = DashSet::new(); + non_vault_tokens.insert(weth); Self { inner, provider, - non_vault_tokens: { - let non_vault_tokens = DashSet::new(); - non_vault_tokens.insert(weth); - non_vault_tokens - }, + non_vault_tokens, } } async fn estimate(&self, token: Address, timeout: Duration) -> NativePriceEstimateResult { - // Known non-vault or zero timeout: delegate directly. A zero timeout is - // used by callers (e.g. the autopilot's WETH price fetch) as a - // "best-effort / use cached data" signal — the inner estimator and its - // callers treat it as advisory, not as a hard cutoff. - if self.non_vault_tokens.contains(&token) { - return self.inner.estimate_native_price(token, timeout).await; - } - let deadline = Instant::now() + timeout; - let time_remaining = || deadline.saturating_duration_since(Instant::now()); + let (underlying, cumulative_rate) = + tokio::time::timeout(timeout, self.unwrap_all_layers(token)) + .await + .map_err(|_| { + PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "timeout while unwrapping EIP-4626 layers for {token}" + )) + })??; - // Iteratively unwrap vault layers, accumulating the conversion rate. + let remaining = deadline.saturating_duration_since(Instant::now()); + let asset_price = self + .inner + .estimate_native_price(underlying, remaining) + .await?; + Ok(asset_price * cumulative_rate) + } + + /// Follows the vault chain (e.g. vault → vault → asset) until reaching a + /// non-vault token, returning the terminal token and the cumulative + /// shares-to-assets rate. + async fn unwrap_all_layers( + &self, + token: Address, + ) -> Result<(Address, f64), PriceEstimationError> { let mut current_token = token; let mut cumulative_rate = 1.0; - - while let Some((asset, rate)) = self - .unwrap_vault_layer(current_token, time_remaining()) - .await? - { + while let Some((asset, rate)) = self.unwrap_vault_layer(current_token).await? { cumulative_rate *= rate; current_token = asset; } - - let asset_price = self - .inner - .estimate_native_price(current_token, time_remaining()) - .await?; - Ok(asset_price * cumulative_rate) + Ok((current_token, cumulative_rate)) } - /// Returns `Ok(Some((asset, rate)))` if `token` is a vault, `Ok(None)` if - /// it is known not to be a vault, or `Err` on a real RPC/computation - /// failure. + /// Returns: + /// - `Ok(Some((asset, rate)))` when `token` is a vault. + /// - `Ok(None)` when it's a plain ERC-20. + /// - `Err` on RPC/computation failures that don't let us classify the + /// token. async fn unwrap_vault_layer( &self, token: Address, - timeout: Duration, ) -> Result, PriceEstimationError> { if self.non_vault_tokens.contains(&token) { return Ok(None); } - let result = tokio::time::timeout(timeout, self.calculate_conversion_rate(token)) - .await - .map_err(|_| { - PriceEstimationError::EstimatorInternal(anyhow::anyhow!( - "timeout exceeded during vault RPC calls for {token}" - )) - })?; - - match result { - Ok(result) => Ok(Some(result)), - // calculate_conversion_rate → fetch_vault_info adds the token to - // non_vault_tokens when asset() reverts but decimals() succeeds. - Err(_) if self.non_vault_tokens.contains(&token) => Ok(None), - Err(e) => Err(e), - } + let Some((asset, vault_decimals)) = self.fetch_vault_info(token).await? else { + self.non_vault_tokens.insert(token); + metrics::non_vault_cache_size(self.non_vault_tokens.len()); + return Ok(None); + }; + let (assets, asset_decimals) = self + .fetch_conversion_data(token, asset, vault_decimals) + .await?; + let rate = conversion_rate(assets, asset_decimals) + .context("conversion rate is not representable as f64") + .map_err(PriceEstimationError::EstimatorInternal)?; + Ok(Some((asset, rate))) } - /// Fetches the vault's underlying asset address and decimals. - /// On `asset()` revert the token is added to the negative cache if - /// `decimals()` succeeded (i.e. it's a valid ERC-20 but not a vault). + /// Fetches the vault's underlying asset address and vault token decimals. + /// + /// Returns: + /// - `Ok(Some(...))` when `token` is a vault. + /// - `Ok(None)` when `asset()` reverts (indicating it is a regular ERC-20). + /// - `Err` on transient transport failures — those are *not* cached as + /// non-vault. async fn fetch_vault_info( &self, token: Address, - ) -> Result<(Address, u8), PriceEstimationError> { + ) -> Result, PriceEstimationError> { let vault = IERC4626::IERC4626::new(token, &self.provider); let vault_erc20 = ERC20::ERC20::new(token, &self.provider); let asset_call = vault.asset(); let decimals_call = vault_erc20.decimals(); let (asset_result, decimals_result) = tokio::join!(asset_call.call(), decimals_call.call()); - let asset = match asset_result { - Ok(asset) => asset, - Err(err) => { - if decimals_result.is_ok() { - self.non_vault_tokens.insert(token); - metrics::non_vault_cache_size(self.non_vault_tokens.len()); - } - return Err(PriceEstimationError::EstimatorInternal(anyhow::anyhow!( - "failed to call asset() on {token}: {err}" - ))); + + match asset_result { + Ok(asset) => { + // EIP-4626 vaults implement ERC-20 so decimals() must succeed too. + let vault_decimals = decimals_result.map_err(|err| { + PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "failed to call decimals() on {token}: {err}" + )) + })?; + Ok(Some((asset, vault_decimals))) } - }; - // EIP-4626 vaults implement ERC-20, so decimals() must succeed if asset() did. - let vault_decimals = decimals_result.map_err(|err| { - PriceEstimationError::EstimatorInternal(anyhow::anyhow!( - "failed to call decimals() on {token}: {err}" - )) - })?; - Ok((asset, vault_decimals)) + // `asset()` reverted but the contract is a valid ERC-20 (decimals() + // succeeded). Classify as non-vault. + Err(err) if err.is_contract_error() && decimals_result.is_ok() => Ok(None), + Err(err) => Err(PriceEstimationError::EstimatorInternal(anyhow::anyhow!( + "failed to call asset() on {token}: {err}" + ))), + } } - /// Queries `convertToAssets(10^vault_decimals)` and the asset's decimals. + /// Fetches `convertToAssets(10^vault_decimals)` — how many atomic units of + /// the underlying asset correspond to one full vault token — and the + /// asset's decimals. async fn fetch_conversion_data( &self, token: Address, @@ -167,24 +172,6 @@ impl Eip4626 { )) }) } - - /// Fetches the underlying asset address and the shares-to-assets - /// conversion rate from on-chain vault calls. On `asset()` revert the - /// token is added to the negative cache. Transient errors (transport - /// failures) are not cached. - async fn calculate_conversion_rate( - &self, - token: Address, - ) -> Result<(Address, f64), PriceEstimationError> { - let (asset, vault_decimals) = self.fetch_vault_info(token).await?; - let (assets, asset_decimals) = self - .fetch_conversion_data(token, asset, vault_decimals) - .await?; - let rate = conversion_rate(assets, asset_decimals) - .context("conversion rate is not representable as f64") - .map_err(PriceEstimationError::EstimatorInternal)?; - Ok((asset, rate)) - } } impl NativePriceEstimating for Eip4626 { From 54f64d7e171a63d816dd1ce5a69036bd1b2532a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:32:55 +0100 Subject: [PATCH 19/20] missing detail --- crates/price-estimation/src/factory.rs | 5 ++--- crates/price-estimation/src/native/eip4626.rs | 10 ++-------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/crates/price-estimation/src/factory.rs b/crates/price-estimation/src/factory.rs index 8800f47517..81189ac2da 100644 --- a/crates/price-estimation/src/factory.rs +++ b/crates/price-estimation/src/factory.rs @@ -1,7 +1,6 @@ use { super::{ - NativePriceEstimator as NativePriceEstimatorSource, - PriceEstimating, + NativePriceEstimator as NativePriceEstimatorSource, PriceEstimating, competition::CompetitionEstimator, external::ExternalPriceEstimator, instrumented::InstrumentedPriceEstimator, @@ -412,7 +411,7 @@ impl<'a> PriceEstimatorFactory<'a> { .expect("failed to build native price estimator"); let inner = if eip4626 { Box::new(InstrumentedPriceEstimator::new( - native::Eip4626::new(inner, self.network.web3.provider.clone(), *weth.address()), + native::Eip4626::new(inner, self.network.web3.provider.clone()), "Eip4626".to_string(), )) } else { diff --git a/crates/price-estimation/src/native/eip4626.rs b/crates/price-estimation/src/native/eip4626.rs index 3d889a596e..7493294ad7 100644 --- a/crates/price-estimation/src/native/eip4626.rs +++ b/crates/price-estimation/src/native/eip4626.rs @@ -33,17 +33,11 @@ pub struct Eip4626 { } impl Eip4626 { - pub fn new( - inner: Box, - provider: AlloyProvider, - weth: Address, - ) -> Self { - let non_vault_tokens = DashSet::new(); - non_vault_tokens.insert(weth); + pub fn new(inner: Box, provider: AlloyProvider) -> Self { Self { inner, provider, - non_vault_tokens, + non_vault_tokens: DashSet::new(), } } From 9ec32bf29401ffa06bcfa7e987758ba4d14547a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:36:39 +0100 Subject: [PATCH 20/20] fmt --- crates/price-estimation/src/factory.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/price-estimation/src/factory.rs b/crates/price-estimation/src/factory.rs index 81189ac2da..6dc1e9c971 100644 --- a/crates/price-estimation/src/factory.rs +++ b/crates/price-estimation/src/factory.rs @@ -1,6 +1,7 @@ use { super::{ - NativePriceEstimator as NativePriceEstimatorSource, PriceEstimating, + NativePriceEstimator as NativePriceEstimatorSource, + PriceEstimating, competition::CompetitionEstimator, external::ExternalPriceEstimator, instrumented::InstrumentedPriceEstimator,