chore: merge main
This commit is contained in:
@@ -55,6 +55,7 @@ contract Constants is Test, BaseConstants {
|
||||
address WTAO_ADDR = address(0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44);
|
||||
address BSGG_ADDR = address(0xdA16Cf041E2780618c49Dbae5d734B89a6Bac9b3);
|
||||
address GHO_ADDR = address(0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f);
|
||||
address ONDO_ADDR = address(0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3);
|
||||
|
||||
// Maverick v2
|
||||
address MAVERICK_V2_FACTORY = 0x0A7e848Aca42d879EF06507Fca0E7b33A0a63c1e;
|
||||
@@ -124,6 +125,12 @@ contract Constants is Test, BaseConstants {
|
||||
// Permit2
|
||||
address PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
|
||||
|
||||
// Bebop Settlement
|
||||
address BEBOP_SETTLEMENT = 0xbbbbbBB520d69a9775E85b458C58c648259FAD5F;
|
||||
|
||||
// Hashflow Router
|
||||
address HASHFLOW_ROUTER = 0x55084eE0fEf03f14a305cd24286359A35D735151;
|
||||
|
||||
// Pool Code Init Hashes
|
||||
bytes32 USV2_POOL_CODE_INIT_HASH =
|
||||
0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f;
|
||||
|
||||
@@ -26,4 +26,26 @@ contract TestUtils is Test {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ pragma solidity ^0.8.26;
|
||||
|
||||
import "./TychoRouterTestSetup.sol";
|
||||
import "./protocols/UniswapV4Utils.sol";
|
||||
import "@src/executors/BebopExecutor.sol";
|
||||
|
||||
contract TychoRouterTestProtocolIntegration is TychoRouterTestSetup {
|
||||
function testMultiProtocolIntegration() public {
|
||||
|
||||
@@ -492,4 +492,95 @@ contract TychoRouterSequentialSwapTest is TychoRouterTestSetup {
|
||||
assertEq(balanceAfter - balanceBefore, 1949668893);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ pragma solidity ^0.8.26;
|
||||
// Executors
|
||||
import {BalancerV2Executor} from "../src/executors/BalancerV2Executor.sol";
|
||||
import {BalancerV3Executor} from "../src/executors/BalancerV3Executor.sol";
|
||||
import {BebopExecutor} from "../src/executors/BebopExecutor.sol";
|
||||
import {CurveExecutor} from "../src/executors/CurveExecutor.sol";
|
||||
import {EkuboExecutor} from "../src/executors/EkuboExecutor.sol";
|
||||
import {HashflowExecutor} from "../src/executors/HashflowExecutor.sol";
|
||||
import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol";
|
||||
import {UniswapV2Executor} from "../src/executors/UniswapV2Executor.sol";
|
||||
import {
|
||||
@@ -73,12 +75,14 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
|
||||
CurveExecutor public curveExecutor;
|
||||
MaverickV2Executor public maverickv2Executor;
|
||||
BalancerV3Executor public balancerV3Executor;
|
||||
BebopExecutor public bebopExecutor;
|
||||
HashflowExecutor public hashflowExecutor;
|
||||
|
||||
function getForkBlock() public view virtual returns (uint256) {
|
||||
return 22082754;
|
||||
}
|
||||
|
||||
function setUp() public {
|
||||
function setUp() public virtual {
|
||||
uint256 forkBlock = getForkBlock();
|
||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||
|
||||
@@ -132,8 +136,11 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
|
||||
maverickv2Executor =
|
||||
new MaverickV2Executor(MAVERICK_V2_FACTORY, 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[1] = address(usv3Executor);
|
||||
executors[2] = address(pancakev3Executor);
|
||||
@@ -143,6 +150,8 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
|
||||
executors[6] = address(curveExecutor);
|
||||
executors[7] = address(maverickv2Executor);
|
||||
executors[8] = address(balancerV3Executor);
|
||||
executors[9] = address(bebopExecutor);
|
||||
executors[10] = address(hashflowExecutor);
|
||||
|
||||
return executors;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -134,4 +134,9 @@ contract BalancerV2ExecutorTest is Constants, TestUtils {
|
||||
assertGt(balanceAfter, balanceBefore);
|
||||
assertEq(balanceAfter - balanceBefore, amountOut);
|
||||
}
|
||||
|
||||
function testExportContract() public {
|
||||
vm.skip(true);
|
||||
exportRuntimeBytecode(address(balancerV2Exposed), "BalancerV2");
|
||||
}
|
||||
}
|
||||
|
||||
470
foundry/test/protocols/Bebop.t.sol
Normal file
470
foundry/test/protocols/Bebop.t.sol
Normal 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();
|
||||
}
|
||||
}
|
||||
316
foundry/test/protocols/Hashflow.t.sol
Normal file
316
foundry/test/protocols/Hashflow.t.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user