Skip to content
Draft
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
75 changes: 35 additions & 40 deletions crates/gem_evm/src/uniswap/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,49 +52,44 @@ pub fn decode_action_data(data: &[u8]) -> Result<Vec<V4Action>, alloy_sol_types:
// The ABI encoding for a sequence of actions is (bytes opcodes, bytes[] action_data)
let (action_opcodes_bytes, action_data_bytes) = <(Bytes, Vec<Bytes>) as SolValue>::abi_decode_sequence(data)?;

let action_opcodes: Vec<u8> = action_opcodes_bytes.to_vec();
let action_data_list: Vec<Vec<u8>> = action_data_bytes.into_iter().map(|b| b.to_vec()).collect();

if action_opcodes.len() != action_data_list.len() {
if action_opcodes_bytes.len() != action_data_bytes.len() {
return Err(alloy_sol_types::Error::Other("Mismatched opcodes and data lengths".into()));
}

let mut decoded_actions = Vec::with_capacity(action_opcodes.len());

for (i, opcode) in action_opcodes.iter().enumerate() {
let action_data = &action_data_list[i];
let action_data_slice = action_data.as_slice();
let action = match *opcode {
SWAP_EXACT_IN_SINGLE_ACTION => V4Action::SWAP_EXACT_IN_SINGLE(<IV4Router::ExactInputSingleParams as SolValue>::abi_decode(action_data_slice)?),
SWAP_EXACT_IN_ACTION => V4Action::SWAP_EXACT_IN(<IV4Router::ExactInputParams as SolValue>::abi_decode(action_data_slice)?),
SWAP_EXACT_OUT_SINGLE_ACTION => V4Action::SWAP_EXACT_OUT_SINGLE(<IV4Router::ExactOutputSingleParams as SolValue>::abi_decode(action_data_slice)?),
SWAP_EXACT_OUT_ACTION => V4Action::SWAP_EXACT_OUT(<IV4Router::ExactOutputParams as SolValue>::abi_decode(action_data_slice)?),
SETTLE_ACTION => {
let (currency, amount, payer_is_user) = <(Address, U256, bool) as SolValue>::abi_decode(action_data_slice)?;
V4Action::SETTLE { currency, amount, payer_is_user }
}
SETTLE_ALL_ACTION => {
let (currency, max_amount) = <(Address, U256) as SolValue>::abi_decode(action_data_slice)?;
V4Action::SETTLE_ALL { currency, max_amount }
}
TAKE_ACTION => {
let (currency, recipient, amount) = <(Address, Address, U256) as SolValue>::abi_decode(action_data_slice)?;
V4Action::TAKE { currency, recipient, amount }
}
TAKE_ALL_ACTION => {
let (currency, min_amount) = <(Address, U256) as SolValue>::abi_decode(action_data_slice)?;
V4Action::TAKE_ALL { currency, min_amount }
}
TAKE_PORTION_ACTION => {
let (currency, recipient, bips) = <(Address, Address, U256) as SolValue>::abi_decode(action_data_slice)?;
V4Action::TAKE_PORTION { currency, recipient, bips }
}
_ => return Err(alloy_sol_types::Error::Other(format!("Unknown action opcode: {opcode}").into())),
};
decoded_actions.push(action);
}

Ok(decoded_actions)
action_opcodes_bytes
.iter()
.zip(action_data_bytes.iter())
.map(|(opcode, action_data)| {
let action_data_slice = action_data.as_ref();
Ok(match *opcode {
SWAP_EXACT_IN_SINGLE_ACTION => V4Action::SWAP_EXACT_IN_SINGLE(<IV4Router::ExactInputSingleParams as SolValue>::abi_decode(action_data_slice)?),
SWAP_EXACT_IN_ACTION => V4Action::SWAP_EXACT_IN(<IV4Router::ExactInputParams as SolValue>::abi_decode(action_data_slice)?),
SWAP_EXACT_OUT_SINGLE_ACTION => V4Action::SWAP_EXACT_OUT_SINGLE(<IV4Router::ExactOutputSingleParams as SolValue>::abi_decode(action_data_slice)?),
SWAP_EXACT_OUT_ACTION => V4Action::SWAP_EXACT_OUT(<IV4Router::ExactOutputParams as SolValue>::abi_decode(action_data_slice)?),
SETTLE_ACTION => {
let (currency, amount, payer_is_user) = <(Address, U256, bool) as SolValue>::abi_decode(action_data_slice)?;
V4Action::SETTLE { currency, amount, payer_is_user }
}
SETTLE_ALL_ACTION => {
let (currency, max_amount) = <(Address, U256) as SolValue>::abi_decode(action_data_slice)?;
V4Action::SETTLE_ALL { currency, max_amount }
}
TAKE_ACTION => {
let (currency, recipient, amount) = <(Address, Address, U256) as SolValue>::abi_decode(action_data_slice)?;
V4Action::TAKE { currency, recipient, amount }
}
TAKE_ALL_ACTION => {
let (currency, min_amount) = <(Address, U256) as SolValue>::abi_decode(action_data_slice)?;
V4Action::TAKE_ALL { currency, min_amount }
}
TAKE_PORTION_ACTION => {
let (currency, recipient, bips) = <(Address, Address, U256) as SolValue>::abi_decode(action_data_slice)?;
V4Action::TAKE_PORTION { currency, recipient, bips }
}
_ => return Err(alloy_sol_types::Error::Other(format!("Unknown action opcode: {opcode}").into())),
})
})
.collect()
}

