Merge branch 'main' into rfqs/tnl/ENG-4798-arc-protocol-state

This commit is contained in:
Tamara
2025-08-21 10:43:50 -04:00
committed by GitHub
21 changed files with 781 additions and 134 deletions

View File

@@ -31,10 +31,6 @@ pub struct Permit2 {
address: Address,
client: EVMProvider,
runtime_handle: Handle,
// Store the runtime to prevent it from being dropped before use.
// This is required since tycho-execution does not have a pre-existing runtime.
// However, if the library is used in a context where a runtime already exists, it is not
// necessary to store it.
#[allow(dead_code)]
runtime: Option<Arc<Runtime>>,
}

View File

@@ -23,10 +23,6 @@ use crate::encoding::{
pub struct ProtocolApprovalsManager {
client: EVMProvider,
runtime_handle: Handle,
// Store the runtime to prevent it from being dropped before use.
// This is required since tycho-execution does not have a pre-existing runtime.
// However, if the library is used in a context where a runtime already exists, it is not
// necessary to store it.
#[allow(dead_code)]
runtime: Option<Arc<Runtime>>,
}

View File

@@ -6,8 +6,8 @@ use crate::encoding::{
errors::EncodingError,
evm::swap_encoder::swap_encoders::{
BalancerV2SwapEncoder, BalancerV3SwapEncoder, BebopSwapEncoder, CurveSwapEncoder,
EkuboSwapEncoder, MaverickV2SwapEncoder, UniswapV2SwapEncoder, UniswapV3SwapEncoder,
UniswapV4SwapEncoder,
EkuboSwapEncoder, HashflowSwapEncoder, MaverickV2SwapEncoder, UniswapV2SwapEncoder,
UniswapV3SwapEncoder, UniswapV4SwapEncoder,
},
swap_encoder::SwapEncoder,
};
@@ -91,6 +91,11 @@ impl SwapEncoderBuilder {
"rfq:bebop" => {
Ok(Box::new(BebopSwapEncoder::new(self.executor_address, self.chain, self.config)?))
}
"rfq:hashflow" => Ok(Box::new(HashflowSwapEncoder::new(
self.executor_address,
self.chain,
self.config,
)?)),
_ => Err(EncodingError::FatalError(format!(
"Unknown protocol system: {}",
self.protocol_system

View File

@@ -664,10 +664,6 @@ pub struct BebopSwapEncoder {
native_token_bebop_address: Bytes,
native_token_address: Bytes,
runtime_handle: Handle,
// Store the runtime to prevent it from being dropped before use.
// This is required since tycho-execution does not have a pre-existing runtime.
// However, if the library is used in a context where a runtime already exists, it is not
// necessary to store it.
#[allow(dead_code)]
runtime: Option<Arc<Runtime>>,
}
@@ -715,28 +711,24 @@ impl SwapEncoder for BebopSwapEncoder {
) -> Result<Vec<u8>, EncodingError> {
let token_in = bytes_to_address(&swap.token_in)?;
let token_out = bytes_to_address(&swap.token_out)?;
let token_approvals_manager = ProtocolApprovalsManager::new()?;
let approval_needed: bool;
if let Some(router_address) = &encoding_context.router_address {
let tycho_router_address = bytes_to_address(router_address)?;
let sender = encoding_context
.router_address
.clone()
.ok_or(EncodingError::FatalError(
"The router address is needed to perform a Hashflow swap".to_string(),
))?;
let approval_needed = if swap.token_in == self.native_token_address {
false
} else {
let tycho_router_address = bytes_to_address(&sender)?;
let settlement_address = Address::from_str(&self.settlement_address)
.map_err(|_| EncodingError::FatalError("Invalid settlement address".to_string()))?;
// Native ETH doesn't need approval, only ERC20 tokens do
if swap.token_in == self.native_token_address {
approval_needed = false;
} else {
approval_needed = token_approvals_manager.approval_needed(
token_in,
tycho_router_address,
settlement_address,
)?;
}
} else {
approval_needed = true;
}
ProtocolApprovalsManager::new()?.approval_needed(
token_in,
tycho_router_address,
settlement_address,
)?
};
let (partial_fill_offset, original_filled_taker_amount, bebop_calldata) =
if let Some(state) = &swap.protocol_state {
@@ -850,6 +842,148 @@ impl SwapEncoder for BebopSwapEncoder {
}
}
#[derive(Clone)]
pub struct HashflowSwapEncoder {
executor_address: String,
hashflow_router_address: String,
native_token_address: Bytes,
runtime_handle: Handle,
#[allow(dead_code)]
runtime: Option<Arc<Runtime>>,
}
impl SwapEncoder for HashflowSwapEncoder {
fn new(
executor_address: String,
chain: Chain,
config: Option<HashMap<String, String>>,
) -> Result<Self, EncodingError> {
let config = config.ok_or(EncodingError::FatalError(
"Missing hashflow specific addresses in config".to_string(),
))?;
let hashflow_router_address = config
.get("hashflow_router_address")
.ok_or(EncodingError::FatalError(
"Missing hashflow router address in config".to_string(),
))?
.to_string();
let native_token_address = chain.native_token().address;
let (runtime_handle, runtime) = get_runtime()?;
Ok(Self {
executor_address,
hashflow_router_address,
native_token_address,
runtime_handle,
runtime,
})
}
fn encode_swap(
&self,
swap: &Swap,
encoding_context: &EncodingContext,
) -> Result<Vec<u8>, EncodingError> {
// Native tokens doesn't need approval, only ERC20 tokens do
let sender = encoding_context
.router_address
.clone()
.ok_or(EncodingError::FatalError(
"The router address is needed to perform a Hashflow swap".to_string(),
))?;
// Native ETH doesn't need approval, only ERC20 tokens do
let approval_needed = if swap.token_in == self.native_token_address {
false
} else {
let tycho_router_address = bytes_to_address(&sender)?;
let hashflow_router_address = Address::from_str(&self.hashflow_router_address)
.map_err(|_| {
EncodingError::FatalError("Invalid hashflow router address address".to_string())
})?;
ProtocolApprovalsManager::new()?.approval_needed(
bytes_to_address(&swap.token_in)?,
tycho_router_address,
hashflow_router_address,
)?
};
// Get quote
let protocol_state = swap
.protocol_state
.as_ref()
.ok_or_else(|| {
EncodingError::FatalError("protocol_state is required for Hashflow".to_string())
})?;
let amount_in = swap
.estimated_amount_in
.as_ref()
.ok_or(EncodingError::FatalError(
"Estimated amount in is mandatory for a Hashflow swap".to_string(),
))?
.clone();
let sender = encoding_context
.router_address
.clone()
.ok_or(EncodingError::FatalError(
"The router address is needed to perform a Hashflow swap".to_string(),
))?;
let signed_quote = block_in_place(|| {
self.runtime_handle.block_on(async {
protocol_state
.as_indicatively_priced()?
.request_signed_quote(GetAmountOutParams {
amount_in,
token_in: swap.token_in.clone(),
token_out: swap.token_out.clone(),
sender,
receiver: encoding_context.receiver.clone(),
})
.await
})
})?;
// Encode packed data for the executor
// Format: approval_needed | transfer_type | hashflow_calldata[..]
let hashflow_fields = [
"pool",
"external_account",
"trader",
"base_token",
"quote_token",
"base_token_amount",
"quote_token_amount",
"quote_expiry",
"nonce",
"tx_id",
"signature",
];
let mut hashflow_calldata = vec![];
for field in &hashflow_fields {
let value = signed_quote
.quote_attributes
.get(*field)
.ok_or(EncodingError::FatalError(format!(
"Hashflow quote must have a {field} attribute"
)))?;
hashflow_calldata.extend_from_slice(value);
}
let args = (
(encoding_context.transfer_type as u8).to_be_bytes(),
(approval_needed as u8).to_be_bytes(),
&hashflow_calldata[..],
);
Ok(args.abi_encode_packed())
}
fn executor_address(&self) -> &str {
&self.executor_address
}
fn clone_box(&self) -> Box<dyn SwapEncoder> {
Box::new(self.clone())
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
@@ -1985,8 +2119,17 @@ mod tests {
};
let bebop_state = MockRFQState {
quote_amount_out,
quote_calldata: bebop_calldata.clone(),
quote_partial_fill_offset: partial_fill_offset,
quote_data: HashMap::from([
("calldata".to_string(), bebop_calldata.clone()),
(
"partial_fill_offset".to_string(),
Bytes::from(
partial_fill_offset
.to_be_bytes()
.to_vec(),
),
),
]),
};
let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC
@@ -2037,4 +2180,159 @@ mod tests {
assert_eq!(hex_swap, expected_swap + &bebop_calldata.to_string()[2..]);
}
}
mod hashflow {
use alloy::hex::encode;
use num_bigint::BigUint;
use super::*;
use crate::encoding::{
evm::testing_utils::MockRFQState,
models::{SwapBuilder, TransferType},
};
fn hashflow_config() -> Option<HashMap<String, String>> {
Some(HashMap::from([(
"hashflow_router_address".to_string(),
"0x55084eE0fEf03f14a305cd24286359A35D735151".to_string(),
)]))
}
#[test]
fn test_encode_hashflow_single_fails_without_protocol_data() {
// Hashflow requires a swap with protocol data, otherwise will return an error
let hashflow_component = ProtocolComponent {
id: String::from("hashflow-rfq"),
protocol_system: String::from("rfq:hashflow"),
..Default::default()
};
let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC
let token_out = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); // WETH
let swap = SwapBuilder::new(hashflow_component, token_in.clone(), token_out.clone())
.estimated_amount_in(BigUint::from_str("3000000000").unwrap())
.build();
let encoding_context = EncodingContext {
receiver: Bytes::from("0xc5564C13A157E6240659fb81882A28091add8670"),
exact_out: false,
router_address: Some(Bytes::zero(20)),
group_token_in: token_in.clone(),
group_token_out: token_out.clone(),
transfer_type: TransferType::Transfer,
};
let encoder = HashflowSwapEncoder::new(
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
Chain::Ethereum,
hashflow_config(),
)
.unwrap();
encoder
.encode_swap(&swap, &encoding_context)
.expect_err("Should returned an error if the swap has no protocol state");
}
#[test]
fn test_encode_hashflow_single_with_protocol_state() {
// 3000 USDC -> 1 WETH using a mocked RFQ state to get a quote
let quote_amount_out = BigUint::from_str("1000000000000000000").unwrap();
let hashflow_component = ProtocolComponent {
id: String::from("hashflow-rfq"),
protocol_system: String::from("rfq:hashflow"),
..Default::default()
};
let hashflow_quote_data = vec![
(
"pool".to_string(),
Bytes::from_str("0x478eca1b93865dca0b9f325935eb123c8a4af011").unwrap(),
),
(
"external_account".to_string(),
Bytes::from_str("0xbee3211ab312a8d065c4fef0247448e17a8da000").unwrap(),
),
(
"trader".to_string(),
Bytes::from_str("0xcd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2").unwrap(),
),
(
"base_token".to_string(),
Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
),
(
"quote_token".to_string(),
Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(),
),
(
"base_token_amount".to_string(),
Bytes::from(biguint_to_u256(&BigUint::from(3000_u64)).to_be_bytes::<32>().to_vec()),
),
(
"quote_token_amount".to_string(),
Bytes::from(biguint_to_u256(&BigUint::from(1_u64)).to_be_bytes::<32>().to_vec()),
),
("quote_expiry".to_string(), Bytes::from(biguint_to_u256(&BigUint::from(1755610328_u64)).to_be_bytes::<32>().to_vec())),
("nonce".to_string(), Bytes::from(biguint_to_u256(&BigUint::from(1755610283723_u64)).to_be_bytes::<32>().to_vec())),
(
"tx_id".to_string(),
Bytes::from_str(
"0x125000064000640000001747eb8c38ffffffffffffff0029642016edb36d0000",
)
.unwrap(),
),
("signature".to_string(), Bytes::from_str("0x6ddb3b21fe8509e274ddf46c55209cdbf30360944abbca6569ed6b26740d052f419964dcb5a3bdb98b4ed1fb3642a2760b8312118599a962251f7a8f73fe4fbe1c").unwrap()),
];
let hashflow_quote_data_values =
hashflow_quote_data
.iter()
.fold(vec![], |mut acc, (_key, value)| {
acc.extend_from_slice(value);
acc
});
let hashflow_calldata = Bytes::from(hashflow_quote_data_values);
let hashflow_state = MockRFQState {
quote_amount_out,
quote_data: hashflow_quote_data
.into_iter()
.collect(),
};
let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC
let token_out = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); // WETH
let swap = SwapBuilder::new(hashflow_component, token_in.clone(), token_out.clone())
.estimated_amount_in(BigUint::from_str("3000000000").unwrap())
.protocol_state(&hashflow_state)
.build();
let encoding_context = EncodingContext {
receiver: Bytes::from("0xc5564C13A157E6240659fb81882A28091add8670"),
exact_out: false,
router_address: Some(Bytes::zero(20)),
group_token_in: token_in.clone(),
group_token_out: token_out.clone(),
transfer_type: TransferType::Transfer,
};
let encoder = HashflowSwapEncoder::new(
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
Chain::Ethereum,
hashflow_config(),
)
.unwrap();
let encoded_swap = encoder
.encode_swap(&swap, &encoding_context)
.unwrap();
let hex_swap = encode(&encoded_swap);
let expected_swap = String::from(concat!(
"01", // transfer type
"01", // approval needed
));
assert_eq!(hex_swap, expected_swap + &hashflow_calldata.to_string()[2..]);
}
}
}

View File

@@ -17,8 +17,7 @@ use tycho_common::{
#[derive(Debug)]
pub struct MockRFQState {
pub quote_amount_out: BigUint,
pub quote_calldata: Bytes,
pub quote_partial_fill_offset: u64,
pub quote_data: HashMap<String, Bytes>,
}
impl ProtocolSim for MockRFQState {
fn fee(&self) -> f64 {
@@ -82,23 +81,12 @@ impl IndicativelyPriced for MockRFQState {
&self,
params: GetAmountOutParams,
) -> Result<SignedQuote, SimulationError> {
let mut quote_attributes: HashMap<String, Bytes> = HashMap::new();
quote_attributes.insert("calldata".to_string(), self.quote_calldata.clone());
quote_attributes.insert(
"partial_fill_offset".to_string(),
Bytes::from(
self.quote_partial_fill_offset
.to_be_bytes()
.to_vec(),
),
);
Ok(SignedQuote {
base_token: params.token_in,
quote_token: params.token_out,
amount_in: params.amount_in,
amount_out: self.quote_amount_out.clone(),
quote_attributes,
quote_attributes: self.quote_data.clone(),
})
}
}

View File

@@ -78,6 +78,9 @@ pub fn get_static_attribute(swap: &Swap, attribute_name: &str) -> Result<Vec<u8>
.to_vec())
}
/// Returns the current Tokio runtime handle, or creates a new one if it doesn't exist.
/// It also returns the runtime to prevent it from being dropped before use.
/// This is required since tycho-execution does not have a pre-existing runtime.
pub fn get_runtime() -> Result<(Handle, Option<Arc<Runtime>>), EncodingError> {
match Handle::try_current() {
Ok(h) => Ok((h, None)),