Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/autopilot/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,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,
&config.native_price_estimation.eip4626,
)
.instrument(info_span!("api_native_price_estimator"))
.await,
Expand All @@ -356,7 +356,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,
&config.native_price_estimation.eip4626,
)
.instrument(info_span!("competition_native_price_updater"))
.await;
Expand Down
35 changes: 26 additions & 9 deletions crates/configs/src/autopilot/native_price.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use {
crate::native_price_estimators::NativePriceEstimators,
crate::{native_price::Eip4626Config, native_price_estimators::NativePriceEstimators},
serde::Deserialize,
std::time::Duration,
};
Expand Down Expand Up @@ -46,15 +46,13 @@ 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,

/// EIP-4626 vault token pricing settings.
#[serde(default)]
pub eip4626: Eip4626Config,
}

#[cfg(any(test, feature = "test-util"))]
Expand All @@ -68,7 +66,7 @@ impl NativePriceConfig {
api_estimators: Default::default(),
cache_refresh_interval: default_native_price_cache_refresh(),
prefetch_time: Duration::from_millis(500),
eip4626: false,
eip4626: Default::default(),
shared: crate::native_price::NativePriceConfig {
cache: crate::native_price::CacheConfig {
max_age: Duration::from_secs(2),
Expand All @@ -82,7 +80,7 @@ impl NativePriceConfig {

#[cfg(test)]
mod tests {
use super::*;
use {super::*, alloy::primitives::address};

#[test]
fn deserialize_full() {
Expand All @@ -97,6 +95,25 @@ mod tests {
assert!(config.api_estimators.is_some());
assert_eq!(config.cache_refresh_interval, Duration::from_secs(30));
assert_eq!(config.prefetch_time, Duration::from_secs(120));
assert!(!config.eip4626.enabled);
assert!(config.eip4626.exemptions.is_empty());
}

#[test]
fn deserialize_eip4626() {
let toml = r#"
estimators = [[{type = "CoinGecko"}]]

[eip4626]
enabled = true
exemptions = ["0x0000000000000000000000000000000000000001"]
"#;
let config: NativePriceConfig = toml::from_str(toml).unwrap();
assert!(config.eip4626.enabled);
assert_eq!(
config.eip4626.exemptions,
[address!("0x0000000000000000000000000000000000000001")]
);
}

#[test]
Expand Down
18 changes: 18 additions & 0 deletions crates/configs/src/native_price.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ impl Default for CacheConfig {
}
}

/// Configuration for pricing EIP-4626 vault tokens by unwrapping them into
/// their underlying asset.
#[derive(Debug, Clone, Default, Deserialize)]
#[cfg_attr(any(test, feature = "test-util"), derive(serde::Serialize))]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Eip4626Config {
/// 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 enabled: bool,

/// Tokens that will not be priced as EIP-4626 — i.e. they'll be sent for
/// estimation without being unwrapped.
#[serde(default)]
pub exemptions: Vec<Address>,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
20 changes: 15 additions & 5 deletions crates/e2e/tests/e2e/eip4626.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use {
},
configs::{
autopilot::{Configuration, native_price::NativePriceConfig},
native_price::Eip4626Config,
native_price_estimators::{NativePriceEstimator, NativePriceEstimators},
test_util::TestDefault,
},
Expand Down Expand Up @@ -97,7 +98,10 @@ async fn eip4626_native_price_test(web3: Web3) {
let driver_url: url::Url = "http://localhost:11088/test_solver".parse().unwrap();
let autopilot_config = Configuration {
native_price_estimation: NativePriceConfig {
eip4626: true,
eip4626: Eip4626Config {
enabled: true,
..Default::default()
},
estimators: NativePriceEstimators::new(vec![vec![NativePriceEstimator::driver(
"test_quoter".to_string(),
driver_url,
Expand Down Expand Up @@ -187,7 +191,10 @@ async fn eip4626_recursive_native_price_test(web3: Web3) {
let driver_url: url::Url = "http://localhost:11088/test_solver".parse().unwrap();
let autopilot_config = Configuration {
native_price_estimation: NativePriceConfig {
eip4626: true,
eip4626: Eip4626Config {
enabled: true,
..Default::default()
},
estimators: NativePriceEstimators::new(vec![vec![NativePriceEstimator::driver(
"test_quoter".to_string(),
driver_url,
Expand Down Expand Up @@ -321,7 +328,10 @@ async fn eip4626_decimal_mismatch_native_price_test(web3: Web3) {
let driver_url: url::Url = "http://localhost:11088/test_solver".parse().unwrap();
let autopilot_config = Configuration {
native_price_estimation: NativePriceConfig {
eip4626: true,
eip4626: Eip4626Config {
enabled: true,
..Default::default()
},
estimators: NativePriceEstimators::new(vec![vec![NativePriceEstimator::driver(
"test_quoter".to_string(),
driver_url,
Expand Down Expand Up @@ -396,7 +406,7 @@ async fn eip4626_empty_revert_terminal_token_test(web3: Web3) {
// unchanged — any fixed value works.
let expected_price = 0.0001;
let inner = FixedPrice(expected_price);
let estimator = Eip4626::new(Box::new(inner), web3.provider);
let estimator = Eip4626::new(Box::new(inner), web3.provider, std::iter::empty());

for token in [BUY_ETH_ADDRESS, USDC, GNO] {
let price = estimator
Expand All @@ -421,7 +431,7 @@ async fn forked_node_mainnet_eip4626_partial_vault_terminal_token() {
async fn eip4626_partial_vault_terminal_token_test(web3: Web3) {
let expected_price = 0.0001;
let inner = FixedPrice(expected_price);
let estimator = Eip4626::new(Box::new(inner), web3.provider);
let estimator = Eip4626::new(Box::new(inner), web3.provider, std::iter::empty());

let price = estimator
.estimate_native_price(WMT_USDC, HEALTHY_PRICE_ESTIMATION_TIME)
Expand Down
2 changes: 1 addition & 1 deletion crates/price-estimation/src/config/native_price.rs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pub use configs::native_price::{CacheConfig, NativePriceConfig};
pub use configs::native_price::{CacheConfig, Eip4626Config, NativePriceConfig};
23 changes: 14 additions & 9 deletions crates/price-estimation/src/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ use {
ExternalSolver,
buffered::{self, BufferedRequest, NativePriceBatchFetching},
competition::PriceRanking,
config::{native_price::NativePriceConfig, price_estimation::BalanceOverridesConfigExt},
config::{
native_price::{Eip4626Config, NativePriceConfig},
price_estimation::BalanceOverridesConfigExt,
},
},
alloy::primitives::Address,
anyhow::{Context as _, Result},
Expand Down Expand Up @@ -404,9 +407,7 @@ impl<'a> PriceEstimatorFactory<'a> {
))
}

/// 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.
/// Creates a native price estimator from the given sources.
pub async fn native_price_estimator(
&mut self,
native: &[Vec<NativePriceEstimatorSource>],
Expand All @@ -432,23 +433,27 @@ 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.
/// If `eip4626` is enabled, the estimator is wrapped in a
/// [`native::Eip4626`] layer that transparently prices vault tokens.
pub async fn caching_native_price_estimator(
&mut self,
native: &[Vec<NativePriceEstimatorSource>],
results_required: NonZeroUsize,
weth: &WETH9::Instance,
cache: native_price_cache::Cache,
eip4626: bool,
eip4626: &Eip4626Config,
) -> 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 {
let inner = if eip4626.enabled {
Box::new(InstrumentedPriceEstimator::new(
native::Eip4626::new(inner, self.network.web3.provider.clone()),
native::Eip4626::new(
inner,
self.network.web3.provider.clone(),
eip4626.exemptions.iter().copied(),
),
"Eip4626".to_string(),
))
} else {
Expand Down
42 changes: 36 additions & 6 deletions crates/price-estimation/src/native/eip4626.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,25 @@ use {
pub struct Eip4626 {
inner: Box<dyn NativePriceEstimating>,
provider: AlloyProvider,
/// Addresses that are known *not* to be (usable) EIP-4626 vaults. Checked
/// before making any RPC calls.
/// Addresses that are known *not* to be (usable) EIP-4626 vaults, plus the
/// configured exemptions. Checked before making any RPC calls.
non_vault_tokens: DashSet<Address>,
}

impl Eip4626 {
pub fn new(inner: Box<dyn NativePriceEstimating>, provider: AlloyProvider) -> Self {
/// `exemptions` are tokens that must never be unwrapped, even if they are
/// valid vaults; they are seeded into the negative cache.
pub fn new(
inner: Box<dyn NativePriceEstimating>,
provider: AlloyProvider,
exemptions: impl IntoIterator<Item = Address>,
) -> Self {
Self {
inner,
provider,
// BUY_ETH_ADDRESS is not ERC-20, but it is a valid estimation address
// so we need to make sure it bypasses the EIP-4626 estimator
non_vault_tokens: DashSet::from_iter([BUY_ETH_ADDRESS]),
non_vault_tokens: exemptions.into_iter().chain([BUY_ETH_ADDRESS]).collect(),
Comment thread
jmg-duarte marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -353,7 +359,7 @@ mod tests {
asserter.push_failure_msg(Cow::from("calls are not being bypassed"));
let web3 = ethrpc::Web3::with_asserter(asserter);

let estimator = Eip4626::new(Box::new(inner), web3.provider);
let estimator = Eip4626::new(Box::new(inner), web3.provider, std::iter::empty());

let result = estimator
.estimate(token, HEALTHY_PRICE_ESTIMATION_TIME)
Expand All @@ -369,6 +375,30 @@ mod tests {
);
}

/// Exempted tokens are priced by the inner estimator directly, without
/// probing the chain for vault info.
#[tokio::test]
async fn exemptions_bypass_eth_calls() {
let token = Address::repeat_byte(0x42);
let expected_price = 1.5;
let mut inner = MockNativePriceEstimating::new();
inner
.expect_estimate_native_price()
.withf(move |t, _| *t == token)
.returning(move |_, _| Box::pin(async move { Ok(expected_price) }));

let asserter = Asserter::new();
asserter.push_failure_msg(Cow::from("calls are not being bypassed"));
let web3 = ethrpc::Web3::with_asserter(asserter);

let estimator = Eip4626::new(Box::new(inner), web3.provider, [token]);

let result = estimator
.estimate(token, HEALTHY_PRICE_ESTIMATION_TIME)
.await;
assert_eq!(result.unwrap(), expected_price);
}

#[tokio::test]
async fn non_vault_tokens_delegate_to_inner() {
let mut inner = MockNativePriceEstimating::new();
Expand Down Expand Up @@ -415,7 +445,7 @@ mod tests {
asserter.push_failure_msg("execution reverted");
let web3 = ethrpc::Web3::with_asserter(asserter);

let estimator = Eip4626::new(Box::new(inner), web3.provider);
let estimator = Eip4626::new(Box::new(inner), web3.provider, std::iter::empty());

let result = estimator
.estimate(token, HEALTHY_PRICE_ESTIMATION_TIME)
Expand Down
Loading