#[rustfmt::skip]
Expand Down
12 changes: 12 additions & 0 deletions crates/gem_evm/src/uniswap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ impl TryFrom<u32> for FeeTier {
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value {
100 => Ok(FeeTier::Hundred),
400 => Ok(FeeTier::FourHundred),
500 => Ok(FeeTier::FiveHundred),
1500 => Ok(FeeTier::ThousandFiveHundred),
2500 => Ok(FeeTier::TwoThousandFiveHundred),
Expand All @@ -62,3 +63,14 @@ impl TryFrom<u32> for FeeTier {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_fee_tier_try_from_four_hundred() {
assert_eq!(FeeTier::try_from(400).unwrap(), FeeTier::FourHundred);
assert_eq!(FeeTier::try_from("400").unwrap(), FeeTier::FourHundred);
}
}
87 changes: 37 additions & 50 deletions crates/gem_evm/src/uniswap/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@ pub struct TokenPairs(pub Vec<TokenPair>);
impl Display for TokenPairs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[")?;
let mut iter = self.0.iter();
if let Some(first) = iter.next() {
write!(f, "{first}")?; // Write first element without a leading comma
for item in iter {
write!(f, ", {item}")?; // Write subsequent elements with a leading comma
if let Some((first, rest)) = self.0.split_first() {
write!(f, "{first}")?;
for item in rest {
write!(f, ", {item}")?;
}
}
write!(f, "]")
Expand All @@ -36,16 +35,20 @@ impl Display for TokenPairs {

impl TokenPair {
pub fn new_two_hop(token_in: &Address, intermediary: &Address, token_out: &Address, fee_tier: FeeTier) -> Vec<TokenPair> {
Self::new_two_hop_with_fees(token_in, intermediary, token_out, fee_tier, fee_tier)
}

pub fn new_two_hop_with_fees(token_in: &Address, intermediary: &Address, token_out: &Address, first_fee_tier: FeeTier, second_fee_tier: FeeTier) -> Vec<TokenPair> {
vec![
TokenPair {
token_in: *token_in,
token_out: *intermediary,
fee_tier,
fee_tier: first_fee_tier,
},
TokenPair {
token_in: *intermediary,
token_out: *token_out,
fee_tier,
fee_tier: second_fee_tier,
},
]
}
Expand All @@ -60,17 +63,15 @@ pub struct BasePair {

impl BasePair {
pub fn path_building_array(&self) -> Vec<Address> {
let mut array = vec![self.native];
array.extend(self.stables.iter().cloned());
// alternatives is not used for path building to reduce requests
array
std::iter::once(self.native).chain(self.stables.iter().copied()).collect()
}

pub fn fee_token_array(&self) -> Vec<Address> {
let mut array = vec![self.native];
array.extend(self.stables.iter().cloned());
array.extend(self.alternatives.iter().cloned());
array
std::iter::once(self.native)
.chain(self.stables.iter().copied())
.chain(self.alternatives.iter().copied())
.collect()
}
}

Expand Down Expand Up @@ -150,57 +151,43 @@ pub fn get_base_pair(chain: &EVMChain, weth_as_native: bool) -> Option<BasePair>
_ => panic!("USDT is not configured for this chain"),
};

let mut stables = vec![];
if !usdc.is_empty() {
stables.push(usdc.parse().ok()?);
}
if !usdt.is_empty() {
stables.push(usdt.parse().ok()?);
}
let alternatives = { if btc.is_empty() { vec![] } else { vec![btc.parse().ok()?] } };
let stables = [usdc, usdt]
.into_iter()
.filter(|token| !token.is_empty())
.map(|token| token.parse().ok())
.collect::<Option<Vec<_>>>()?;
let alternatives = if btc.is_empty() { vec![] } else { vec![btc.parse().ok()?] };

Some(BasePair { native, stables, alternatives })
}

pub fn build_direct_pair(token_in: &Address, token_out: &Address, fee_tier: FeeTier) -> Bytes {
let mut bytes: Vec<u8> = vec![];
let fee = U24::from(fee_tier.as_u24());
bytes.extend(token_in.as_slice());
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();

Bytes::from([token_in.as_slice(), fee.as_slice(), token_out.as_slice()].concat())
}

pub fn validate_pairs(token_pairs: &[TokenPair]) -> bool {
// verify token in and out are chained
let mut iter = token_pairs.iter().peekable();
let mut valid = true;
while let Some(current_pair) = iter.next() {
if let Some(next_pair) = iter.peek()
&& current_pair.token_out != next_pair.token_in
{
valid = false;
break;
}
}
valid
token_pairs.windows(2).all(|pairs| pairs[0].token_out == pairs[1].token_in)
}

pub fn build_pairs(token_pairs: &[TokenPair]) -> Bytes {
let valid = validate_pairs(token_pairs);
if !valid {
if !validate_pairs(token_pairs) {
panic!("invalid token pairs");
}

let mut bytes: Vec<u8> = vec![];
for (idx, token_pair) in token_pairs.iter().enumerate() {
let fee = U24::from(token_pair.fee_tier.as_u24());
if idx == 0 {
bytes.extend(token_pair.token_in.as_slice());
}
bytes.extend(&fee.to_be_bytes_vec());
bytes.extend(token_pair.token_out.as_slice());
}
let bytes = token_pairs
.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();

if idx == 0 {
[token_pair.token_in.as_slice(), fee.as_slice(), token_pair.token_out.as_slice()].concat()
} else {
[fee.as_slice(), token_pair.token_out.as_slice()].concat()
}
})
.collect::<Vec<u8>>();
Bytes::from(bytes)
}

Expand Down
13 changes: 10 additions & 3 deletions crates/swapper/src/uniswap/quote_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use gem_jsonrpc::types::{JsonRpcError, JsonRpcResponse, JsonRpcResult, JsonRpcRe
#[derive(Debug)]
pub struct QuoteResult {
pub amount_out: U256,
pub fee_tier_idx: usize,
pub route_idx: usize,
pub batch_idx: usize,
}

Expand All @@ -22,10 +22,10 @@ where
.0
.iter()
.enumerate()
.filter_map(|(fee_idx, result)| match result {
.filter_map(|(route_idx, result)| match result {
JsonRpcResult::Value(value) => decoder(value).ok().map(|quoter_tuple| QuoteResult {
amount_out: quoter_tuple.0,
fee_tier_idx: fee_idx,
route_idx,
batch_idx,
}),
_ => None,
Expand All @@ -37,3 +37,10 @@ where
.max_by_key(|quote| quote.amount_out)
.ok_or(SwapperError::NoQuoteAvailable)
}

pub fn get_selected_candidate<'a, T>(candidates: &'a [Vec<T>], quote: &QuoteResult) -> Result<&'a T, SwapperError> {
candidates
.get(quote.batch_idx)
.and_then(|batch| batch.get(quote.route_idx))
.ok_or(SwapperError::InvalidRoute)
}
45 changes: 21 additions & 24 deletions crates/swapper/src/uniswap/swap_route.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::Route;
use crate::{Route, SwapperError};
use alloy_primitives::Address;
use gem_evm::uniswap::path::BasePair;
use primitives::AssetId;
use gem_evm::uniswap::path::{BasePair, TokenPair};
use primitives::{AssetId, Chain};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand All @@ -23,26 +23,23 @@ pub fn get_intermediaries_by_array(token_in: &Address, token_out: &Address, arra
.collect()
}

pub fn build_swap_route(token_in: &AssetId, intermediary: Option<&AssetId>, token_out: &AssetId, route_data: &RouteData) -> Vec<Route> {
let data = serde_json::to_string(route_data).unwrap();
if let Some(intermediary) = intermediary {
vec![
Route {
input: token_in.clone(),
output: intermediary.clone(),
route_data: data.clone(),
},
Route {
input: intermediary.clone(),
output: token_out.clone(),
route_data: data,
},
]
} else {
vec![Route {
input: token_in.clone(),
output: token_out.clone(),
route_data: data,
}]
pub fn build_swap_route(chain: Chain, token_pairs: &[TokenPair], min_amount_out: &str) -> Result<Vec<Route>, SwapperError> {
if token_pairs.is_empty() {
return Err(SwapperError::InvalidRoute);
}

token_pairs
.iter()
.map(|pair| {
let route_data = RouteData {
fee_tier: (pair.fee_tier as u32).to_string(),
min_amount_out: min_amount_out.to_string(),
};
Ok(Route {
input: AssetId::from(chain, Some(pair.token_in.to_checksum(None))),
output: AssetId::from(chain, Some(pair.token_out.to_checksum(None))),
route_data: serde_json::to_string(&route_data).map_err(|_| SwapperError::InvalidRoute)?,
})
})
.collect()
}
Loading
Loading