Skip to content

feat: add metric rfq integration#1000

Open
zach030 wants to merge 18 commits into
mainfrom
feat/metric-rfq
Open

feat: add metric rfq integration#1000
zach030 wants to merge 18 commits into
mainfrom
feat/metric-rfq

Conversation

@zach030
Copy link
Copy Markdown
Contributor

@zach030 zach030 commented May 8, 2026

No description provided.

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

Comment thread crates/tycho-execution/contracts/src/executors/MetricExecutor.sol Outdated
tokens: HashSet::new(),
tvl: 0.0,
quote_tokens: None,
base_url: "http://54.199.103.16:8080".to_string(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this URL?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is metric API endpoint

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this configurable somehow? If they change this URL - which is not unlikely, we'd need to release another version and users won't be able to fix until we act

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is configurable now: users can override it with .base_url(...) function, and I also added env config for the default URL.

Comment thread crates/tycho-execution/contracts/src/executors/MetricExecutor.sol
let (amount_out_human, max_output) = match direction {
MetricDirection::ZeroForOne => {
let price = self.bid_ask.bid_price()?;
(amount_in_human * price, self.bid_ask.total_token1_available()?)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no price impact depending on the amount_in?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went back to the docs and realized I had missed the depth field from the /bid_ask endpoint.

Metric exposes orderbook-like depth bins on both the bid and ask sides. The volume is cumulative across bins, and within each bin the price follows a linear segment instead of staying flat.

I updated the implementation to handle those bins properly, and added comments with the formulas used for calculations.

You can also refer to the Metric docs for the depth description [here].(https://oracle-based-omm.gitbook.io/metric/RSm94m71kqtGICv4iKRj/developers/api#depth)

let zero_for_one = zero_for_one(swap)?;
let price_limit_x64 =
if zero_for_one { Bytes::zero(32) } else { u256_bytes(&BigUint::from(u128::MAX)) };
let oracle_update = if self.request_oracle_update {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that, on Mainnet, the user will always pay for its transaction + the Oracle Update TX? If that's the case, can't we make this configurable?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, it would be nice to try to swap without the oracle update and, if the TX reverts because the oracle didn't update, send the oracle update. Not sure how much that would add in gas or if that's feasible

.unwrap_or_default()
.into(),
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let entries: [(&str, Vec<u8>); 9] = [
    ("bid_adj", bid_ask.bid_adj.as_bytes().to_vec()),
    ("ask_adj", bid_ask.ask_adj.as_bytes().to_vec()),
    ("total_token0_available", bid_ask.total_token0_available.as_bytes().to_vec()),
    ("total_token1_available", bid_ask.total_token1_available.as_bytes().to_vec()),
    ("latest_block", bid_ask.latest_block.to_string().into_bytes()),
    ("block_ts", bid_ask.block_ts.to_string().into_bytes()),
    ("server_ts", bid_ask.server_ts.to_string().into_bytes()),
    ("quote_expiration", bid_ask.quote_expiration.to_string().into_bytes()),
    ("depth", serde_json::to_vec(&bid_ask.depth).unwrap_or_default()),
];

for (key, bytes) in entries {
    attributes.insert(key.to_string(), bytes.into());
}

is easier to read IMO - what do u think?

async fn fetch_bid_ask(&self, pool: &Bytes) -> Result<MetricBidAskResponse, RFQError> {
let endpoint =
format!("{}/{}/bid_ask", self.chain_endpoint, bytes_to_address_string(pool)?);
let http_client = Client::new();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By recreating the client here every time, you need to always redo TLS handshake and that adds latency. If you create the client once and reuse it you will get a performance increase on the requests

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, now the client is cached with OnceLock, so we create it once and reuse it across requests.


#[async_trait]
impl RFQClient for MetricClient {
fn stream(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be async fn?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stream returns a BoxStream directly and does the async work inside the stream

Ok(biguint_to_u256(&value))
}

fn stable_decimals(address: &Bytes) -> Option<u8> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big fan of this - if the RFQ adds a new stable we'd need to release a new Tycho-Simulation version?
Can we use Tycho data here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, agreed. I changed this so we can pass token metadata in from tycho data via .token_metadata(...).

tokens: HashSet::new(),
tvl: 0.0,
quote_tokens: None,
base_url: "http://54.199.103.16:8080".to_string(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this configurable somehow? If they change this URL - which is not unlikely, we'd need to release another version and users won't be able to fix until we act

let res = GetAmountOutResult {
amount: capped_amount.clone(),
gas: BigUint::from(170_000u64),
new_state: self.clone_box(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we calculate the depth impact of this swap and update the new_state with it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, I’m not sure how a Metric swap should update the state, and the other RFQ sims also leave new_state unchanged.

fn encoder(oracle_address: &Bytes) -> MetricSwapEncoder {
MetricSwapEncoder::new(
Bytes::zero(20),
Chain::Ethereum,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should chain be hardcoded here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is hardcoded for testing, and the test tokens are also on mainnet, so I think it should be fine.

};
let candidate = if &pool.token0 == token && quote_tokens.contains(&pool.token1) {
let Some(quote_token) = token_metadata.get(&pool.token1) else {
continue;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't these else cases an error? Shouldn't we log something here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, I added some logs when there's no candidate.

@zach030 zach030 force-pushed the feat/metric-rfq branch from 0ebca5f to f4f2758 Compare June 2, 2026 03:06
Comment thread crates/tycho-execution/config/protocol_specific_addresses.json Outdated
Comment thread crates/tycho-execution/contracts/src/executors/MetricExecutor.sol Outdated
Comment thread crates/tycho-integration-test/src/main.rs Outdated
Comment thread crates/tycho-simulation/examples/rfq_quickstart/main.rs Outdated
#[serde(skip_serializing, default)]
secret_key: Option<String>,
#[serde(skip, default = "OnceLock::new")]
http_client: OnceLock<Client>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it a OnceLock?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to reuse the same Client across requests, address this comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we reuse it if it was just http_client: Client?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

3 participants