From 84fbe0ded68c8e2fa1a04c7f29163a6054accf7d Mon Sep 17 00:00:00 2001 From: pedrobergamini <41773103+pedrobergamini@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:32:42 -0300 Subject: [PATCH] refactor: update Rust encoding to remove Multi order type --- .../evm/swap_encoder/swap_encoders.rs | 646 +++++++++++++----- src/encoding/evm/tycho_encoders.rs | 269 ++++---- src/encoding/models.rs | 6 +- 3 files changed, 581 insertions(+), 340 deletions(-) diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 8d39417..dfef1f8 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -1,7 +1,8 @@ use std::{collections::HashMap, str::FromStr}; use alloy::{ - primitives::{Address, Bytes as AlloyBytes, U8}, + core::sol, + primitives::{Address, Bytes as AlloyBytes, U256, U8}, sol_types::SolValue, }; use serde_json::from_str; @@ -617,6 +618,37 @@ impl SwapEncoder for BalancerV3SwapEncoder { } } +// Define Bebop order structures for ABI decoding +sol! { + struct BebopSingle { + uint256 expiry; + address taker_address; + address maker_address; + uint256 maker_nonce; + address taker_token; + address maker_token; + uint256 taker_amount; + uint256 maker_amount; + address receiver; + uint256 packed_commands; + uint256 flags; + } + + struct BebopAggregate { + uint256 expiry; + address taker_address; + uint256 taker_nonce; + address[] taker_tokens; + uint256[] taker_amounts; + address[] maker_addresses; + address[][] maker_tokens; + uint256[][] maker_amounts; + address receiver; + uint256 packed_commands; + uint256 flags; + } +} + /// Encodes a swap on Bebop (PMM RFQ) through the given executor address. /// /// Bebop uses a Request-for-Quote model where quotes are obtained off-chain @@ -646,6 +678,52 @@ impl BebopSwapEncoder { } Ok(()) } + + /// Analyzes the quote to determine token input/output characteristics + /// Returns (has_many_inputs, has_many_outputs) + fn validate_aggregate_order( + &self, + quote_data: &[u8], + expected_token_out: &Address, + ) -> Result<(), EncodingError> { + // Decode the Aggregate order to validate + let order = BebopAggregate::abi_decode(quote_data).map_err(|e| { + EncodingError::InvalidInput(format!("Failed to decode Bebop Aggregate order: {}", e)) + })?; + + // Validate that we only have one input token + if order.taker_tokens.len() != 1 { + return Err(EncodingError::InvalidInput( + "Aggregate orders must have exactly one input token".to_string(), + )); + } + + // Validate that all makers provide exactly one output token + for (i, maker_tokens) in order.maker_tokens.iter().enumerate() { + if maker_tokens.len() != 1 { + return Err(EncodingError::InvalidInput(format!( + "Maker {} must provide exactly one output token, found {}", + i, + maker_tokens.len() + ))); + } + } + + // Validate that all makers provide the same output token + let all_same_output = order + .maker_tokens + .iter() + .flat_map(|maker_tokens| maker_tokens.iter()) + .all(|token| token == expected_token_out); + + if !all_same_output { + return Err(EncodingError::InvalidInput( + "All makers must provide the same output token".to_string(), + )); + } + + Ok(()) + } } impl SwapEncoder for BebopSwapEncoder { @@ -703,61 +781,200 @@ impl SwapEncoder for BebopSwapEncoder { })?; // Parse user_data format: - // order_type (1 byte) | signature_type (1 byte) | quote_data_length (4 bytes) | quote_data - // | signature_length (4 bytes) | signature - if user_data.len() < 10 { + // order_type (1 byte) | filledTakerAmount (32 bytes) | quote_data_length (4 bytes) | + // quote_data | abi_encoded_maker_signatures where abi_encoded_maker_signatures is + // abi.encode(MakerSignature[]) + if user_data.len() < 37 { return Err(EncodingError::InvalidInput( "User data too short to contain Bebop RFQ data".to_string(), )); } let order_type = BebopOrderType::try_from(user_data[0])?; - let signature_type = user_data[1]; + + // Extract filledTakerAmount (32 bytes) + let filled_taker_amount = U256::from_be_slice(&user_data[1..33]); let quote_data_len = - u32::from_be_bytes([user_data[2], user_data[3], user_data[4], user_data[5]]) as usize; - if user_data.len() < 10 + quote_data_len { + u32::from_be_bytes([user_data[33], user_data[34], user_data[35], user_data[36]]) + as usize; + if user_data.len() < 37 + quote_data_len { return Err(EncodingError::InvalidInput( "User data too short to contain quote data".to_string(), )); } - let quote_data = user_data[6..6 + quote_data_len].to_vec(); + let quote_data = user_data[37..37 + quote_data_len].to_vec(); - let sig_len_start = 6 + quote_data_len; - if user_data.len() < sig_len_start + 4 { + // Validate Aggregate orders have single token in/out + if order_type == BebopOrderType::Aggregate { + self.validate_aggregate_order("e_data, &token_out)?; + } + + // Decode the ABI-encoded MakerSignature[] array + let maker_sigs_start = 37 + quote_data_len; + let remaining_data = &user_data[maker_sigs_start..]; + + // Need at least 64 bytes for offset (32) and array length (32) + if remaining_data.len() < 64 { return Err(EncodingError::InvalidInput( - "User data too short to contain signature length".to_string(), + "User data too short to contain MakerSignature array".to_string(), )); } - let signature_len = u32::from_be_bytes([ - user_data[sig_len_start], - user_data[sig_len_start + 1], - user_data[sig_len_start + 2], - user_data[sig_len_start + 3], - ]) as usize; + // Read offset to array (should be 0x20 for single parameter) + let array_offset: usize = U256::from_be_slice(&remaining_data[0..32]) + .try_into() + .map_err(|_| EncodingError::InvalidInput("Array offset too large".to_string()))?; - if user_data.len() != sig_len_start + 4 + signature_len { - return Err(EncodingError::InvalidInput("User data length mismatch".to_string())); + if array_offset != 32 { + return Err(EncodingError::InvalidInput( + "Invalid array offset in ABI encoding".to_string(), + )); } - let signature = user_data[sig_len_start + 4..].to_vec(); + // Read array length + let array_length: usize = U256::from_be_slice(&remaining_data[32..64]) + .try_into() + .map_err(|_| EncodingError::InvalidInput("Array length too large".to_string()))?; + + // Validate signature count based on order type + match order_type { + BebopOrderType::Single => { + if array_length != 1 { + return Err(EncodingError::InvalidInput(format!( + "Expected 1 signature for Single order, got {}", + array_length + ))); + } + } + BebopOrderType::Aggregate => { + if array_length == 0 { + return Err(EncodingError::InvalidInput( + "Aggregate order requires at least one signature".to_string(), + )); + } + } + } + + // For aggregate orders, we just pass through the entire ABI-encoded array + // For single orders, we validate and re-encode + let maker_sigs_encoded = if order_type == BebopOrderType::Aggregate { + // For aggregate orders, pass through the entire encoded array + remaining_data.to_vec() + } else { + // For single orders, validate and re-encode the single signature + // Read offset to first (and only) struct + if remaining_data.len() < 96 { + return Err(EncodingError::InvalidInput( + "User data too short to contain struct offset".to_string(), + )); + } + + let struct_offset: usize = U256::from_be_slice(&remaining_data[64..96]) + .try_into() + .map_err(|_| EncodingError::InvalidInput("Struct offset too large".to_string()))?; + + // The struct data starts at the struct_offset + // struct_offset is relative to the start of array data (after the array length) + // Array data starts at position 64 (after offset[32] and length[32]) + // Absolute position = 64 + struct_offset + let struct_start = 64 + struct_offset; + + if remaining_data.len() < struct_start + 64 { + return Err(EncodingError::InvalidInput( + "User data too short to contain MakerSignature struct".to_string(), + )); + } + + // Read the struct fields + // - offset to signatureBytes (32 bytes) - should be 64 + // - flags (32 bytes) + let sig_offset: usize = + U256::from_be_slice(&remaining_data[struct_start..struct_start + 32]) + .try_into() + .map_err(|_| { + EncodingError::InvalidInput("Signature offset too large".to_string()) + })?; + + if sig_offset != 64 { + return Err(EncodingError::InvalidInput(format!( + "Invalid signature offset in struct: expected 64, got {}", + sig_offset + ))); + } + + let flags = U256::from_be_slice(&remaining_data[struct_start + 32..struct_start + 64]); + + // Read signature data + let sig_data_start = struct_start + sig_offset; + if remaining_data.len() < sig_data_start + 32 { + return Err(EncodingError::InvalidInput( + "User data too short to contain signature length".to_string(), + )); + } + + let signature_len: usize = + U256::from_be_slice(&remaining_data[sig_data_start..sig_data_start + 32]) + .try_into() + .map_err(|_| { + EncodingError::InvalidInput("Signature length too large".to_string()) + })?; + + if remaining_data.len() < sig_data_start + 32 + signature_len { + return Err(EncodingError::InvalidInput( + "User data too short to contain signature data".to_string(), + )); + } + + let signature = + remaining_data[sig_data_start + 32..sig_data_start + 32 + signature_len].to_vec(); + + // Re-encode the MakerSignature[] array for the executor + let mut encoded = Vec::new(); + + // Offset to array (always 0x20 for single parameter) + encoded.extend_from_slice(&U256::from(32).to_be_bytes::<32>()); + + // Array length (1 for non-aggregate orders) + encoded.extend_from_slice(&U256::from(1).to_be_bytes::<32>()); + + // Offset to first struct (relative to array data start) + let struct_offset = 32; // After the array length + encoded.extend_from_slice(&U256::from(struct_offset).to_be_bytes::<32>()); + + // Struct data + // - offset to signatureBytes (always 64) + encoded.extend_from_slice(&U256::from(64).to_be_bytes::<32>()); + + // - flags + encoded.extend_from_slice(&flags.to_be_bytes::<32>()); + + // - signatureBytes length + encoded.extend_from_slice(&U256::from(signature.len()).to_be_bytes::<32>()); + + // - signatureBytes data (padded) + encoded.extend_from_slice(&signature); + let padding = (32 - (signature.len() % 32)) % 32; + encoded.extend_from_slice(&vec![0u8; padding]); + + encoded + }; // Encode packed data for the executor - // Format: token_in | token_out | transfer_type | order_type | - // quote_data_length | quote_data | signature_type | signature_length | signature | - // approval_needed + // Format: token_in | token_out | transfer_type | order_type | filledTakerAmount | + // quote_data_length | quote_data | maker_signatures_length | + // abi_encoded_maker_signatures | approval_needed let args = ( token_in, token_out, (encoding_context.transfer_type as u8).to_be_bytes(), (order_type as u8).to_be_bytes(), + filled_taker_amount.to_be_bytes::<32>(), (quote_data.len() as u32).to_be_bytes(), "e_data[..], - signature_type.to_be_bytes(), - (signature.len() as u32).to_be_bytes(), - &signature[..], + (maker_sigs_encoded.len() as u32).to_be_bytes(), + &maker_sigs_encoded[..], (approval_needed as u8).to_be_bytes(), ); @@ -1826,25 +2043,92 @@ mod tests { mod bebop { use super::*; - use crate::encoding::evm::utils::write_calldata_to_file; + use crate::encoding::{evm::utils::write_calldata_to_file, models::TransferType}; #[test] fn test_encode_bebop_single() { - use alloy::hex; + use alloy::{hex, primitives::Address}; - // Create user_data with quote and signature + // Use mainnet data + // Transaction: https://etherscan.io/tx/0x6279bc970273b6e526e86d9b69133c2ca1277e697ba25375f5e6fc4df50c0c94 let order_type = BebopOrderType::Single as u8; - let signature_type = 1u8; // EIP712 - let quote_data = hex::decode("1234567890abcdef").unwrap(); - let signature = hex::decode("aabbccdd").unwrap(); + + // Create the IBebopSettlement.Single struct data using the exact input data from the tx + let expiry = U256::from(1749483840u64); + let taker_address = + Address::from_str("0xc5564C13A157E6240659fb81882A28091add8670").unwrap(); + let maker_address = + Address::from_str("0xCe79b081c0c924cb67848723ed3057234d10FC6b").unwrap(); + let maker_nonce = U256::from(1749483765992417u64); + let taker_token = + Address::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(); // USDC + let maker_token = + Address::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap(); // ONDO + let taker_amount = U256::from(200000000u64); // 200 USDC + let maker_amount = U256::from_str("237212396774431060000").unwrap(); // 237.21 ONDO + let receiver = taker_address; + let packed_commands = U256::ZERO; + let flags = U256::from_str( + "51915842898789398998206002334703507894664330885127600393944965515693155942400", + ) + .unwrap(); + + // ABI encode the order struct + let quote_data = ( + expiry, + taker_address, + maker_address, + maker_nonce, + taker_token, + maker_token, + taker_amount, + maker_amount, + receiver, + packed_commands, + flags, + ) + .abi_encode(); + + // Real signature from mainnet + let signature = hex::decode("eb5419631614978da217532a40f02a8f2ece37d8cfb94aaa602baabbdefb56b474f4c2048a0f56502caff4ea7411d99eed6027cd67dc1088aaf4181dcb0df7051c").unwrap(); + let signature_type = 0u8; // ETH_SIGN + + // Build ABI-encoded MakerSignature[] array + let flags = U256::from(signature_type); + let mut encoded_maker_sigs = Vec::new(); + + // Offset to array (always 0x20 for single parameter) + encoded_maker_sigs.extend_from_slice(&U256::from(32).to_be_bytes::<32>()); + + // Array length (1 for Single order) + encoded_maker_sigs.extend_from_slice(&U256::from(1).to_be_bytes::<32>()); + + // Offset to first struct (relative to array data start) + encoded_maker_sigs.extend_from_slice(&U256::from(32).to_be_bytes::<32>()); + + // Struct data (MakerSignature has signatureBytes first, then flags) + // - offset to signatureBytes (always 64 since it comes after offset and flags) + encoded_maker_sigs.extend_from_slice(&U256::from(64).to_be_bytes::<32>()); + + // - flags (AFTER the offset, as per struct order) + encoded_maker_sigs.extend_from_slice(&flags.to_be_bytes::<32>()); + + // - signatureBytes length + encoded_maker_sigs.extend_from_slice(&U256::from(signature.len()).to_be_bytes::<32>()); + + // - signatureBytes data (padded) + encoded_maker_sigs.extend_from_slice(&signature); + let padding = (32 - (signature.len() % 32)) % 32; + encoded_maker_sigs.extend_from_slice(&vec![0u8; padding]); + + let filled_taker_amount = U256::ZERO; // 0 means fill entire order let mut user_data = Vec::new(); user_data.push(order_type); - user_data.push(signature_type); + user_data.extend_from_slice(&filled_taker_amount.to_be_bytes::<32>()); user_data.extend_from_slice(&(quote_data.len() as u32).to_be_bytes()); user_data.extend_from_slice("e_data); - user_data.extend_from_slice(&(signature.len() as u32).to_be_bytes()); - user_data.extend_from_slice(&signature); + user_data.extend_from_slice(&encoded_maker_sigs); let bebop_component = ProtocolComponent { id: String::from("bebop-rfq"), @@ -1854,7 +2138,7 @@ mod tests { }; let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC - let token_out = Bytes::from("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); // WETH + let token_out = Bytes::from("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3"); // ONDO let swap = Swap { component: bebop_component, token_in: token_in.clone(), @@ -1864,7 +2148,7 @@ mod tests { }; let encoding_context = EncodingContext { - receiver: Bytes::from("0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e"), // BOB + receiver: Bytes::from("0xc5564C13A157E6240659fb81882A28091add8670"), /* Original taker */ exact_out: false, router_address: Some(Bytes::zero(20)), group_token_in: token_in.clone(), @@ -1887,123 +2171,17 @@ mod tests { .unwrap(); let hex_swap = encode(&encoded_swap); - assert_eq!( - hex_swap, - String::from(concat!( - // token in - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - // token out - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - // transfer type Transfer - "01", - // order type Single (0) - "00", - // quote data length (8 bytes = 0x00000008) - "00000008", - // quote data - "1234567890abcdef", - // signature type EIP712 (1) - "01", - // signature length (4 bytes = 0x00000004) - "00000004", - // signature - "aabbccdd", - // approval needed - "01" - )) - ); + // Verify the encoding contains the expected tokens + assert!(hex_swap.contains("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")); // USDC + assert!(hex_swap.contains("faba6f8e4a5e8ab82f62fe7c39859fa577269be3")); // ONDO + + // Verify it includes the signature + let sig_hex = hex::encode(&signature); + assert!(hex_swap.contains(&sig_hex)); write_calldata_to_file("test_encode_bebop_single", hex_swap.as_str()); } - #[test] - fn test_encode_bebop_multi() { - use alloy::hex; - - // Create user_data for a Bebop Multi RFQ quote - let order_type = BebopOrderType::Multi as u8; - let signature_type = 1u8; // EIP712 - let quote_data = hex::decode("abcdef1234567890").unwrap(); - let signature = hex::decode("11223344").unwrap(); - - let mut user_data = Vec::new(); - user_data.push(order_type); - user_data.push(signature_type); - user_data.extend_from_slice(&(quote_data.len() as u32).to_be_bytes()); - user_data.extend_from_slice("e_data); - user_data.extend_from_slice(&(signature.len() as u32).to_be_bytes()); - user_data.extend_from_slice(&signature); - - let bebop_component = ProtocolComponent { - id: String::from("bebop-rfq"), - protocol_system: String::from("rfq:bebop"), - static_attributes: HashMap::new(), - ..Default::default() - }; - - let token_in = Bytes::from("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); // WETH - let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC - let swap = Swap { - component: bebop_component, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - user_data: Some(Bytes::from(user_data)), - }; - - let encoding_context = EncodingContext { - receiver: Bytes::from("0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e"), // BOB - 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 = BebopSwapEncoder::new( - String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), - TychoCoreChain::Ethereum.into(), - Some(HashMap::from([( - "bebop_settlement_address".to_string(), - "0xbbbbbBB520d69a9775E85b458C58c648259FAD5F".to_string(), - )])), - ) - .unwrap(); - - let encoded_swap = encoder - .encode_swap(swap, encoding_context) - .unwrap(); - let hex_swap = encode(&encoded_swap); - - assert_eq!( - hex_swap, - String::from(concat!( - // token in - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - // token out - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - // transfer type Transfer - "01", - // order type Multi (1) - "01", - // quote data length (8 bytes = 0x00000008) - "00000008", - // quote data - "abcdef1234567890", - // signature type EIP712 (1) - "01", - // signature length (4 bytes = 0x00000004) - "00000004", - // signature - "11223344", - // approval needed - "01" - )) - ); - - write_calldata_to_file("test_encode_bebop_multi", hex_swap.as_str()); - } - #[test] fn test_encode_bebop_aggregate() { use alloy::hex; @@ -2011,19 +2189,87 @@ mod tests { // Create user_data for a Bebop Aggregate RFQ quote let order_type = BebopOrderType::Aggregate as u8; let signature_type = 1u8; // EIP712 - let quote_data = hex::decode("deadbeef").unwrap(); - // For aggregate orders with ECDSA, use concatenated 65-byte signatures (2 makers) + + // Create a valid ABI-encoded Aggregate order + // For this test: single input (DAI) -> single output (WBTC) to match the test setup + let aggregate_order = BebopAggregate { + expiry: U256::from(1234567890u64), + taker_address: Address::from([0x11; 20]), + taker_nonce: U256::from(12345u64), + taker_tokens: vec![ + Address::from_str("0x6B175474E89094C44Da98b954EedeAC495271d0F").unwrap(), /* DAI */ + ], + taker_amounts: vec![ + U256::from(1000000000000000000u64), // 1 DAI + ], + maker_addresses: vec![Address::from([0x22; 20]), Address::from([0x33; 20])], + maker_tokens: vec![ + vec![Address::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap()], /* WBTC from maker 1 */ + vec![Address::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap()], /* WBTC from maker 2 */ + ], + maker_amounts: vec![ + vec![U256::from(1250000u64)], // 0.0125 WBTC from maker 1 + vec![U256::from(1250000u64)], // 0.0125 WBTC from maker 2 + ], + receiver: Address::from([0x44; 20]), + packed_commands: U256::from(0), + flags: U256::from(0), + }; + + let quote_data = aggregate_order.abi_encode(); + // For aggregate orders with ECDSA, use 65-byte signatures (2 makers) let sig1 = hex::decode("0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001").unwrap(); let sig2 = hex::decode("1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111101").unwrap(); - let signature = [sig1, sig2].concat(); + + // Build ABI-encoded MakerSignature[] array with 2 signatures + let flags = U256::from(signature_type); + let mut encoded_maker_sigs = Vec::new(); + + // Offset to array (always 0x20 for single parameter) + encoded_maker_sigs.extend_from_slice(&U256::from(32).to_be_bytes::<32>()); + + // Array length (2 for Aggregate order with 2 makers) + encoded_maker_sigs.extend_from_slice(&U256::from(2).to_be_bytes::<32>()); + + // Offsets to structs (relative to array data start) + // First struct at offset 64 (after the 2 offset values) + encoded_maker_sigs.extend_from_slice(&U256::from(64).to_be_bytes::<32>()); + // Second struct will be after first struct's data + // First struct size: 64 (fixed) + 32 (length) + 96 (65 bytes padded) = 192 + encoded_maker_sigs.extend_from_slice(&U256::from(64 + 192).to_be_bytes::<32>()); + + // First struct data (MakerSignature has signatureBytes first, then flags) + // - offset to signatureBytes (always 64) + encoded_maker_sigs.extend_from_slice(&U256::from(64).to_be_bytes::<32>()); + // - flags (AFTER the offset) + encoded_maker_sigs.extend_from_slice(&flags.to_be_bytes::<32>()); + // - signatureBytes length + encoded_maker_sigs.extend_from_slice(&U256::from(sig1.len()).to_be_bytes::<32>()); + // - signatureBytes data (padded) + encoded_maker_sigs.extend_from_slice(&sig1); + let padding1 = (32 - (sig1.len() % 32)) % 32; + encoded_maker_sigs.extend_from_slice(&vec![0u8; padding1]); + + // Second struct data (MakerSignature has signatureBytes first, then flags) + // - offset to signatureBytes (always 64) + encoded_maker_sigs.extend_from_slice(&U256::from(64).to_be_bytes::<32>()); + // - flags (AFTER the offset) + encoded_maker_sigs.extend_from_slice(&flags.to_be_bytes::<32>()); + // - signatureBytes length + encoded_maker_sigs.extend_from_slice(&U256::from(sig2.len()).to_be_bytes::<32>()); + // - signatureBytes data (padded) + encoded_maker_sigs.extend_from_slice(&sig2); + let padding2 = (32 - (sig2.len() % 32)) % 32; + encoded_maker_sigs.extend_from_slice(&vec![0u8; padding2]); + + let filled_taker_amount = U256::from(1000000u64); // 1 USDC (6 decimals) let mut user_data = Vec::new(); user_data.push(order_type); - user_data.push(signature_type); + user_data.extend_from_slice(&filled_taker_amount.to_be_bytes::<32>()); user_data.extend_from_slice(&(quote_data.len() as u32).to_be_bytes()); user_data.extend_from_slice("e_data); - user_data.extend_from_slice(&(signature.len() as u32).to_be_bytes()); - user_data.extend_from_slice(&signature); + user_data.extend_from_slice(&encoded_maker_sigs); let bebop_component = ProtocolComponent { id: String::from("bebop-rfq"), @@ -2066,33 +2312,71 @@ mod tests { .unwrap(); let hex_swap = encode(&encoded_swap); - assert_eq!( - hex_swap, - String::from(concat!( - // token in - "6b175474e89094c44da98b954eedeac495271d0f", - // token out - "2260fac5e5542a773aa44fbcfedf7c193bc2c599", - // transfer type TransferFrom - "00", - // order type Aggregate (2) - "02", - // quote data length (4 bytes = 0x00000004) - "00000004", - // quote data - "deadbeef", - // signature type EIP712 (1) - "01", - // signature length (130 bytes = 2 * 65-byte EIP712 signatures) - "00000082", - // concatenated EIP712 signatures (2 * 65 bytes) - "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", - "1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111101", - // approval needed - "01" - )) + // Calculate expected hex for ABI-encoded maker signatures array with 2 signatures + let _expected_abi_maker_sigs = concat!( + // offset to array + "0000000000000000000000000000000000000000000000000000000000000020", + // array length (2) + "0000000000000000000000000000000000000000000000000000000000000002", + // offset to first struct (relative to array data start) + "0000000000000000000000000000000000000000000000000000000000000040", + // offset to second struct (64 + 192 = 256 = 0x100) + "0000000000000000000000000000000000000000000000000000000000000100", + // First struct data: + // - offset to signatureBytes + "0000000000000000000000000000000000000000000000000000000000000040", + // - flags + "0000000000000000000000000000000000000000000000000000000000000001", + // - signatureBytes length (65 = 0x41) + "0000000000000000000000000000000000000000000000000000000000000041", + // - signatureBytes (65 bytes padded to 96) + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0100000000000000000000000000000000000000000000000000000000000000", + // Second struct data: + // - offset to signatureBytes + "0000000000000000000000000000000000000000000000000000000000000040", + // - flags + "0000000000000000000000000000000000000000000000000000000000000001", + // - signatureBytes length (65 = 0x41) + "0000000000000000000000000000000000000000000000000000000000000041", + // - signatureBytes (65 bytes padded to 96) + "1111111111111111111111111111111111111111111111111111111111111111", + "1111111111111111111111111111111111111111111111111111111111111111", + "0100000000000000000000000000000000000000000000000000000000000000" ); + // The quote data is now an ABI-encoded BebopAggregate struct + // Calculate its actual length + let quote_data_hex = hex::encode("e_data); + let quote_data_length = format!("{:08x}", quote_data.len()); + + let expected_hex = format!( + "{}{}{}{}{}{}{}{}{}{}", + // token in + "6b175474e89094c44da98b954eedeac495271d0f", + // token out + "2260fac5e5542a773aa44fbcfedf7c193bc2c599", + // transfer type TransferFrom + "00", + // order type Aggregate (1) + "01", + // filledTakerAmount (1000000 = 0x0f4240) + "00000000000000000000000000000000000000000000000000000000000f4240", + // quote data length + "e_data_length, + // quote data + "e_data_hex, + // maker signatures length (0x200 = 512 bytes) + "00000200", + // abi-encoded maker signatures + &encode(&encoded_maker_sigs), + // approval needed + "01" + ); + + assert_eq!(hex_swap, expected_hex); + write_calldata_to_file("test_encode_bebop_aggregate", hex_swap.as_str()); } } diff --git a/src/encoding/evm/tycho_encoders.rs b/src/encoding/evm/tycho_encoders.rs index 043a8fd..f6d103b 100644 --- a/src/encoding/evm/tycho_encoders.rs +++ b/src/encoding/evm/tycho_encoders.rs @@ -1261,20 +1261,91 @@ mod tests { models::BebopOrderType, }; - /// Helper function to build Bebop user_data + /// Builds Bebop user_data with support for single or multiple signatures + /// + /// # Arguments + /// * `order_type` - The type of Bebop order (Single or Aggregate) + /// * `filled_taker_amount` - Amount to fill (0 means fill entire order) + /// * `quote_data` - The ABI-encoded order data + /// * `signatures` - Vector of (signature_bytes, signature_type) tuples + /// - For Single orders: expects exactly 1 signature + /// - For Aggregate orders: expects 1 or more signatures (one per maker) fn build_bebop_user_data( order_type: BebopOrderType, - signature_type: u8, + filled_taker_amount: U256, quote_data: &[u8], - signature: &[u8], + signatures: Vec<(Vec, u8)>, // (signature, signature_type) ) -> Bytes { + // ABI encode MakerSignature[] array + // Format: offset_to_array | array_length | [offset_to_struct_i]... | [struct_i_data]... + let mut encoded_maker_sigs = Vec::new(); + + // Calculate total size needed + let array_offset = 32; // offset to array start + let array_length_size = 32; + let struct_offsets_size = 32 * signatures.len(); + let _header_size = array_length_size + struct_offsets_size; + + // Build each struct's data and calculate offsets + let mut struct_data = Vec::new(); + let mut struct_offsets = Vec::new(); + // Offsets are relative to the start of array data, not the absolute position + // Array data starts after array length, so first offset is after all offset values + let mut current_offset = struct_offsets_size; // Just the space for offsets, not including array length + + for (signature, signature_type) in &signatures { + struct_offsets.push(current_offset); + + // Each struct contains: + // - offset to signatureBytes (32 bytes) - always 0x40 (64) + // - flags (32 bytes) + // - signatureBytes length (32 bytes) + // - signatureBytes data (padded to 32 bytes) + let mut struct_bytes = Vec::new(); + + // Offset to signatureBytes within this struct + struct_bytes.extend_from_slice(&U256::from(64).to_be_bytes::<32>()); + + // Flags (contains signature type) - AFTER the offset, not before! + let flags = U256::from(*signature_type); + struct_bytes.extend_from_slice(&flags.to_be_bytes::<32>()); + + // SignatureBytes length + struct_bytes.extend_from_slice(&U256::from(signature.len()).to_be_bytes::<32>()); + + // SignatureBytes data (padded to 32 byte boundary) + struct_bytes.extend_from_slice(signature); + let padding = (32 - (signature.len() % 32)) % 32; + struct_bytes.extend_from_slice(&vec![0u8; padding]); + + current_offset += struct_bytes.len(); + struct_data.push(struct_bytes); + } + + // Build the complete ABI encoded array + // Offset to array (always 0x20 for a single parameter) + encoded_maker_sigs.extend_from_slice(&U256::from(array_offset).to_be_bytes::<32>()); + + // Array length + encoded_maker_sigs.extend_from_slice(&U256::from(signatures.len()).to_be_bytes::<32>()); + + // Struct offsets (relative to start of array data) + for offset in struct_offsets { + encoded_maker_sigs.extend_from_slice(&U256::from(offset).to_be_bytes::<32>()); + } + + // Struct data + for data in struct_data { + encoded_maker_sigs.extend_from_slice(&data); + } + + // Build complete user_data let mut user_data = Vec::new(); user_data.push(order_type as u8); - user_data.push(signature_type); + user_data.extend_from_slice(&filled_taker_amount.to_be_bytes::<32>()); user_data.extend_from_slice(&(quote_data.len() as u32).to_be_bytes()); user_data.extend_from_slice(quote_data); - user_data.extend_from_slice(&(signature.len() as u32).to_be_bytes()); - user_data.extend_from_slice(signature); + user_data.extend_from_slice(&encoded_maker_sigs); Bytes::from(user_data) } @@ -2375,7 +2446,7 @@ mod tests { 3600; // Current time + 1 hour let taker_address = Address::ZERO; let maker_address = - Address::from_str("0xbbbbbBB520d69a9775E85b458C58c648259FAD5F").unwrap(); + Address::from_str("0x1234567890123456789012345678901234567890").unwrap(); // Use a proper maker address let maker_nonce = 1u64; let taker_token = Address::from_str(&usdc.to_string()).unwrap(); let maker_token = Address::from_str(&dai.to_string()).unwrap(); @@ -2408,9 +2479,9 @@ mod tests { // Build user_data with the quote and signature let user_data = build_bebop_user_data( BebopOrderType::Single, - 1, // EIP712 signature type + U256::from(0), // 0 means fill entire order "e_data, - &signature, + vec![(signature, 1)], // EIP712 signature type ); let bebop_component = ProtocolComponent { @@ -3759,30 +3830,30 @@ mod tests { #[test] fn test_single_encoding_strategy_bebop() { - // USDC -> (Bebop RFQ) -> WETH + // Use the same mainnet data from Solidity tests + // Transaction: https://etherscan.io/tx/0x6279bc970273b6e526e86d9b69133c2ca1277e697ba25375f5e6fc4df50c0c94 let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC - let token_out = Bytes::from("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); // WETH - let amount_in = BigUint::from_str("1000_000000").unwrap(); // 1000 USDC - let amount_out = BigUint::from_str("400000000000000000").unwrap(); // 0.4 WETH + let token_out = Bytes::from("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3"); // ONDO + let amount_in = BigUint::from_str("200000000").unwrap(); // 200 USDC + let amount_out = BigUint::from_str("237212396774431060000").unwrap(); // 237.21 ONDO - // Create a valid Bebop Single order struct that matches IBebopSettlement.Single - // The mock settlement expects this exact structure - let expiry = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() + - 3600; // Current time + 1 hour - let taker_address = Address::ZERO; + // Create the exact same order from mainnet + let expiry = 1749483840u64; + let taker_address = + Address::from_str("0xc5564C13A157E6240659fb81882A28091add8670").unwrap(); let maker_address = - Address::from_str("0xbbbbbBB520d69a9775E85b458C58c648259FAD5F").unwrap(); - let maker_nonce = 1u64; + Address::from_str("0xCe79b081c0c924cb67848723ed3057234d10FC6b").unwrap(); + let maker_nonce = 1749483765992417u64; let taker_token = Address::from_str(&token_in.to_string()).unwrap(); let maker_token = Address::from_str(&token_out.to_string()).unwrap(); let taker_amount = U256::from_str(&amount_in.to_string()).unwrap(); let maker_amount = U256::from_str(&amount_out.to_string()).unwrap(); - let receiver = - Address::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(); // Alice's address - the actual receiver + let receiver = taker_address; let packed_commands = U256::ZERO; + let flags = U256::from_str( + "51915842898789398998206002334703507894664330885127600393944965515693155942400", + ) + .unwrap(); // Encode using standard ABI encoding (not packed) let quote_data = ( @@ -3796,18 +3867,19 @@ mod tests { maker_amount, receiver, packed_commands, - U256::from(0u64), // flags as uint256 + flags, ) .abi_encode(); - let signature = hex::decode("aabbccdd").unwrap(); + // Real signature from mainnet + let signature = hex::decode("eb5419631614978da217532a40f02a8f2ece37d8cfb94aaa602baabbdefb56b474f4c2048a0f56502caff4ea7411d99eed6027cd67dc1088aaf4181dcb0df7051c").unwrap(); // Build user_data with the quote and signature let user_data = build_bebop_user_data( BebopOrderType::Single, - 1, // EIP712 signature type + U256::ZERO, // 0 means fill entire order "e_data, - &signature, + vec![(signature, 0)], // ETH_SIGN signature type ); let bebop_component = ProtocolComponent { @@ -3833,9 +3905,9 @@ mod tests { given_amount: amount_in, checked_token: token_out, checked_amount: amount_out, // Expected output amount - // Alice - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2") + // Use the original taker address + sender: Bytes::from_str("0xc5564C13A157E6240659fb81882A28091add8670").unwrap(), + receiver: Bytes::from_str("0xc5564C13A157E6240659fb81882A28091add8670") .unwrap(), swaps: vec![swap], ..Default::default() @@ -3863,122 +3935,6 @@ mod tests { ); } - #[test] - fn test_single_encoding_strategy_bebop_multi() { - // For Multi orders, we'll demonstrate a single-token-in, multi-token-out scenario - // WETH -> USDC + WBTC - let token_in = Bytes::from("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); // WETH - let token_out_1 = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC - let token_out_2 = Bytes::from("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"); // WBTC - - let amount_in = BigUint::from_str("1000000000000000000").unwrap(); // 1 WETH - let amount_out = BigUint::from_str("3000000000").unwrap(); // 3000 USDC (primary output) - - // Create a valid Bebop Multi order struct - let expiry = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() + - 3600; - let taker_address = Address::ZERO; - let maker_address = - Address::from_str("0xbbbbbBB520d69a9775E85b458C58c648259FAD5F").unwrap(); - let maker_nonce = 2u64; - - // Multi order: single taker token, multiple maker tokens - let taker_tokens = vec![Address::from_str(&token_in.to_string()).unwrap()]; - let maker_tokens = vec![ - Address::from_str(&token_out_1.to_string()).unwrap(), - Address::from_str(&token_out_2.to_string()).unwrap(), - ]; - let taker_amounts = vec![U256::from_str(&amount_in.to_string()).unwrap()]; - let maker_amounts = vec![ - U256::from_str(&amount_out.to_string()).unwrap(), - U256::from_str("10000000").unwrap(), // 0.1 WBTC - ]; - let receiver = - Address::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(); // Alice - let packed_commands = U256::ZERO; - - // Encode Multi order - let quote_data = ( - expiry, - taker_address, - maker_address, - maker_nonce, - taker_tokens, - maker_tokens, - taker_amounts, - maker_amounts, - receiver, - packed_commands, - U256::from(0u64), // flags - ) - .abi_encode(); - - let signature = hex::decode("11223344").unwrap(); - - let user_data = build_bebop_user_data( - BebopOrderType::Multi, - 1, // EIP712 signature type - "e_data, - &signature, - ); - - let bebop_component = ProtocolComponent { - id: String::from("bebop-rfq"), - protocol_system: String::from("rfq:bebop"), - static_attributes: HashMap::new(), - ..Default::default() - }; - - let swap = Swap { - component: bebop_component, - token_in: token_in.clone(), - token_out: token_out_1.clone(), // Primary output token - split: 0f64, - user_data: Some(user_data), - }; - - let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); - - let solution = Solution { - exact_out: false, - given_token: token_in, - given_amount: amount_in, - checked_token: token_out_1, - checked_amount: amount_out, - // Alice - sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), - receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2") - .unwrap(), - swaps: vec![swap], - ..Default::default() - }; - - let encoded_solution = encoder - .encode_solutions(vec![solution.clone()]) - .unwrap()[0] - .clone(); - - let calldata = encode_tycho_router_call( - eth_chain().id, - encoded_solution, - &solution, - UserTransferType::TransferFrom, - eth(), - None, - ) - .unwrap() - .data; - let hex_calldata = hex::encode(&calldata); - - write_calldata_to_file( - "test_single_encoding_strategy_bebop_multi", - hex_calldata.as_str(), - ); - } - #[test] fn test_single_encoding_strategy_bebop_aggregate() { // For simplicity, let's use the same tokens as the Single test but with Aggregate @@ -4036,19 +3992,22 @@ mod tests { ) .abi_encode(); - // For aggregate orders with ECDSA signatures, use concatenated 65-byte signatures - // This example has 2 makers, so 2x 65-byte signatures = 130 bytes + // For aggregate orders with multiple makers, we need multiple signatures + // This example has 2 makers, so 2 separate 65-byte signatures let sig1 = hex::decode("1b47a665f9a5e14b5208015d11f9143e27b93dc5a0d8c892ec5326eda1e5df3c42a987d0b2ea5b8be8f0e5c326bd4ec0321b10c6e9b4e5f8a0b8d5e6f7c8a9b01b").unwrap(); let sig2 = hex::decode("2c58b665f9a5e14b5208015d11f9143e27b93dc5a0d8c892ec5326eda1e5df3c53b987d0b2ea5b8be8f0e5c326bd4ec0321b10c6e9b4e5f8a0b8d5e6f7c8a9b01c").unwrap(); - let mut signature = Vec::new(); - signature.extend_from_slice(&sig1); - signature.extend_from_slice(&sig2); + + // Build user_data with multiple signatures for the aggregate order + let signatures = vec![ + (sig1, 1u8), // EIP712 signature type for maker 1 + (sig2, 1u8), // EIP712 signature type for maker 2 + ]; let user_data = build_bebop_user_data( BebopOrderType::Aggregate, - 1, // EIP712 signature type + U256::from(0), // 0 means fill entire aggregate order "e_data, - &signature, + signatures, ); let bebop_component = ProtocolComponent { diff --git a/src/encoding/models.rs b/src/encoding/models.rs index 598e5c7..ee64a70 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -214,8 +214,7 @@ pub enum TransferType { #[derive(Clone, Copy, Debug, PartialEq)] pub enum BebopOrderType { Single = 0, - Multi = 1, - Aggregate = 2, + Aggregate = 1, } impl TryFrom for BebopOrderType { @@ -224,8 +223,7 @@ impl TryFrom for BebopOrderType { fn try_from(value: u8) -> Result { match value { 0 => Ok(BebopOrderType::Single), - 1 => Ok(BebopOrderType::Multi), - 2 => Ok(BebopOrderType::Aggregate), + 1 => Ok(BebopOrderType::Aggregate), _ => Err(EncodingError::InvalidInput(format!("Invalid Bebop order type: {}", value))), } }