Skip to content

[codex] Improve Uniswap quote routing#1132

Draft
gemcoder21 wants to merge 1 commit into
mainfrom
codex/uniswap-quote-improvements
Draft

[codex] Improve Uniswap quote routing#1132
gemcoder21 wants to merge 1 commit into
mainfrom
codex/uniswap-quote-improvements

Conversation

@gemcoder21
Copy link
Copy Markdown
Contributor

Overview

Improves Uniswap v3/v4 quote quality while keeping routing single-route, exact-input, and limited to direct or two-hop paths.

Problem

Several quote and execution paths could return worse or inconsistent user quotes:

  • use_max_amount quotes did not consistently reserve native fees before quote-data construction.
  • v3/v4 command builders reapplied slippage even though route data already carried the executable minimum output.
  • two-hop routing only tried same-fee paths and route metadata only kept one fee tier.
  • v4 selected-route reconstruction could pick the wrong path by deriving it from batch indexes.
  • v3 output-native routes could send WETH to the user before attempting router-side unwrap.

Solution

  • Use quote_value_after_reserve_by_chain() in v3/v4 quote paths and carry quote.from_value through quote data, Permit2, native tx value, and approval checks.
  • Store executable min_amount_out in route data and use it directly in v3/v4 command builders.
  • Build per-hop route data with each hop's fee tier and generate mixed-fee two-hop Cartesian candidates.
  • Carry selected candidate paths alongside quote responses so v4 route reconstruction uses the actual winning path.
  • Add support for the 400 fee tier and propagate invalid-route errors instead of route unwraps.
  • Fix v3 output-native commands so WETH is routed to the router before UNWRAP_WETH.
  • Clean up redundant clones, allocations, and branch checks introduced during the implementation.

Validation

  • cargo test -p swapper uniswap --no-default-features
  • cargo test -p gem_evm uniswap
  • just test swapper
  • cargo clippy -p swapper -- -D warnings
  • cargo clippy -p gem_evm -- -D warnings
  • just format
  • git diff --check

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the Uniswap V3 and V4 swapper implementations to support mixed fee tiers in multi-hop routes and improves code quality through the use of iterators and functional patterns. Key changes include the introduction of new_two_hop_with_fees, a more robust route selection mechanism via get_selected_candidate, and updated command building logic for both V3 and V4. Feedback highlights a critical issue where the Universal Router's TRANSFER command is incorrectly used for native ETH fees, along with suggestions to replace unsafe unwrap() calls with proper error handling and to remove redundant type conversions in path building.

Comment on lines +34 to +40
let fee_command = if input_is_native {
// if input is native ETH, we can transfer directly
commands.push(UniversalRouterCommand::TRANSFER(Transfer {
UniversalRouterCommand::TRANSFER(Transfer {
token: *token_in,
recipient: fee_recipient,
value: U256::from(fee),
}));
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The Universal Router's TRANSFER command (opcode 0x05) is designed for ERC20 tokens and does not support native ETH. When input_is_native is true, token_in is Address::ZERO, and this command will likely fail. For native ETH fees, you should use PAY_PORTION with the special ETH sentinel address 0x0000000000000000000000000000000000000001. Since the fee is calculated using fee_options.bps, PAY_PORTION is the standard way to handle this in the Universal Router.

Suggested change
let fee_command = if input_is_native {
// if input is native ETH, we can transfer directly
commands.push(UniversalRouterCommand::TRANSFER(Transfer {
UniversalRouterCommand::TRANSFER(Transfer {
token: *token_in,
recipient: fee_recipient,
value: U256::from(fee),
}));
})
let fee_command = if input_is_native {
// if input is native ETH, we use PAY_PORTION with the ETH sentinel
UniversalRouterCommand::PAY_PORTION(PayPortion {
token: Address::from_str("0x0000000000000000000000000000000000000001").unwrap(),
recipient: fee_recipient,
bips: U256::from(fee_options.bps),
})
} else {

if pay_fees {
let swap_recipient = if unwrap_output_weth { address_this } else { recipient };
let swap_commands = if let Some(fee_options) = fee_options {
let fee_recipient = Address::from_str(fee_options.address.as_str()).unwrap();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using unwrap() on Address::from_str can cause the service to panic if an invalid fee address is provided in the request options. It is safer to handle this as an error using the existing eth_address::parse_str utility.

Suggested change
let fee_recipient = Address::from_str(fee_options.address.as_str()).unwrap();
let fee_recipient = eth_address::parse_str(fee_options.address.as_str())?;


if pay_fees {
let swap_commands = if let Some(fee_options) = fee_options {
let fee_recipient = Address::from_str(fee_options.address.as_str()).unwrap();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using unwrap() on Address::from_str can cause the service to panic if an invalid fee address is provided in the request options. It is safer to handle this as an error using the existing eth_address::parse_str utility.

Suggested change
let fee_recipient = Address::from_str(fee_options.address.as_str()).unwrap();
let fee_recipient = eth_address::parse_str(fee_options.address.as_str())?;

bytes.extend(&fee.to_be_bytes_vec());
bytes.extend(token_out.as_slice());
Bytes::from(bytes)
let fee = U24::from(fee_tier.as_u24()).to_be_bytes_vec();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

fee_tier.as_u24() already returns a U24 type. Wrapping it in U24::from() is redundant.

Suggested change
let fee = U24::from(fee_tier.as_u24()).to_be_bytes_vec();
let fee = fee_tier.as_u24().to_be_bytes_vec();

.iter()
.enumerate()
.flat_map(|(idx, token_pair)| {
let fee = U24::from(token_pair.fee_tier.as_u24()).to_be_bytes_vec();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

token_pair.fee_tier.as_u24() already returns a U24 type. Wrapping it in U24::from() is redundant.

Suggested change
let fee = U24::from(token_pair.fee_tier.as_u24()).to_be_bytes_vec();
let fee = token_pair.fee_tier.as_u24().to_be_bytes_vec();

@gemcoder21 gemcoder21 requested a review from 0xh3rman May 15, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant