chore: merge main

This commit is contained in:
TAMARA LIPOWSKI
2025-08-21 14:31:05 -04:00
45 changed files with 3514 additions and 854 deletions

View File

@@ -1,3 +1,101 @@
## [0.119.0](https://github.com/propeller-heads/tycho-execution/compare/0.118.0...0.119.0) (2025-08-21)
### Features
* Add BytecodeExporter to TestUtils ([cea964e](https://github.com/propeller-heads/tycho-execution/commit/cea964e0a54a75b1bd89007794daf467b812f021))
## [0.118.0](https://github.com/propeller-heads/tycho-execution/compare/0.117.0...0.118.0) (2025-08-21)
### Features
* Use Arc instead of & for protocol_state in Swap ([a2a5319](https://github.com/propeller-heads/tycho-execution/commit/a2a53195edbc631bea8fbc758a3bf5ee33a87dda))
### Bug Fixes
* Updates to new datatype after merging main ([e9c1a39](https://github.com/propeller-heads/tycho-execution/commit/e9c1a39608bba786bbed2b53c4596e94ddcbe03c))
## [0.117.0](https://github.com/propeller-heads/tycho-execution/compare/0.116.0...0.117.0) (2025-08-21)
### Features
* implement `SwapEncoder` for `Hashflow` ([93db953](https://github.com/propeller-heads/tycho-execution/commit/93db953c620f4d52e8852ff8148f2dfdbc580029))
### Bug Fixes
* in bebop's encode_swap, fail early if router address is not present ([c013bf7](https://github.com/propeller-heads/tycho-execution/commit/c013bf707225b171e76f3f3dac88624b6a3458dc))
* in hashflow's encode_swap, fail early if router address is not present ([c506f2c](https://github.com/propeller-heads/tycho-execution/commit/c506f2c048a20d1ffd066d2903d2cc469fd167ed))
* in HashflowExecutor, _balanceOf must use `trader` address instead of the executor's to get the balance ([a09d648](https://github.com/propeller-heads/tycho-execution/commit/a09d648f3c50f87b392c7c5eb14af07307c5ccea))
## [0.116.0](https://github.com/propeller-heads/tycho-execution/compare/0.115.0...0.116.0) (2025-08-19)
### Features
* Bebop executor in base ([c974312](https://github.com/propeller-heads/tycho-execution/commit/c974312a377d950948e76dd1dbb3303421f9f5f8))
* Support Bebop quote through IndicativelyPriced state ([2292061](https://github.com/propeller-heads/tycho-execution/commit/22920617ebca35199a8cc927245cfa7e6209ade8))
* Update BebopExecutor deployed address ([72b60aa](https://github.com/propeller-heads/tycho-execution/commit/72b60aa324d136ab999ff04339433a83743aee5e))
### Bug Fixes
* Add bebop to protocol specific addresses ([6e68d19](https://github.com/propeller-heads/tycho-execution/commit/6e68d199c90395a9204329e462e5027e81e61171))
* Implement From<SimulationError> for EncodingError ([987956e](https://github.com/propeller-heads/tycho-execution/commit/987956eb0bea2a46115d160ea05db395ce17077a))
## [0.115.0](https://github.com/propeller-heads/tycho-execution/compare/0.114.0...0.115.0) (2025-08-14)
### Features
* Deploy BebopExecutor ([f9ef8e0](https://github.com/propeller-heads/tycho-execution/commit/f9ef8e0dbdfb73db132f4932911fb5a666f6fa25))
* implement encoder and executor with single order support ([7e7e3f0](https://github.com/propeller-heads/tycho-execution/commit/7e7e3f0e0187adc2f7966fd108e9edcb5e05f20f))
* support multi and aggregate orders ([ad0a999](https://github.com/propeller-heads/tycho-execution/commit/ad0a9991da57348a026bc43d3ca230898822614e))
### Bug Fixes
* After merge fixes with SwapBuilder ([d301dfc](https://github.com/propeller-heads/tycho-execution/commit/d301dfcc44575157997d95a571633ef4e659a197))
* After merging main fixes ([003ab7c](https://github.com/propeller-heads/tycho-execution/commit/003ab7cea82fec67dbd54bb1248f75e8c7c74c50))
* **Bebop:** Fix AggregateOrder tests ([7024da3](https://github.com/propeller-heads/tycho-execution/commit/7024da395d9d959fc9edecc398f8a161f76d4de1))
* **Bebop:** Fix encoding and tests ([e793478](https://github.com/propeller-heads/tycho-execution/commit/e79347842f74e05b078d3b2da4f6fad099a6927d))
* cargo and forge fmt ([17e5354](https://github.com/propeller-heads/tycho-execution/commit/17e535494b5f2f211eef889c768f05f00843dc5b))
* Fix test imports ([210d4fa](https://github.com/propeller-heads/tycho-execution/commit/210d4fa604de7610ce01fd918e57970dd5498e6b))
* Move Bebop tests according to new setup ([01ab5d2](https://github.com/propeller-heads/tycho-execution/commit/01ab5d22b182072a65221f620df7fade3ab6fe7a))
* SafeCast => V4SafeCast to fix build issue ([946c439](https://github.com/propeller-heads/tycho-execution/commit/946c4391e825661126fc30e013ac95e87e904fdc))
* Simplify the BebopExecutor and fix Single tests ([76a09d0](https://github.com/propeller-heads/tycho-execution/commit/76a09d0402f34563a121f0973589b523ffdf3a8f))
## [0.114.0](https://github.com/propeller-heads/tycho-execution/compare/0.113.0...0.114.0) (2025-08-14)
### Features
* add hashflow executor ([e03ecf4](https://github.com/propeller-heads/tycho-execution/commit/e03ecf48d50f08b739a9d862b4746c3a055ea6e9))
## [0.113.0](https://github.com/propeller-heads/tycho-execution/compare/0.112.2...0.113.0) (2025-08-14)
### Features
* Add estimated_amount_in to Swap. Add SwapBuilder ([5eb9973](https://github.com/propeller-heads/tycho-execution/commit/5eb9973dbd5ca4eb2f4d15c3f4803b4bc7dfa1ea))
## [0.112.2](https://github.com/propeller-heads/tycho-execution/compare/0.112.1...0.112.2) (2025-08-07)
### Bug Fixes
* Encode sequential swaps with unwrapping WETH correctly ([c42a5da](https://github.com/propeller-heads/tycho-execution/commit/c42a5dae6a55d315360eaaa510d9dcaf7ef8e4c0))
## [0.112.1](https://github.com/propeller-heads/tycho-execution/compare/0.112.0...0.112.1) (2025-07-31)
### Bug Fixes
* Rename mainnet to ethereum in txServiceUrls for Safe ([51d9484](https://github.com/propeller-heads/tycho-execution/commit/51d9484de0b4c75b78dc8ca839f4ec7df40f972e))
## [0.112.0](https://github.com/propeller-heads/tycho-execution/compare/0.111.0...0.112.0) (2025-07-22) ## [0.112.0](https://github.com/propeller-heads/tycho-execution/compare/0.111.0...0.112.0) (2025-07-22)

8
Cargo.lock generated
View File

@@ -4634,9 +4634,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "tycho-common" name = "tycho-common"
version = "0.78.2" version = "0.81.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af4c7e2c8e194a3e9dfc5911ff0ec273b7dd52acb71dfdcf21351dd78a49576" checksum = "68d44780891254a0155d1aaa765a8625b48672158b6e12ce4f06771e9d369ffa"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -4659,9 +4659,10 @@ dependencies = [
[[package]] [[package]]
name = "tycho-execution" name = "tycho-execution"
version = "0.112.0" version = "0.119.0"
dependencies = [ dependencies = [
"alloy", "alloy",
"async-trait",
"chrono", "chrono",
"clap", "clap",
"dotenv", "dotenv",
@@ -4676,6 +4677,7 @@ dependencies = [
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio", "tokio",
"tycho-common", "tycho-common",
"tycho-execution",
] ]
[[package]] [[package]]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "tycho-execution" name = "tycho-execution"
version = "0.112.0" version = "0.119.0"
edition = "2021" edition = "2021"
description = "Provides tools for encoding and executing swaps against Tycho router and protocol executors." description = "Provides tools for encoding and executing swaps against Tycho router and protocol executors."
repository = "https://github.com/propeller-heads/tycho-execution" repository = "https://github.com/propeller-heads/tycho-execution"
@@ -37,17 +37,19 @@ tokio = { version = "1.38.0", features = ["full"] }
chrono = "0.4.39" chrono = "0.4.39"
clap = { version = "4.5.3", features = ["derive"] } clap = { version = "4.5.3", features = ["derive"] }
once_cell = "1.20.2" once_cell = "1.20.2"
tycho-common = ">0.78.1" tycho-common = ">=0.81.6"
alloy = { version = "1.0.6", features = ["providers", "rpc-types-eth", "eip712", "signer-local", "node-bindings"], optional = true } alloy = { version = "1.0.6", features = ["providers", "rpc-types-eth", "eip712", "signer-local", "node-bindings"], optional = true }
async-trait = { version = "0.1.88", optional = true }
[dev-dependencies] [dev-dependencies]
rstest = "0.24.0" rstest = "0.24.0"
tycho-execution = { path = "../tycho-execution", features = ["test-utils"] }
[features] [features]
default = ["evm"] default = ["evm"]
evm = ["alloy"] evm = ["alloy"]
fork-tests = [] fork-tests = []
test-utils = ["async-trait"]
[profile.bench] [profile.bench]
debug = true debug = true

View File

@@ -10,12 +10,15 @@
"ekubo_v2": "0x263DD7AD20983b5E0392bf1F09C4493500EDb333", "ekubo_v2": "0x263DD7AD20983b5E0392bf1F09C4493500EDb333",
"vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165", "vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165",
"vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B", "vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B",
"vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec" "vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec",
"rfq:bebop": "0xFE42BFb115eD9671011cA52BDD23A52A2e077a7c",
"rfq:hashflow": "0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f"
}, },
"base": { "base": {
"uniswap_v2": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700", "uniswap_v2": "0xF744EBfaA580cF3fFc25aD046E92BD8B770a0700",
"uniswap_v3": "0x647bffbf8bd72bf6341ecba8b0279e090313a40d", "uniswap_v3": "0x647bffbf8bd72bf6341ecba8b0279e090313a40d",
"uniswap_v4_hooks": "0x7Dfa502736C7bd84DA1402F7524214215BC9534d" "uniswap_v4_hooks": "0x7Dfa502736C7bd84DA1402F7524214215BC9534d",
"rfq:bebop": "0x489A3f531dA3873D6585BF3f8E0dEE48CAC6F7BC"
}, },
"unichain": { "unichain": {
"uniswap_v2": "0x00C1b81e3C8f6347E69e2DDb90454798A6Be975E", "uniswap_v2": "0x00C1b81e3C8f6347E69e2DDb90454798A6Be975E",

View File

@@ -5,8 +5,20 @@
}, },
"vm:curve": { "vm:curve": {
"native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" "native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
},
"rfq:bebop": {
"bebop_settlement_address": "0xbbbbbBB520d69a9775E85b458C58c648259FAD5F",
"native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
},
"rfq:hashflow": {
"hashflow_router_address": "0x55084eE0fEf03f14a305cd24286359A35D735151"
}
},
"base": {
"rfq:bebop": {
"bebop_settlement_address": "0xbbbbbBB520d69a9775E85b458C58c648259FAD5F",
"native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
} }
}, },
"base": {},
"unichain": {} "unichain": {}
} }

View File

@@ -10,6 +10,8 @@
"ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598", "ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598",
"vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211", "vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211",
"vm:maverick_v2": "0xA4AD4f68d0b91CFD19687c881e50f3A00242828c", "vm:maverick_v2": "0xA4AD4f68d0b91CFD19687c881e50f3A00242828c",
"vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb" "vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb",
"rfq:bebop": "0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF",
"rfq:hashflow": "0x15cF58144EF33af1e14b5208015d11F9143E27b9"
} }
} }

Binary file not shown.

View File

@@ -7,7 +7,7 @@ use tycho_common::{
}; };
use tycho_execution::encoding::{ use tycho_execution::encoding::{
evm::encoder_builders::TychoRouterEncoderBuilder, evm::encoder_builders::TychoRouterEncoderBuilder,
models::{Solution, Swap, UserTransferType}, models::{Solution, Swap, SwapBuilder, UserTransferType},
}; };
fn main() { fn main() {
@@ -44,6 +44,7 @@ fn main() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
// Then we create a solution object with the previous swap // Then we create a solution object with the previous swap
@@ -87,56 +88,51 @@ fn main() {
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f")
.expect("Failed to create DAI address"); .expect("Failed to create DAI address");
let swap_weth_dai = Swap { let swap_weth_dai = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.5f64, )
user_data: None, .split(0.5)
protocol_state: None, .build();
};
let swap_weth_wbtc = Swap { // Split 0 represents the remaining 50%, but to avoid any rounding errors we set this to
component: ProtocolComponent { // 0 to signify "the remainder of the WETH value". It should still be very close to 50%
let swap_weth_wbtc = SwapBuilder::new(
ProtocolComponent {
id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(), id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: wbtc.clone(), wbtc.clone(),
// This represents the remaining 50%, but to avoid any rounding errors we set this to )
// 0 to signify "the remainder of the WETH value". It should still be very close to 50% .build();
split: 0f64,
user_data: None, let swap_dai_usdc = SwapBuilder::new(
protocol_state: None, ProtocolComponent {
};
let swap_dai_usdc = Swap {
component: ProtocolComponent {
id: "0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5".to_string(), id: "0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai.clone(), dai.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None, let swap_wbtc_usdc = SwapBuilder::new(
}; ProtocolComponent {
let swap_wbtc_usdc = Swap {
component: ProtocolComponent {
id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(), id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: wbtc.clone(), wbtc.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None,
};
let mut complex_solution = solution.clone(); let mut complex_solution = solution.clone();
complex_solution.swaps = vec![swap_weth_dai, swap_weth_wbtc, swap_dai_usdc, swap_wbtc_usdc]; complex_solution.swaps = vec![swap_weth_dai, swap_weth_wbtc, swap_dai_usdc, swap_wbtc_usdc];

View File

@@ -83,6 +83,7 @@ fn main() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let swap_usdc_usdt = Swap { let swap_usdc_usdt = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -101,6 +102,7 @@ fn main() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
// Then we create a solution object with the previous swap // Then we create a solution object with the previous swap

View File

@@ -7,7 +7,7 @@ evm_version = 'cancun'
optimizer = true optimizer = true
optimizer_runs = 200 optimizer_runs = 200
via_ir = true via_ir = true
fs_permissions = [{ access = "read", path = "./test/assets" }] fs_permissions = [{ access = "read", path = "./test/assets" }, { access = "write", path = "./test" }]
[profile.production] [profile.production]
src = 'src' src = 'src'

View File

@@ -81,6 +81,11 @@ const executors_to_deploy = {
}, },
// Args: Permit2 // Args: Permit2
{exchange: "BalancerV3Executor", args: ["0x000000000022D473030F116dDEE9F6B43aC78BA3"]}, {exchange: "BalancerV3Executor", args: ["0x000000000022D473030F116dDEE9F6B43aC78BA3"]},
// Args: Bebop Settlement contract, Permit2
{
exchange: "BebopExecutor",
args: ["0xbbbbbBB520d69a9775E85b458C58c648259FAD5F", "0x000000000022D473030F116dDEE9F6B43aC78BA3"]
},
], ],
"base": [ "base": [
// Args: Factory, Pool Init Code Hash, Permit2, Fee BPS // Args: Factory, Pool Init Code Hash, Permit2, Fee BPS
@@ -116,6 +121,11 @@ const executors_to_deploy = {
"0x000000000022D473030F116dDEE9F6B43aC78BA3" "0x000000000022D473030F116dDEE9F6B43aC78BA3"
] ]
}, },
// Args: Bebop Settlement contract, Permit2
{
exchange: "BebopExecutor",
args: ["0xbbbbbBB520d69a9775E85b458C58c648259FAD5F", "0x000000000022D473030F116dDEE9F6B43aC78BA3"]
},
], ],
"unichain": [ "unichain": [
// Args: Factory, Pool Init Code Hash, Permit2, Fee BPS // Args: Factory, Pool Init Code Hash, Permit2, Fee BPS

View File

@@ -0,0 +1,179 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "@interfaces/IExecutor.sol";
import "../RestrictTransferFrom.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import {
IERC20,
SafeERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";
/// @title BebopExecutor
/// @notice Executor for Bebop PMM RFQ (Request for Quote) swaps
/// @dev Handles Single and Aggregate RFQ swaps through Bebop settlement contract
/// @dev Only supports single token in to single token out swaps
contract BebopExecutor is IExecutor, RestrictTransferFrom {
using Math for uint256;
using SafeERC20 for IERC20;
using Address for address;
/// @notice Bebop-specific errors
error BebopExecutor__InvalidDataLength();
error BebopExecutor__ZeroAddress();
/// @notice The Bebop settlement contract address
address public immutable bebopSettlement;
constructor(address _bebopSettlement, address _permit2)
RestrictTransferFrom(_permit2)
{
if (_bebopSettlement == address(0)) revert BebopExecutor__ZeroAddress();
bebopSettlement = _bebopSettlement;
}
/// @notice Executes a swap through Bebop's PMM RFQ system
/// @param givenAmount The amount of input token to swap
/// @param data Encoded swap data containing tokens and bebop calldata
/// @return calculatedAmount The amount of output token received
function swap(uint256 givenAmount, bytes calldata data)
external
payable
virtual
override
returns (uint256 calculatedAmount)
{
(
address tokenIn,
address tokenOut,
TransferType transferType,
uint8 partialFillOffset,
uint256 originalFilledTakerAmount,
bool approvalNeeded,
address receiver,
bytes memory bebopCalldata
) = _decodeData(data);
_transfer(address(this), transferType, address(tokenIn), givenAmount);
// Modify the filledTakerAmount in the calldata
// If the filledTakerAmount is the same as the original, the original calldata is returned
bytes memory finalCalldata = _modifyFilledTakerAmount(
bebopCalldata,
givenAmount,
originalFilledTakerAmount,
partialFillOffset
);
// Approve Bebop settlement to spend tokens if needed
if (approvalNeeded) {
// slither-disable-next-line unused-return
IERC20(tokenIn).forceApprove(bebopSettlement, type(uint256).max);
}
uint256 balanceBefore = _balanceOf(tokenOut, receiver);
uint256 ethValue = tokenIn == address(0) ? givenAmount : 0;
// Use OpenZeppelin's Address library for safe call with value
// This will revert if the call fails
// slither-disable-next-line unused-return
bebopSettlement.functionCallWithValue(finalCalldata, ethValue);
uint256 balanceAfter = _balanceOf(tokenOut, receiver);
calculatedAmount = balanceAfter - balanceBefore;
}
/// @dev Decodes the packed calldata
function _decodeData(bytes calldata data)
internal
pure
returns (
address tokenIn,
address tokenOut,
TransferType transferType,
uint8 partialFillOffset,
uint256 originalFilledTakerAmount,
bool approvalNeeded,
address receiver,
bytes memory bebopCalldata
)
{
// Need at least 95 bytes for the minimum fixed fields
// 20 + 20 + 1 + 1 (offset) + 32 (original amount) + 1 (approval) + 20 (receiver) = 95
if (data.length < 95) revert BebopExecutor__InvalidDataLength();
tokenIn = address(bytes20(data[0:20]));
tokenOut = address(bytes20(data[20:40]));
transferType = TransferType(uint8(data[40]));
partialFillOffset = uint8(data[41]);
originalFilledTakerAmount = uint256(bytes32(data[42:74]));
approvalNeeded = data[74] != 0;
receiver = address(bytes20(data[75:95]));
bebopCalldata = data[95:];
}
/// @dev Modifies the filledTakerAmount in the bebop calldata to handle slippage
/// @param bebopCalldata The original calldata for the bebop settlement
/// @param givenAmount The actual amount available from the router
/// @param originalFilledTakerAmount The original amount expected when the quote was generated
/// @param partialFillOffset The offset from Bebop API indicating where filledTakerAmount is located
/// @return The modified calldata with updated filledTakerAmount
function _modifyFilledTakerAmount(
bytes memory bebopCalldata,
uint256 givenAmount,
uint256 originalFilledTakerAmount,
uint8 partialFillOffset
) internal pure returns (bytes memory) {
// Use the offset from Bebop API to locate filledTakerAmount
// Position = 4 bytes (selector) + offset * 32 bytes
uint256 filledTakerAmountPos = 4 + uint256(partialFillOffset) * 32;
// Cap the fill amount at what we actually have available
uint256 newFilledTakerAmount = originalFilledTakerAmount > givenAmount
? givenAmount
: originalFilledTakerAmount;
// If the new filledTakerAmount is the same as the original, return the original calldata
if (newFilledTakerAmount == originalFilledTakerAmount) {
return bebopCalldata;
}
// Use assembly to modify the filledTakerAmount at the correct position
// slither-disable-next-line assembly
assembly {
// Get pointer to the data portion of the bytes array
let dataPtr := add(bebopCalldata, 0x20)
// Calculate the actual position and store the new value
let actualPos := add(dataPtr, filledTakerAmountPos)
mstore(actualPos, newFilledTakerAmount)
}
return bebopCalldata;
}
/// @dev Returns the balance of a token or ETH for an account
/// @param token The token address, or address(0) for ETH
/// @param account The account to get the balance of
/// @return balance The balance of the token or ETH for the account
function _balanceOf(address token, address account)
internal
view
returns (uint256)
{
return token == address(0)
? account.balance
: IERC20(token).balanceOf(account);
}
/**
* @dev Allow receiving ETH for settlement calls that require ETH
* This is needed when the executor handles native ETH swaps
* In production, ETH typically comes from router or settlement contracts
* In tests, it may come from EOA addresses via the test harness
*/
receive() external payable {
// Allow ETH transfers for Bebop settlement functionality
}
}

View File

@@ -0,0 +1,130 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "../RestrictTransferFrom.sol";
import "@interfaces/IExecutor.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
error HashflowExecutor__InvalidHashflowRouter();
error HashflowExecutor__InvalidDataLength();
interface IHashflowRouter {
struct RFQTQuote {
address pool;
address externalAccount;
address trader;
address effectiveTrader;
address baseToken;
address quoteToken;
uint256 effectiveBaseTokenAmount;
uint256 baseTokenAmount;
uint256 quoteTokenAmount;
uint256 quoteExpiry;
uint256 nonce;
bytes32 txid;
bytes signature; // ECDSA signature of the quote, 65 bytes
}
function tradeRFQT(RFQTQuote calldata quote) external payable;
}
contract HashflowExecutor is IExecutor, RestrictTransferFrom {
using SafeERC20 for IERC20;
address public constant NATIVE_TOKEN =
0x0000000000000000000000000000000000000000;
/// @notice The Hashflow router address
address public immutable hashflowRouter;
constructor(address _hashflowRouter, address _permit2)
RestrictTransferFrom(_permit2)
{
if (_hashflowRouter == address(0)) {
revert HashflowExecutor__InvalidHashflowRouter();
}
hashflowRouter = _hashflowRouter;
}
function swap(uint256 givenAmount, bytes calldata data)
external
payable
returns (uint256 calculatedAmount)
{
(
IHashflowRouter.RFQTQuote memory quote,
bool approvalNeeded,
TransferType transferType
) = _decodeData(data);
// Slippage checks
if (givenAmount > quote.baseTokenAmount) {
// Do not transfer more than the quote's maximum permitted amount.
givenAmount = quote.baseTokenAmount;
}
quote.effectiveBaseTokenAmount = givenAmount;
if (approvalNeeded && quote.baseToken != NATIVE_TOKEN) {
// slither-disable-next-line unused-return
IERC20(quote.baseToken).forceApprove(
hashflowRouter, type(uint256).max
);
}
uint256 ethValue = 0;
if (quote.baseToken == NATIVE_TOKEN) {
ethValue = quote.effectiveBaseTokenAmount;
}
_transfer(
address(this), transferType, address(quote.baseToken), givenAmount
);
uint256 balanceBefore = _balanceOf(quote.trader, quote.quoteToken);
IHashflowRouter(hashflowRouter).tradeRFQT{value: ethValue}(quote);
uint256 balanceAfter = _balanceOf(quote.trader, quote.quoteToken);
calculatedAmount = balanceAfter - balanceBefore;
}
function _decodeData(bytes calldata data)
internal
pure
returns (
IHashflowRouter.RFQTQuote memory quote,
bool approvalNeeded,
TransferType transferType
)
{
if (data.length != 327) {
revert HashflowExecutor__InvalidDataLength();
}
transferType = TransferType(uint8(data[0]));
approvalNeeded = data[1] != 0;
quote.pool = address(bytes20(data[2:22]));
quote.externalAccount = address(bytes20(data[22:42]));
quote.trader = address(bytes20(data[42:62]));
// Assumes we never set the effectiveTrader when requesting a quote.
quote.effectiveTrader = quote.trader;
quote.baseToken = address(bytes20(data[62:82]));
quote.quoteToken = address(bytes20(data[82:102]));
// Not included in the calldata. Will be set in the swap function.
quote.effectiveBaseTokenAmount = 0;
quote.baseTokenAmount = uint256(bytes32(data[102:134]));
quote.quoteTokenAmount = uint256(bytes32(data[134:166]));
quote.quoteExpiry = uint256(bytes32(data[166:198]));
quote.nonce = uint256(bytes32(data[198:230]));
quote.txid = bytes32(data[230:262]);
quote.signature = data[262:327];
}
function _balanceOf(address trader, address token)
internal
view
returns (uint256 balance)
{
balance = token == NATIVE_TOKEN
? trader.balance
: IERC20(token).balanceOf(trader);
}
}

View File

@@ -19,7 +19,8 @@ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol"; import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
import {IUnlockCallback} from import {IUnlockCallback} from
"@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {SafeCast as V4SafeCast} from
"@uniswap/v4-core/src/libraries/SafeCast.sol";
import {TransientStateLibrary} from import {TransientStateLibrary} from
"@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
import "../RestrictTransferFrom.sol"; import "../RestrictTransferFrom.sol";
@@ -43,7 +44,7 @@ contract UniswapV4Executor is
{ {
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
using CurrencyLibrary for Currency; using CurrencyLibrary for Currency;
using SafeCast for *; using V4SafeCast for *;
using TransientStateLibrary for IPoolManager; using TransientStateLibrary for IPoolManager;
using LibPrefixLengthEncodedByteArray for bytes; using LibPrefixLengthEncodedByteArray for bytes;

View File

@@ -60,10 +60,9 @@ contract UniswapXFiller is AccessControl, IReactorCallback {
ResolvedOrder[] calldata resolvedOrders, ResolvedOrder[] calldata resolvedOrders,
bytes calldata callbackData bytes calldata callbackData
) external onlyRole(REACTOR_ROLE) { ) external onlyRole(REACTOR_ROLE) {
require( if (resolvedOrders.length != 1) {
resolvedOrders.length == 1, revert UniswapXFiller__BatchExecutionNotSupported();
UniswapXFiller__BatchExecutionNotSupported() }
);
ResolvedOrder memory order = resolvedOrders[0]; ResolvedOrder memory order = resolvedOrders[0];

View File

@@ -55,6 +55,7 @@ contract Constants is Test, BaseConstants {
address WTAO_ADDR = address(0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44); address WTAO_ADDR = address(0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44);
address BSGG_ADDR = address(0xdA16Cf041E2780618c49Dbae5d734B89a6Bac9b3); address BSGG_ADDR = address(0xdA16Cf041E2780618c49Dbae5d734B89a6Bac9b3);
address GHO_ADDR = address(0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f); address GHO_ADDR = address(0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f);
address ONDO_ADDR = address(0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3);
// Maverick v2 // Maverick v2
address MAVERICK_V2_FACTORY = 0x0A7e848Aca42d879EF06507Fca0E7b33A0a63c1e; address MAVERICK_V2_FACTORY = 0x0A7e848Aca42d879EF06507Fca0E7b33A0a63c1e;
@@ -124,6 +125,12 @@ contract Constants is Test, BaseConstants {
// Permit2 // Permit2
address PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; address PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
// Bebop Settlement
address BEBOP_SETTLEMENT = 0xbbbbbBB520d69a9775E85b458C58c648259FAD5F;
// Hashflow Router
address HASHFLOW_ROUTER = 0x55084eE0fEf03f14a305cd24286359A35D735151;
// Pool Code Init Hashes // Pool Code Init Hashes
bytes32 USV2_POOL_CODE_INIT_HASH = bytes32 USV2_POOL_CODE_INIT_HASH =
0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f; 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f;

View File

@@ -26,4 +26,26 @@ contract TestUtils is Test {
revert("Test calldata not found"); revert("Test calldata not found");
} }
/// @notice Export the runtime bytecode of a deployed contract to a JSON file.
/// @dev
/// This function captures the runtime bytecode (including immutables) of the deployed
/// contract at `contractAddr` and writes it to a JSON file under `test/<name>.runtime.json`.
/// The resulting file is intended to be used for SDK testing in another repository and
/// should be copied there. It **should not** be committed in this repository.
/// @param contractAddr The address of the deployed contract to extract runtime bytecode from.
/// @param contractName The base filename for the exported JSON file.
function exportRuntimeBytecode(
address contractAddr,
string memory contractName
) internal {
bytes memory runtime = contractAddr.code;
string memory hexCode = vm.toString(runtime);
string memory json =
string.concat('{"runtimeBytecode":"', hexCode, '"}');
string memory path =
string.concat("test/", contractName, ".runtime.json");
vm.writeFile(path, json);
}
} }

View File

@@ -3,6 +3,7 @@ pragma solidity ^0.8.26;
import "./TychoRouterTestSetup.sol"; import "./TychoRouterTestSetup.sol";
import "./protocols/UniswapV4Utils.sol"; import "./protocols/UniswapV4Utils.sol";
import "@src/executors/BebopExecutor.sol";
contract TychoRouterTestProtocolIntegration is TychoRouterTestSetup { contract TychoRouterTestProtocolIntegration is TychoRouterTestSetup {
function testMultiProtocolIntegration() public { function testMultiProtocolIntegration() public {

View File

@@ -492,4 +492,95 @@ contract TychoRouterSequentialSwapTest is TychoRouterTestSetup {
assertEq(balanceAfter - balanceBefore, 1949668893); assertEq(balanceAfter - balanceBefore, 1949668893);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0); assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
} }
function testSequentialSwapWithUnwrapIntegration() public {
// Performs a sequential swap from USDC to ETH through WBTC using USV2 pools and unwrapping in
// the end
deal(USDC_ADDR, ALICE, 3_000_000_000);
uint256 balanceBefore = ALICE.balance;
// Approve permit2
vm.startPrank(ALICE);
IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_sequential_swap_strategy_encoder_unwrap");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = ALICE.balance;
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 1404194006633772805);
}
}
contract TychoRouterSequentialSwapTestForBebop is TychoRouterTestSetup {
function getForkBlock() public pure override returns (uint256) {
return 23139046;
}
function testUSV3BebopIntegration() public {
// Performs a sequential swap from WETH to WBTC through USDC using USV3 and Bebop RFQ
//
// WETH ──(USV3)──> USDC ───(Bebop RFQ)──> WBTC
// The Bebop order expects:
// - 2021750881 USDC input -> 1672307 WBTC output
uint256 amountIn = 1 ether;
uint256 expectedAmountOut = 1672307;
deal(WETH_ADDR, BOB, amountIn);
uint256 balanceBefore = IERC20(WBTC_ADDR).balanceOf(BOB);
vm.startPrank(BOB);
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData = loadCallDataFromFile("test_uniswap_v3_bebop");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(WBTC_ADDR).balanceOf(BOB);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, expectedAmountOut);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
}
contract TychoRouterSequentialSwapTestForHashflow is TychoRouterTestSetup {
function getForkBlock() public pure override returns (uint256) {
return 23175437;
}
function testUSV3HashflowIntegration() public {
// Performs a sequential swap from WETH to WBTC through USDC using USV3 and Hashflow RFQ
//
// WETH ──(USV3)──> USDC ───(Hashflow RFQ)──> WBTC
// The Uniswap pool outputs:
// - 1 weth -> 4322430557 USDC
// The Hashflow tradeRFQT call expects:
// - 4308094737 USDC input -> 3714751 WBTC output
// The difference in USDC (14335820) will stay in the TychoRouter contract
uint256 amountIn = 1 ether;
uint256 expectedAmountOut = 3714751;
deal(WETH_ADDR, ALICE, amountIn);
uint256 balanceBefore = IERC20(WBTC_ADDR).balanceOf(ALICE);
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData = loadCallDataFromFile("test_uniswap_v3_hashflow");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(WBTC_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, expectedAmountOut);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 14335820);
}
} }

View File

@@ -4,8 +4,10 @@ pragma solidity ^0.8.26;
// Executors // Executors
import {BalancerV2Executor} from "../src/executors/BalancerV2Executor.sol"; import {BalancerV2Executor} from "../src/executors/BalancerV2Executor.sol";
import {BalancerV3Executor} from "../src/executors/BalancerV3Executor.sol"; import {BalancerV3Executor} from "../src/executors/BalancerV3Executor.sol";
import {BebopExecutor} from "../src/executors/BebopExecutor.sol";
import {CurveExecutor} from "../src/executors/CurveExecutor.sol"; import {CurveExecutor} from "../src/executors/CurveExecutor.sol";
import {EkuboExecutor} from "../src/executors/EkuboExecutor.sol"; import {EkuboExecutor} from "../src/executors/EkuboExecutor.sol";
import {HashflowExecutor} from "../src/executors/HashflowExecutor.sol";
import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol"; import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol";
import {UniswapV2Executor} from "../src/executors/UniswapV2Executor.sol"; import {UniswapV2Executor} from "../src/executors/UniswapV2Executor.sol";
import { import {
@@ -73,12 +75,14 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
CurveExecutor public curveExecutor; CurveExecutor public curveExecutor;
MaverickV2Executor public maverickv2Executor; MaverickV2Executor public maverickv2Executor;
BalancerV3Executor public balancerV3Executor; BalancerV3Executor public balancerV3Executor;
BebopExecutor public bebopExecutor;
HashflowExecutor public hashflowExecutor;
function getForkBlock() public view virtual returns (uint256) { function getForkBlock() public view virtual returns (uint256) {
return 22082754; return 22082754;
} }
function setUp() public { function setUp() public virtual {
uint256 forkBlock = getForkBlock(); uint256 forkBlock = getForkBlock();
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
@@ -132,8 +136,11 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
maverickv2Executor = maverickv2Executor =
new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS); new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS);
balancerV3Executor = new BalancerV3Executor(PERMIT2_ADDRESS); balancerV3Executor = new BalancerV3Executor(PERMIT2_ADDRESS);
bebopExecutor = new BebopExecutor(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
hashflowExecutor =
new HashflowExecutor(HASHFLOW_ROUTER, PERMIT2_ADDRESS);
address[] memory executors = new address[](9); address[] memory executors = new address[](11);
executors[0] = address(usv2Executor); executors[0] = address(usv2Executor);
executors[1] = address(usv3Executor); executors[1] = address(usv3Executor);
executors[2] = address(pancakev3Executor); executors[2] = address(pancakev3Executor);
@@ -143,6 +150,8 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
executors[6] = address(curveExecutor); executors[6] = address(curveExecutor);
executors[7] = address(maverickv2Executor); executors[7] = address(maverickv2Executor);
executors[8] = address(balancerV3Executor); executors[8] = address(balancerV3Executor);
executors[9] = address(bebopExecutor);
executors[10] = address(hashflowExecutor);
return executors; return executors;
} }

File diff suppressed because one or more lines are too long

View File

@@ -134,4 +134,9 @@ contract BalancerV2ExecutorTest is Constants, TestUtils {
assertGt(balanceAfter, balanceBefore); assertGt(balanceAfter, balanceBefore);
assertEq(balanceAfter - balanceBefore, amountOut); assertEq(balanceAfter - balanceBefore, amountOut);
} }
function testExportContract() public {
vm.skip(true);
exportRuntimeBytecode(address(balancerV2Exposed), "BalancerV2");
}
} }

View File

@@ -0,0 +1,470 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "../TestUtils.sol";
import "../TychoRouterTestSetup.sol";
import "@src/executors/BebopExecutor.sol";
import {Constants} from "../Constants.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Permit2TestHelper} from "../Permit2TestHelper.sol";
import {SafeERC20} from
"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract BebopExecutorExposed is BebopExecutor {
constructor(address _bebopSettlement, address _permit2)
BebopExecutor(_bebopSettlement, _permit2)
{}
function decodeData(bytes calldata data)
external
pure
returns (
address tokenIn,
address tokenOut,
TransferType transferType,
uint8 partialFillOffset,
uint256 originalFilledTakerAmount,
bool approvalNeeded,
address receiver,
bytes memory bebopCalldata
)
{
return _decodeData(data);
}
}
contract BebopExecutorTest is Constants, Permit2TestHelper, TestUtils {
using SafeERC20 for IERC20;
BebopExecutorExposed bebopExecutor;
IERC20 WETH = IERC20(WETH_ADDR);
IERC20 USDC = IERC20(USDC_ADDR);
IERC20 DAI = IERC20(DAI_ADDR);
IERC20 WBTC = IERC20(WBTC_ADDR);
IERC20 ONDO = IERC20(ONDO_ADDR);
IERC20 USDT = IERC20(USDT_ADDR);
function testDecodeData() public {
vm.createSelectFork(vm.rpcUrl("mainnet"), 22667985);
bebopExecutor =
new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
bytes memory bebopCalldata = abi.encodePacked(
bytes4(0x4dcebcba), // swapSingle selector
hex"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000068470140"
);
uint256 originalAmountIn = 200000000; // 200 USDC
bytes memory params = abi.encodePacked(
USDC_ADDR,
ONDO_ADDR,
uint8(RestrictTransferFrom.TransferType.Transfer),
uint8(2),
originalAmountIn,
true,
address(123),
bebopCalldata
);
(
address tokenIn,
address tokenOut,
RestrictTransferFrom.TransferType transferType,
uint8 decodedPartialFillOffset,
uint256 decodedOriginalAmountIn,
bool decodedApprovalNeeded,
address decodedReceiver,
bytes memory decodedBebopCalldata
) = bebopExecutor.decodeData(params);
assertEq(tokenIn, USDC_ADDR, "tokenIn mismatch");
assertEq(tokenOut, ONDO_ADDR, "tokenOut mismatch");
assertEq(
uint8(transferType),
uint8(RestrictTransferFrom.TransferType.Transfer),
"transferType mismatch"
);
assertEq(
keccak256(decodedBebopCalldata),
keccak256(bebopCalldata),
"bebopCalldata mismatch"
);
assertEq(decodedPartialFillOffset, 2, "partialFillOffset mismatch");
assertEq(
decodedOriginalAmountIn,
originalAmountIn,
"originalAmountIn mismatch"
);
assertTrue(decodedApprovalNeeded, "approvalNeeded should be true");
assertEq(decodedReceiver, address(123), "receiver mismatch");
}
// Single Order Tests
function testSingleOrder() public {
// 1 WETH -> WBTC
vm.createSelectFork(vm.rpcUrl("mainnet"), 23124275);
bebopExecutor =
new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Quote made manually using the BebopExecutor as the taker and receiver
bytes memory bebopCalldata =
hex"4dcebcba00000000000000000000000000000000000000000000000000000000689b137a0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000bee3211ab312a8d065c4fef0247448e17a8da000000000000000000000000000000000000000000000000000279ead5d9683d8a5000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000037337c0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000000000000000000000000000000000000000000000f71248bc6c123bbf12adc837470f75640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000418e9b0fb72ed9b86f7a7345026269c02b9056efcdfb67a377c7ff6c4a62a4807a7671ae759edf29aea1b2cb8efc8659e3aedac72943cd3607985a1849256358641c00000000000000000000000000000000000000000000000000000000000000";
address tokenIn = WETH_ADDR;
address tokenOut = WBTC_ADDR;
RestrictTransferFrom.TransferType transferType =
RestrictTransferFrom.TransferType.None;
uint8 partialFillOffset = 12;
uint256 amountIn = 1000000000000000000;
bool approvalNeeded = true;
uint256 expectedAmountOut = 3617660;
deal(tokenIn, address(bebopExecutor), amountIn);
bytes memory params = abi.encodePacked(
tokenIn,
tokenOut,
transferType,
partialFillOffset,
amountIn,
approvalNeeded,
address(bebopExecutor),
bebopCalldata
);
uint256 initialTokenOutBalance =
IERC20(tokenOut).balanceOf(address(bebopExecutor));
uint256 amountOut = bebopExecutor.swap(amountIn, params);
assertEq(amountOut, expectedAmountOut, "Incorrect amount out");
assertEq(
IERC20(tokenOut).balanceOf(address(bebopExecutor))
- initialTokenOutBalance,
expectedAmountOut,
"WBTC should be at receiver"
);
assertEq(
IERC20(tokenIn).balanceOf(address(bebopExecutor)),
0,
"WETH left in executor"
);
}
function testSingleOrderSellingETH() public {
// 1 WETH -> WBTC
vm.createSelectFork(vm.rpcUrl("mainnet"), 23124275);
bebopExecutor =
new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Quote made manually using the BebopExecutor as the taker and receiver
bytes memory bebopCalldata =
hex"4dcebcba00000000000000000000000000000000000000000000000000000000689ca0cd0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f0000000000000000000000000000000000000000000000002a65384e77863d8e000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000003a96a10000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000000000000000000000000000000000000000000001c6d9e514c7a64e5c0e239b532e1a3ea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041905d474b362c4a7c901c6a4ccb5c30670a0c602456f52761b47a0a35fc3944ec1fa224bc3bc6e8925cb15258efad2cf79e22ce9720f2302d4a1a2811c54fb4341c00000000000000000000000000000000000000000000000000000000000000";
address tokenIn = address(0);
address tokenOut = WBTC_ADDR;
RestrictTransferFrom.TransferType transferType =
RestrictTransferFrom.TransferType.None;
uint8 partialFillOffset = 12;
uint256 amountIn = 1000000000000000000;
bool approvalNeeded = false;
uint256 expectedAmountOut = 3839649;
vm.deal(address(bebopExecutor), amountIn);
bytes memory params = abi.encodePacked(
tokenIn,
tokenOut,
transferType,
partialFillOffset,
amountIn,
approvalNeeded,
address(bebopExecutor),
bebopCalldata
);
uint256 initialTokenOutBalance =
IERC20(tokenOut).balanceOf(address(bebopExecutor));
uint256 amountOut = bebopExecutor.swap(amountIn, params);
assertEq(amountOut, expectedAmountOut, "Incorrect amount out");
assertEq(
IERC20(tokenOut).balanceOf(address(bebopExecutor))
- initialTokenOutBalance,
expectedAmountOut,
"WBTC should be at receiver"
);
assertEq(address(bebopExecutor).balance, 0, "ETH left in executor");
}
function testSingleOrder_PartialFill() public {
// 0.5 WETH -> WBTC with a quote for 1 WETH
vm.createSelectFork(vm.rpcUrl("mainnet"), 23124275);
bebopExecutor =
new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Quote made manually using the BebopExecutor as the taker and receiver (the same as testSingleOrder)
bytes memory bebopCalldata =
hex"4dcebcba00000000000000000000000000000000000000000000000000000000689b137a0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000bee3211ab312a8d065c4fef0247448e17a8da000000000000000000000000000000000000000000000000000279ead5d9683d8a5000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000037337c0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000000000000000000000000000000000000000000000f71248bc6c123bbf12adc837470f75640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000418e9b0fb72ed9b86f7a7345026269c02b9056efcdfb67a377c7ff6c4a62a4807a7671ae759edf29aea1b2cb8efc8659e3aedac72943cd3607985a1849256358641c00000000000000000000000000000000000000000000000000000000000000";
address tokenIn = WETH_ADDR;
address tokenOut = WBTC_ADDR;
RestrictTransferFrom.TransferType transferType =
RestrictTransferFrom.TransferType.None;
uint8 partialFillOffset = 12;
// filling only half of the quote
uint256 amountIn = 1000000000000000000 / 2;
bool approvalNeeded = true;
uint256 expectedAmountOut = 3617660 / 2;
deal(tokenIn, address(bebopExecutor), amountIn);
bytes memory params = abi.encodePacked(
tokenIn,
tokenOut,
transferType,
partialFillOffset,
amountIn * 2, // this is the original amount in
approvalNeeded,
address(bebopExecutor),
bebopCalldata
);
uint256 initialTokenOutBalance =
IERC20(tokenOut).balanceOf(address(bebopExecutor));
uint256 amountOut = bebopExecutor.swap(amountIn, params);
assertEq(amountOut, expectedAmountOut, "Incorrect partial amount out");
assertEq(
IERC20(tokenOut).balanceOf(address(bebopExecutor))
- initialTokenOutBalance,
expectedAmountOut,
"WETH should be at receiver"
);
assertEq(
IERC20(tokenIn).balanceOf(address(bebopExecutor)),
0,
"WBTC left in executor"
);
}
// Aggregate Order Tests
function testAggregateOrder() public {
// 20k USDC -> ONDO
vm.createSelectFork(vm.rpcUrl("mainnet"), 23126278);
bebopExecutor =
new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Quote made manually using the BebopExecutor as the taker and receiver
bytes memory bebopCalldata =
hex"a2f7489300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000689b715d0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c00000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000005a0e0c07568b14a2d2c1b4d196000fc12bc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000002a65384e777abcfe0000000000000000000000000000000000000000000000002a65384e777abcff0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be300000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000236ddb7a7000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000002713a105900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000001e7dc63f0c1d9d93df4000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000021960567af238bcfd0000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041275c4b7c3df4bfa5c33da3443d817cc6ab568ec8b0fddc30445adff2e870cdcd7d8738e23b795c2fb1ee112e12716bcef1cf648bd1ded17ef10ae493d687322e1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004187ef3d632a640b09df5f39b2fb4c5b9afb7ab4f2782fee450b17e2363d27303b45ec55b154a63993106bfc28bb4accc10fb40f7927509fed554fac01a5d88bae1c00000000000000000000000000000000000000000000000000000000000000";
address tokenIn = USDC_ADDR;
address tokenOut = ONDO_ADDR;
RestrictTransferFrom.TransferType transferType =
RestrictTransferFrom.TransferType.None;
uint8 partialFillOffset = 2;
// filling only half of the quote
uint256 amountIn = 20000000000;
bool approvalNeeded = true;
// maker amounts from quote
uint256 expectedAmountOut =
(8999445165322964385268 + 9912843438638420000000);
deal(tokenIn, address(bebopExecutor), amountIn);
bytes memory params = abi.encodePacked(
tokenIn,
tokenOut,
transferType,
partialFillOffset,
amountIn,
approvalNeeded,
address(bebopExecutor),
bebopCalldata
);
uint256 initialTokenOutBalance =
IERC20(tokenOut).balanceOf(address(bebopExecutor));
uint256 amountOut = bebopExecutor.swap(amountIn, params);
assertEq(amountOut, expectedAmountOut, "Incorrect amount out");
assertEq(
IERC20(tokenOut).balanceOf(address(bebopExecutor))
- initialTokenOutBalance,
expectedAmountOut,
"ONDO should be at receiver"
);
assertEq(
IERC20(tokenIn).balanceOf(address(bebopExecutor)),
0,
"USDC left in executor"
);
}
function testAggregateOrder_PartialFill() public {
// 10k USDC -> ONDO with a quote for 20k USDC
vm.createSelectFork(vm.rpcUrl("mainnet"), 23126278);
bebopExecutor =
new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Quote made manually using the BebopExecutor as the taker and receiver
bytes memory bebopCalldata =
hex"a2f7489300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000689b715d0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c00000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f00000000000000000000000000000000000000000000000000000000000005a0e0c07568b14a2d2c1b4d196000fc12bc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000002a65384e777abcfe0000000000000000000000000000000000000000000000002a65384e777abcff0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be300000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000236ddb7a7000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000002713a105900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000001e7dc63f0c1d9d93df4000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000021960567af238bcfd0000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041275c4b7c3df4bfa5c33da3443d817cc6ab568ec8b0fddc30445adff2e870cdcd7d8738e23b795c2fb1ee112e12716bcef1cf648bd1ded17ef10ae493d687322e1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004187ef3d632a640b09df5f39b2fb4c5b9afb7ab4f2782fee450b17e2363d27303b45ec55b154a63993106bfc28bb4accc10fb40f7927509fed554fac01a5d88bae1c00000000000000000000000000000000000000000000000000000000000000";
address tokenIn = USDC_ADDR;
address tokenOut = ONDO_ADDR;
RestrictTransferFrom.TransferType transferType =
RestrictTransferFrom.TransferType.None;
uint8 partialFillOffset = 2;
// filling only half of the quote
uint256 amountIn = 20000000000 / 2;
bool approvalNeeded = true;
// maker amounts from quote
uint256 expectedAmountOut =
(8999445165322964385268 + 9912843438638420000000) / 2;
deal(tokenIn, address(bebopExecutor), amountIn);
bytes memory params = abi.encodePacked(
tokenIn,
tokenOut,
transferType,
partialFillOffset,
amountIn * 2, // this is the original amount from the quote
approvalNeeded,
address(bebopExecutor),
bebopCalldata
);
uint256 initialTokenOutBalance =
IERC20(tokenOut).balanceOf(address(bebopExecutor));
uint256 amountOut = bebopExecutor.swap(amountIn, params);
assertEq(amountOut, expectedAmountOut, "Incorrect amount out");
assertEq(
IERC20(tokenOut).balanceOf(address(bebopExecutor))
- initialTokenOutBalance,
expectedAmountOut,
"ONDO should be at receiver"
);
assertEq(
IERC20(tokenIn).balanceOf(address(bebopExecutor)),
1, // because of integer division, there is 1 USDC left in the executor
"USDC left in executor"
);
}
function testInvalidDataLength() public {
vm.createSelectFork(vm.rpcUrl("mainnet"), 22667985);
bebopExecutor =
new BebopExecutorExposed(BEBOP_SETTLEMENT, PERMIT2_ADDRESS);
// Create a mock bebop calldata
bytes memory bebopCalldata = hex"47fb5891" // swapSingle selector
hex"1234567890abcdef"; // some mock data
// Create params with correct length first
uint256 originalAmountIn = 1e18;
bytes memory validParams = abi.encodePacked(
WETH_ADDR,
USDC_ADDR,
uint8(RestrictTransferFrom.TransferType.Transfer),
uint8(2),
originalAmountIn,
true,
address(bebopExecutor),
bebopCalldata
);
// Verify valid params work
bebopExecutor.decodeData(validParams);
// In the new format, adding extra bytes at the end doesn't fail
// because bebopCalldata is variable length at the end
// So test with extra bytes should not revert
bytes memory paramsWithExtra = abi.encodePacked(validParams, hex"ff");
// This should work as the extra byte becomes part of bebopCalldata
bebopExecutor.decodeData(paramsWithExtra);
// Try with insufficient data, should fail
bytes memory tooShortParams = abi.encodePacked(
WETH_ADDR,
USDC_ADDR,
uint8(RestrictTransferFrom.TransferType.Transfer)
);
// Missing rest of the data
vm.expectRevert(BebopExecutor.BebopExecutor__InvalidDataLength.selector);
bebopExecutor.decodeData(tooShortParams);
}
}
contract TychoRouterForBebopTest is TychoRouterTestSetup {
// Override the fork block for Bebop tests
function getForkBlock() public pure override returns (uint256) {
return 22667986;
}
function testSingleBebopIntegration() public {
// The calldata swaps 200 USDC for ONDO
address user = 0xd2068e04Cf586f76EEcE7BA5bEB779D7bB1474A1;
deal(USDC_ADDR, user, 200000000); // 200 USDC
uint256 expAmountOut = 194477331556159832309; // Expected ONDO amount from quote
uint256 ondoBefore = IERC20(ONDO_ADDR).balanceOf(user);
vm.startPrank(user);
IERC20(USDC_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_single_encoding_strategy_bebop");
(bool success,) = tychoRouterAddr.call(callData);
assertTrue(success, "Call Failed");
uint256 ondoReceived = IERC20(ONDO_ADDR).balanceOf(user) - ondoBefore;
assertEq(ondoReceived, expAmountOut);
assertEq(
IERC20(USDC_ADDR).balanceOf(tychoRouterAddr),
0,
"USDC left in router"
);
vm.stopPrank();
}
function testBebopAggregateIntegration() public {
// The calldata swaps 20k USDC for ONDO using multiple market makers
address user = 0xd2068e04Cf586f76EEcE7BA5bEB779D7bB1474A1;
deal(USDC_ADDR, user, 20000000000); // 20k USDC
uint256 expAmountOut = 18699321819466078474202; // Expected ONDO amount from quote
uint256 ondoBefore = IERC20(ONDO_ADDR).balanceOf(user);
vm.startPrank(user);
IERC20(USDC_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData = loadCallDataFromFile(
"test_single_encoding_strategy_bebop_aggregate"
);
(bool success,) = tychoRouterAddr.call(callData);
assertTrue(success, "Call Failed");
uint256 ondoReceived = IERC20(ONDO_ADDR).balanceOf(user) - ondoBefore;
assertEq(ondoReceived, expAmountOut);
assertEq(
IERC20(USDC_ADDR).balanceOf(tychoRouterAddr),
0,
"USDC left in router"
);
vm.stopPrank();
}
}

View File

@@ -0,0 +1,316 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "../TychoRouterTestSetup.sol";
import "@src/executors/HashflowExecutor.sol";
import "forge-std/Test.sol";
import {Constants} from "../Constants.sol";
contract HashflowUtils is Test {
constructor() {}
function encodeRfqtQuote(
IHashflowRouter.RFQTQuote memory quote,
bool approvalNeeded,
RestrictTransferFrom.TransferType transferType
) internal pure returns (bytes memory) {
return abi.encodePacked(
uint8(transferType), // transferType (1 byte)
approvalNeeded, // needsApproval (1 byte)
quote.pool, // pool (20 bytes)
quote.externalAccount, // externalAccount (20 bytes)
quote.trader, // trader (20 bytes)
quote.baseToken, // baseToken (20 bytes)
quote.quoteToken, // quoteToken (20 bytes)
quote.baseTokenAmount, // baseTokenAmount (32 bytes)
quote.quoteTokenAmount, // quoteTokenAmount (32 bytes)
quote.quoteExpiry, // quoteExpiry (32 bytes)
quote.nonce, // nonce (32 bytes)
quote.txid, // txid (32 bytes)
quote.signature // signature data
);
}
function encodeRfqtQuoteWithDefaults(IHashflowRouter.RFQTQuote memory quote)
internal
pure
returns (bytes memory)
{
return
encodeRfqtQuote(quote, true, RestrictTransferFrom.TransferType.None);
}
}
contract HashflowExecutorECR20Test is Constants, HashflowUtils {
using SafeERC20 for IERC20;
HashflowExecutorExposed executor;
uint256 forkBlock;
IERC20 WETH = IERC20(WETH_ADDR);
IERC20 USDC = IERC20(USDC_ADDR);
function setUp() public {
forkBlock = 23188416; // Using expiry date: 1755766775, ECR20
vm.createSelectFork("mainnet", forkBlock);
executor = new HashflowExecutorExposed(HASHFLOW_ROUTER, PERMIT2_ADDRESS);
}
function testDecodeParams() public view {
IHashflowRouter.RFQTQuote memory expected_quote = rfqtQuote();
bytes memory encodedQuote = encodeRfqtQuoteWithDefaults(expected_quote);
(
IHashflowRouter.RFQTQuote memory quote,
bool approvalNeeded,
RestrictTransferFrom.TransferType transferType
) = executor.decodeData(encodedQuote);
assertEq(quote.pool, expected_quote.pool, "pool mismatch");
assertEq(
quote.externalAccount,
expected_quote.externalAccount,
"externalAccount mismatch"
);
assertEq(quote.trader, expected_quote.trader, "trader mismatch");
assertEq(
quote.effectiveTrader,
expected_quote.effectiveTrader,
"effectiveTrader mismatch"
);
assertEq(
quote.baseToken, expected_quote.baseToken, "baseToken mismatch"
);
assertEq(
quote.quoteToken, expected_quote.quoteToken, "quoteToken mismatch"
);
assertEq(
quote.effectiveBaseTokenAmount,
expected_quote.effectiveBaseTokenAmount,
"effectiveBaseTokenAmount mismatch"
);
assertEq(
quote.baseTokenAmount,
expected_quote.baseTokenAmount,
"baseTokenAmount mismatch"
);
assertEq(
quote.quoteTokenAmount,
expected_quote.quoteTokenAmount,
"quoteTokenAmount mismatch"
);
assertEq(
quote.quoteExpiry,
expected_quote.quoteExpiry,
"quoteExpiry mismatch"
);
assertEq(quote.nonce, expected_quote.nonce, "nonce mismatch");
assertEq(quote.txid, expected_quote.txid, "txid mismatch");
assertEq(
quote.signature, expected_quote.signature, "signature mismatch"
);
assertEq(approvalNeeded, true, "Approval flag mismatch");
assertEq(
uint8(transferType),
uint8(RestrictTransferFrom.TransferType.None),
"Transfer type mismatch"
);
}
function testDecodeParamsInvalidDataLength() public {
bytes memory invalidData = new bytes(10);
vm.expectRevert(HashflowExecutor__InvalidDataLength.selector);
executor.decodeData(invalidData);
}
function testSwapNoSlippage() public {
address trader = address(ALICE);
IHashflowRouter.RFQTQuote memory quote = rfqtQuote();
uint256 amountIn = quote.baseTokenAmount;
bytes memory encodedQuote = encodeRfqtQuoteWithDefaults(quote);
deal(WETH_ADDR, address(executor), amountIn);
uint256 balanceBefore = USDC.balanceOf(trader);
vm.prank(trader);
uint256 amountOut = executor.swap(amountIn, encodedQuote);
uint256 balanceAfter = USDC.balanceOf(trader);
assertGt(balanceAfter, balanceBefore);
assertEq(balanceAfter - balanceBefore, amountOut);
assertEq(amountOut, quote.quoteTokenAmount);
}
function testSwapRouterAmountUnderQuoteAmount() public {
address trader = address(ALICE);
IHashflowRouter.RFQTQuote memory quote = rfqtQuote();
uint256 amountIn = quote.baseTokenAmount - 1;
bytes memory encodedQuote = encodeRfqtQuoteWithDefaults(quote);
deal(WETH_ADDR, address(executor), amountIn);
uint256 balanceBefore = USDC.balanceOf(trader);
vm.prank(trader);
uint256 amountOut = executor.swap(amountIn, encodedQuote);
uint256 balanceAfter = USDC.balanceOf(trader);
assertGt(balanceAfter, balanceBefore);
assertEq(balanceAfter - balanceBefore, amountOut);
assertLt(amountOut, quote.quoteTokenAmount);
}
function testSwapRouterAmountOverQuoteAmount() public {
address trader = address(ALICE);
IHashflowRouter.RFQTQuote memory quote = rfqtQuote();
uint256 amountIn = quote.baseTokenAmount + 1;
bytes memory encodedQuote = encodeRfqtQuoteWithDefaults(quote);
deal(WETH_ADDR, address(executor), amountIn);
uint256 balanceBefore = USDC.balanceOf(trader);
vm.prank(trader);
uint256 amountOut = executor.swap(amountIn, encodedQuote);
uint256 balanceAfter = USDC.balanceOf(trader);
assertGt(balanceAfter, balanceBefore);
assertEq(balanceAfter - balanceBefore, amountOut);
assertEq(amountOut, quote.quoteTokenAmount);
}
function rfqtQuote()
internal
view
returns (IHashflowRouter.RFQTQuote memory)
{
return IHashflowRouter.RFQTQuote({
pool: address(0x5d8853028fbF6a2da43c7A828cc5f691E9456B44),
externalAccount: address(0x9bA0CF1588E1DFA905eC948F7FE5104dD40EDa31),
trader: address(ALICE),
effectiveTrader: address(ALICE),
baseToken: WETH_ADDR,
quoteToken: USDC_ADDR,
effectiveBaseTokenAmount: 0,
baseTokenAmount: 1000000000000000000,
quoteTokenAmount: 4286117034,
quoteExpiry: 1755766775,
nonce: 1755766744988,
txid: bytes32(
uint256(
0x12500006400064000186078c183380ffffffffffffff00296d737ff6ae950000
)
),
signature: hex"649d31cd74f1b11b4a3b32bd38c2525d78ce8f23bc2eaf7700899c3a396d3a137c861737dc780fa154699eafb3108a34cbb2d4e31a6f0623c169cc19e0fa296a1c"
});
}
}
contract HashflowExecutorNativeTest is Constants, HashflowUtils {
using SafeERC20 for IERC20;
HashflowExecutorExposed executor;
uint256 forkBlock;
IERC20 WETH = IERC20(WETH_ADDR);
IERC20 USDC = IERC20(USDC_ADDR);
function setUp() public {
forkBlock = 23188504; // Using expiry date: 1755767859, Native
vm.createSelectFork("mainnet", forkBlock);
executor = new HashflowExecutorExposed(HASHFLOW_ROUTER, PERMIT2_ADDRESS);
}
function testSwapNoSlippage() public {
address trader = address(ALICE);
IHashflowRouter.RFQTQuote memory quote = rfqtQuote();
uint256 amountIn = quote.baseTokenAmount;
bytes memory encodedQuote = encodeRfqtQuoteWithDefaults(quote);
vm.deal(address(executor), amountIn);
uint256 balanceBefore = USDC.balanceOf(trader);
vm.prank(trader);
uint256 amountOut = executor.swap(amountIn, encodedQuote);
uint256 balanceAfter = USDC.balanceOf(trader);
assertGt(balanceAfter, balanceBefore);
assertEq(balanceAfter - balanceBefore, amountOut);
assertEq(amountOut, quote.quoteTokenAmount);
}
function rfqtQuote()
internal
view
returns (IHashflowRouter.RFQTQuote memory)
{
return IHashflowRouter.RFQTQuote({
pool: address(0x713DC4Df480235dBe2fB766E7120Cbd4041Dcb58),
externalAccount: address(0x111BB8c3542F2B92fb41B8d913c01D3788431111),
trader: address(ALICE),
effectiveTrader: address(ALICE),
baseToken: address(0x0000000000000000000000000000000000000000),
quoteToken: USDC_ADDR,
effectiveBaseTokenAmount: 0,
baseTokenAmount: 10000000000000000,
quoteTokenAmount: 42586008,
quoteExpiry: 1755767859,
nonce: 1755767819299,
txid: bytes32(
uint256(
0x1250000640006400018380fd594810ffffffffffffff00296d83e467cddd0000
)
),
signature: hex"63c1c9c7d6902d1d4d2ae82777015433ef08366dde1c579a8c4cbc01059166064246f61f15b2cb130be8f2b28ea40d2c3586ef0133647fefa30003e70ffbd6131b"
});
}
}
contract HashflowExecutorExposed is HashflowExecutor {
constructor(address _hashflowRouter, address _permit2)
HashflowExecutor(_hashflowRouter, _permit2)
{}
function decodeData(bytes calldata data)
external
pure
returns (
IHashflowRouter.RFQTQuote memory quote,
bool approvalNeeded,
TransferType transferType
)
{
return _decodeData(data);
}
}
contract TychoRouterSingleSwapTestForHashflow is TychoRouterTestSetup {
function getForkBlock() public pure override returns (uint256) {
return 23175437;
}
function testHashflowIntegration() public {
// Performs a swap from USDC to WBTC using Hashflow RFQ
//
// USDC ───(Hashflow RFQ)──> WBTC
// The Hashflow order expects:
// - 4308094737 USDC input -> 3714751 WBTC output
uint256 amountIn = 4308094737;
uint256 expectedAmountOut = 3714751;
deal(USDC_ADDR, ALICE, amountIn);
uint256 balanceBefore = IERC20(WBTC_ADDR).balanceOf(ALICE);
vm.startPrank(ALICE);
IERC20(USDC_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_single_encoding_strategy_hashflow");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(WBTC_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, expectedAmountOut);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
}

View File

@@ -1,6 +1,7 @@
use std::{io, str::Utf8Error}; use std::{io, str::Utf8Error};
use thiserror::Error; use thiserror::Error;
use tycho_common::simulation::errors::SimulationError;
/// Represents the outer-level, user-facing errors of the tycho-execution encoding package. /// Represents the outer-level, user-facing errors of the tycho-execution encoding package.
/// ///
@@ -41,3 +42,15 @@ impl From<Utf8Error> for EncodingError {
EncodingError::FatalError(err.to_string()) EncodingError::FatalError(err.to_string())
} }
} }
impl From<SimulationError> for EncodingError {
fn from(err: SimulationError) -> Self {
match err {
SimulationError::FatalError(err_msg) => EncodingError::FatalError(err_msg),
SimulationError::InvalidInput(err_msg, ..) => EncodingError::InvalidInput(err_msg),
SimulationError::RecoverableError(error_msg) => {
EncodingError::RecoverableError(error_msg)
}
}
}
}

View File

@@ -31,10 +31,6 @@ pub struct Permit2 {
address: Address, address: Address,
client: EVMProvider, client: EVMProvider,
runtime_handle: Handle, 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)] #[allow(dead_code)]
runtime: Option<Arc<Runtime>>, runtime: Option<Arc<Runtime>>,
} }

View File

@@ -23,10 +23,6 @@ use crate::encoding::{
pub struct ProtocolApprovalsManager { pub struct ProtocolApprovalsManager {
client: EVMProvider, client: EVMProvider,
runtime_handle: Handle, 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)] #[allow(dead_code)]
runtime: Option<Arc<Runtime>>, runtime: Option<Arc<Runtime>>,
} }

View File

@@ -12,15 +12,15 @@ use crate::encoding::{evm::constants::GROUPABLE_PROTOCOLS, models::Swap};
/// * `swaps`: Vec<Swap>, the sequence of swaps to be executed as a group /// * `swaps`: Vec<Swap>, the sequence of swaps to be executed as a group
/// * `split`: f64, the split percentage of the first swap in the group /// * `split`: f64, the split percentage of the first swap in the group
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct SwapGroup<'a> { pub struct SwapGroup {
pub token_in: Bytes, pub token_in: Bytes,
pub token_out: Bytes, pub token_out: Bytes,
pub protocol_system: String, pub protocol_system: String,
pub swaps: Vec<Swap<'a>>, pub swaps: Vec<Swap>,
pub split: f64, pub split: f64,
} }
impl<'a> PartialEq for SwapGroup<'a> { impl PartialEq for SwapGroup {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.token_in == other.token_in && self.token_in == other.token_in &&
self.token_out == other.token_out && self.token_out == other.token_out &&
@@ -34,7 +34,7 @@ impl<'a> PartialEq for SwapGroup<'a> {
/// ///
/// An example where this applies is the case of USV4, which uses a PoolManager contract /// An example where this applies is the case of USV4, which uses a PoolManager contract
/// to save token transfers on consecutive swaps. /// to save token transfers on consecutive swaps.
pub fn group_swaps<'a>(swaps: &'a Vec<Swap<'a>>) -> Vec<SwapGroup<'a>> { pub fn group_swaps(swaps: &Vec<Swap>) -> Vec<SwapGroup> {
let mut grouped_swaps: Vec<SwapGroup> = Vec::new(); let mut grouped_swaps: Vec<SwapGroup> = Vec::new();
let mut current_group: Option<SwapGroup> = None; let mut current_group: Option<SwapGroup> = None;
let mut last_swap_protocol = "".to_string(); let mut last_swap_protocol = "".to_string();
@@ -87,7 +87,7 @@ mod tests {
use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use tycho_common::{models::protocol::ProtocolComponent, Bytes};
use super::*; use super::*;
use crate::encoding::models::Swap; use crate::encoding::models::SwapBuilder;
fn weth() -> Bytes { fn weth() -> Bytes {
Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec()) Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec())
@@ -105,41 +105,26 @@ mod tests {
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swap_weth_wbtc = Swap { let swap_weth_wbtc = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent { protocol_system: "uniswap_v4_hooks".to_string(), ..Default::default() },
protocol_system: "uniswap_v4_hooks".to_string(), weth.clone(),
..Default::default() wbtc.clone(),
}, )
token_in: weth.clone(), .build();
token_out: wbtc.clone(),
// This represents the remaining 50%, but to avoid any rounding errors we set this to let swap_wbtc_usdc = SwapBuilder::new(
// 0 to signify "the remainder of the WETH value". It should still be very close to 50% ProtocolComponent { protocol_system: "uniswap_v4_hooks".to_string(), ..Default::default() },
split: 0f64, wbtc.clone(),
user_data: None, usdc.clone(),
protocol_state: None, )
}; .build();
let swap_wbtc_usdc = Swap {
component: ProtocolComponent { let swap_usdc_dai = SwapBuilder::new(
protocol_system: "uniswap_v4_hooks".to_string(), ProtocolComponent { protocol_system: "uniswap_v2".to_string(), ..Default::default() },
..Default::default() usdc.clone(),
}, dai.clone(),
token_in: wbtc.clone(), )
token_out: usdc.clone(), .build();
split: 0f64,
user_data: None,
protocol_state: None,
};
let swap_usdc_dai = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: usdc.clone(),
token_out: dai.clone(),
split: 0f64,
user_data: None,
protocol_state: None,
};
let swaps = vec![swap_weth_wbtc.clone(), swap_wbtc_usdc.clone(), swap_usdc_dai.clone()]; let swaps = vec![swap_weth_wbtc.clone(), swap_wbtc_usdc.clone(), swap_usdc_dai.clone()];
let grouped_swaps = group_swaps(&swaps); let grouped_swaps = group_swaps(&swaps);
@@ -179,52 +164,34 @@ mod tests {
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swap_wbtc_weth = Swap { let swap_wbtc_weth = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent { protocol_system: "uniswap_v4_hooks".to_string(), ..Default::default() },
protocol_system: "uniswap_v4_hooks".to_string(), wbtc.clone(),
..Default::default() weth.clone(),
}, )
token_in: wbtc.clone(), .build();
token_out: weth.clone(), let swap_weth_usdc = SwapBuilder::new(
split: 0f64, ProtocolComponent { protocol_system: "uniswap_v4_hooks".to_string(), ..Default::default() },
user_data: None, weth.clone(),
protocol_state: None, usdc.clone(),
}; )
let swap_weth_usdc = Swap { .split(0.5f64)
component: ProtocolComponent { .build();
protocol_system: "uniswap_v4_hooks".to_string(), let swap_weth_dai = SwapBuilder::new(
..Default::default() ProtocolComponent { protocol_system: "uniswap_v4_hooks".to_string(), ..Default::default() },
}, weth.clone(),
token_in: weth.clone(), dai.clone(),
token_out: usdc.clone(), )
split: 0.5f64, .build();
user_data: None, // Split 0 represents the remaining 50%, but to avoid any rounding errors we set this to
protocol_state: None,
};
let swap_weth_dai = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v4_hooks".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
// This represents the remaining 50%, but to avoid any rounding errors we set this to
// 0 to signify "the remainder of the WETH value". It should still be very close to 50% // 0 to signify "the remainder of the WETH value". It should still be very close to 50%
split: 0f64,
user_data: None, let swap_dai_usdc = SwapBuilder::new(
protocol_state: None, ProtocolComponent { protocol_system: "uniswap_v4_hooks".to_string(), ..Default::default() },
}; dai.clone(),
let swap_dai_usdc = Swap { usdc.clone(),
component: ProtocolComponent { )
protocol_system: "uniswap_v4_hooks".to_string(), .build();
..Default::default()
},
token_in: dai.clone(),
token_out: usdc.clone(),
split: 0f64,
user_data: None,
protocol_state: None,
};
let swaps = vec![ let swaps = vec![
swap_wbtc_weth.clone(), swap_wbtc_weth.clone(),
swap_weth_usdc.clone(), swap_weth_usdc.clone(),
@@ -275,52 +242,38 @@ mod tests {
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swap_weth_wbtc = Swap { let swap_weth_wbtc = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
protocol_system: "vm:balancer_v3".to_string(), protocol_system: "vm:balancer_v3".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: wbtc.clone(), wbtc.clone(),
split: 0.5f64, )
user_data: None, .split(0.5f64)
protocol_state: None, .build();
};
let swap_wbtc_usdc = Swap { let swap_wbtc_usdc = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
protocol_system: "vm:balancer_v3".to_string(), protocol_system: "vm:balancer_v3".to_string(),
..Default::default() ..Default::default()
}, },
token_in: wbtc.clone(), wbtc.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None, let swap_weth_dai = SwapBuilder::new(
}; ProtocolComponent { protocol_system: "uniswap_v4_hooks".to_string(), ..Default::default() },
let swap_weth_dai = Swap { weth.clone(),
component: ProtocolComponent { dai.clone(),
protocol_system: "uniswap_v4_hooks".to_string(), )
..Default::default() .build();
}, let swap_dai_usdc = SwapBuilder::new(
token_in: weth.clone(), ProtocolComponent { protocol_system: "uniswap_v4_hooks".to_string(), ..Default::default() },
token_out: dai.clone(), dai.clone(),
// This represents the remaining 50%, but to avoid any rounding errors we set this to usdc.clone(),
// 0 to signify "the remainder of the WETH value". It should still be very close to 50% )
split: 0f64, .build();
user_data: None,
protocol_state: None,
};
let swap_dai_usdc = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v4_hooks".to_string(),
..Default::default()
},
token_in: dai.clone(),
token_out: usdc.clone(),
split: 0f64,
user_data: None,
protocol_state: None,
};
let swaps = vec![ let swaps = vec![
swap_weth_wbtc.clone(), swap_weth_wbtc.clone(),

View File

@@ -5,5 +5,7 @@ mod encoding_utils;
mod group_swaps; mod group_swaps;
pub mod strategy_encoder; pub mod strategy_encoder;
mod swap_encoder; mod swap_encoder;
#[cfg(feature = "test-utils")]
pub mod testing_utils;
pub mod tycho_encoders; pub mod tycho_encoders;
pub mod utils; pub mod utils;

View File

@@ -237,10 +237,11 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder {
let grouped_swaps = group_swaps(&solution.swaps); let grouped_swaps = group_swaps(&solution.swaps);
let mut wrap = false; let (mut wrap, mut unwrap) = (false, false);
if let Some(action) = &solution.native_action { if let Some(action) = &solution.native_action {
if action == &NativeAction::Wrap { match *action {
wrap = true NativeAction::Wrap => wrap = true,
NativeAction::Unwrap => unwrap = true,
} }
} }
@@ -260,7 +261,7 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder {
let next_swap = grouped_swaps.get(i + 1); let next_swap = grouped_swaps.get(i + 1);
let (swap_receiver, next_swap_optimization) = self let (swap_receiver, next_swap_optimization) = self
.transfer_optimization .transfer_optimization
.get_receiver(&solution.receiver, next_swap)?; .get_receiver(&solution.receiver, next_swap, unwrap)?;
next_in_between_swap_optimization_allowed = next_swap_optimization; next_in_between_swap_optimization_allowed = next_swap_optimization;
let transfer = self let transfer = self
@@ -544,7 +545,6 @@ mod tests {
}; };
use super::*; use super::*;
use crate::encoding::models::Swap;
fn eth_chain() -> Chain { fn eth_chain() -> Chain {
Chain::Ethereum Chain::Ethereum
@@ -565,8 +565,8 @@ mod tests {
} }
mod single { mod single {
use super::*; use super::*;
use crate::encoding::models::SwapBuilder;
#[test] #[test]
fn test_single_swap_strategy_encoder() { fn test_single_swap_strategy_encoder() {
// Performs a single swap from WETH to DAI on a USV2 pool, with no grouping // Performs a single swap from WETH to DAI on a USV2 pool, with no grouping
@@ -575,18 +575,16 @@ mod tests {
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None,
};
let swap_encoder_registry = get_swap_encoder_registry(); let swap_encoder_registry = get_swap_encoder_registry();
let encoder = SingleSwapStrategyEncoder::new( let encoder = SingleSwapStrategyEncoder::new(
eth_chain(), eth_chain(),
@@ -637,18 +635,16 @@ mod tests {
let checked_amount = BigUint::from_str("1_640_000000000000000000").unwrap(); let checked_amount = BigUint::from_str("1_640_000000000000000000").unwrap();
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None,
};
let swap_encoder_registry = get_swap_encoder_registry(); let swap_encoder_registry = get_swap_encoder_registry();
let encoder = SingleSwapStrategyEncoder::new( let encoder = SingleSwapStrategyEncoder::new(
eth_chain(), eth_chain(),
@@ -698,6 +694,7 @@ mod tests {
mod sequential { mod sequential {
use super::*; use super::*;
use crate::encoding::models::SwapBuilder;
#[test] #[test]
fn test_sequential_swap_strategy_encoder_no_permit2() { fn test_sequential_swap_strategy_encoder_no_permit2() {
@@ -709,30 +706,26 @@ mod tests {
let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let swap_weth_wbtc = Swap { let swap_weth_wbtc = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(), id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: wbtc.clone(), wbtc.clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None, let swap_wbtc_usdc = SwapBuilder::new(
}; ProtocolComponent {
let swap_wbtc_usdc = Swap {
component: ProtocolComponent {
id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(), id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: wbtc.clone(), wbtc.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None,
};
let swap_encoder_registry = get_swap_encoder_registry(); let swap_encoder_registry = get_swap_encoder_registry();
let encoder = SequentialSwapStrategyEncoder::new( let encoder = SequentialSwapStrategyEncoder::new(
eth_chain(), eth_chain(),
@@ -790,6 +783,7 @@ mod tests {
mod split { mod split {
use super::*; use super::*;
use crate::encoding::models::SwapBuilder;
#[test] #[test]
fn test_split_input_cyclic_swap() { fn test_split_input_cyclic_swap() {
@@ -805,8 +799,8 @@ mod tests {
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
// USDC -> WETH (Pool 1) - 60% of input // USDC -> WETH (Pool 1) - 60% of input
let swap_usdc_weth_pool1 = Swap { let swap_usdc_weth_pool1 = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3 id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3
* Pool 1 */ * Pool 1 */
protocol_system: "uniswap_v3".to_string(), protocol_system: "uniswap_v3".to_string(),
@@ -820,16 +814,15 @@ mod tests {
}, },
..Default::default() ..Default::default()
}, },
token_in: usdc.clone(), usdc.clone(),
token_out: weth.clone(), weth.clone(),
split: 0.6f64, // 60% of input )
user_data: None, .split(0.6f64)
protocol_state: None, .build();
};
// USDC -> WETH (Pool 2) - 40% of input (remaining) // USDC -> WETH (Pool 2) - 40% of input (remaining)
let swap_usdc_weth_pool2 = Swap { let swap_usdc_weth_pool2 = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3 id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3
* Pool 2 */ * Pool 2 */
protocol_system: "uniswap_v3".to_string(), protocol_system: "uniswap_v3".to_string(),
@@ -843,16 +836,14 @@ mod tests {
}, },
..Default::default() ..Default::default()
}, },
token_in: usdc.clone(), usdc.clone(),
token_out: weth.clone(), weth.clone(),
split: 0f64, )
user_data: None, // Remaining 40% .build();
protocol_state: None,
};
// WETH -> USDC (Pool 2) // WETH -> USDC (Pool 2)
let swap_weth_usdc_pool2 = Swap { let swap_weth_usdc_pool2 = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), /* USDC-WETH USV2 id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), /* USDC-WETH USV2
* Pool 2 */ * Pool 2 */
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
@@ -866,13 +857,10 @@ mod tests {
}, },
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0.0f64, )
user_data: None, .build();
protocol_state: None,
};
let swap_encoder_registry = get_swap_encoder_registry(); let swap_encoder_registry = get_swap_encoder_registry();
let encoder = SplitSwapStrategyEncoder::new( let encoder = SplitSwapStrategyEncoder::new(
eth_chain(), eth_chain(),
@@ -961,8 +949,8 @@ mod tests {
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let swap_usdc_weth_v2 = Swap { let swap_usdc_weth_v2 = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), // USDC-WETH USV2 id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), // USDC-WETH USV2
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
static_attributes: { static_attributes: {
@@ -975,15 +963,13 @@ mod tests {
}, },
..Default::default() ..Default::default()
}, },
token_in: usdc.clone(), usdc.clone(),
token_out: weth.clone(), weth.clone(),
split: 0.0f64, )
user_data: None, .build();
protocol_state: None,
};
let swap_weth_usdc_v3_pool1 = Swap { let swap_weth_usdc_v3_pool1 = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3 id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3
* Pool 1 */ * Pool 1 */
protocol_system: "uniswap_v3".to_string(), protocol_system: "uniswap_v3".to_string(),
@@ -997,17 +983,16 @@ mod tests {
}, },
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0.6f64, )
user_data: None, .split(0.6f64)
protocol_state: None, .build();
};
let swap_weth_usdc_v3_pool2 = Swap { let swap_weth_usdc_v3_pool2 = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3 id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3
* Pool 2 */ * Pool 1 */
protocol_system: "uniswap_v3".to_string(), protocol_system: "uniswap_v3".to_string(),
static_attributes: { static_attributes: {
let mut attrs = HashMap::new(); let mut attrs = HashMap::new();
@@ -1019,12 +1004,10 @@ mod tests {
}, },
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0.0f64, )
user_data: None, .build();
protocol_state: None,
};
let swap_encoder_registry = get_swap_encoder_registry(); let swap_encoder_registry = get_swap_encoder_registry();
let encoder = SplitSwapStrategyEncoder::new( let encoder = SplitSwapStrategyEncoder::new(

View File

@@ -197,7 +197,7 @@ mod tests {
use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use tycho_common::{models::protocol::ProtocolComponent, Bytes};
use super::*; use super::*;
use crate::encoding::models::Swap; use crate::encoding::models::{Swap, SwapBuilder};
#[test] #[test]
fn test_validate_path_single_swap() { fn test_validate_path_single_swap() {
@@ -205,18 +205,16 @@ mod tests {
let eth = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(); let eth = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap();
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swaps = vec![Swap { let swaps = vec![SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0f64, )
user_data: None, .build()];
protocol_state: None,
}];
let result = validator.validate_swap_path(&swaps, &weth, &dai, &None, &eth, &weth); let result = validator.validate_swap_path(&swaps, &weth, &dai, &None, &eth, &weth);
assert_eq!(result, Ok(())); assert_eq!(result, Ok(()));
} }
@@ -229,30 +227,27 @@ mod tests {
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let swaps = vec![ let swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.5f64, )
user_data: None, .split(0.5f64)
protocol_state: None, .build(),
}, SwapBuilder::new(
Swap { ProtocolComponent {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai.clone(), dai.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0f64, )
user_data: None, .build(),
protocol_state: None,
},
]; ];
let result = validator.validate_swap_path(&swaps, &weth, &usdc, &None, &eth, &weth); let result = validator.validate_swap_path(&swaps, &weth, &usdc, &None, &eth, &weth);
assert_eq!(result, Ok(())); assert_eq!(result, Ok(()));
@@ -268,31 +263,28 @@ mod tests {
let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap();
let disconnected_swaps = vec![ let disconnected_swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.5, )
user_data: None, .split(0.5f64)
protocol_state: None, .build(),
},
// This swap is disconnected from the WETH->DAI path // This swap is disconnected from the WETH->DAI path
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool2".to_string(), id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: wbtc.clone(), wbtc.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0.0, )
user_data: None, .build(),
protocol_state: None,
},
]; ];
let result = let result =
validator.validate_swap_path(&disconnected_swaps, &weth, &usdc, &None, &eth, &weth); validator.validate_swap_path(&disconnected_swaps, &weth, &usdc, &None, &eth, &weth);
@@ -310,30 +302,26 @@ mod tests {
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let cyclic_swaps = vec![ let cyclic_swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: usdc.clone(), usdc.clone(),
token_out: weth.clone(), weth.clone(),
split: 0f64, )
user_data: None, .build(),
protocol_state: None, SwapBuilder::new(
}, ProtocolComponent {
Swap {
component: ProtocolComponent {
id: "pool2".to_string(), id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0f64, )
user_data: None, .build(),
protocol_state: None,
},
]; ];
// Test with USDC as both given token and checked token // Test with USDC as both given token and checked token
@@ -349,18 +337,17 @@ mod tests {
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let unreachable_swaps = vec![Swap { let unreachable_swaps = vec![SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 1.0, )
user_data: None, .split(1.0)
protocol_state: None, .build()];
}];
let result = let result =
validator.validate_swap_path(&unreachable_swaps, &weth, &usdc, &None, &eth, &weth); validator.validate_swap_path(&unreachable_swaps, &weth, &usdc, &None, &eth, &weth);
assert!(matches!( assert!(matches!(
@@ -389,18 +376,16 @@ mod tests {
let validator = SplitSwapValidator; let validator = SplitSwapValidator;
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swaps = vec![Swap { let swaps = vec![SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0f64, )
user_data: None, .build()];
protocol_state: None,
}];
let result = validator.validate_split_percentages(&swaps); let result = validator.validate_split_percentages(&swaps);
assert_eq!(result, Ok(())); assert_eq!(result, Ok(()));
} }
@@ -413,42 +398,38 @@ mod tests {
// Valid case: Multiple swaps with proper splits (50%, 30%, remainder) // Valid case: Multiple swaps with proper splits (50%, 30%, remainder)
let valid_swaps = vec![ let valid_swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.5, )
user_data: None, .split(0.5)
protocol_state: None, .build(),
}, SwapBuilder::new(
Swap { ProtocolComponent {
component: ProtocolComponent {
id: "pool2".to_string(), id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.3, )
user_data: None, .split(0.3)
protocol_state: None, .build(),
}, SwapBuilder::new(
Swap { ProtocolComponent {
component: ProtocolComponent {
id: "pool3".to_string(), id: "pool3".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.0, // Remainder (20%) )
user_data: None, .build(),
protocol_state: None,
},
]; ];
assert!(validator assert!(validator
.validate_split_percentages(&valid_swaps) .validate_split_percentages(&valid_swaps)
@@ -462,30 +443,28 @@ mod tests {
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let invalid_total_swaps = vec![ let invalid_total_swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.7, )
user_data: None, .split(0.7)
protocol_state: None, .build(),
}, SwapBuilder::new(
Swap { ProtocolComponent {
component: ProtocolComponent {
id: "pool2".to_string(), id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.3, )
user_data: None, .split(0.3)
protocol_state: None, .build(),
},
]; ];
assert!(matches!( assert!(matches!(
validator.validate_split_percentages(&invalid_total_swaps), validator.validate_split_percentages(&invalid_total_swaps),
@@ -500,30 +479,27 @@ mod tests {
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let invalid_zero_position_swaps = vec![ let invalid_zero_position_swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.0, )
user_data: None, .build(),
protocol_state: None, SwapBuilder::new(
}, ProtocolComponent {
Swap {
component: ProtocolComponent {
id: "pool2".to_string(), id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.5, )
user_data: None, .split(0.5)
protocol_state: None, .build(),
},
]; ];
assert!(matches!( assert!(matches!(
validator.validate_split_percentages(&invalid_zero_position_swaps), validator.validate_split_percentages(&invalid_zero_position_swaps),
@@ -538,42 +514,38 @@ mod tests {
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let invalid_overflow_swaps = vec![ let invalid_overflow_swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.6, )
user_data: None, .split(0.6)
protocol_state: None, .build(),
}, SwapBuilder::new(
Swap { ProtocolComponent {
component: ProtocolComponent {
id: "pool2".to_string(), id: "pool2".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.5, )
user_data: None, .split(0.5)
protocol_state: None, .build(),
}, SwapBuilder::new(
Swap { ProtocolComponent {
component: ProtocolComponent {
id: "pool3".to_string(), id: "pool3".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: dai.clone(), dai.clone(),
split: 0.0, )
user_data: None, .build(),
protocol_state: None,
},
]; ];
assert!(matches!( assert!(matches!(
validator.validate_split_percentages(&invalid_overflow_swaps), validator.validate_split_percentages(&invalid_overflow_swaps),
@@ -588,18 +560,16 @@ mod tests {
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(); let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
let swaps = vec![Swap { let swaps = vec![SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth.clone(), weth.clone(),
token_out: usdc.clone(), usdc.clone(),
split: 0f64, )
user_data: None, .build()];
protocol_state: None,
}];
let result = validator.validate_swap_path( let result = validator.validate_swap_path(
&swaps, &swaps,
@@ -619,18 +589,16 @@ mod tests {
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(); let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
let swaps = vec![Swap { let swaps = vec![SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "pool1".to_string(), id: "pool1".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: usdc.clone(), usdc.clone(),
token_out: weth.clone(), weth.clone(),
split: 0f64, )
user_data: None, .build()];
protocol_state: None,
}];
let result = validator.validate_swap_path( let result = validator.validate_swap_path(
&swaps, &swaps,

View File

@@ -82,6 +82,7 @@ impl TransferOptimization {
&self, &self,
solution_receiver: &Bytes, solution_receiver: &Bytes,
next_swap: Option<&SwapGroup>, next_swap: Option<&SwapGroup>,
unwrap: bool,
) -> Result<(Bytes, bool), EncodingError> { ) -> Result<(Bytes, bool), EncodingError> {
if let Some(next) = next_swap { if let Some(next) = next_swap {
// if the protocol of the next swap supports transfer in optimization // if the protocol of the next swap supports transfer in optimization
@@ -104,10 +105,14 @@ impl TransferOptimization {
} }
} else { } else {
// last swap - there is no next swap // last swap - there is no next swap
if unwrap {
Ok((self.router_address.clone(), false))
} else {
Ok((solution_receiver.clone(), false)) Ok((solution_receiver.clone(), false))
} }
} }
} }
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@@ -116,7 +121,7 @@ mod tests {
use tycho_common::models::protocol::ProtocolComponent; use tycho_common::models::protocol::ProtocolComponent;
use super::*; use super::*;
use crate::encoding::models::Swap; use crate::encoding::models::SwapBuilder;
fn weth() -> Bytes { fn weth() -> Bytes {
Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec()) Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec())
@@ -169,18 +174,16 @@ mod tests {
#[case] expected_transfer: TransferType, #[case] expected_transfer: TransferType,
) { ) {
// The swap token is the same as the given token, which is not the native token // The swap token is the same as the given token, which is not the native token
let swaps = vec![Swap { let swaps = vec![SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
..Default::default() ..Default::default()
}, },
token_in: swap_token_in.clone(), swap_token_in.clone(),
token_out: dai(), dai(),
split: 0f64, )
user_data: None, .build()];
protocol_state: None,
}];
let swap = SwapGroup { let swap = SwapGroup {
protocol_system: protocol, protocol_system: protocol,
token_in: swap_token_in, token_in: swap_token_in,
@@ -204,16 +207,19 @@ mod tests {
} }
#[rstest] #[rstest]
// there is no next swap -> receiver is the solution receiver // there is no next swap but there is an unwrap -> receiver is the router
#[case(None, receiver(), false)] #[case(None, true, router_address(), false)]
// there is no next swap and no unwrap -> receiver is the solution receiver
#[case(None, false, receiver(), false)]
// protocol of next swap supports transfer in optimization // protocol of next swap supports transfer in optimization
#[case(Some("uniswap_v2"), component_id(), true)] #[case(Some("uniswap_v2"), false, component_id(), true)]
// protocol of next swap supports transfer in optimization but is callback constrained // protocol of next swap supports transfer in optimization but is callback constrained
#[case(Some("uniswap_v3"), router_address(), false)] #[case(Some("uniswap_v3"), false, router_address(), false)]
// protocol of next swap does not support transfer in optimization // protocol of next swap does not support transfer in optimization
#[case(Some("vm:curve"), router_address(), false)] #[case(Some("vm:curve"), false, router_address(), false)]
fn test_get_receiver( fn test_get_receiver(
#[case] protocol: Option<&str>, #[case] protocol: Option<&str>,
#[case] unwrap: bool,
#[case] expected_receiver: Bytes, #[case] expected_receiver: Bytes,
#[case] expected_optimization: bool, #[case] expected_optimization: bool,
) { ) {
@@ -232,22 +238,20 @@ mod tests {
token_in: usdc(), token_in: usdc(),
token_out: dai(), token_out: dai(),
split: 0f64, split: 0f64,
swaps: vec![Swap { swaps: vec![SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
protocol_system: protocol.unwrap().to_string(), protocol_system: protocol.unwrap().to_string(),
id: component_id().to_string(), id: component_id().to_string(),
..Default::default() ..Default::default()
}, },
token_in: usdc(), usdc(),
token_out: dai(), dai(),
split: 0f64, )
user_data: None, .build()],
protocol_state: None,
}],
}) })
}; };
let result = optimization.get_receiver(&receiver(), next_swap.as_ref()); let result = optimization.get_receiver(&receiver(), next_swap.as_ref(), unwrap);
assert!(result.is_ok()); assert!(result.is_ok());
let (actual_receiver, optimization_flag) = result.unwrap(); let (actual_receiver, optimization_flag) = result.unwrap();

View File

@@ -5,8 +5,9 @@ use tycho_common::models::Chain;
use crate::encoding::{ use crate::encoding::{
errors::EncodingError, errors::EncodingError,
evm::swap_encoder::swap_encoders::{ evm::swap_encoder::swap_encoders::{
BalancerV2SwapEncoder, BalancerV3SwapEncoder, CurveSwapEncoder, EkuboSwapEncoder, BalancerV2SwapEncoder, BalancerV3SwapEncoder, BebopSwapEncoder, CurveSwapEncoder,
MaverickV2SwapEncoder, UniswapV2SwapEncoder, UniswapV3SwapEncoder, UniswapV4SwapEncoder, EkuboSwapEncoder, HashflowSwapEncoder, MaverickV2SwapEncoder, UniswapV2SwapEncoder,
UniswapV3SwapEncoder, UniswapV4SwapEncoder,
}, },
swap_encoder::SwapEncoder, swap_encoder::SwapEncoder,
}; };
@@ -87,6 +88,14 @@ impl SwapEncoderBuilder {
self.chain, self.chain,
self.config, self.config,
)?)), )?)),
"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!( _ => Err(EncodingError::FatalError(format!(
"Unknown protocol system: {}", "Unknown protocol system: {}",
self.protocol_system self.protocol_system

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
// This module is used in integration tests as well
use std::{any::Any, collections::HashMap};
use async_trait::async_trait;
use num_bigint::BigUint;
use tycho_common::{
dto::ProtocolStateDelta,
models::{protocol::GetAmountOutParams, token::Token},
simulation::{
errors::{SimulationError, TransitionError},
indicatively_priced::{IndicativelyPriced, SignedQuote},
protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
},
Bytes,
};
#[derive(Debug)]
pub struct MockRFQState {
pub quote_amount_out: BigUint,
pub quote_data: HashMap<String, Bytes>,
}
impl ProtocolSim for MockRFQState {
fn fee(&self) -> f64 {
panic!("MockRFQState does not implement fee")
}
fn spot_price(&self, _base: &Token, _quote: &Token) -> Result<f64, SimulationError> {
panic!("MockRFQState does not implement fee")
}
fn get_amount_out(
&self,
_amount_in: BigUint,
_token_in: &Token,
_token_out: &Token,
) -> Result<GetAmountOutResult, SimulationError> {
panic!("MockRFQState does not implement fee")
}
fn get_limits(
&self,
_sell_token: Bytes,
_buy_token: Bytes,
) -> Result<(BigUint, BigUint), SimulationError> {
panic!("MockRFQState does not implement fee")
}
fn delta_transition(
&mut self,
_delta: ProtocolStateDelta,
_tokens: &HashMap<Bytes, Token>,
_balances: &Balances,
) -> Result<(), TransitionError<String>> {
panic!("MockRFQState does not implement fee")
}
fn clone_box(&self) -> Box<dyn ProtocolSim> {
panic!("MockRFQState does not implement fee")
}
fn as_any(&self) -> &dyn Any {
panic!("MockRFQState does not implement fee")
}
fn as_any_mut(&mut self) -> &mut dyn Any {
panic!("MockRFQState does not implement fee")
}
fn eq(&self, _other: &dyn ProtocolSim) -> bool {
panic!("MockRFQState does not implement fee")
}
fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
Ok(self)
}
}
#[async_trait]
impl IndicativelyPriced for MockRFQState {
async fn request_signed_quote(
&self,
params: GetAmountOutParams,
) -> Result<SignedQuote, SimulationError> {
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: self.quote_data.clone(),
})
}
}

View File

@@ -406,7 +406,7 @@ mod tests {
use tycho_common::models::{protocol::ProtocolComponent, Chain}; use tycho_common::models::{protocol::ProtocolComponent, Chain};
use super::*; use super::*;
use crate::encoding::models::Swap; use crate::encoding::models::{Swap, SwapBuilder};
fn dai() -> Bytes { fn dai() -> Bytes {
Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap() Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap()
@@ -435,48 +435,44 @@ mod tests {
// Fee and tick spacing information for this test is obtained by querying the // Fee and tick spacing information for this test is obtained by querying the
// USV4 Position Manager contract: 0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e // USV4 Position Manager contract: 0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e
// Using the poolKeys function with the first 25 bytes of the pool id // Using the poolKeys function with the first 25 bytes of the pool id
fn swap_usdc_eth_univ4() -> Swap<'static> { fn swap_usdc_eth_univ4() -> Swap {
let pool_fee_usdc_eth = Bytes::from(BigInt::from(3000).to_signed_bytes_be()); let pool_fee_usdc_eth = Bytes::from(BigInt::from(3000).to_signed_bytes_be());
let tick_spacing_usdc_eth = Bytes::from(BigInt::from(60).to_signed_bytes_be()); let tick_spacing_usdc_eth = Bytes::from(BigInt::from(60).to_signed_bytes_be());
let mut static_attributes_usdc_eth: HashMap<String, Bytes> = HashMap::new(); let mut static_attributes_usdc_eth: HashMap<String, Bytes> = HashMap::new();
static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth); static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth);
static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth); static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth);
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d" id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d"
.to_string(), .to_string(),
protocol_system: "uniswap_v4_hooks".to_string(), protocol_system: "uniswap_v4_hooks".to_string(),
static_attributes: static_attributes_usdc_eth, static_attributes: static_attributes_usdc_eth,
..Default::default() ..Default::default()
}, },
token_in: usdc().clone(), usdc().clone(),
token_out: eth().clone(), eth().clone(),
split: 0f64, )
user_data: None, .build()
protocol_state: None,
}
} }
fn swap_eth_pepe_univ4() -> Swap<'static> { fn swap_eth_pepe_univ4() -> Swap {
let pool_fee_eth_pepe = Bytes::from(BigInt::from(25000).to_signed_bytes_be()); let pool_fee_eth_pepe = Bytes::from(BigInt::from(25000).to_signed_bytes_be());
let tick_spacing_eth_pepe = Bytes::from(BigInt::from(500).to_signed_bytes_be()); let tick_spacing_eth_pepe = Bytes::from(BigInt::from(500).to_signed_bytes_be());
let mut static_attributes_eth_pepe: HashMap<String, Bytes> = HashMap::new(); let mut static_attributes_eth_pepe: HashMap<String, Bytes> = HashMap::new();
static_attributes_eth_pepe.insert("key_lp_fee".into(), pool_fee_eth_pepe); static_attributes_eth_pepe.insert("key_lp_fee".into(), pool_fee_eth_pepe);
static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe); static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe);
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9" id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9"
.to_string(), .to_string(),
protocol_system: "uniswap_v4_hooks".to_string(), protocol_system: "uniswap_v4_hooks".to_string(),
static_attributes: static_attributes_eth_pepe, static_attributes: static_attributes_eth_pepe,
..Default::default() ..Default::default()
}, },
token_in: eth().clone(), eth().clone(),
token_out: pepe().clone(), pepe().clone(),
split: 0f64, )
user_data: None, .build()
protocol_state: None,
}
} }
fn router_address() -> Bytes { fn router_address() -> Bytes {
@@ -514,18 +510,16 @@ mod tests {
fn test_encode_router_calldata_single_swap() { fn test_encode_router_calldata_single_swap() {
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let eth_amount_in = BigUint::from(1000u32); let eth_amount_in = BigUint::from(1000u32);
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth(), weth().clone(),
token_out: dai(), dai().clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None,
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -580,31 +574,27 @@ mod tests {
fn test_encode_router_calldata_sequential_swap() { fn test_encode_router_calldata_sequential_swap() {
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let eth_amount_in = BigUint::from(1000u32); let eth_amount_in = BigUint::from(1000u32);
let swap_weth_dai = Swap { let swap_weth_dai = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth(), weth().clone(),
token_out: dai(), dai().clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None,
};
let swap_dai_usdc = Swap { let swap_dai_usdc = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai().clone(),
token_out: usdc(), usdc().clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None,
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -674,18 +664,16 @@ mod tests {
#[test] #[test]
fn test_validate_passes_for_wrap() { fn test_validate_passes_for_wrap() {
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth(), weth().clone(),
token_out: dai(), dai().clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None,
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -704,18 +692,16 @@ mod tests {
#[test] #[test]
fn test_validate_fails_for_wrap_wrong_input() { fn test_validate_fails_for_wrap_wrong_input() {
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth(), weth().clone(),
token_out: dai(), dai().clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None,
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -739,18 +725,16 @@ mod tests {
#[test] #[test]
fn test_validate_fails_for_wrap_wrong_first_swap() { fn test_validate_fails_for_wrap_wrong_first_swap() {
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: eth(), eth().clone(),
token_out: dai(), dai().clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None,
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -794,18 +778,16 @@ mod tests {
#[test] #[test]
fn test_validate_passes_for_unwrap() { fn test_validate_passes_for_unwrap() {
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai().clone(),
token_out: weth(), weth().clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None,
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -823,18 +805,16 @@ mod tests {
#[test] #[test]
fn test_validate_fails_for_unwrap_wrong_output() { fn test_validate_fails_for_unwrap_wrong_output() {
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai().clone(),
token_out: weth(), weth().clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None,
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -859,18 +839,16 @@ mod tests {
#[test] #[test]
fn test_validate_fails_for_unwrap_wrong_last_swap() { fn test_validate_fails_for_unwrap_wrong_last_swap() {
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai().clone(),
token_out: eth(), eth().clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None,
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -900,42 +878,36 @@ mod tests {
// (some of the pool addresses in this test are fake) // (some of the pool addresses in this test are fake)
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swaps = vec![ let swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai().clone(),
token_out: weth(), weth().clone(),
split: 0.5f64, )
user_data: None, .build(),
protocol_state: None, SwapBuilder::new(
}, ProtocolComponent {
Swap {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(), id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai().clone(),
token_out: weth(), weth().clone(),
split: 0f64, )
user_data: None, .build(),
protocol_state: None, SwapBuilder::new(
}, ProtocolComponent {
Swap {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(), id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth(), weth().clone(),
token_out: dai(), dai().clone(),
split: 0f64, )
user_data: None, .build(),
protocol_state: None,
},
]; ];
let solution = Solution { let solution = Solution {
@@ -958,54 +930,46 @@ mod tests {
// (some of the pool addresses in this test are fake) // (some of the pool addresses in this test are fake)
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swaps = vec![ let swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai().clone(),
token_out: weth(), weth().clone(),
split: 0f64, )
user_data: None, .build(),
protocol_state: None, SwapBuilder::new(
}, ProtocolComponent {
Swap {
component: ProtocolComponent {
id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(), id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth(), weth().clone(),
token_out: usdc(), usdc().clone(),
split: 0f64, )
user_data: None, .build(),
protocol_state: None, SwapBuilder::new(
}, ProtocolComponent {
Swap {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(), id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: usdc(), usdc().clone(),
token_out: dai(), dai().clone(),
split: 0f64, )
user_data: None, .build(),
protocol_state: None, SwapBuilder::new(
}, ProtocolComponent {
Swap {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(), id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai().clone(),
token_out: wbtc(), wbtc().clone(),
split: 0f64, )
user_data: None, .build(),
protocol_state: None,
},
]; ];
let solution = Solution { let solution = Solution {
@@ -1035,42 +999,37 @@ mod tests {
// (some of the pool addresses in this test are fake) // (some of the pool addresses in this test are fake)
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swaps = vec![ let swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth(), weth(),
token_out: dai(), dai(),
split: 0f64, )
user_data: None, .build(),
protocol_state: None, SwapBuilder::new(
}, ProtocolComponent {
Swap {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(), id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai(),
token_out: weth(), weth(),
split: 0.5f64, )
user_data: None, .split(0.5)
protocol_state: None, .build(),
}, SwapBuilder::new(
Swap { ProtocolComponent {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(), id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai(),
token_out: weth(), weth(),
split: 0f64, )
user_data: None, .build(),
protocol_state: None,
},
]; ];
let solution = Solution { let solution = Solution {
@@ -1093,30 +1052,26 @@ mod tests {
// (some of the pool addresses in this test are fake) // (some of the pool addresses in this test are fake)
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let swaps = vec![ let swaps = vec![
Swap { SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: weth(), weth(),
token_out: dai(), dai(),
split: 0f64, )
user_data: None, .build(),
protocol_state: None, SwapBuilder::new(
}, ProtocolComponent {
Swap { id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: dai(), dai(),
token_out: weth(), weth(),
split: 0f64, )
user_data: None, .build(),
protocol_state: None,
},
]; ];
let solution = Solution { let solution = Solution {
@@ -1150,7 +1105,7 @@ mod tests {
use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use tycho_common::{models::protocol::ProtocolComponent, Bytes};
use super::*; use super::*;
use crate::encoding::models::{Solution, Swap}; use crate::encoding::models::Solution;
#[test] #[test]
fn test_executor_encoder_encode() { fn test_executor_encoder_encode() {
@@ -1160,18 +1115,16 @@ mod tests {
let token_in = weth(); let token_in = weth();
let token_out = dai(); let token_out = dai();
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: token_in.clone(), token_in.clone(),
token_out: token_out.clone(), token_out.clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None,
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,
@@ -1222,17 +1175,16 @@ mod tests {
let token_in = weth(); let token_in = weth();
let token_out = dai(); let token_out = dai();
let swap = Swap { let swap = SwapBuilder::new(
component: ProtocolComponent { ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(), protocol_system: "uniswap_v2".to_string(),
..Default::default() ..Default::default()
}, },
token_in: token_in.clone(), token_in.clone(),
token_out: token_out.clone(), token_out.clone(),
split: 0f64, )
user_data: None, .build();
protocol_state: None,
};
let solution = Solution { let solution = Solution {
exact_out: false, exact_out: false,

View File

@@ -78,6 +78,9 @@ pub fn get_static_attribute(swap: &Swap, attribute_name: &str) -> Result<Vec<u8>
.to_vec()) .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> { pub fn get_runtime() -> Result<(Handle, Option<Arc<Runtime>>), EncodingError> {
match Handle::try_current() { match Handle::try_current() {
Ok(h) => Ok((h, None)), Ok(h) => Ok((h, None)),

View File

@@ -1,3 +1,5 @@
use std::sync::Arc;
use clap::ValueEnum; use clap::ValueEnum;
use num_bigint::BigUint; use num_bigint::BigUint;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -35,7 +37,7 @@ pub enum UserTransferType {
/// Represents a solution containing details describing an order, and instructions for filling /// Represents a solution containing details describing an order, and instructions for filling
/// the order. /// the order.
#[derive(Clone, Default, Debug, Deserialize, Serialize)] #[derive(Clone, Default, Debug, Deserialize, Serialize)]
pub struct Solution<'a> { pub struct Solution {
/// Address of the sender. /// Address of the sender.
pub sender: Bytes, pub sender: Bytes,
/// Address of the receiver. /// Address of the receiver.
@@ -55,7 +57,7 @@ pub struct Solution<'a> {
#[serde(with = "biguint_string")] #[serde(with = "biguint_string")]
pub checked_amount: BigUint, pub checked_amount: BigUint,
/// List of swaps to fulfill the solution. /// List of swaps to fulfill the solution.
pub swaps: Vec<Swap<'a>>, pub swaps: Vec<Swap>,
/// If set, the corresponding native action will be executed. /// If set, the corresponding native action will be executed.
pub native_action: Option<NativeAction>, pub native_action: Option<NativeAction>,
} }
@@ -74,7 +76,7 @@ pub enum NativeAction {
/// Represents a swap operation to be performed on a pool. /// Represents a swap operation to be performed on a pool.
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Swap<'a> { pub struct Swap {
/// Protocol component from tycho indexer /// Protocol component from tycho indexer
pub component: ProtocolComponent, pub component: ProtocolComponent,
/// Token being input into the pool. /// Token being input into the pool.
@@ -88,33 +90,106 @@ pub struct Swap<'a> {
pub user_data: Option<Bytes>, pub user_data: Option<Bytes>,
/// Optional protocol state used to perform the swap. /// Optional protocol state used to perform the swap.
#[serde(skip)] #[serde(skip)]
pub protocol_state: Option<&'a dyn ProtocolSim>, pub protocol_state: Option<Arc<dyn ProtocolSim>>,
/// Optional estimated amount in for this Swap. This is necessary for RFQ protocols. This value
/// is used to request the quote
pub estimated_amount_in: Option<BigUint>,
} }
impl<'a> Swap<'a> { impl Swap {
pub fn new<T: Into<ProtocolComponent>>( pub fn new<T: Into<ProtocolComponent>>(
component: T, component: T,
token_in: Bytes, token_in: Bytes,
token_out: Bytes, token_out: Bytes,
split: f64, split: f64,
user_data: Option<Bytes>, user_data: Option<Bytes>,
protocol_state: Option<&'a dyn ProtocolSim>, protocol_state: Option<Arc<dyn ProtocolSim>>,
estimated_amount_in: Option<BigUint>,
) -> Self { ) -> Self {
Self { component: component.into(), token_in, token_out, split, user_data, protocol_state } Self {
component: component.into(),
token_in,
token_out,
split,
user_data,
protocol_state,
estimated_amount_in,
}
} }
} }
impl<'a> PartialEq for Swap<'a> { impl PartialEq for Swap {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.component == other.component && self.component == other.component &&
self.token_in == other.token_in && self.token_in == other.token_in &&
self.token_out == other.token_out && self.token_out == other.token_out &&
self.split == other.split && self.split == other.split &&
self.user_data == other.user_data self.user_data == other.user_data &&
self.estimated_amount_in == other.estimated_amount_in
// Skip protocol_state comparison since trait objects don't implement PartialEq // Skip protocol_state comparison since trait objects don't implement PartialEq
} }
} }
pub struct SwapBuilder {
component: ProtocolComponent,
token_in: Bytes,
token_out: Bytes,
split: f64,
user_data: Option<Bytes>,
protocol_state: Option<Arc<dyn ProtocolSim>>,
estimated_amount_in: Option<BigUint>,
}
impl SwapBuilder {
pub fn new<T: Into<ProtocolComponent>>(
component: T,
token_in: Bytes,
token_out: Bytes,
) -> Self {
Self {
component: component.into(),
token_in,
token_out,
split: 0.0,
user_data: None,
protocol_state: None,
estimated_amount_in: None,
}
}
pub fn split(mut self, split: f64) -> Self {
self.split = split;
self
}
pub fn user_data(mut self, user_data: Bytes) -> Self {
self.user_data = Some(user_data);
self
}
pub fn protocol_state(mut self, protocol_state: Arc<dyn ProtocolSim>) -> Self {
self.protocol_state = Some(protocol_state);
self
}
pub fn estimated_amount_in(mut self, estimated_amount_in: BigUint) -> Self {
self.estimated_amount_in = Some(estimated_amount_in);
self
}
pub fn build(self) -> Swap {
Swap {
component: self.component,
token_in: self.token_in,
token_out: self.token_out,
split: self.split,
user_data: self.user_data,
protocol_state: self.protocol_state,
estimated_amount_in: self.estimated_amount_in,
}
}
}
/// Represents a transaction to be executed. /// Represents a transaction to be executed.
/// ///
/// # Fields /// # Fields
@@ -262,6 +337,7 @@ mod tests {
0.5, 0.5,
user_data.clone(), user_data.clone(),
None, None,
None,
); );
assert_eq!(swap.token_in, Bytes::from("0x12")); assert_eq!(swap.token_in, Bytes::from("0x12"));
assert_eq!(swap.token_out, Bytes::from("0x34")); assert_eq!(swap.token_out, Bytes::from("0x34"));

View File

@@ -3,7 +3,10 @@ pub mod encoding;
use std::str::FromStr; use std::str::FromStr;
use alloy::{primitives::B256, signers::local::PrivateKeySigner}; use alloy::{
primitives::{B256, U256},
signers::local::PrivateKeySigner,
};
use tycho_common::{models::Chain, Bytes}; use tycho_common::{models::Chain, Bytes};
use tycho_execution::encoding::{ use tycho_execution::encoding::{
evm::encoder_builders::TychoRouterEncoderBuilder, models::UserTransferType, evm::encoder_builders::TychoRouterEncoderBuilder, models::UserTransferType,
@@ -14,6 +17,14 @@ pub fn router_address() -> Bytes {
Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap() Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap()
} }
pub fn bob_address() -> Bytes {
Bytes::from_str("0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e").unwrap()
}
pub fn alice_address() -> Bytes {
Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap()
}
pub fn eth_chain() -> Chain { pub fn eth_chain() -> Chain {
Chain::Ethereum Chain::Ethereum
} }
@@ -46,6 +57,10 @@ pub fn usdt() -> Bytes {
Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap() Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap()
} }
pub fn ondo() -> Bytes {
Bytes::from_str("0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3").unwrap()
}
pub fn get_signer() -> PrivateKeySigner { pub fn get_signer() -> PrivateKeySigner {
// Set up a mock private key for signing (Alice's pk in our contract tests) // Set up a mock private key for signing (Alice's pk in our contract tests)
let private_key = let private_key =
@@ -57,10 +72,26 @@ pub fn get_signer() -> PrivateKeySigner {
pub fn get_tycho_router_encoder(user_transfer_type: UserTransferType) -> Box<dyn TychoEncoder> { pub fn get_tycho_router_encoder(user_transfer_type: UserTransferType) -> Box<dyn TychoEncoder> {
TychoRouterEncoderBuilder::new() TychoRouterEncoderBuilder::new()
.chain(tycho_common::models::Chain::Ethereum) .chain(Chain::Ethereum)
.user_transfer_type(user_transfer_type) .user_transfer_type(user_transfer_type)
.executors_file_path("config/test_executor_addresses.json".to_string()) .executors_file_path("config/test_executor_addresses.json".to_string())
.router_address(router_address()) .router_address(router_address())
.build() .build()
.expect("Failed to build encoder") .expect("Failed to build encoder")
} }
/// Builds the complete Bebop calldata in the format expected by the encoder
/// Returns: [ partial_fill_offset (u8) | original_taker_amount (U256) | calldata (bytes (selector +
/// ABI encoded params)) ]
pub fn build_bebop_calldata(
calldata: &[u8],
partial_fill_offset: u8,
original_taker_amount: U256,
) -> Bytes {
let mut user_data = Vec::with_capacity(1 + 32 + calldata.len());
user_data.push(partial_fill_offset);
user_data.extend_from_slice(&original_taker_amount.to_be_bytes::<32>());
user_data.extend_from_slice(calldata);
Bytes::from(user_data)
}

View File

@@ -1,15 +1,19 @@
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr, sync::Arc};
use alloy::hex::encode; use alloy::hex::encode;
use num_bigint::{BigInt, BigUint}; use num_bigint::{BigInt, BigUint};
use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use tycho_common::{models::protocol::ProtocolComponent, Bytes};
use tycho_execution::encoding::{ use tycho_execution::encoding::{
evm::utils::write_calldata_to_file, evm::{
models::{Solution, Swap, UserTransferType}, testing_utils::MockRFQState,
utils::{biguint_to_u256, write_calldata_to_file},
},
models::{Solution, Swap, SwapBuilder, UserTransferType},
}; };
use crate::common::{ use crate::common::{
encoding::encode_tycho_router_call, eth, eth_chain, get_signer, get_tycho_router_encoder, weth, alice_address, bob_address, encoding::encode_tycho_router_call, eth, eth_chain, get_signer,
get_tycho_router_encoder, usdc, wbtc, weth,
}; };
mod common; mod common;
@@ -49,6 +53,7 @@ fn test_uniswap_v3_uniswap_v2() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let swap_wbtc_usdc = Swap { let swap_wbtc_usdc = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -61,6 +66,7 @@ fn test_uniswap_v3_uniswap_v2() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -130,6 +136,7 @@ fn test_uniswap_v3_uniswap_v3() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let swap_wbtc_usdc = Swap { let swap_wbtc_usdc = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -150,6 +157,7 @@ fn test_uniswap_v3_uniswap_v3() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -218,6 +226,7 @@ fn test_uniswap_v3_curve() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let swap_wbtc_usdt = Swap { let swap_wbtc_usdt = Swap {
@@ -248,6 +257,7 @@ fn test_uniswap_v3_curve() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -308,6 +318,7 @@ fn test_balancer_v2_uniswap_v2() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let swap_wbtc_usdc = Swap { let swap_wbtc_usdc = Swap {
@@ -321,6 +332,7 @@ fn test_balancer_v2_uniswap_v2() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -384,6 +396,7 @@ fn test_multi_protocol() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let balancer_swap_weth_wbtc = Swap { let balancer_swap_weth_wbtc = Swap {
@@ -397,6 +410,7 @@ fn test_multi_protocol() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let curve_swap_wbtc_usdt = Swap { let curve_swap_wbtc_usdt = Swap {
@@ -427,6 +441,7 @@ fn test_multi_protocol() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
// Ekubo // Ekubo
@@ -450,6 +465,7 @@ fn test_multi_protocol() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
// USV4 // USV4
@@ -474,6 +490,7 @@ fn test_multi_protocol() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -547,6 +564,7 @@ fn test_uniswap_v3_balancer_v3() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let swap_wbtc_qnt = Swap { let swap_wbtc_qnt = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -559,6 +577,7 @@ fn test_uniswap_v3_balancer_v3() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -593,3 +612,227 @@ fn test_uniswap_v3_balancer_v3() {
let hex_calldata = encode(&calldata); let hex_calldata = encode(&calldata);
write_calldata_to_file("test_uniswap_v3_balancer_v3", hex_calldata.as_str()); write_calldata_to_file("test_uniswap_v3_balancer_v3", hex_calldata.as_str());
} }
#[test]
fn test_uniswap_v3_bebop() {
// Note: This test does not assert anything. It is only used to obtain
// integration test data for our router solidity test.
//
// Performs a sequential swap from WETH to WBTC through USDC using USV3 and
// Bebop RFQ
//
// WETH ───(USV3)──> USDC ───(Bebop RFQ)──> WBTC
let weth = weth();
let usdc = usdc();
let wbtc = wbtc();
// First swap: WETH -> USDC via UniswapV3
let swap_weth_usdc = SwapBuilder::new(
ProtocolComponent {
id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* WETH-USDC USV3 Pool
* 0.05% */
protocol_system: "uniswap_v3".to_string(),
static_attributes: {
let mut attrs = HashMap::new();
attrs
.insert("fee".to_string(), Bytes::from(BigInt::from(500).to_signed_bytes_be()));
attrs
},
..Default::default()
},
weth.clone(),
usdc.clone(),
)
.build();
// Second swap: USDC -> WBTC via Bebop RFQ using real order data
let bebop_calldata = Bytes::from_str("0x4dcebcba00000000000000000000000000000000000000000000000000000000689dcb3c0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000bee3211ab312a8d065c4fef0247448e17a8da0000000000000000000000000000000000000000000000000002901f2d62bc91b77000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000000000000000000000000000000000007881786100000000000000000000000000000000000000000000000000000000001984730000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000000000000000000000000000000000000000000000000000000000000000a02bc8495ad1c76c31d466ce719f80400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041f3a03b07f390cd707402912278414c46190ca8ca362dd218b9a58956178cb6ee0e5755db7abe02fe15d498d092d4c6865a5eb18486b3e45e27d50d34b87bf1e21c00000000000000000000000000000000000000000000000000000000000000").unwrap();
let partial_fill_offset = 12u64;
let quote_amount_out = BigUint::from_str("1672307").unwrap();
let bebop_state = MockRFQState {
quote_amount_out,
quote_data: HashMap::from([
("calldata".to_string(), bebop_calldata),
(
"partial_fill_offset".to_string(),
Bytes::from(
partial_fill_offset
.to_be_bytes()
.to_vec(),
),
),
]),
};
let bebop_component = ProtocolComponent {
id: String::from("bebop-rfq"),
protocol_system: String::from("rfq:bebop"),
..Default::default()
};
let swap_usdc_wbtc = SwapBuilder::new(bebop_component, usdc.clone(), wbtc.clone())
.estimated_amount_in(BigUint::from_str("2021750881").unwrap())
.protocol_state(Arc::new(bebop_state))
.build();
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let solution = Solution {
exact_out: false,
given_token: weth,
given_amount: BigUint::from_str("1000000000000000000").unwrap(), // 0.099 WETH
checked_token: wbtc,
checked_amount: BigUint::from_str("1672307").unwrap(),
sender: bob_address(),
receiver: bob_address(),
swaps: vec![swap_weth_usdc, swap_usdc_wbtc],
..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 = encode(&calldata);
write_calldata_to_file("test_uniswap_v3_bebop", hex_calldata.as_str());
}
#[test]
#[ignore]
fn test_uniswap_v3_hashflow() {
// Note: This test does not assert anything. It is only used to obtain
// integration test data for our router solidity test.
//
// Performs a sequential swap from WETH to WBTC through USDC using USV3 and
// Hashflow RFQ
//
// WETH ───(USV3)──> USDC ───(Hashflow RFQ)──> WBTC
let weth = weth();
let usdc = usdc();
let wbtc = wbtc();
// First swap: WETH -> USDC via UniswapV3
let swap_weth_usdc = SwapBuilder::new(
ProtocolComponent {
id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* WETH-USDC USV3 Pool
* 0.05% */
protocol_system: "uniswap_v3".to_string(),
static_attributes: {
let mut attrs = HashMap::new();
attrs
.insert("fee".to_string(), Bytes::from(BigInt::from(500).to_signed_bytes_be()));
attrs
},
..Default::default()
},
weth.clone(),
usdc.clone(),
)
.build();
// Second swap: USDC -> WBTC via Hashflow RFQ using real order data
let quote_amount_out = BigUint::from_str("3714751").unwrap();
let hashflow_state = MockRFQState {
quote_amount_out,
quote_data: HashMap::from([
(
"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(4308094737_u64)).to_be_bytes::<32>().to_vec()),
),
(
"quote_token_amount".to_string(),
Bytes::from(biguint_to_u256(&BigUint::from(3714751_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_component = ProtocolComponent {
id: String::from("hashflow-rfq"),
protocol_system: String::from("rfq:hashflow"),
..Default::default()
};
let swap_usdc_wbtc = SwapBuilder::new(hashflow_component, usdc.clone(), wbtc.clone())
.estimated_amount_in(BigUint::from_str("4308094737").unwrap())
.protocol_state(Arc::new(hashflow_state))
.build();
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let solution = Solution {
exact_out: false,
given_token: weth,
given_amount: BigUint::from_str("1000000000000000000").unwrap(),
checked_token: wbtc,
checked_amount: BigUint::from_str("3714751").unwrap(),
sender: alice_address(),
receiver: alice_address(),
swaps: vec![swap_weth_usdc, swap_usdc_wbtc],
..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 = encode(&calldata);
write_calldata_to_file("test_uniswap_v3_hashflow", hex_calldata.as_str());
}

View File

@@ -1,18 +1,22 @@
mod common; mod common;
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr, sync::Arc};
use alloy::hex::encode; use alloy::{hex, hex::encode};
use num_bigint::{BigInt, BigUint}; use num_bigint::{BigInt, BigUint};
use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use tycho_common::{models::protocol::ProtocolComponent, Bytes};
use tycho_execution::encoding::{ use tycho_execution::encoding::{
evm::utils::write_calldata_to_file, evm::{
models::{Solution, Swap, UserTransferType}, testing_utils::MockRFQState,
utils::{biguint_to_u256, write_calldata_to_file},
},
models::{Solution, Swap, SwapBuilder, UserTransferType},
}; };
use crate::common::{ use crate::common::{
encoding::encode_tycho_router_call, eth, eth_chain, get_signer, get_tycho_router_encoder, pepe, alice_address, build_bebop_calldata, encoding::encode_tycho_router_call, eth, eth_chain,
usdc, weth, get_signer, get_tycho_router_encoder, ondo, pepe, usdc, wbtc, weth,
}; };
#[test] #[test]
fn test_single_encoding_strategy_ekubo() { fn test_single_encoding_strategy_ekubo() {
// ETH ──(EKUBO)──> USDC // ETH ──(EKUBO)──> USDC
@@ -41,6 +45,7 @@ fn test_single_encoding_strategy_ekubo() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -94,6 +99,7 @@ fn test_single_encoding_strategy_maverick() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -159,6 +165,7 @@ fn test_single_encoding_strategy_usv4_eth_in() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -226,6 +233,7 @@ fn test_single_encoding_strategy_usv4_eth_out() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -301,6 +309,7 @@ fn test_single_encoding_strategy_usv4_grouped_swap() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let swap_eth_pepe = Swap { let swap_eth_pepe = Swap {
@@ -315,6 +324,7 @@ fn test_single_encoding_strategy_usv4_grouped_swap() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -427,6 +437,7 @@ fn test_single_encoding_strategy_curve() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -495,6 +506,7 @@ fn test_single_encoding_strategy_curve_st_eth() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -549,6 +561,7 @@ fn test_single_encoding_strategy_balancer_v3() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -584,3 +597,230 @@ fn test_single_encoding_strategy_balancer_v3() {
let hex_calldata = encode(&calldata); let hex_calldata = encode(&calldata);
write_calldata_to_file("test_single_encoding_strategy_balancer_v3", hex_calldata.as_str()); write_calldata_to_file("test_single_encoding_strategy_balancer_v3", hex_calldata.as_str());
} }
#[test]
fn test_single_encoding_strategy_bebop() {
// The quote was done separately where the sender is the router and the receiver is a random
// user
let _router = Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap();
let user = Bytes::from_str("0xd2068e04cf586f76eece7ba5beb779d7bb1474a1").unwrap();
let token_in = usdc();
let token_out = ondo();
let amount_in = BigUint::from_str("200000000").unwrap(); // 200 USDC
let amount_out = BigUint::from_str("194477331556159832309").unwrap(); // 203.8 ONDO
let partial_fill_offset = 12;
let calldata = Bytes::from_str("0x4dcebcba00000000000000000000000000000000000000000000000000000000689b548f0000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d0139500000000000000000000000067336cec42645f55059eff241cb02ea5cc52ff86000000000000000000000000000000000000000000000000279ead5d9685f25b000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be3000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000a8aea46aa4ec5c0f5000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000000005230bcb979c81cebf94a3b5c08bcfa300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000414ce40058ff07f11d9224c2c8d1e58369e4a90173856202d8d2a17da48058ad683dedb742eda0d4c0cf04cf1c09138898dd7fd06f97268ea7f74ef9b42d29bf4c1b00000000000000000000000000000000000000000000000000000000000000").unwrap();
let user_data =
build_bebop_calldata(&calldata, partial_fill_offset, biguint_to_u256(&amount_in));
let bebop_component = ProtocolComponent {
id: String::from("bebop-rfq"),
protocol_system: String::from("rfq:bebop"),
static_attributes: HashMap::new(), // No static attributes needed
..Default::default()
};
let swap = SwapBuilder::new(bebop_component, token_in.clone(), token_out.clone())
.user_data(user_data)
.build();
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,
checked_amount: amount_out, // Expected output amount
sender: user.clone(),
receiver: user,
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", hex_calldata.as_str());
}
#[test]
fn test_single_encoding_strategy_bebop_aggregate() {
// The quote was done separately where the sender is the router and the receiver is a random
// user
let _router = Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap();
let user = Bytes::from_str("0xd2068e04cf586f76eece7ba5beb779d7bb1474a1").unwrap();
let token_in = usdc();
let token_out = ondo();
let amount_in = BigUint::from_str("20000000000").unwrap(); // 20k USDC
let amount_out = BigUint::from_str("18699321819466078474202").unwrap(); // 203.8 ONDO
let partial_fill_offset = 2;
let calldata = Bytes::from_str("0xa2f7489300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000689b78880000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c0000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000005a060a5c2aaaaa2fe2cda34423cac76a84c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000002901f2d62bb356ca0000000000000000000000000000000000000000000000002901f2d62bb356cb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000001000000000000000000000000faba6f8e4a5e8ab82f62fe7c39859fa577269be30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000044f83c726000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000589400da00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000003aa5f96046644f6e37a000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000004b51a26526ddbeec60000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000417ab4332f2b091d87d56d04eee35dd49452782c782de71608c0425c5ae41f1d7e147173851c870d76720ce07d45cd8622352716b1c7965819ee2bf8c573c499ae1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000410c8da2637aa929e11caff9afdfc4c489320c6dba77cc934d88ba8956e365fd1d48983087c6e474bbb828181cdfdd17317c4c9c3ee4bc98e3769d0c05cc7a285e1c00000000000000000000000000000000000000000000000000000000000000").unwrap();
let user_data =
build_bebop_calldata(&calldata, partial_fill_offset, biguint_to_u256(&amount_in));
let bebop_component = ProtocolComponent {
id: String::from("bebop-rfq"),
protocol_system: String::from("rfq:bebop"),
static_attributes: HashMap::new(),
..Default::default()
};
let swap = SwapBuilder::new(bebop_component, token_in.clone(), token_out.clone())
.user_data(user_data)
.build();
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let solution = Solution {
exact_out: false,
given_token: token_in.clone(),
given_amount: amount_in,
checked_token: token_out,
checked_amount: amount_out,
sender: user.clone(),
receiver: user,
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_aggregate", hex_calldata.as_str());
}
#[test]
fn test_single_encoding_strategy_hashflow() {
// Note: This test does not assert anything. It is only used to obtain
// integration test data for our router solidity test.
//
// Performs a swap from USDC to WBTC using Hashflow RFQ
//
// USDC ───(Hashflow RFQ)──> WBTC
let usdc = usdc();
let wbtc = wbtc();
// USDC -> WBTC via Hashflow RFQ using real order data
let quote_amount_out = BigUint::from_str("3714751").unwrap();
let hashflow_state = MockRFQState {
quote_amount_out,
quote_data: HashMap::from([
(
"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(4308094737_u64)).to_be_bytes::<32>().to_vec()),
),
(
"quote_token_amount".to_string(),
Bytes::from(biguint_to_u256(&BigUint::from(3714751_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_component = ProtocolComponent {
id: String::from("hashflow-rfq"),
protocol_system: String::from("rfq:hashflow"),
..Default::default()
};
let swap_usdc_wbtc = SwapBuilder::new(hashflow_component, usdc.clone(), wbtc.clone())
.estimated_amount_in(BigUint::from_str("4308094737").unwrap())
.protocol_state(Arc::new(hashflow_state))
.build();
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
let solution = Solution {
exact_out: false,
given_token: usdc,
given_amount: BigUint::from_str("4308094737").unwrap(),
checked_token: wbtc,
checked_amount: BigUint::from_str("3714751").unwrap(),
sender: alice_address(),
receiver: alice_address(),
swaps: vec![swap_usdc_wbtc],
..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 = encode(&calldata);
write_calldata_to_file("test_single_encoding_strategy_hashflow", hex_calldata.as_str());
}

View File

@@ -6,7 +6,7 @@ use num_bigint::{BigInt, BigUint};
use tycho_common::{models::protocol::ProtocolComponent, Bytes}; use tycho_common::{models::protocol::ProtocolComponent, Bytes};
use tycho_execution::encoding::{ use tycho_execution::encoding::{
evm::utils::write_calldata_to_file, evm::utils::write_calldata_to_file,
models::{Solution, Swap, UserTransferType}, models::{NativeAction, Solution, Swap, UserTransferType},
}; };
use crate::common::{ use crate::common::{
@@ -19,7 +19,7 @@ fn test_sequential_swap_strategy_encoder() {
// Note: This test does not assert anything. It is only used to obtain integration // Note: This test does not assert anything. It is only used to obtain integration
// test data for our router solidity test. // test data for our router solidity test.
// //
// Performs a sequential swap from WETH to USDC though WBTC using USV2 pools // Performs a sequential swap from WETH to USDC through WBTC using USV2 pools
// //
// WETH ───(USV2)──> WBTC ───(USV2)──> USDC // WETH ───(USV2)──> WBTC ───(USV2)──> USDC
@@ -38,6 +38,7 @@ fn test_sequential_swap_strategy_encoder() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let swap_wbtc_usdc = Swap { let swap_wbtc_usdc = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -50,6 +51,7 @@ fn test_sequential_swap_strategy_encoder() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -106,6 +108,7 @@ fn test_sequential_swap_strategy_encoder_no_permit2() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let swap_wbtc_usdc = Swap { let swap_wbtc_usdc = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -118,6 +121,7 @@ fn test_sequential_swap_strategy_encoder_no_permit2() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -220,6 +224,7 @@ fn test_sequential_strategy_cyclic_swap() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
// WETH -> USDC (Pool 2) // WETH -> USDC (Pool 2)
@@ -243,6 +248,7 @@ fn test_sequential_strategy_cyclic_swap() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -316,3 +322,77 @@ fn test_sequential_strategy_cyclic_swap() {
assert_eq!(hex_calldata[1224..], expected_swaps); assert_eq!(hex_calldata[1224..], expected_swaps);
write_calldata_to_file("test_sequential_strategy_cyclic_swap", hex_calldata.as_str()); write_calldata_to_file("test_sequential_strategy_cyclic_swap", hex_calldata.as_str());
} }
#[test]
fn test_sequential_swap_strategy_encoder_unwrap() {
// Note: This test does not assert anything. It is only used to obtain integration
// test data for our router solidity test.
//
// Performs a sequential swap from USDC to ETH through WBTC using USV2 pools and unwrapping in
// the end
//
// USDC ───(USV2)──> WBTC ───(USV2)──> WETH -> ETH
let weth = weth();
let wbtc = wbtc();
let usdc = usdc();
let swap_usdc_wbtc = Swap {
component: ProtocolComponent {
id: "0x004375Dff511095CC5A197A54140a24eFEF3A416".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: usdc.clone(),
token_out: wbtc.clone(),
split: 0f64,
user_data: None,
protocol_state: None,
estimated_amount_in: None,
};
let swap_wbtc_weth = Swap {
component: ProtocolComponent {
id: "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: wbtc.clone(),
token_out: weth.clone(),
split: 0f64,
user_data: None,
protocol_state: None,
estimated_amount_in: None,
};
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
let solution = Solution {
exact_out: false,
given_token: usdc,
given_amount: BigUint::from_str("3_000_000_000").unwrap(),
checked_token: eth(),
checked_amount: BigUint::from_str("26173932").unwrap(),
sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
swaps: vec![swap_usdc_wbtc, swap_wbtc_weth],
native_action: Some(NativeAction::Unwrap),
};
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::TransferFromPermit2,
&eth(),
Some(get_signer()),
)
.unwrap()
.data;
let hex_calldata = encode(&calldata);
write_calldata_to_file("test_sequential_swap_strategy_encoder_unwrap", hex_calldata.as_str());
}

View File

@@ -34,6 +34,7 @@ fn test_single_swap_strategy_encoder() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -121,6 +122,7 @@ fn test_single_swap_strategy_encoder_no_permit2() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
@@ -204,6 +206,7 @@ fn test_single_swap_strategy_encoder_no_transfer_in() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::None); let encoder = get_tycho_router_encoder(UserTransferType::None);
@@ -288,6 +291,7 @@ fn test_single_swap_strategy_encoder_wrap() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -341,6 +345,7 @@ fn test_single_swap_strategy_encoder_unwrap() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);

View File

@@ -43,6 +43,7 @@ fn test_split_swap_strategy_encoder() {
split: 0.5f64, split: 0.5f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let swap_weth_wbtc = Swap { let swap_weth_wbtc = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -58,6 +59,7 @@ fn test_split_swap_strategy_encoder() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let swap_dai_usdc = Swap { let swap_dai_usdc = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -70,6 +72,7 @@ fn test_split_swap_strategy_encoder() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let swap_wbtc_usdc = Swap { let swap_wbtc_usdc = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -82,6 +85,7 @@ fn test_split_swap_strategy_encoder() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -149,6 +153,7 @@ fn test_split_input_cyclic_swap() {
split: 0.6f64, // 60% of input split: 0.6f64, // 60% of input
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
// USDC -> WETH (Pool 2) - 40% of input (remaining) // USDC -> WETH (Pool 2) - 40% of input (remaining)
@@ -172,6 +177,7 @@ fn test_split_input_cyclic_swap() {
split: 0f64, split: 0f64,
user_data: None, // Remaining 40% user_data: None, // Remaining 40%
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
// WETH -> USDC (Pool 2) // WETH -> USDC (Pool 2)
@@ -195,6 +201,7 @@ fn test_split_input_cyclic_swap() {
split: 0.0f64, split: 0.0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);
@@ -316,6 +323,7 @@ fn test_split_output_cyclic_swap() {
split: 0.0f64, split: 0.0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let swap_weth_usdc_v3_pool1 = Swap { let swap_weth_usdc_v3_pool1 = Swap {
@@ -336,6 +344,7 @@ fn test_split_output_cyclic_swap() {
split: 0.6f64, split: 0.6f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let swap_weth_usdc_v3_pool2 = Swap { let swap_weth_usdc_v3_pool2 = Swap {
@@ -358,6 +367,7 @@ fn test_split_output_cyclic_swap() {
split: 0.0f64, split: 0.0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2); let encoder = get_tycho_router_encoder(UserTransferType::TransferFromPermit2);

View File

@@ -52,6 +52,7 @@ fn test_sequential_swap_usx() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let swap_usdc_usdt = Swap { let swap_usdc_usdt = Swap {
component: ProtocolComponent { component: ProtocolComponent {
@@ -70,6 +71,7 @@ fn test_sequential_swap_usx() {
split: 0f64, split: 0f64,
user_data: None, user_data: None,
protocol_state: None, protocol_state: None,
estimated_amount_in: None,
}; };
let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);