From a8925c9afb67c9002b8a794d1dc4c5579bb602ef Mon Sep 17 00:00:00 2001 From: Igor Markin Date: Wed, 4 Feb 2026 17:19:48 +0000 Subject: [PATCH 01/15] Initial --- config/executor_addresses.json | 1 + config/test_executor_addresses.json | 3 +- foundry/foundry.lock | 35 ++ foundry/src/executors/LiquoriceExecutor.sol | 205 ++++++++ foundry/test/assets/calldata.txt | 1 + foundry/test/protocols/Liquorice.t.sol | 231 +++++++++ src/encoding/evm/constants.rs | 1 + src/encoding/evm/swap_encoder/liquorice.rs | 439 ++++++++++++++++++ src/encoding/evm/swap_encoder/mod.rs | 1 + .../evm/swap_encoder/swap_encoder_registry.rs | 4 + tests/protocol_integration_tests.rs | 96 ++++ 11 files changed, 1016 insertions(+), 1 deletion(-) create mode 100644 foundry/foundry.lock create mode 100644 foundry/src/executors/LiquoriceExecutor.sol create mode 100644 foundry/test/protocols/Liquorice.t.sol create mode 100644 src/encoding/evm/swap_encoder/liquorice.rs diff --git a/config/executor_addresses.json b/config/executor_addresses.json index db357761..f1cb29b4 100644 --- a/config/executor_addresses.json +++ b/config/executor_addresses.json @@ -13,6 +13,7 @@ "vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec", "rfq:bebop": "0xFE42BFb115eD9671011cA52BDD23A52A2e077a7c", "rfq:hashflow": "0x19e49Db786c87F4e46B10aFb21c0C06d34270f98", + "rfq:liquorice": "0x0000000000000000000000000000000000000000", "fluid_v1": "0x7191A6a3006ea020C73aCD7068295B9B3767a3Bb", "rocketpool": "0x495c0e0f98011df5856A96236cA87D9Fb00B8d0B", "erc4626": "0x842c7e2dA0781B1e8Ea40c377898EFF4002eaB22", diff --git a/config/test_executor_addresses.json b/config/test_executor_addresses.json index 0ed15187..7e856aed 100644 --- a/config/test_executor_addresses.json +++ b/config/test_executor_addresses.json @@ -13,6 +13,7 @@ "vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb", "rfq:bebop": "0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF", "rfq:hashflow": "0x15cF58144EF33af1e14b5208015d11F9143E27b9", + "rfq:liquorice": "0x0000000000000000000000000000000000000000", "fluid_v1": "0x212224D2F2d262cd093eE13240ca4873fcCBbA3C", "rocketpool": "0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7", "erc4626": "0xD16d567549A2a2a2005aEACf7fB193851603dd70", @@ -22,4 +23,4 @@ "base": { "aerodrome_slipstreams": "0x2a07706473244BC757E10F2a9E86fB532828afe3" } -} +} \ No newline at end of file diff --git a/foundry/foundry.lock b/foundry/foundry.lock new file mode 100644 index 00000000..98495f0a --- /dev/null +++ b/foundry/foundry.lock @@ -0,0 +1,35 @@ +{ + "lib/balancer-v2-monorepo": { + "rev": "36d282374b457dddea828be7884ee0d185db06ba" + }, + "lib/balancer-v3-monorepo": { + "rev": "84cfb0d9d0bd024f92c7a95c64ab7ab8b4c12f4b" + }, + "lib/ekubo-v3": { + "rev": "81c2c2642afe321e7f5d7de70f8b3be18f6f80b3" + }, + "lib/forge-std": { + "rev": "1801b0541f4fda118a10798fd3486bb7051c5dd6" + }, + "lib/openzeppelin-contracts": { + "rev": "acd4ff74de833399287ed6b31b4debf6b2b35527" + }, + "lib/permit2": { + "rev": "cc56ad0f3439c502c246fc5cfcc3db92bb8b7219" + }, + "lib/solady": { + "rev": "c9e079c0ca836dcc52777a1fa7227ef28e3537b3" + }, + "lib/v2-core": { + "rev": "4dd59067c76dea4a0e8e4bfdda41877a6b16dedc" + }, + "lib/v3-core": { + "rev": "d8b1c635c275d2a9450bd6a78f3fa2484fef73eb" + }, + "lib/v4-core": { + "rev": "59d3ecf53afa9264a16bba0e38f4c5d2231f80bc" + }, + "lib/v4-periphery": { + "rev": "ad04c9f24a170accf5ea1b2836bbafd514537ca6" + } +} \ No newline at end of file diff --git a/foundry/src/executors/LiquoriceExecutor.sol b/foundry/src/executors/LiquoriceExecutor.sol new file mode 100644 index 00000000..5f602e52 --- /dev/null +++ b/foundry/src/executors/LiquoriceExecutor.sol @@ -0,0 +1,205 @@ +// 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 LiquoriceExecutor +/// @notice Executor for Liquorice RFQ (Request for Quote) swaps +/// @dev Handles RFQ swaps through Liquorice settlement contracts with support for +/// partial fills and dynamic allowance management +contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { + using Math for uint256; + using SafeERC20 for IERC20; + using Address for address; + + /// @notice Liquorice-specific errors + error LiquoriceExecutor__InvalidDataLength(); + + /// @notice Maximum number of allowances supported + uint8 public constant MAX_ALLOWANCES = 5; + + // TODO: add _liquoriceSettlement address argument + constructor(address _permit2) RestrictTransferFrom(_permit2) {} + + /// @notice Executes a swap through Liquorice's RFQ system + /// @param givenAmount The amount of input token to swap + /// @param data Encoded swap data containing tokens, allowances, and liquorice 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 originalBaseTokenAmount, + address[] memory allowanceSpenders, + bool[] memory approvalsNeeded, // TODO: change to bool approvalNeeded, and give approval to _liquoriceBalanceManager + address targetContract, // TODO: remove, use _liquoriceSettlement instead + address receiver, + bytes memory liquoriceCalldata + ) = _decodeData(data); + + // Transfer tokens to executor + _transfer(address(this), transferType, tokenIn, givenAmount); + + // Grant approvals to spenders as needed + for (uint256 i = 0; i < allowanceSpenders.length; i++) { + if (approvalsNeeded[i] && tokenIn != address(0)) { + // slither-disable-next-line unused-return + IERC20(tokenIn).forceApprove( + allowanceSpenders[i], + type(uint256).max + ); + } + } + + // Modify the fill amount in the calldata if partial fill is supported + // If partialFillOffset is 0, partial fill is not supported + bytes memory finalCalldata = liquoriceCalldata; + if (partialFillOffset > 0) { + finalCalldata = _modifyFilledTakerAmount( + liquoriceCalldata, + givenAmount, + originalBaseTokenAmount, + partialFillOffset + ); + } + + uint256 balanceBefore = _balanceOf(tokenOut, receiver); + uint256 ethValue = tokenIn == address(0) ? givenAmount : 0; + + // Execute the swap by forwarding calldata to target contract + // slither-disable-next-line unused-return + targetContract.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 originalBaseTokenAmount, + address[] memory allowanceSpenders, + bool[] memory approvalsNeeded, + address targetContract, + address receiver, + bytes memory liquoriceCalldata + ) + { + // Minimum fixed fields before allowances: + // tokenIn (20) + tokenOut (20) + transferType (1) + partialFillOffset (1) + + // originalBaseTokenAmount (32) + numAllowances (1) = 75 bytes + // Plus at minimum: targetContract (20) + receiver (20) = 40 bytes + // Total minimum: 115 bytes (with 0 allowances) + if (data.length < 115) revert LiquoriceExecutor__InvalidDataLength(); + + tokenIn = address(bytes20(data[0:20])); + tokenOut = address(bytes20(data[20:40])); + transferType = TransferType(uint8(data[40])); + partialFillOffset = uint8(data[41]); + originalBaseTokenAmount = uint256(bytes32(data[42:74])); + + uint8 numAllowances = uint8(data[74]); + + // Each allowance: spender (20) + approvalNeeded (1) = 21 bytes + uint256 allowancesEnd = 75 + (uint256(numAllowances) * 21); + + // Validate data length includes all allowances plus target, receiver, and at least some calldata + if (data.length < allowancesEnd + 40) { + revert LiquoriceExecutor__InvalidDataLength(); + } + + allowanceSpenders = new address[](numAllowances); + approvalsNeeded = new bool[](numAllowances); + + for (uint256 i = 0; i < numAllowances; i++) { + uint256 offset = 75 + (i * 21); + allowanceSpenders[i] = address(bytes20(data[offset:offset + 20])); + approvalsNeeded[i] = data[offset + 20] != 0; + } + + targetContract = address( + bytes20(data[allowancesEnd:allowancesEnd + 20]) + ); + receiver = address( + bytes20(data[allowancesEnd + 20:allowancesEnd + 40]) + ); + liquoriceCalldata = data[allowancesEnd + 40:]; + } + + /// @dev Modifies the filledTakerAmount in the liquorice calldata to handle slippage + /// @param liquoriceCalldata The original calldata for the liquorice settlement + /// @param givenAmount The actual amount available from the router + /// @param originalBaseTokenAmount The original amount expected when the quote was generated + /// @param partialFillOffset The offset from Liquorice API indicating where the fill amount is located + /// @return The modified calldata with updated fill amount + function _modifyFilledTakerAmount( + bytes memory liquoriceCalldata, + uint256 givenAmount, + uint256 originalBaseTokenAmount, + uint8 partialFillOffset + ) internal pure returns (bytes memory) { + // Use the offset from Liquorice API to locate the fill amount + // Position = 4 bytes (selector) + offset * 32 bytes + uint256 fillAmountPos = 4 + uint256(partialFillOffset) * 32; + + // Cap the fill amount at what we actually have available + uint256 newFillAmount = originalBaseTokenAmount > givenAmount + ? givenAmount + : originalBaseTokenAmount; + + // If the new fill amount is the same as the original, return the original calldata + if (newFillAmount == originalBaseTokenAmount) { + return liquoriceCalldata; + } + + // Use assembly to modify the fill amount at the correct position + // slither-disable-next-line assembly + assembly { + // Get pointer to the data portion of the bytes array + let dataPtr := add(liquoriceCalldata, 0x20) + + // Calculate the actual position and store the new value + let actualPos := add(dataPtr, fillAmountPos) + mstore(actualPos, newFillAmount) + } + + return liquoriceCalldata; + } + + /// @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 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 + receive() external payable {} +} diff --git a/foundry/test/assets/calldata.txt b/foundry/test/assets/calldata.txt index e9102124..00ffdf56 100644 --- a/foundry/test/assets/calldata.txt +++ b/foundry/test/assets/calldata.txt @@ -63,3 +63,4 @@ test_encoding_strategy_curve_lido_sequential_swap:e21dd0d30000000000000000000000 test_ekubo_v3_encode_swap_multi:01cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48517e506700271aea091b02f42756f5e174af5230000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000a7c5ac471b4880000032 test_single_encoding_strategy_ekubo_v3:5c4b639c0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000007113aa49bac059d709dd0a18d6bb63290076a702d702cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48517e506700271aea091b02f42756f5e174af5230000000000000000000000000000000000000000000000000000000 test_single_ekubo_v3_grouped_swap:5c4b639c00000000000000000000000000000000000000000000000000000002540be400000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a513aa49bac059d709dd0a18d6bb63290076a702d700cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2dac17f958d2ee523a2206206994597c13d831ec7a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000a7c5ac471b48800000320000000000000000000000000000000000000000517e506700271aea091b02f42756f5e174af5230000000000000000000000000000000000000000000000000000000000000000000000000000000 +test_single_encoding_strategy_liquorice:5c4b639c00000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000bd0000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000c00000000000000000000000000000000000000000000000000000000b2d05e000171d9750ecf0c5081fae4e3edc4253e52024b0b590171d9750ecf0c5081fae4e3edc4253e52024b0b59cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2deadbeef1234567890abcdef1234567890abcdef1234567890abcdef1234567890000000 diff --git a/foundry/test/protocols/Liquorice.t.sol b/foundry/test/protocols/Liquorice.t.sol new file mode 100644 index 00000000..eb4906f3 --- /dev/null +++ b/foundry/test/protocols/Liquorice.t.sol @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "../TestUtils.sol"; +import "../TychoRouterTestSetup.sol"; +import "@src/executors/LiquoriceExecutor.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 LiquoriceExecutorExposed is LiquoriceExecutor { + constructor(address _permit2) LiquoriceExecutor(_permit2) {} + + function decodeData(bytes calldata data) + external + pure + returns ( + address tokenIn, + address tokenOut, + TransferType transferType, + uint8 partialFillOffset, + uint256 originalBaseTokenAmount, + address[] memory allowanceSpenders, + bool[] memory approvalsNeeded, + address targetContract, + address receiver, + bytes memory liquoriceCalldata + ) + { + return _decodeData(data); + } +} + +contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { + using SafeERC20 for IERC20; + + LiquoriceExecutorExposed liquoriceExecutor; + + IERC20 WETH = IERC20(WETH_ADDR); + IERC20 USDC = IERC20(USDC_ADDR); + IERC20 WBTC = IERC20(WBTC_ADDR); + + function setUp() public { + vm.createSelectFork(vm.rpcUrl("mainnet"), 22667985); + liquoriceExecutor = new LiquoriceExecutorExposed(PERMIT2_ADDRESS); + } + + function testDecodeData_NoAllowances() public view { + bytes memory liquoriceCalldata = abi.encodePacked( + bytes4(0xdeadbeef), // mock selector + hex"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + ); + + uint256 originalAmount = 1000000000; // 1000 USDC + address targetContract = address(0x1234567890123456789012345678901234567890); + address receiver = address(0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD); + + // Encode with 0 allowances + bytes memory params = abi.encodePacked( + USDC_ADDR, // tokenIn (20 bytes) + WETH_ADDR, // tokenOut (20 bytes) + uint8(RestrictTransferFrom.TransferType.Transfer), // transferType (1 byte) + uint8(5), // partialFillOffset (1 byte) + originalAmount, // originalBaseTokenAmount (32 bytes) + uint8(0), // numAllowances (1 byte) + targetContract, // targetContract (20 bytes) + receiver, // receiver (20 bytes) + liquoriceCalldata // variable length + ); + + ( + address decodedTokenIn, + address decodedTokenOut, + RestrictTransferFrom.TransferType decodedTransferType, + uint8 decodedPartialFillOffset, + uint256 decodedOriginalAmount, + address[] memory decodedSpenders, + bool[] memory decodedApprovals, + address decodedTarget, + address decodedReceiver, + bytes memory decodedCalldata + ) = liquoriceExecutor.decodeData(params); + + assertEq(decodedTokenIn, USDC_ADDR, "tokenIn mismatch"); + assertEq(decodedTokenOut, WETH_ADDR, "tokenOut mismatch"); + assertEq( + uint8(decodedTransferType), + uint8(RestrictTransferFrom.TransferType.Transfer), + "transferType mismatch" + ); + assertEq(decodedPartialFillOffset, 5, "partialFillOffset mismatch"); + assertEq( + decodedOriginalAmount, originalAmount, "originalAmount mismatch" + ); + assertEq(decodedSpenders.length, 0, "spenders length mismatch"); + assertEq(decodedApprovals.length, 0, "approvals length mismatch"); + assertEq(decodedTarget, targetContract, "targetContract mismatch"); + assertEq(decodedReceiver, receiver, "receiver mismatch"); + assertEq( + keccak256(decodedCalldata), + keccak256(liquoriceCalldata), + "calldata mismatch" + ); + } + + function testDecodeData_WithAllowances() public view { + bytes memory liquoriceCalldata = hex"deadbeef"; + + uint256 originalAmount = 2000000000; // 2000 USDC + address targetContract = address(0x1111111111111111111111111111111111111111); + address receiver = address(0x2222222222222222222222222222222222222222); + address spender1 = address(0x3333333333333333333333333333333333333333); + address spender2 = address(0x4444444444444444444444444444444444444444); + + // Encode with 2 allowances + bytes memory params = abi.encodePacked( + USDC_ADDR, // tokenIn (20 bytes) + WBTC_ADDR, // tokenOut (20 bytes) + uint8(RestrictTransferFrom.TransferType.None), // transferType (1 byte) + uint8(12), // partialFillOffset (1 byte) + originalAmount, // originalBaseTokenAmount (32 bytes) + uint8(2), // numAllowances (1 byte) + spender1, // allowance spender 1 (20 bytes) + uint8(1), // approval needed 1 (1 byte) - true + spender2, // allowance spender 2 (20 bytes) + uint8(0), // approval needed 2 (1 byte) - false + targetContract, // targetContract (20 bytes) + receiver, // receiver (20 bytes) + liquoriceCalldata // variable length + ); + + ( + address decodedTokenIn, + address decodedTokenOut, + RestrictTransferFrom.TransferType decodedTransferType, + uint8 decodedPartialFillOffset, + uint256 decodedOriginalAmount, + address[] memory decodedSpenders, + bool[] memory decodedApprovals, + address decodedTarget, + address decodedReceiver, + bytes memory decodedCalldata + ) = liquoriceExecutor.decodeData(params); + + assertEq(decodedTokenIn, USDC_ADDR, "tokenIn mismatch"); + assertEq(decodedTokenOut, WBTC_ADDR, "tokenOut mismatch"); + assertEq( + uint8(decodedTransferType), + uint8(RestrictTransferFrom.TransferType.None), + "transferType mismatch" + ); + assertEq(decodedPartialFillOffset, 12, "partialFillOffset mismatch"); + assertEq( + decodedOriginalAmount, originalAmount, "originalAmount mismatch" + ); + assertEq(decodedSpenders.length, 2, "spenders length mismatch"); + assertEq(decodedApprovals.length, 2, "approvals length mismatch"); + assertEq(decodedSpenders[0], spender1, "spender1 mismatch"); + assertEq(decodedSpenders[1], spender2, "spender2 mismatch"); + assertTrue(decodedApprovals[0], "approval1 should be true"); + assertFalse(decodedApprovals[1], "approval2 should be false"); + assertEq(decodedTarget, targetContract, "targetContract mismatch"); + assertEq(decodedReceiver, receiver, "receiver mismatch"); + assertEq( + keccak256(decodedCalldata), + keccak256(liquoriceCalldata), + "calldata mismatch" + ); + } + + function testDecodeData_InvalidDataLength() public { + // Too short - missing required fields + bytes memory tooShort = abi.encodePacked( + USDC_ADDR, // tokenIn (20 bytes) + WETH_ADDR, // tokenOut (20 bytes) + uint8(RestrictTransferFrom.TransferType.Transfer) // transferType (1 byte) + // missing: partialFillOffset, originalAmount, numAllowances, target, receiver + ); + + vm.expectRevert( + LiquoriceExecutor.LiquoriceExecutor__InvalidDataLength.selector + ); + liquoriceExecutor.decodeData(tooShort); + } + + function testDecodeData_InvalidDataLength_MissingAllowanceData() public { + // Has numAllowances = 1 but doesn't include the allowance data + bytes memory missingAllowance = abi.encodePacked( + USDC_ADDR, // tokenIn (20 bytes) + WETH_ADDR, // tokenOut (20 bytes) + uint8(RestrictTransferFrom.TransferType.Transfer), // transferType (1 byte) + uint8(5), // partialFillOffset (1 byte) + uint256(1000000000), // originalAmount (32 bytes) + uint8(1), // numAllowances = 1 (1 byte) + // Missing: spender (20 bytes) + approvalNeeded (1 byte) + address(0x1234567890123456789012345678901234567890), // target (20 bytes) + address(0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD) // receiver (20 bytes) + ); + + vm.expectRevert( + LiquoriceExecutor.LiquoriceExecutor__InvalidDataLength.selector + ); + liquoriceExecutor.decodeData(missingAllowance); + } + + function testDecodeData_ZeroPartialFillOffset() public view { + bytes memory liquoriceCalldata = hex"cafebabe"; + address targetContract = address(0x5555555555555555555555555555555555555555); + address receiver = address(0x6666666666666666666666666666666666666666); + + // partialFillOffset = 0 means partial fill not supported + bytes memory params = abi.encodePacked( + WETH_ADDR, + USDC_ADDR, + uint8(RestrictTransferFrom.TransferType.Transfer), + uint8(0), // partialFillOffset = 0 + uint256(1e18), + uint8(0), // no allowances + targetContract, + receiver, + liquoriceCalldata + ); + + (,,, uint8 decodedOffset,,,,,,) = liquoriceExecutor.decodeData(params); + + assertEq(decodedOffset, 0, "partialFillOffset should be 0"); + } +} diff --git a/src/encoding/evm/constants.rs b/src/encoding/evm/constants.rs index a313bb78..d1a46d9b 100644 --- a/src/encoding/evm/constants.rs +++ b/src/encoding/evm/constants.rs @@ -37,6 +37,7 @@ pub static FUNDS_IN_ROUTER_PROTOCOLS: LazyLock> = LazyLock set.insert("vm:curve"); set.insert("rfq:bebop"); set.insert("rfq:hashflow"); + set.insert("rfq:liquorice"); set.insert("rocketpool"); set.insert("erc4626"); set.insert("lido"); diff --git a/src/encoding/evm/swap_encoder/liquorice.rs b/src/encoding/evm/swap_encoder/liquorice.rs new file mode 100644 index 00000000..fbafd2e0 --- /dev/null +++ b/src/encoding/evm/swap_encoder/liquorice.rs @@ -0,0 +1,439 @@ +use std::{collections::HashMap, str::FromStr, sync::Arc}; + +use alloy::primitives::Address; +use serde::Deserialize; +use tokio::{ + runtime::{Handle, Runtime}, + task::block_in_place, +}; +use tycho_common::{ + models::{protocol::GetAmountOutParams, Chain}, + Bytes, +}; + +use crate::encoding::{ + errors::EncodingError, + evm::{ + approvals::protocol_approvals_manager::ProtocolApprovalsManager, + utils::{bytes_to_address, get_runtime}, + }, + models::{EncodingContext, Swap}, + swap_encoder::SwapEncoder, +}; + +/// Allowance structure from Liquorice API +#[derive(Debug, Clone, Deserialize)] +struct LiquoriceAllowance { + #[allow(dead_code)] + token: String, + spender: String, + #[allow(dead_code)] + amount: String, +} + +/// Encodes a swap on Liquorice (RFQ) through the given executor address. +/// +/// Liquorice uses a Request-for-Quote model where quotes are obtained off-chain +/// and settled on-chain. The executor receives pre-encoded calldata from the API. +/// +/// # Fields +/// * `executor_address` - The address of the executor contract that will perform the swap. +/// * `native_token_address` - The chain's native token address. +#[derive(Clone)] +pub struct LiquoriceSwapEncoder { + executor_address: Bytes, + native_token_address: Bytes, + runtime_handle: Handle, + #[allow(dead_code)] + runtime: Option>, +} + +impl SwapEncoder for LiquoriceSwapEncoder { + fn new( + executor_address: Bytes, + chain: Chain, + _config: Option>, + ) -> Result { + // No protocol-specific config needed for Liquorice + // The target contract comes from the quote itself + let native_token_address = chain.native_token().address; + let (runtime_handle, runtime) = get_runtime()?; + Ok(Self { + executor_address, + native_token_address, + runtime_handle, + runtime, + }) + } + + fn encode_swap( + &self, + swap: &Swap, + encoding_context: &EncodingContext, + ) -> Result, EncodingError> { + let token_in = bytes_to_address(swap.token_in())?; + let token_out = bytes_to_address(swap.token_out())?; + + // Get protocol state and request signed quote + let protocol_state = swap + .get_protocol_state() + .as_ref() + .ok_or_else(|| { + EncodingError::FatalError("protocol_state is required for Liquorice".to_string()) + })?; + + let estimated_amount_in = swap + .get_estimated_amount_in() + .clone() + .ok_or(EncodingError::FatalError( + "Estimated amount in is mandatory for a Liquorice swap".to_string(), + ))?; + + let sender = encoding_context + .router_address + .clone() + .ok_or(EncodingError::FatalError( + "The router address is needed to perform a Liquorice swap".to_string(), + ))?; + + let params = GetAmountOutParams { + amount_in: estimated_amount_in.clone(), + token_in: swap.token_in().clone(), + token_out: swap.token_out().clone(), + sender, + receiver: encoding_context.receiver.clone(), + }; + + let signed_quote = block_in_place(|| { + self.runtime_handle.block_on(async { + protocol_state + .as_indicatively_priced()? + .request_signed_quote(params) + .await + }) + })?; + + // Extract required fields from quote + let target_contract = signed_quote + .quote_attributes + .get("target_contract") + .ok_or(EncodingError::FatalError( + "Liquorice quote must have a target_contract attribute".to_string(), + ))?; + + let liquorice_calldata = signed_quote + .quote_attributes + .get("calldata") + .ok_or(EncodingError::FatalError( + "Liquorice quote must have a calldata attribute".to_string(), + ))?; + + let base_token_amount = signed_quote + .quote_attributes + .get("base_token_amount") + .ok_or(EncodingError::FatalError( + "Liquorice quote must have a base_token_amount attribute".to_string(), + ))?; + + // Get partial fill offset (defaults to 0 if not present, meaning no partial fill support) + let partial_fill_offset: u8 = signed_quote + .quote_attributes + .get("partial_fill_offset") + .map(|b| { + // Take the last byte (u8 value) + b.last().copied().unwrap_or(0) + }) + .unwrap_or(0); + + // Parse original base token amount (U256 encoded as 32 bytes) + let original_base_token_amount = if base_token_amount.len() == 32 { + base_token_amount.to_vec() + } else { + // Pad to 32 bytes if needed + let mut padded = vec![0u8; 32]; + let start = 32 - base_token_amount.len(); + padded[start..].copy_from_slice(base_token_amount); + padded + }; + + // Parse allowances from quote + let allowances: Vec = signed_quote + .quote_attributes + .get("allowances") + .map(|bytes| { + let json_str = String::from_utf8_lossy(bytes); + serde_json::from_str(&json_str).unwrap_or_default() + }) + .unwrap_or_default(); + + // Check which allowances need approval + let executor_address = bytes_to_address(&self.executor_address)?; + let approvals_manager = ProtocolApprovalsManager::new()?; + + let mut allowance_data: Vec<(Address, bool)> = Vec::new(); + for allowance in &allowances { + let spender = Address::from_str(&allowance.spender).map_err(|_| { + EncodingError::FatalError(format!( + "Invalid allowance spender address: {}", + allowance.spender + )) + })?; + + // Check if approval is needed from executor to spender + let approval_needed = if *swap.token_in() == self.native_token_address { + false + } else { + approvals_manager.approval_needed(token_in, executor_address, spender)? + }; + + allowance_data.push((spender, approval_needed)); + } + + let receiver = bytes_to_address(&encoding_context.receiver)?; + let target = bytes_to_address(target_contract)?; + + // Encode packed data for the executor + // Format: token_in | token_out | transfer_type | partial_fill_offset | + // original_base_token_amount | num_allowances | + // [allowance_spender | approval_needed]... | + // target_contract | receiver | liquorice_calldata + let mut encoded = Vec::new(); + + // Fixed fields + encoded.extend_from_slice(token_in.as_slice()); // 20 bytes + encoded.extend_from_slice(token_out.as_slice()); // 20 bytes + encoded.push(encoding_context.transfer_type as u8); // 1 byte + encoded.push(partial_fill_offset); // 1 byte + encoded.extend_from_slice(&original_base_token_amount); // 32 bytes + + // Allowances + encoded.push(allowance_data.len() as u8); // 1 byte + for (spender, approval_needed) in &allowance_data { + encoded.extend_from_slice(spender.as_slice()); // 20 bytes + encoded.push(*approval_needed as u8); // 1 byte + } + + // Target and receiver + encoded.extend_from_slice(target.as_slice()); // 20 bytes + encoded.extend_from_slice(receiver.as_slice()); // 20 bytes + + // Calldata (variable length) + encoded.extend_from_slice(liquorice_calldata); + + Ok(encoded) + } + + fn executor_address(&self) -> &Bytes { + &self.executor_address + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use alloy::hex::encode; + use num_bigint::BigUint; + use tycho_common::models::protocol::ProtocolComponent; + + use super::*; + use crate::encoding::{ + evm::{ + swap_encoder::liquorice::LiquoriceSwapEncoder, testing_utils::MockRFQState, + utils::biguint_to_u256, + }, + models::TransferType, + }; + + #[test] + fn test_encode_liquorice_single_fails_without_protocol_data() { + let liquorice_component = ProtocolComponent { + id: String::from("liquorice-rfq"), + protocol_system: String::from("rfq:liquorice"), + ..Default::default() + }; + + let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC + let token_out = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); // WETH + + let swap = Swap::new(liquorice_component, token_in.clone(), token_out.clone()) + .estimated_amount_in(BigUint::from_str("3000000000").unwrap()); + + let encoding_context = EncodingContext { + receiver: Bytes::from("0xc5564C13A157E6240659fb81882A28091add8670"), + exact_out: false, + router_address: Some(Bytes::zero(20)), + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + transfer_type: TransferType::Transfer, + historical_trade: false, + }; + + let encoder = LiquoriceSwapEncoder::new( + Bytes::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), + Chain::Ethereum, + None, + ) + .unwrap(); + + encoder + .encode_swap(&swap, &encoding_context) + .expect_err("Should return an error if the swap has no protocol state"); + } + + #[test] + fn test_encode_liquorice_single_with_protocol_state() { + // 3000 USDC -> 1 WETH using a mocked RFQ state to get a quote + let quote_amount_out = BigUint::from_str("1000000000000000000").unwrap(); + let liquorice_calldata = Bytes::from_str("0xdeadbeef1234567890").unwrap(); + let base_token_amount = biguint_to_u256(&BigUint::from(3000000000_u64)) + .to_be_bytes::<32>() + .to_vec(); + + let liquorice_component = ProtocolComponent { + id: String::from("liquorice-rfq"), + protocol_system: String::from("rfq:liquorice"), + ..Default::default() + }; + + // Allowances JSON - one spender needing approval + let allowances_json = + r#"[{"token":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","spender":"0x71D9750ECF0c5081FAE4E3EDC4253E52024b0B59","amount":"3000000000"}]"#; + + let liquorice_state = MockRFQState { + quote_amount_out, + quote_data: HashMap::from([ + ( + "target_contract".to_string(), + Bytes::from_str("0x71D9750ECF0c5081FAE4E3EDC4253E52024b0B59").unwrap(), + ), + ("calldata".to_string(), liquorice_calldata.clone()), + ("base_token_amount".to_string(), Bytes::from(base_token_amount.clone())), + ("partial_fill_offset".to_string(), Bytes::from(vec![12u8])), + ("allowances".to_string(), Bytes::from(allowances_json.as_bytes().to_vec())), + ]), + }; + + let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC + let token_out = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); // WETH + + let swap = Swap::new(liquorice_component, token_in.clone(), token_out.clone()) + .estimated_amount_in(BigUint::from_str("3000000000").unwrap()) + .protocol_state(Arc::new(liquorice_state)); + + let encoding_context = EncodingContext { + receiver: Bytes::from("0xc5564C13A157E6240659fb81882A28091add8670"), + exact_out: false, + router_address: Some(Bytes::zero(20)), + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + transfer_type: TransferType::Transfer, + historical_trade: false, + }; + + let encoder = LiquoriceSwapEncoder::new( + Bytes::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), + Chain::Ethereum, + None, + ) + .unwrap(); + + let encoded_swap = encoder + .encode_swap(&swap, &encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); + + // Expected format: + // token_in (20) | token_out (20) | transfer_type (1) | partial_fill_offset (1) | + // original_base_token_amount (32) | num_allowances (1) | + // [spender (20) | approval_needed (1)]... | + // target_contract (20) | receiver (20) | calldata (variable) + let expected_swap = String::from(concat!( + // token_in (USDC) + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // token_out (WETH) + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + // transfer_type + "01", + // partial_fill_offset + "0c", + // original_base_token_amount (3000000000 as U256) + "00000000000000000000000000000000000000000000000000000000b2d05e00", + // num_allowances + "01", + // allowance spender + "71d9750ecf0c5081fae4e3edc4253e52024b0b59", + // approval_needed + "01", + // target_contract + "71d9750ecf0c5081fae4e3edc4253e52024b0b59", + // receiver + "c5564c13a157e6240659fb81882a28091add8670", + )); + assert_eq!(hex_swap, expected_swap + &liquorice_calldata.to_string()[2..]); + } + + #[test] + fn test_encode_liquorice_no_allowances() { + // Test with empty allowances + let quote_amount_out = BigUint::from_str("1000000000000000000").unwrap(); + let liquorice_calldata = Bytes::from_str("0xabcdef").unwrap(); + let base_token_amount = biguint_to_u256(&BigUint::from(1000000000_u64)) + .to_be_bytes::<32>() + .to_vec(); + + let liquorice_component = ProtocolComponent { + id: String::from("liquorice-rfq"), + protocol_system: String::from("rfq:liquorice"), + ..Default::default() + }; + + let liquorice_state = MockRFQState { + quote_amount_out, + quote_data: HashMap::from([ + ( + "target_contract".to_string(), + Bytes::from_str("0x71D9750ECF0c5081FAE4E3EDC4253E52024b0B59").unwrap(), + ), + ("calldata".to_string(), liquorice_calldata.clone()), + ("base_token_amount".to_string(), Bytes::from(base_token_amount)), + ("allowances".to_string(), Bytes::from("[]".as_bytes().to_vec())), + ]), + }; + + let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); + let token_out = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); + + let swap = Swap::new(liquorice_component, token_in.clone(), token_out.clone()) + .estimated_amount_in(BigUint::from_str("1000000000").unwrap()) + .protocol_state(Arc::new(liquorice_state)); + + let encoding_context = EncodingContext { + receiver: Bytes::from("0xc5564C13A157E6240659fb81882A28091add8670"), + exact_out: false, + router_address: Some(Bytes::zero(20)), + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + transfer_type: TransferType::Transfer, + historical_trade: false, + }; + + let encoder = LiquoriceSwapEncoder::new( + Bytes::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), + Chain::Ethereum, + None, + ) + .unwrap(); + + let encoded_swap = encoder + .encode_swap(&swap, &encoding_context) + .unwrap(); + + // Verify num_allowances is 0 (at byte position 74) + assert_eq!(encoded_swap[74], 0); + } +} diff --git a/src/encoding/evm/swap_encoder/mod.rs b/src/encoding/evm/swap_encoder/mod.rs index 5d62fffc..b6f42258 100644 --- a/src/encoding/evm/swap_encoder/mod.rs +++ b/src/encoding/evm/swap_encoder/mod.rs @@ -8,6 +8,7 @@ mod erc_4626; mod fluid_v1; mod hashflow; mod lido; +mod liquorice; mod maverick_v2; mod rocketpool; mod slipstreams; diff --git a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs index 0756c612..1ed9a404 100644 --- a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs +++ b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs @@ -11,6 +11,7 @@ use crate::encoding::{ bebop::BebopSwapEncoder, curve::CurveSwapEncoder, ekubo::EkuboSwapEncoder, ekubo_v3::EkuboV3SwapEncoder, erc_4626::ERC4626SwapEncoder, fluid_v1::FluidV1SwapEncoder, hashflow::HashflowSwapEncoder, lido::LidoSwapEncoder, + liquorice::LiquoriceSwapEncoder, maverick_v2::MaverickV2SwapEncoder, rocketpool::RocketpoolSwapEncoder, slipstreams::SlipstreamsSwapEncoder, uniswap_v2::UniswapV2SwapEncoder, uniswap_v3::UniswapV3SwapEncoder, uniswap_v4::UniswapV4SwapEncoder, @@ -135,6 +136,9 @@ impl SwapEncoderRegistry { "rfq:hashflow" => { Ok(Box::new(HashflowSwapEncoder::new(executor_address, self.chain, config)?)) } + "rfq:liquorice" => { + Ok(Box::new(LiquoriceSwapEncoder::new(executor_address, self.chain, config)?)) + } "fluid_v1" => { Ok(Box::new(FluidV1SwapEncoder::new(executor_address, self.chain, config)?)) } diff --git a/tests/protocol_integration_tests.rs b/tests/protocol_integration_tests.rs index 3bbe00f6..8517803f 100644 --- a/tests/protocol_integration_tests.rs +++ b/tests/protocol_integration_tests.rs @@ -1894,3 +1894,99 @@ fn test_single_swap_with_univ4_angstrom() { // following assert would fail assert_eq!(hex_calldata[904..].len(), 1152); } + +#[test] +fn test_single_encoding_strategy_liquorice() { + // 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 WETH using Liquorice RFQ + // + // USDC ───(Liquorice RFQ)──> WETH + + let usdc = usdc(); + let weth = weth(); + + // USDC -> WETH via Liquorice RFQ using mock order data + let quote_amount_out = BigUint::from_str("1000000000000000000").unwrap(); // 1 WETH + + // Mock calldata for Liquorice settlement + let mock_calldata = Bytes::from( + hex::decode("deadbeef1234567890abcdef1234567890abcdef1234567890abcdef1234567890") + .unwrap(), + ); + + // Allowances JSON - one spender + let allowances_json = + r#"[{"token":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","spender":"0x71D9750ECF0c5081FAE4E3EDC4253E52024b0B59","amount":"3000000000"}]"#; + + let liquorice_state = MockRFQState { + quote_amount_out, + quote_data: HashMap::from([ + ( + "target_contract".to_string(), + Bytes::from_str("0x71D9750ECF0c5081FAE4E3EDC4253E52024b0B59").unwrap(), + ), + ("calldata".to_string(), mock_calldata), + ( + "base_token_amount".to_string(), + Bytes::from( + biguint_to_u256(&BigUint::from(3000000000_u64)) + .to_be_bytes::<32>() + .to_vec(), + ), + ), + ( + "partial_fill_offset".to_string(), + Bytes::from(vec![12u8]), // offset = 12 + ), + ( + "allowances".to_string(), + Bytes::from(allowances_json.as_bytes().to_vec()), + ), + ]), + }; + + let liquorice_component = ProtocolComponent { + id: String::from("liquorice-rfq"), + protocol_system: String::from("rfq:liquorice"), + ..Default::default() + }; + + let swap_usdc_weth = Swap::new(liquorice_component, usdc.clone(), weth.clone()) + .estimated_amount_in(BigUint::from_str("3000000000").unwrap()) + .protocol_state(Arc::new(liquorice_state)); + + let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); + + let solution = Solution { + exact_out: false, + given_token: usdc, + given_amount: BigUint::from_str("3000000000").unwrap(), + checked_token: weth, + checked_amount: BigUint::from_str("1000000000000000000").unwrap(), + sender: alice_address(), + receiver: alice_address(), + swaps: vec![swap_usdc_weth], + ..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, + ð(), + None, + ) + .unwrap() + .data; + + let hex_calldata = encode(&calldata); + write_calldata_to_file("test_single_encoding_strategy_liquorice", hex_calldata.as_str()); +} From e61af02481702f2e390fbae720d2088616cf6fff Mon Sep 17 00:00:00 2001 From: Igor Markin Date: Wed, 4 Feb 2026 18:28:04 +0000 Subject: [PATCH 02/15] Rework target contract and approvals --- config/protocol_specific_addresses.json | 3 + foundry/src/executors/LiquoriceExecutor.sol | 115 ++++++-------- .../src/liquorice/ILiquoriceSettlement.sol | 6 + foundry/test/assets/calldata.txt | 2 +- foundry/test/protocols/Liquorice.t.sol | 82 +++------- src/encoding/evm/swap_encoder/liquorice.rs | 150 ++++++------------ tests/protocol_integration_tests.rs | 12 -- 7 files changed, 128 insertions(+), 242 deletions(-) create mode 100644 foundry/src/liquorice/ILiquoriceSettlement.sol diff --git a/config/protocol_specific_addresses.json b/config/protocol_specific_addresses.json index 31ed4ba3..25a92f8b 100644 --- a/config/protocol_specific_addresses.json +++ b/config/protocol_specific_addresses.json @@ -16,6 +16,9 @@ "rfq:hashflow": { "hashflow_router_address": "0x55084eE0fEf03f14a305cd24286359A35D735151" }, + "rfq:liquorice": { + "balance_manager_address": "0x0000000000000000000000000000000000000000" + }, "lido": { "st_eth_address": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", "wst_eth_address": "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0" diff --git a/foundry/src/executors/LiquoriceExecutor.sol b/foundry/src/executors/LiquoriceExecutor.sol index 5f602e52..36c74fcc 100644 --- a/foundry/src/executors/LiquoriceExecutor.sol +++ b/foundry/src/executors/LiquoriceExecutor.sol @@ -9,6 +9,7 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/Address.sol"; +import "../liquorice/ILiquoriceSettlement.sol"; /// @title LiquoriceExecutor /// @notice Executor for Liquorice RFQ (Request for Quote) swaps @@ -21,30 +22,38 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { /// @notice Liquorice-specific errors error LiquoriceExecutor__InvalidDataLength(); + error LiquoriceExecutor__ZeroAddress(); - /// @notice Maximum number of allowances supported - uint8 public constant MAX_ALLOWANCES = 5; + /// @notice The Liquorice settlement contract address + address public immutable liquoriceSettlement; - // TODO: add _liquoriceSettlement address argument - constructor(address _permit2) RestrictTransferFrom(_permit2) {} + constructor(address _liquoriceSettlement, address _permit2) + RestrictTransferFrom(_permit2) + { + if (_liquoriceSettlement == address(0)) { + revert LiquoriceExecutor__ZeroAddress(); + } + liquoriceSettlement = _liquoriceSettlement; + } /// @notice Executes a swap through Liquorice's RFQ system /// @param givenAmount The amount of input token to swap - /// @param data Encoded swap data containing tokens, allowances, and liquorice calldata + /// @param data Encoded swap data containing tokens and liquorice calldata /// @return calculatedAmount The amount of output token received - function swap( - uint256 givenAmount, - bytes calldata data - ) external payable virtual override returns (uint256 calculatedAmount) { + function swap(uint256 givenAmount, bytes calldata data) + external + payable + virtual + override + returns (uint256 calculatedAmount) + { ( address tokenIn, address tokenOut, TransferType transferType, uint8 partialFillOffset, uint256 originalBaseTokenAmount, - address[] memory allowanceSpenders, - bool[] memory approvalsNeeded, // TODO: change to bool approvalNeeded, and give approval to _liquoriceBalanceManager - address targetContract, // TODO: remove, use _liquoriceSettlement instead + bool approvalNeeded, address receiver, bytes memory liquoriceCalldata ) = _decodeData(data); @@ -52,15 +61,13 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { // Transfer tokens to executor _transfer(address(this), transferType, tokenIn, givenAmount); - // Grant approvals to spenders as needed - for (uint256 i = 0; i < allowanceSpenders.length; i++) { - if (approvalsNeeded[i] && tokenIn != address(0)) { - // slither-disable-next-line unused-return - IERC20(tokenIn).forceApprove( - allowanceSpenders[i], - type(uint256).max - ); - } + // Grant approval to Liquorice balance manager if needed + if (approvalNeeded && tokenIn != address(0)) { + address balanceManager = address( + ILiquoriceSettlement(liquoriceSettlement).BALANCE_MANAGER() + ); + // slither-disable-next-line unused-return + IERC20(tokenIn).forceApprove(balanceManager, type(uint256).max); } // Modify the fill amount in the calldata if partial fill is supported @@ -78,18 +85,16 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { uint256 balanceBefore = _balanceOf(tokenOut, receiver); uint256 ethValue = tokenIn == address(0) ? givenAmount : 0; - // Execute the swap by forwarding calldata to target contract + // Execute the swap by forwarding calldata to settlement contract // slither-disable-next-line unused-return - targetContract.functionCallWithValue(finalCalldata, ethValue); + liquoriceSettlement.functionCallWithValue(finalCalldata, ethValue); uint256 balanceAfter = _balanceOf(tokenOut, receiver); calculatedAmount = balanceAfter - balanceBefore; } /// @dev Decodes the packed calldata - function _decodeData( - bytes calldata data - ) + function _decodeData(bytes calldata data) internal pure returns ( @@ -98,52 +103,24 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { TransferType transferType, uint8 partialFillOffset, uint256 originalBaseTokenAmount, - address[] memory allowanceSpenders, - bool[] memory approvalsNeeded, - address targetContract, + bool approvalNeeded, address receiver, bytes memory liquoriceCalldata ) { - // Minimum fixed fields before allowances: + // Minimum fixed fields: // tokenIn (20) + tokenOut (20) + transferType (1) + partialFillOffset (1) + - // originalBaseTokenAmount (32) + numAllowances (1) = 75 bytes - // Plus at minimum: targetContract (20) + receiver (20) = 40 bytes - // Total minimum: 115 bytes (with 0 allowances) - if (data.length < 115) revert LiquoriceExecutor__InvalidDataLength(); + // originalBaseTokenAmount (32) + approvalNeeded (1) + receiver (20) = 95 bytes + if (data.length < 95) revert LiquoriceExecutor__InvalidDataLength(); tokenIn = address(bytes20(data[0:20])); tokenOut = address(bytes20(data[20:40])); transferType = TransferType(uint8(data[40])); partialFillOffset = uint8(data[41]); originalBaseTokenAmount = uint256(bytes32(data[42:74])); - - uint8 numAllowances = uint8(data[74]); - - // Each allowance: spender (20) + approvalNeeded (1) = 21 bytes - uint256 allowancesEnd = 75 + (uint256(numAllowances) * 21); - - // Validate data length includes all allowances plus target, receiver, and at least some calldata - if (data.length < allowancesEnd + 40) { - revert LiquoriceExecutor__InvalidDataLength(); - } - - allowanceSpenders = new address[](numAllowances); - approvalsNeeded = new bool[](numAllowances); - - for (uint256 i = 0; i < numAllowances; i++) { - uint256 offset = 75 + (i * 21); - allowanceSpenders[i] = address(bytes20(data[offset:offset + 20])); - approvalsNeeded[i] = data[offset + 20] != 0; - } - - targetContract = address( - bytes20(data[allowancesEnd:allowancesEnd + 20]) - ); - receiver = address( - bytes20(data[allowancesEnd + 20:allowancesEnd + 40]) - ); - liquoriceCalldata = data[allowancesEnd + 40:]; + approvalNeeded = data[74] != 0; + receiver = address(bytes20(data[75:95])); + liquoriceCalldata = data[95:]; } /// @dev Modifies the filledTakerAmount in the liquorice calldata to handle slippage @@ -190,14 +167,14 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { /// @param token The token address, or address(0) for ETH /// @param account The account to get the balance of /// @return 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); + 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 diff --git a/foundry/src/liquorice/ILiquoriceSettlement.sol b/foundry/src/liquorice/ILiquoriceSettlement.sol new file mode 100644 index 00000000..4a93b1d0 --- /dev/null +++ b/foundry/src/liquorice/ILiquoriceSettlement.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +interface ILiquoriceSettlement { + function BALANCE_MANAGER() external view returns (address); +} diff --git a/foundry/test/assets/calldata.txt b/foundry/test/assets/calldata.txt index 00ffdf56..a8a89e6c 100644 --- a/foundry/test/assets/calldata.txt +++ b/foundry/test/assets/calldata.txt @@ -63,4 +63,4 @@ test_encoding_strategy_curve_lido_sequential_swap:e21dd0d30000000000000000000000 test_ekubo_v3_encode_swap_multi:01cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48517e506700271aea091b02f42756f5e174af5230000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000a7c5ac471b4880000032 test_single_encoding_strategy_ekubo_v3:5c4b639c0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000007113aa49bac059d709dd0a18d6bb63290076a702d702cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48517e506700271aea091b02f42756f5e174af5230000000000000000000000000000000000000000000000000000000 test_single_ekubo_v3_grouped_swap:5c4b639c00000000000000000000000000000000000000000000000000000002540be400000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a513aa49bac059d709dd0a18d6bb63290076a702d700cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2dac17f958d2ee523a2206206994597c13d831ec7a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000a7c5ac471b48800000320000000000000000000000000000000000000000517e506700271aea091b02f42756f5e174af5230000000000000000000000000000000000000000000000000000000000000000000000000000000 -test_single_encoding_strategy_liquorice:5c4b639c00000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000bd0000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000c00000000000000000000000000000000000000000000000000000000b2d05e000171d9750ecf0c5081fae4e3edc4253e52024b0b590171d9750ecf0c5081fae4e3edc4253e52024b0b59cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2deadbeef1234567890abcdef1234567890abcdef1234567890abcdef1234567890000000 +test_single_encoding_strategy_liquorice:5c4b639c00000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000c00000000000000000000000000000000000000000000000000000000b2d05e0001cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2deadbeef1234567890abcdef1234567890abcdef1234567890abcdef1234567890000000000000000000000000 diff --git a/foundry/test/protocols/Liquorice.t.sol b/foundry/test/protocols/Liquorice.t.sol index eb4906f3..78558892 100644 --- a/foundry/test/protocols/Liquorice.t.sol +++ b/foundry/test/protocols/Liquorice.t.sol @@ -12,7 +12,9 @@ import { } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; contract LiquoriceExecutorExposed is LiquoriceExecutor { - constructor(address _permit2) LiquoriceExecutor(_permit2) {} + constructor(address _liquoriceSettlement, address _permit2) + LiquoriceExecutor(_liquoriceSettlement, _permit2) + {} function decodeData(bytes calldata data) external @@ -23,9 +25,7 @@ contract LiquoriceExecutorExposed is LiquoriceExecutor { TransferType transferType, uint8 partialFillOffset, uint256 originalBaseTokenAmount, - address[] memory allowanceSpenders, - bool[] memory approvalsNeeded, - address targetContract, + bool approvalNeeded, address receiver, bytes memory liquoriceCalldata ) @@ -39,34 +39,35 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { LiquoriceExecutorExposed liquoriceExecutor; + address constant LIQUORICE_SETTLEMENT = + 0x0448633eb8B0A42EfED924C42069E0DcF08fb552; + IERC20 WETH = IERC20(WETH_ADDR); IERC20 USDC = IERC20(USDC_ADDR); IERC20 WBTC = IERC20(WBTC_ADDR); function setUp() public { vm.createSelectFork(vm.rpcUrl("mainnet"), 22667985); - liquoriceExecutor = new LiquoriceExecutorExposed(PERMIT2_ADDRESS); + liquoriceExecutor = + new LiquoriceExecutorExposed(LIQUORICE_SETTLEMENT, PERMIT2_ADDRESS); } - function testDecodeData_NoAllowances() public view { + function testDecodeData_NoApproval() public view { bytes memory liquoriceCalldata = abi.encodePacked( bytes4(0xdeadbeef), // mock selector hex"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" ); uint256 originalAmount = 1000000000; // 1000 USDC - address targetContract = address(0x1234567890123456789012345678901234567890); address receiver = address(0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD); - // Encode with 0 allowances bytes memory params = abi.encodePacked( USDC_ADDR, // tokenIn (20 bytes) WETH_ADDR, // tokenOut (20 bytes) uint8(RestrictTransferFrom.TransferType.Transfer), // transferType (1 byte) uint8(5), // partialFillOffset (1 byte) originalAmount, // originalBaseTokenAmount (32 bytes) - uint8(0), // numAllowances (1 byte) - targetContract, // targetContract (20 bytes) + uint8(0), // approvalNeeded (1 byte) - false receiver, // receiver (20 bytes) liquoriceCalldata // variable length ); @@ -77,9 +78,7 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { RestrictTransferFrom.TransferType decodedTransferType, uint8 decodedPartialFillOffset, uint256 decodedOriginalAmount, - address[] memory decodedSpenders, - bool[] memory decodedApprovals, - address decodedTarget, + bool decodedApprovalNeeded, address decodedReceiver, bytes memory decodedCalldata ) = liquoriceExecutor.decodeData(params); @@ -95,9 +94,7 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { assertEq( decodedOriginalAmount, originalAmount, "originalAmount mismatch" ); - assertEq(decodedSpenders.length, 0, "spenders length mismatch"); - assertEq(decodedApprovals.length, 0, "approvals length mismatch"); - assertEq(decodedTarget, targetContract, "targetContract mismatch"); + assertFalse(decodedApprovalNeeded, "approvalNeeded should be false"); assertEq(decodedReceiver, receiver, "receiver mismatch"); assertEq( keccak256(decodedCalldata), @@ -106,28 +103,19 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { ); } - function testDecodeData_WithAllowances() public view { + function testDecodeData_WithApproval() public view { bytes memory liquoriceCalldata = hex"deadbeef"; uint256 originalAmount = 2000000000; // 2000 USDC - address targetContract = address(0x1111111111111111111111111111111111111111); address receiver = address(0x2222222222222222222222222222222222222222); - address spender1 = address(0x3333333333333333333333333333333333333333); - address spender2 = address(0x4444444444444444444444444444444444444444); - // Encode with 2 allowances bytes memory params = abi.encodePacked( USDC_ADDR, // tokenIn (20 bytes) WBTC_ADDR, // tokenOut (20 bytes) uint8(RestrictTransferFrom.TransferType.None), // transferType (1 byte) uint8(12), // partialFillOffset (1 byte) originalAmount, // originalBaseTokenAmount (32 bytes) - uint8(2), // numAllowances (1 byte) - spender1, // allowance spender 1 (20 bytes) - uint8(1), // approval needed 1 (1 byte) - true - spender2, // allowance spender 2 (20 bytes) - uint8(0), // approval needed 2 (1 byte) - false - targetContract, // targetContract (20 bytes) + uint8(1), // approvalNeeded (1 byte) - true receiver, // receiver (20 bytes) liquoriceCalldata // variable length ); @@ -138,9 +126,7 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { RestrictTransferFrom.TransferType decodedTransferType, uint8 decodedPartialFillOffset, uint256 decodedOriginalAmount, - address[] memory decodedSpenders, - bool[] memory decodedApprovals, - address decodedTarget, + bool decodedApprovalNeeded, address decodedReceiver, bytes memory decodedCalldata ) = liquoriceExecutor.decodeData(params); @@ -156,13 +142,7 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { assertEq( decodedOriginalAmount, originalAmount, "originalAmount mismatch" ); - assertEq(decodedSpenders.length, 2, "spenders length mismatch"); - assertEq(decodedApprovals.length, 2, "approvals length mismatch"); - assertEq(decodedSpenders[0], spender1, "spender1 mismatch"); - assertEq(decodedSpenders[1], spender2, "spender2 mismatch"); - assertTrue(decodedApprovals[0], "approval1 should be true"); - assertFalse(decodedApprovals[1], "approval2 should be false"); - assertEq(decodedTarget, targetContract, "targetContract mismatch"); + assertTrue(decodedApprovalNeeded, "approvalNeeded should be true"); assertEq(decodedReceiver, receiver, "receiver mismatch"); assertEq( keccak256(decodedCalldata), @@ -177,7 +157,7 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { USDC_ADDR, // tokenIn (20 bytes) WETH_ADDR, // tokenOut (20 bytes) uint8(RestrictTransferFrom.TransferType.Transfer) // transferType (1 byte) - // missing: partialFillOffset, originalAmount, numAllowances, target, receiver + // missing: partialFillOffset, originalAmount, approvalNeeded, receiver ); vm.expectRevert( @@ -186,29 +166,8 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { liquoriceExecutor.decodeData(tooShort); } - function testDecodeData_InvalidDataLength_MissingAllowanceData() public { - // Has numAllowances = 1 but doesn't include the allowance data - bytes memory missingAllowance = abi.encodePacked( - USDC_ADDR, // tokenIn (20 bytes) - WETH_ADDR, // tokenOut (20 bytes) - uint8(RestrictTransferFrom.TransferType.Transfer), // transferType (1 byte) - uint8(5), // partialFillOffset (1 byte) - uint256(1000000000), // originalAmount (32 bytes) - uint8(1), // numAllowances = 1 (1 byte) - // Missing: spender (20 bytes) + approvalNeeded (1 byte) - address(0x1234567890123456789012345678901234567890), // target (20 bytes) - address(0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD) // receiver (20 bytes) - ); - - vm.expectRevert( - LiquoriceExecutor.LiquoriceExecutor__InvalidDataLength.selector - ); - liquoriceExecutor.decodeData(missingAllowance); - } - function testDecodeData_ZeroPartialFillOffset() public view { bytes memory liquoriceCalldata = hex"cafebabe"; - address targetContract = address(0x5555555555555555555555555555555555555555); address receiver = address(0x6666666666666666666666666666666666666666); // partialFillOffset = 0 means partial fill not supported @@ -218,13 +177,12 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { uint8(RestrictTransferFrom.TransferType.Transfer), uint8(0), // partialFillOffset = 0 uint256(1e18), - uint8(0), // no allowances - targetContract, + uint8(0), // approvalNeeded = false receiver, liquoriceCalldata ); - (,,, uint8 decodedOffset,,,,,,) = liquoriceExecutor.decodeData(params); + (,,, uint8 decodedOffset,,,,) = liquoriceExecutor.decodeData(params); assertEq(decodedOffset, 0, "partialFillOffset should be 0"); } diff --git a/src/encoding/evm/swap_encoder/liquorice.rs b/src/encoding/evm/swap_encoder/liquorice.rs index fbafd2e0..f875893e 100644 --- a/src/encoding/evm/swap_encoder/liquorice.rs +++ b/src/encoding/evm/swap_encoder/liquorice.rs @@ -1,7 +1,6 @@ use std::{collections::HashMap, str::FromStr, sync::Arc}; use alloy::primitives::Address; -use serde::Deserialize; use tokio::{ runtime::{Handle, Runtime}, task::block_in_place, @@ -21,16 +20,6 @@ use crate::encoding::{ swap_encoder::SwapEncoder, }; -/// Allowance structure from Liquorice API -#[derive(Debug, Clone, Deserialize)] -struct LiquoriceAllowance { - #[allow(dead_code)] - token: String, - spender: String, - #[allow(dead_code)] - amount: String, -} - /// Encodes a swap on Liquorice (RFQ) through the given executor address. /// /// Liquorice uses a Request-for-Quote model where quotes are obtained off-chain @@ -38,10 +27,12 @@ struct LiquoriceAllowance { /// /// # Fields /// * `executor_address` - The address of the executor contract that will perform the swap. +/// * `balance_manager_address` - The address of the Liquorice balance manager contract. /// * `native_token_address` - The chain's native token address. #[derive(Clone)] pub struct LiquoriceSwapEncoder { executor_address: Bytes, + balance_manager_address: Bytes, native_token_address: Bytes, runtime_handle: Handle, #[allow(dead_code)] @@ -52,14 +43,29 @@ impl SwapEncoder for LiquoriceSwapEncoder { fn new( executor_address: Bytes, chain: Chain, - _config: Option>, + config: Option>, ) -> Result { - // No protocol-specific config needed for Liquorice - // The target contract comes from the quote itself + let config = config.ok_or(EncodingError::FatalError( + "Missing liquorice specific addresses in config".to_string(), + ))?; + let balance_manager_address = config + .get("balance_manager_address") + .map(|s| { + Bytes::from_str(s).map_err(|_| { + EncodingError::FatalError( + "Invalid liquorice balance manager address".to_string(), + ) + }) + }) + .ok_or(EncodingError::FatalError( + "Missing liquorice balance manager address in config".to_string(), + )) + .flatten()?; let native_token_address = chain.native_token().address; let (runtime_handle, runtime) = get_runtime()?; Ok(Self { executor_address, + balance_manager_address, native_token_address, runtime_handle, runtime, @@ -114,13 +120,6 @@ impl SwapEncoder for LiquoriceSwapEncoder { })?; // Extract required fields from quote - let target_contract = signed_quote - .quote_attributes - .get("target_contract") - .ok_or(EncodingError::FatalError( - "Liquorice quote must have a target_contract attribute".to_string(), - ))?; - let liquorice_calldata = signed_quote .quote_attributes .get("calldata") @@ -156,65 +155,33 @@ impl SwapEncoder for LiquoriceSwapEncoder { padded }; - // Parse allowances from quote - let allowances: Vec = signed_quote - .quote_attributes - .get("allowances") - .map(|bytes| { - let json_str = String::from_utf8_lossy(bytes); - serde_json::from_str(&json_str).unwrap_or_default() - }) - .unwrap_or_default(); - - // Check which allowances need approval - let executor_address = bytes_to_address(&self.executor_address)?; - let approvals_manager = ProtocolApprovalsManager::new()?; - - let mut allowance_data: Vec<(Address, bool)> = Vec::new(); - for allowance in &allowances { - let spender = Address::from_str(&allowance.spender).map_err(|_| { - EncodingError::FatalError(format!( - "Invalid allowance spender address: {}", - allowance.spender - )) - })?; - - // Check if approval is needed from executor to spender - let approval_needed = if *swap.token_in() == self.native_token_address { - false - } else { - approvals_manager.approval_needed(token_in, executor_address, spender)? - }; - - allowance_data.push((spender, approval_needed)); - } + // Check if approval is needed from executor to balance manager + let approval_needed = if *swap.token_in() == self.native_token_address { + false + } else { + let executor_address = bytes_to_address(&self.executor_address)?; + let balance_manager_address = Address::from_slice(&self.balance_manager_address); + ProtocolApprovalsManager::new()?.approval_needed( + token_in, + executor_address, + balance_manager_address, + )? + }; let receiver = bytes_to_address(&encoding_context.receiver)?; - let target = bytes_to_address(target_contract)?; // Encode packed data for the executor // Format: token_in | token_out | transfer_type | partial_fill_offset | - // original_base_token_amount | num_allowances | - // [allowance_spender | approval_needed]... | - // target_contract | receiver | liquorice_calldata + // original_base_token_amount | approval_needed | + // receiver | liquorice_calldata let mut encoded = Vec::new(); - // Fixed fields encoded.extend_from_slice(token_in.as_slice()); // 20 bytes encoded.extend_from_slice(token_out.as_slice()); // 20 bytes encoded.push(encoding_context.transfer_type as u8); // 1 byte encoded.push(partial_fill_offset); // 1 byte encoded.extend_from_slice(&original_base_token_amount); // 32 bytes - - // Allowances - encoded.push(allowance_data.len() as u8); // 1 byte - for (spender, approval_needed) in &allowance_data { - encoded.extend_from_slice(spender.as_slice()); // 20 bytes - encoded.push(*approval_needed as u8); // 1 byte - } - - // Target and receiver - encoded.extend_from_slice(target.as_slice()); // 20 bytes + encoded.push(approval_needed as u8); // 1 byte encoded.extend_from_slice(receiver.as_slice()); // 20 bytes // Calldata (variable length) @@ -249,6 +216,13 @@ mod tests { models::TransferType, }; + fn liquorice_config() -> Option> { + Some(HashMap::from([( + "balance_manager_address".to_string(), + "0x71D9750ECF0c5081FAE4E3EDC4253E52024b0B59".to_string(), + )])) + } + #[test] fn test_encode_liquorice_single_fails_without_protocol_data() { let liquorice_component = ProtocolComponent { @@ -276,7 +250,7 @@ mod tests { let encoder = LiquoriceSwapEncoder::new( Bytes::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), Chain::Ethereum, - None, + liquorice_config(), ) .unwrap(); @@ -300,21 +274,12 @@ mod tests { ..Default::default() }; - // Allowances JSON - one spender needing approval - let allowances_json = - r#"[{"token":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","spender":"0x71D9750ECF0c5081FAE4E3EDC4253E52024b0B59","amount":"3000000000"}]"#; - let liquorice_state = MockRFQState { quote_amount_out, quote_data: HashMap::from([ - ( - "target_contract".to_string(), - Bytes::from_str("0x71D9750ECF0c5081FAE4E3EDC4253E52024b0B59").unwrap(), - ), ("calldata".to_string(), liquorice_calldata.clone()), ("base_token_amount".to_string(), Bytes::from(base_token_amount.clone())), ("partial_fill_offset".to_string(), Bytes::from(vec![12u8])), - ("allowances".to_string(), Bytes::from(allowances_json.as_bytes().to_vec())), ]), }; @@ -338,7 +303,7 @@ mod tests { let encoder = LiquoriceSwapEncoder::new( Bytes::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), Chain::Ethereum, - None, + liquorice_config(), ) .unwrap(); @@ -349,9 +314,8 @@ mod tests { // Expected format: // token_in (20) | token_out (20) | transfer_type (1) | partial_fill_offset (1) | - // original_base_token_amount (32) | num_allowances (1) | - // [spender (20) | approval_needed (1)]... | - // target_contract (20) | receiver (20) | calldata (variable) + // original_base_token_amount (32) | approval_needed (1) | + // receiver (20) | calldata (variable) let expected_swap = String::from(concat!( // token_in (USDC) "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", @@ -363,14 +327,8 @@ mod tests { "0c", // original_base_token_amount (3000000000 as U256) "00000000000000000000000000000000000000000000000000000000b2d05e00", - // num_allowances - "01", - // allowance spender - "71d9750ecf0c5081fae4e3edc4253e52024b0b59", // approval_needed "01", - // target_contract - "71d9750ecf0c5081fae4e3edc4253e52024b0b59", // receiver "c5564c13a157e6240659fb81882a28091add8670", )); @@ -378,8 +336,8 @@ mod tests { } #[test] - fn test_encode_liquorice_no_allowances() { - // Test with empty allowances + fn test_encode_liquorice_no_approval_needed() { + // Test with no approval needed (approval byte at position 74 should be 0) let quote_amount_out = BigUint::from_str("1000000000000000000").unwrap(); let liquorice_calldata = Bytes::from_str("0xabcdef").unwrap(); let base_token_amount = biguint_to_u256(&BigUint::from(1000000000_u64)) @@ -395,13 +353,8 @@ mod tests { let liquorice_state = MockRFQState { quote_amount_out, quote_data: HashMap::from([ - ( - "target_contract".to_string(), - Bytes::from_str("0x71D9750ECF0c5081FAE4E3EDC4253E52024b0B59").unwrap(), - ), ("calldata".to_string(), liquorice_calldata.clone()), ("base_token_amount".to_string(), Bytes::from(base_token_amount)), - ("allowances".to_string(), Bytes::from("[]".as_bytes().to_vec())), ]), }; @@ -425,7 +378,7 @@ mod tests { let encoder = LiquoriceSwapEncoder::new( Bytes::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), Chain::Ethereum, - None, + liquorice_config(), ) .unwrap(); @@ -433,7 +386,8 @@ mod tests { .encode_swap(&swap, &encoding_context) .unwrap(); - // Verify num_allowances is 0 (at byte position 74) - assert_eq!(encoded_swap[74], 0); + // Verify approval_needed byte at position 74 + // (20 + 20 + 1 + 1 + 32 = 74) + assert_eq!(encoded_swap[74], 1); // approval needed (new executor, no prior approval) } } diff --git a/tests/protocol_integration_tests.rs b/tests/protocol_integration_tests.rs index 8517803f..74f77b93 100644 --- a/tests/protocol_integration_tests.rs +++ b/tests/protocol_integration_tests.rs @@ -1916,17 +1916,9 @@ fn test_single_encoding_strategy_liquorice() { .unwrap(), ); - // Allowances JSON - one spender - let allowances_json = - r#"[{"token":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","spender":"0x71D9750ECF0c5081FAE4E3EDC4253E52024b0B59","amount":"3000000000"}]"#; - let liquorice_state = MockRFQState { quote_amount_out, quote_data: HashMap::from([ - ( - "target_contract".to_string(), - Bytes::from_str("0x71D9750ECF0c5081FAE4E3EDC4253E52024b0B59").unwrap(), - ), ("calldata".to_string(), mock_calldata), ( "base_token_amount".to_string(), @@ -1940,10 +1932,6 @@ fn test_single_encoding_strategy_liquorice() { "partial_fill_offset".to_string(), Bytes::from(vec![12u8]), // offset = 12 ), - ( - "allowances".to_string(), - Bytes::from(allowances_json.as_bytes().to_vec()), - ), ]), }; From 9f75d046f5189465f5d3b0985e6edd8f6edb2c2f Mon Sep 17 00:00:00 2001 From: Igor Markin Date: Thu, 5 Feb 2026 19:54:07 +0000 Subject: [PATCH 03/15] Handle minBaseTokenAmount in partial fills --- foundry/src/executors/LiquoriceExecutor.sol | 103 +++++++++------ foundry/test/protocols/Liquorice.t.sol | 138 +++++++++----------- src/encoding/evm/swap_encoder/liquorice.rs | 47 +++++-- 3 files changed, 165 insertions(+), 123 deletions(-) diff --git a/foundry/src/executors/LiquoriceExecutor.sol b/foundry/src/executors/LiquoriceExecutor.sol index 36c74fcc..235dbde0 100644 --- a/foundry/src/executors/LiquoriceExecutor.sol +++ b/foundry/src/executors/LiquoriceExecutor.sol @@ -23,13 +23,15 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { /// @notice Liquorice-specific errors error LiquoriceExecutor__InvalidDataLength(); error LiquoriceExecutor__ZeroAddress(); + error LiquoriceExecutor__AmountBelowMinimum(); /// @notice The Liquorice settlement contract address address public immutable liquoriceSettlement; - constructor(address _liquoriceSettlement, address _permit2) - RestrictTransferFrom(_permit2) - { + constructor( + address _liquoriceSettlement, + address _permit2 + ) RestrictTransferFrom(_permit2) { if (_liquoriceSettlement == address(0)) { revert LiquoriceExecutor__ZeroAddress(); } @@ -40,27 +42,22 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { /// @param givenAmount The amount of input token to swap /// @param data Encoded swap data containing tokens and liquorice calldata /// @return calculatedAmount The amount of output token received - function swap(uint256 givenAmount, bytes calldata data) - external - payable - virtual - override - returns (uint256 calculatedAmount) - { + function swap( + uint256 givenAmount, + bytes calldata data + ) external payable virtual override returns (uint256 calculatedAmount) { ( address tokenIn, address tokenOut, TransferType transferType, uint8 partialFillOffset, uint256 originalBaseTokenAmount, + uint256 minBaseTokenAmount, bool approvalNeeded, address receiver, bytes memory liquoriceCalldata ) = _decodeData(data); - // Transfer tokens to executor - _transfer(address(this), transferType, tokenIn, givenAmount); - // Grant approval to Liquorice balance manager if needed if (approvalNeeded && tokenIn != address(0)) { address balanceManager = address( @@ -70,14 +67,22 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { IERC20(tokenIn).forceApprove(balanceManager, type(uint256).max); } + givenAmount = _clampAmount( + givenAmount, + originalBaseTokenAmount, + minBaseTokenAmount + ); + + // Transfer tokens to executor + _transfer(address(this), transferType, tokenIn, givenAmount); + // Modify the fill amount in the calldata if partial fill is supported // If partialFillOffset is 0, partial fill is not supported bytes memory finalCalldata = liquoriceCalldata; - if (partialFillOffset > 0) { + if (partialFillOffset > 0 && originalBaseTokenAmount > givenAmount) { finalCalldata = _modifyFilledTakerAmount( liquoriceCalldata, givenAmount, - originalBaseTokenAmount, partialFillOffset ); } @@ -94,7 +99,9 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { } /// @dev Decodes the packed calldata - function _decodeData(bytes calldata data) + function _decodeData( + bytes calldata data + ) internal pure returns ( @@ -103,6 +110,7 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { TransferType transferType, uint8 partialFillOffset, uint256 originalBaseTokenAmount, + uint256 minBaseTokenAmount, bool approvalNeeded, address receiver, bytes memory liquoriceCalldata @@ -110,45 +118,56 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { { // Minimum fixed fields: // tokenIn (20) + tokenOut (20) + transferType (1) + partialFillOffset (1) + - // originalBaseTokenAmount (32) + approvalNeeded (1) + receiver (20) = 95 bytes - if (data.length < 95) revert LiquoriceExecutor__InvalidDataLength(); + // originalBaseTokenAmount (32) + minBaseTokenAmount (32) + + // approvalNeeded (1) + receiver (20) = 127 bytes + if (data.length < 127) revert LiquoriceExecutor__InvalidDataLength(); tokenIn = address(bytes20(data[0:20])); tokenOut = address(bytes20(data[20:40])); transferType = TransferType(uint8(data[40])); partialFillOffset = uint8(data[41]); originalBaseTokenAmount = uint256(bytes32(data[42:74])); - approvalNeeded = data[74] != 0; - receiver = address(bytes20(data[75:95])); - liquoriceCalldata = data[95:]; + minBaseTokenAmount = uint256(bytes32(data[74:106])); + approvalNeeded = data[106] != 0; + receiver = address(bytes20(data[107:127])); + liquoriceCalldata = data[127:]; + } + + /// @dev Clamps the given amount to be within the valid range for the quote + /// @param givenAmount The amount provided by the router + /// @param originalBaseTokenAmount The maximum amount the quote supports + /// @param minBaseTokenAmount The minimum amount required for partial fills + /// @return The clamped amount + function _clampAmount( + uint256 givenAmount, + uint256 originalBaseTokenAmount, + uint256 minBaseTokenAmount + ) internal pure returns (uint256) { + // For partially filled quotes, revert if below minimum amount requirement + if (givenAmount < minBaseTokenAmount) { + revert LiquoriceExecutor__AmountBelowMinimum(); + } + // It is possible to have a quote with lesser amount than was requested + if (givenAmount > originalBaseTokenAmount) { + return originalBaseTokenAmount; + } + return givenAmount; } /// @dev Modifies the filledTakerAmount in the liquorice calldata to handle slippage /// @param liquoriceCalldata The original calldata for the liquorice settlement /// @param givenAmount The actual amount available from the router - /// @param originalBaseTokenAmount The original amount expected when the quote was generated /// @param partialFillOffset The offset from Liquorice API indicating where the fill amount is located /// @return The modified calldata with updated fill amount function _modifyFilledTakerAmount( bytes memory liquoriceCalldata, uint256 givenAmount, - uint256 originalBaseTokenAmount, uint8 partialFillOffset ) internal pure returns (bytes memory) { // Use the offset from Liquorice API to locate the fill amount // Position = 4 bytes (selector) + offset * 32 bytes uint256 fillAmountPos = 4 + uint256(partialFillOffset) * 32; - // Cap the fill amount at what we actually have available - uint256 newFillAmount = originalBaseTokenAmount > givenAmount - ? givenAmount - : originalBaseTokenAmount; - - // If the new fill amount is the same as the original, return the original calldata - if (newFillAmount == originalBaseTokenAmount) { - return liquoriceCalldata; - } - // Use assembly to modify the fill amount at the correct position // slither-disable-next-line assembly assembly { @@ -157,7 +176,7 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { // Calculate the actual position and store the new value let actualPos := add(dataPtr, fillAmountPos) - mstore(actualPos, newFillAmount) + mstore(actualPos, givenAmount) } return liquoriceCalldata; @@ -167,14 +186,14 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { /// @param token The token address, or address(0) for ETH /// @param account The account to get the balance of /// @return 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); + 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 diff --git a/foundry/test/protocols/Liquorice.t.sol b/foundry/test/protocols/Liquorice.t.sol index 78558892..dd8ff093 100644 --- a/foundry/test/protocols/Liquorice.t.sol +++ b/foundry/test/protocols/Liquorice.t.sol @@ -12,11 +12,14 @@ import { } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; contract LiquoriceExecutorExposed is LiquoriceExecutor { - constructor(address _liquoriceSettlement, address _permit2) - LiquoriceExecutor(_liquoriceSettlement, _permit2) - {} - - function decodeData(bytes calldata data) + constructor( + address _liquoriceSettlement, + address _permit2 + ) LiquoriceExecutor(_liquoriceSettlement, _permit2) {} + + function decodeData( + bytes calldata data + ) external pure returns ( @@ -25,6 +28,7 @@ contract LiquoriceExecutorExposed is LiquoriceExecutor { TransferType transferType, uint8 partialFillOffset, uint256 originalBaseTokenAmount, + uint256 minBaseTokenAmount, bool approvalNeeded, address receiver, bytes memory liquoriceCalldata @@ -32,6 +36,19 @@ contract LiquoriceExecutorExposed is LiquoriceExecutor { { return _decodeData(data); } + + function clampAmount( + uint256 givenAmount, + uint256 originalBaseTokenAmount, + uint256 minBaseTokenAmount + ) external pure returns (uint256) { + return + _clampAmount( + givenAmount, + originalBaseTokenAmount, + minBaseTokenAmount + ); + } } contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { @@ -48,17 +65,20 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { function setUp() public { vm.createSelectFork(vm.rpcUrl("mainnet"), 22667985); - liquoriceExecutor = - new LiquoriceExecutorExposed(LIQUORICE_SETTLEMENT, PERMIT2_ADDRESS); + liquoriceExecutor = new LiquoriceExecutorExposed( + LIQUORICE_SETTLEMENT, + PERMIT2_ADDRESS + ); } - function testDecodeData_NoApproval() public view { + function testDecodeData() public view { bytes memory liquoriceCalldata = abi.encodePacked( bytes4(0xdeadbeef), // mock selector hex"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" ); uint256 originalAmount = 1000000000; // 1000 USDC + uint256 minAmount = 800000000; // 800 USDC address receiver = address(0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD); bytes memory params = abi.encodePacked( @@ -67,6 +87,7 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { uint8(RestrictTransferFrom.TransferType.Transfer), // transferType (1 byte) uint8(5), // partialFillOffset (1 byte) originalAmount, // originalBaseTokenAmount (32 bytes) + minAmount, // minBaseTokenAmount (32 bytes) uint8(0), // approvalNeeded (1 byte) - false receiver, // receiver (20 bytes) liquoriceCalldata // variable length @@ -78,6 +99,7 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { RestrictTransferFrom.TransferType decodedTransferType, uint8 decodedPartialFillOffset, uint256 decodedOriginalAmount, + uint256 decodedMinAmount, bool decodedApprovalNeeded, address decodedReceiver, bytes memory decodedCalldata @@ -92,8 +114,11 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { ); assertEq(decodedPartialFillOffset, 5, "partialFillOffset mismatch"); assertEq( - decodedOriginalAmount, originalAmount, "originalAmount mismatch" + decodedOriginalAmount, + originalAmount, + "originalAmount mismatch" ); + assertEq(decodedMinAmount, minAmount, "minAmount mismatch"); assertFalse(decodedApprovalNeeded, "approvalNeeded should be false"); assertEq(decodedReceiver, receiver, "receiver mismatch"); assertEq( @@ -103,54 +128,6 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { ); } - function testDecodeData_WithApproval() public view { - bytes memory liquoriceCalldata = hex"deadbeef"; - - uint256 originalAmount = 2000000000; // 2000 USDC - address receiver = address(0x2222222222222222222222222222222222222222); - - bytes memory params = abi.encodePacked( - USDC_ADDR, // tokenIn (20 bytes) - WBTC_ADDR, // tokenOut (20 bytes) - uint8(RestrictTransferFrom.TransferType.None), // transferType (1 byte) - uint8(12), // partialFillOffset (1 byte) - originalAmount, // originalBaseTokenAmount (32 bytes) - uint8(1), // approvalNeeded (1 byte) - true - receiver, // receiver (20 bytes) - liquoriceCalldata // variable length - ); - - ( - address decodedTokenIn, - address decodedTokenOut, - RestrictTransferFrom.TransferType decodedTransferType, - uint8 decodedPartialFillOffset, - uint256 decodedOriginalAmount, - bool decodedApprovalNeeded, - address decodedReceiver, - bytes memory decodedCalldata - ) = liquoriceExecutor.decodeData(params); - - assertEq(decodedTokenIn, USDC_ADDR, "tokenIn mismatch"); - assertEq(decodedTokenOut, WBTC_ADDR, "tokenOut mismatch"); - assertEq( - uint8(decodedTransferType), - uint8(RestrictTransferFrom.TransferType.None), - "transferType mismatch" - ); - assertEq(decodedPartialFillOffset, 12, "partialFillOffset mismatch"); - assertEq( - decodedOriginalAmount, originalAmount, "originalAmount mismatch" - ); - assertTrue(decodedApprovalNeeded, "approvalNeeded should be true"); - assertEq(decodedReceiver, receiver, "receiver mismatch"); - assertEq( - keccak256(decodedCalldata), - keccak256(liquoriceCalldata), - "calldata mismatch" - ); - } - function testDecodeData_InvalidDataLength() public { // Too short - missing required fields bytes memory tooShort = abi.encodePacked( @@ -166,24 +143,39 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { liquoriceExecutor.decodeData(tooShort); } - function testDecodeData_ZeroPartialFillOffset() public view { - bytes memory liquoriceCalldata = hex"cafebabe"; - address receiver = address(0x6666666666666666666666666666666666666666); - - // partialFillOffset = 0 means partial fill not supported - bytes memory params = abi.encodePacked( - WETH_ADDR, - USDC_ADDR, - uint8(RestrictTransferFrom.TransferType.Transfer), - uint8(0), // partialFillOffset = 0 - uint256(1e18), - uint8(0), // approvalNeeded = false - receiver, - liquoriceCalldata + function testClampAmount_WithinRange() public view { + // givenAmount is within [minBaseTokenAmount, originalBaseTokenAmount] + uint256 result = liquoriceExecutor.clampAmount( + 500, // givenAmount + 1000, // originalBaseTokenAmount + 100 // minBaseTokenAmount ); + assertEq(result, 500, "Should return givenAmount when within range"); + } - (,,, uint8 decodedOffset,,,,) = liquoriceExecutor.decodeData(params); + function testClampAmount_ExceedsMax() public view { + // givenAmount exceeds originalBaseTokenAmount + uint256 result = liquoriceExecutor.clampAmount( + 1500, // givenAmount + 1000, // originalBaseTokenAmount + 100 // minBaseTokenAmount + ); + assertEq( + result, + 1000, + "Should clamp to originalBaseTokenAmount when exceeded" + ); + } - assertEq(decodedOffset, 0, "partialFillOffset should be 0"); + function testClampAmount_BelowMin_Reverts() public { + // givenAmount is below minBaseTokenAmount - should revert + vm.expectRevert( + LiquoriceExecutor.LiquoriceExecutor__AmountBelowMinimum.selector + ); + liquoriceExecutor.clampAmount( + 50, // givenAmount + 1000, // originalBaseTokenAmount + 100 // minBaseTokenAmount + ); } } diff --git a/src/encoding/evm/swap_encoder/liquorice.rs b/src/encoding/evm/swap_encoder/liquorice.rs index f875893e..0e6bbc55 100644 --- a/src/encoding/evm/swap_encoder/liquorice.rs +++ b/src/encoding/evm/swap_encoder/liquorice.rs @@ -134,7 +134,8 @@ impl SwapEncoder for LiquoriceSwapEncoder { "Liquorice quote must have a base_token_amount attribute".to_string(), ))?; - // Get partial fill offset (defaults to 0 if not present, meaning no partial fill support) + // Get partial fill offset (defaults to 0 if not present, meaning partial fill is not + // available for the quote) let partial_fill_offset: u8 = signed_quote .quote_attributes .get("partial_fill_offset") @@ -144,6 +145,13 @@ impl SwapEncoder for LiquoriceSwapEncoder { }) .unwrap_or(0); + // Get min base token amount (defaults to original base token amount if partiall fill is not + // available for the quote) + let min_base_token_amount = signed_quote + .quote_attributes + .get("min_base_token_amount") + .unwrap_or(base_token_amount); + // Parse original base token amount (U256 encoded as 32 bytes) let original_base_token_amount = if base_token_amount.len() == 32 { base_token_amount.to_vec() @@ -155,6 +163,16 @@ impl SwapEncoder for LiquoriceSwapEncoder { padded }; + // Parse min base token amount (U256 encoded as 32 bytes) + let min_base_token_amount = if min_base_token_amount.len() == 32 { + min_base_token_amount.to_vec() + } else { + let mut padded = vec![0u8; 32]; + let start = 32 - min_base_token_amount.len(); + padded[start..].copy_from_slice(min_base_token_amount); + padded + }; + // Check if approval is needed from executor to balance manager let approval_needed = if *swap.token_in() == self.native_token_address { false @@ -172,8 +190,8 @@ impl SwapEncoder for LiquoriceSwapEncoder { // Encode packed data for the executor // Format: token_in | token_out | transfer_type | partial_fill_offset | - // original_base_token_amount | approval_needed | - // receiver | liquorice_calldata + // original_base_token_amount | min_base_token_amount | + // approval_needed | receiver | liquorice_calldata let mut encoded = Vec::new(); encoded.extend_from_slice(token_in.as_slice()); // 20 bytes @@ -181,6 +199,7 @@ impl SwapEncoder for LiquoriceSwapEncoder { encoded.push(encoding_context.transfer_type as u8); // 1 byte encoded.push(partial_fill_offset); // 1 byte encoded.extend_from_slice(&original_base_token_amount); // 32 bytes + encoded.extend_from_slice(&min_base_token_amount); // 32 bytes encoded.push(approval_needed as u8); // 1 byte encoded.extend_from_slice(receiver.as_slice()); // 20 bytes @@ -274,11 +293,16 @@ mod tests { ..Default::default() }; + let min_base_token_amount = biguint_to_u256(&BigUint::from(2500000000_u64)) + .to_be_bytes::<32>() + .to_vec(); + let liquorice_state = MockRFQState { quote_amount_out, quote_data: HashMap::from([ ("calldata".to_string(), liquorice_calldata.clone()), ("base_token_amount".to_string(), Bytes::from(base_token_amount.clone())), + ("min_base_token_amount".to_string(), Bytes::from(min_base_token_amount.clone())), ("partial_fill_offset".to_string(), Bytes::from(vec![12u8])), ]), }; @@ -314,8 +338,8 @@ mod tests { // Expected format: // token_in (20) | token_out (20) | transfer_type (1) | partial_fill_offset (1) | - // original_base_token_amount (32) | approval_needed (1) | - // receiver (20) | calldata (variable) + // original_base_token_amount (32) | min_base_token_amount (32) | + // approval_needed (1) | receiver (20) | calldata (variable) let expected_swap = String::from(concat!( // token_in (USDC) "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", @@ -327,6 +351,8 @@ mod tests { "0c", // original_base_token_amount (3000000000 as U256) "00000000000000000000000000000000000000000000000000000000b2d05e00", + // min_base_token_amount (2500000000 as U256) + "0000000000000000000000000000000000000000000000000000000094f5f200", // approval_needed "01", // receiver @@ -350,11 +376,16 @@ mod tests { ..Default::default() }; + let min_base_token_amount = biguint_to_u256(&BigUint::from(800000000_u64)) + .to_be_bytes::<32>() + .to_vec(); + let liquorice_state = MockRFQState { quote_amount_out, quote_data: HashMap::from([ ("calldata".to_string(), liquorice_calldata.clone()), ("base_token_amount".to_string(), Bytes::from(base_token_amount)), + ("min_base_token_amount".to_string(), Bytes::from(min_base_token_amount)), ]), }; @@ -386,8 +417,8 @@ mod tests { .encode_swap(&swap, &encoding_context) .unwrap(); - // Verify approval_needed byte at position 74 - // (20 + 20 + 1 + 1 + 32 = 74) - assert_eq!(encoded_swap[74], 1); // approval needed (new executor, no prior approval) + // Verify approval_needed byte at position 106 + // (20 + 20 + 1 + 1 + 32 + 32 = 106) + assert_eq!(encoded_swap[106], 1); // approval needed (new executor, no prior approval) } } From 65aa9d25daceeb4c208e51244446812318212379 Mon Sep 17 00:00:00 2001 From: Igor Markin Date: Fri, 6 Feb 2026 15:24:52 +0000 Subject: [PATCH 04/15] Implement settle tests --- foundry/src/executors/LiquoriceExecutor.sol | 15 +- .../src/liquorice/ILiquoriceSettlement.sol | 6 - foundry/test/protocols/Liquorice.t.sol | 235 +++++++++++++++++- 3 files changed, 246 insertions(+), 10 deletions(-) delete mode 100644 foundry/src/liquorice/ILiquoriceSettlement.sol diff --git a/foundry/src/executors/LiquoriceExecutor.sol b/foundry/src/executors/LiquoriceExecutor.sol index 235dbde0..40919604 100644 --- a/foundry/src/executors/LiquoriceExecutor.sol +++ b/foundry/src/executors/LiquoriceExecutor.sol @@ -9,7 +9,16 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/Address.sol"; -import "../liquorice/ILiquoriceSettlement.sol"; + +interface ILiquoriceSettlement { + function BALANCE_MANAGER() external view returns (address); + function AUTHENTICATOR() external view returns (address); +} + +interface IAllowListAuthentication { + function addSolver(address _solver) external; + function addMaker(address _maker) external; +} /// @title LiquoriceExecutor /// @notice Executor for Liquorice RFQ (Request for Quote) swaps @@ -165,8 +174,8 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { uint8 partialFillOffset ) internal pure returns (bytes memory) { // Use the offset from Liquorice API to locate the fill amount - // Position = 4 bytes (selector) + offset * 32 bytes - uint256 fillAmountPos = 4 + uint256(partialFillOffset) * 32; + // Position = 4 bytes (selector) + offset bytes + uint256 fillAmountPos = 4 + uint256(partialFillOffset); // Use assembly to modify the fill amount at the correct position // slither-disable-next-line assembly diff --git a/foundry/src/liquorice/ILiquoriceSettlement.sol b/foundry/src/liquorice/ILiquoriceSettlement.sol deleted file mode 100644 index 4a93b1d0..00000000 --- a/foundry/src/liquorice/ILiquoriceSettlement.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.26; - -interface ILiquoriceSettlement { - function BALANCE_MANAGER() external view returns (address); -} diff --git a/foundry/test/protocols/Liquorice.t.sol b/foundry/test/protocols/Liquorice.t.sol index dd8ff093..1c4c0c53 100644 --- a/foundry/test/protocols/Liquorice.t.sol +++ b/foundry/test/protocols/Liquorice.t.sol @@ -54,8 +54,11 @@ contract LiquoriceExecutorExposed is LiquoriceExecutor { contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { using SafeERC20 for IERC20; + ILiquoriceSettlement liquoriceSettlement; + IAllowListAuthentication authenticator; LiquoriceExecutorExposed liquoriceExecutor; + address constant AUTH_MANAGER = 0x000438801500c89E225E8D6CB69D9c14dD05e000; address constant LIQUORICE_SETTLEMENT = 0x0448633eb8B0A42EfED924C42069E0DcF08fb552; @@ -63,12 +66,242 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { IERC20 USDC = IERC20(USDC_ADDR); IERC20 WBTC = IERC20(WBTC_ADDR); + address constant MAKER = 0x06465bcEEaef280Bb7340A58D75dfc5E1F687058; + uint256 constant FORK_BLOCK = 24_392_845; + function setUp() public { - vm.createSelectFork(vm.rpcUrl("mainnet"), 22667985); + vm.createSelectFork(vm.rpcUrl("mainnet"), FORK_BLOCK); + liquoriceExecutor = new LiquoriceExecutorExposed( LIQUORICE_SETTLEMENT, PERMIT2_ADDRESS ); + + liquoriceSettlement = ILiquoriceSettlement(LIQUORICE_SETTLEMENT); + authenticator = IAllowListAuthentication( + liquoriceSettlement.AUTHENTICATOR() + ); + + vm.prank(AUTH_MANAGER); + authenticator.addSolver(address(liquoriceExecutor)); + vm.prank(AUTH_MANAGER); + authenticator.addMaker(MAKER); + + address balanceManager = liquoriceSettlement.BALANCE_MANAGER(); + vm.prank(MAKER); + WETH.approve(balanceManager, type(uint256).max); + } + + function testSettleSingle() public { + // 3000 USDC -> 1 WETH + bytes + memory liquoriceCalldata = hex"9935c86800000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f687058000000000000000000000000000000000000000000000000000000000000000131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000041883a6506193307eebda0f3adf2cb81f84a073e030749055ebb18cbf98704eef100a03c307266527d706f9a5c3e08ed0988f5b130bc5327e0ad62dde6f3709d251b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; + + address tokenIn = USDC_ADDR; + address tokenOut = WETH_ADDR; + RestrictTransferFrom.TransferType transferType = RestrictTransferFrom + .TransferType + .None; + uint8 partialFillOffset = 96; // PARTIAL_FILL_OFFSET_SETTLE_SINGLE_ORDER + uint256 amountIn = 3000e6; // 3000 USDC + bool approvalNeeded = true; + uint256 expectedAmountOut = 1 ether; + + // Fund maker with WETH (what trader receives) + deal(WETH_ADDR, MAKER, expectedAmountOut); + // Fund executor with USDC (what trader sells) + deal(tokenIn, address(liquoriceExecutor), amountIn); + + bytes memory params = abi.encodePacked( + tokenIn, + tokenOut, + uint8(transferType), + partialFillOffset, + amountIn, // originalBaseTokenAmount + amountIn, // minBaseTokenAmount (same for full fill) + uint8(approvalNeeded ? 1 : 0), + address(liquoriceExecutor), + liquoriceCalldata + ); + + uint256 initialTokenOutBalance = IERC20(tokenOut).balanceOf( + address(liquoriceExecutor) + ); + + uint256 amountOut = liquoriceExecutor.swap(amountIn, params); + + assertEq(amountOut, expectedAmountOut, "Incorrect amount out"); + assertEq( + IERC20(tokenOut).balanceOf(address(liquoriceExecutor)) - + initialTokenOutBalance, + expectedAmountOut, + "WETH should be at receiver" + ); + assertEq( + IERC20(tokenIn).balanceOf(address(liquoriceExecutor)), + 0, + "USDC left in executor" + ); + } + + function testSettleSingle_PartialFill() public { + // 1500 USDC -> 0.5 WETH (50% partial fill of 3000 USDC -> 1 WETH quote) + bytes + memory liquoriceCalldata = hex"9935c86800000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f687058000000000000000000000000000000000000000000000000000000000000000131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000041883a6506193307eebda0f3adf2cb81f84a073e030749055ebb18cbf98704eef100a03c307266527d706f9a5c3e08ed0988f5b130bc5327e0ad62dde6f3709d251b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; + + address tokenIn = USDC_ADDR; + address tokenOut = WETH_ADDR; + RestrictTransferFrom.TransferType transferType = RestrictTransferFrom + .TransferType + .None; + uint8 partialFillOffset = 96; // PARTIAL_FILL_OFFSET_SETTLE_SINGLE_ORDER + uint256 originalAmountIn = 3000e6; // Original quote: 3000 USDC + uint256 amountIn = 1500e6; // Partial fill: 1500 USDC (50%) + uint256 minAmountIn = 1500e6; // Minimum allowed + bool approvalNeeded = true; + uint256 expectedAmountOut = 0.5 ether; // 50% of 1 WETH + + // Fund maker with WETH (only need 0.5 for partial fill) + deal(WETH_ADDR, MAKER, expectedAmountOut); + // Fund executor with partial USDC amount + deal(tokenIn, address(liquoriceExecutor), amountIn); + + bytes memory params = abi.encodePacked( + tokenIn, + tokenOut, + uint8(transferType), + partialFillOffset, + originalAmountIn, // originalBaseTokenAmount from quote + minAmountIn, // minBaseTokenAmount for partial fill + uint8(approvalNeeded ? 1 : 0), + address(liquoriceExecutor), + liquoriceCalldata + ); + + uint256 initialTokenOutBalance = IERC20(tokenOut).balanceOf( + address(liquoriceExecutor) + ); + + uint256 amountOut = liquoriceExecutor.swap(amountIn, params); + + assertEq(amountOut, expectedAmountOut, "Incorrect partial amount out"); + assertEq( + IERC20(tokenOut).balanceOf(address(liquoriceExecutor)) - + initialTokenOutBalance, + expectedAmountOut, + "WETH should be at receiver" + ); + assertEq( + IERC20(tokenIn).balanceOf(address(liquoriceExecutor)), + 0, + "USDC left in executor" + ); + } + + function testSettle() public { + // 3000 USDC -> 1 WETH using settle() function + bytes + memory liquoriceCalldata = hex"cba673a700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000448633eb8b0a42efed924c42069e0dcf08fb5520000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f6870580000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000411d85c337d0e071eb601d8a90e2e8dd0afb61db200a1614c4afe5d26ff0c11bd402e018ab5fdbf8386437d7594af4383cf020a75c96e02c0a208f0b06e86115401b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; + + address tokenIn = USDC_ADDR; + address tokenOut = WETH_ADDR; + RestrictTransferFrom.TransferType transferType = RestrictTransferFrom + .TransferType + .None; + uint8 partialFillOffset = 32; // PARTIAL_FILL_OFFSET_SETTLE_ORDER + uint256 amountIn = 3000e6; // 3000 USDC + bool approvalNeeded = true; + uint256 expectedAmountOut = 1 ether; + + // Fund maker with WETH + deal(WETH_ADDR, MAKER, expectedAmountOut); + // Fund executor with USDC + deal(tokenIn, address(liquoriceExecutor), amountIn); + + bytes memory params = abi.encodePacked( + tokenIn, + tokenOut, + uint8(transferType), + partialFillOffset, + amountIn, // originalBaseTokenAmount + amountIn, // minBaseTokenAmount (same for full fill) + uint8(approvalNeeded ? 1 : 0), + address(liquoriceExecutor), + liquoriceCalldata + ); + + uint256 initialTokenOutBalance = IERC20(tokenOut).balanceOf( + address(liquoriceExecutor) + ); + + uint256 amountOut = liquoriceExecutor.swap(amountIn, params); + + assertEq(amountOut, expectedAmountOut, "Incorrect amount out"); + assertEq( + IERC20(tokenOut).balanceOf(address(liquoriceExecutor)) - + initialTokenOutBalance, + expectedAmountOut, + "WETH should be at receiver" + ); + assertEq( + IERC20(tokenIn).balanceOf(address(liquoriceExecutor)), + 0, + "USDC left in executor" + ); + } + + function testSettle_PartialFill() public { + // 1500 USDC -> 0.5 WETH (50% partial fill) using settle() function + bytes + memory liquoriceCalldata = hex"cba673a700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f687058000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000448633eb8b0a42efed924c42069e0dcf08fb5520000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f6870580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000041e89ad636a6d749213b9339ac5218229adaa53bdf96d457ee2cebfd4fd02909bf678953c976ceca7307ef2e73c5687c33738a6a1e4130e378d220b68d9c59e18b1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; + + address tokenIn = USDC_ADDR; + address tokenOut = WETH_ADDR; + RestrictTransferFrom.TransferType transferType = RestrictTransferFrom + .TransferType + .None; + uint8 partialFillOffset = 32; // PARTIAL_FILL_OFFSET_SETTLE_ORDER + uint256 originalAmountIn = 3000e6; // Original quote: 3000 USDC + uint256 amountIn = 1500e6; // Partial fill: 1500 USDC (50%) + uint256 minAmountIn = 1500e6; // Minimum allowed + bool approvalNeeded = true; + uint256 expectedAmountOut = 0.5 ether; // 50% of 1 WETH + + // Fund maker with WETH (only need 0.5 for partial fill) + deal(WETH_ADDR, MAKER, expectedAmountOut); + // Fund executor with partial USDC amount + deal(tokenIn, address(liquoriceExecutor), amountIn); + + bytes memory params = abi.encodePacked( + tokenIn, + tokenOut, + uint8(transferType), + partialFillOffset, + originalAmountIn, // originalBaseTokenAmount from quote + minAmountIn, // minBaseTokenAmount for partial fill + uint8(approvalNeeded ? 1 : 0), + address(liquoriceExecutor), + liquoriceCalldata + ); + + uint256 initialTokenOutBalance = IERC20(tokenOut).balanceOf( + address(liquoriceExecutor) + ); + + uint256 amountOut = liquoriceExecutor.swap(amountIn, params); + + assertEq(amountOut, expectedAmountOut, "Incorrect partial amount out"); + assertEq( + IERC20(tokenOut).balanceOf(address(liquoriceExecutor)) - + initialTokenOutBalance, + expectedAmountOut, + "WETH should be at receiver" + ); + assertEq( + IERC20(tokenIn).balanceOf(address(liquoriceExecutor)), + 0, + "USDC left in executor" + ); } function testDecodeData() public view { From 53922e6090d9ce3abfe7b48b625c4245517cce2e Mon Sep 17 00:00:00 2001 From: Igor Markin Date: Mon, 9 Feb 2026 16:47:41 +0000 Subject: [PATCH 05/15] Implement integration tests with TychoRouter --- config/test_executor_addresses.json | 4 +- foundry/test/Constants.sol | 3 + foundry/test/TychoRouterTestSetup.sol | 165 +++++++++++++++---------- foundry/test/assets/calldata.txt | 2 + foundry/test/protocols/Liquorice.t.sol | 92 +++++++++++++- tests/protocol_integration_tests.rs | 138 +++++++++++++++++++-- 6 files changed, 325 insertions(+), 79 deletions(-) diff --git a/config/test_executor_addresses.json b/config/test_executor_addresses.json index 7e856aed..8e0a3144 100644 --- a/config/test_executor_addresses.json +++ b/config/test_executor_addresses.json @@ -13,7 +13,7 @@ "vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb", "rfq:bebop": "0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF", "rfq:hashflow": "0x15cF58144EF33af1e14b5208015d11F9143E27b9", - "rfq:liquorice": "0x0000000000000000000000000000000000000000", + "rfq:liquorice": "0xDB25A7b768311dE128BBDa7B8426c3f9C74f3240", "fluid_v1": "0x212224D2F2d262cd093eE13240ca4873fcCBbA3C", "rocketpool": "0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7", "erc4626": "0xD16d567549A2a2a2005aEACf7fB193851603dd70", @@ -23,4 +23,4 @@ "base": { "aerodrome_slipstreams": "0x2a07706473244BC757E10F2a9E86fB532828afe3" } -} \ No newline at end of file +} diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index e80a990e..55db8d4f 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -156,6 +156,9 @@ contract Constants is Test, BaseConstants { // Hashflow Router address HASHFLOW_ROUTER = 0x55084eE0fEf03f14a305cd24286359A35D735151; + // Liquorice Settlement + address LIQUORICE_SETTLEMENT = 0x0448633eb8B0A42EfED924C42069E0DcF08fb552; + // Pool Code Init Hashes bytes32 USV2_POOL_CODE_INIT_HASH = 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f; diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 4e5822a5..573d3621 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -9,6 +9,7 @@ import {CurveExecutor} from "../src/executors/CurveExecutor.sol"; import {EkuboExecutor} from "../src/executors/EkuboExecutor.sol"; import {EkuboV3Executor} from "../src/executors/EkuboV3Executor.sol"; import {HashflowExecutor} from "../src/executors/HashflowExecutor.sol"; +import {LiquoriceExecutor} from "../src/executors/LiquoriceExecutor.sol"; import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol"; import {UniswapV2Executor} from "../src/executors/UniswapV2Executor.sol"; import { @@ -49,7 +50,10 @@ contract TychoRouterExposed is TychoRouter { bool transferFromNeeded ) external { _tstoreTransferFromInfo( - tokenIn, amountIn, isPermit2, transferFromNeeded + tokenIn, + amountIn, + isPermit2, + transferFromNeeded ); } @@ -61,10 +65,10 @@ contract TychoRouterExposed is TychoRouter { return _splitSwap(amountIn, nTokens, swaps); } - function exposedSequentialSwap(uint256 amountIn, bytes calldata swaps) - external - returns (uint256) - { + function exposedSequentialSwap( + uint256 amountIn, + bytes calldata swaps + ) external returns (uint256) { return _sequentialSwap(amountIn, swaps); } } @@ -84,6 +88,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { BalancerV3Executor public balancerV3Executor; BebopExecutor public bebopExecutor; HashflowExecutor public hashflowExecutor; + LiquoriceExecutor public liquoriceExecutor; FluidV1Executor public fluidV1Executor; SlipstreamsExecutor public slipstreamsExecutor; RocketpoolExecutor public rocketpoolExecutor; @@ -122,7 +127,8 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { tychoRouter.grantRole(keccak256("PAUSER_ROLE"), PAUSER); tychoRouter.grantRole(keccak256("UNPAUSER_ROLE"), UNPAUSER); tychoRouter.grantRole( - keccak256("EXECUTOR_SETTER_ROLE"), EXECUTOR_SETTER + keccak256("EXECUTOR_SETTER_ROLE"), + EXECUTOR_SETTER ); return tychoRouter; } @@ -139,28 +145,52 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { address ekuboV2MevResist = 0x553a2EFc570c9e104942cEC6aC1c18118e54C091; IPoolManager poolManager = IPoolManager(poolManagerAddress); - usv2Executor = - new UniswapV2Executor(factoryV2, initCodeV2, PERMIT2_ADDRESS, 30); - usv3Executor = - new UniswapV3Executor(factoryV3, initCodeV3, PERMIT2_ADDRESS); - usv4Executor = - new UniswapV4Executor(poolManager, ANGSTROM_HOOK, PERMIT2_ADDRESS); + usv2Executor = new UniswapV2Executor( + factoryV2, + initCodeV2, + PERMIT2_ADDRESS, + 30 + ); + usv3Executor = new UniswapV3Executor( + factoryV3, + initCodeV3, + PERMIT2_ADDRESS + ); + usv4Executor = new UniswapV4Executor( + poolManager, + ANGSTROM_HOOK, + PERMIT2_ADDRESS + ); pancakev3Executor = new UniswapV3Executor( - factoryPancakeV3, initCodePancakeV3, PERMIT2_ADDRESS + factoryPancakeV3, + initCodePancakeV3, + PERMIT2_ADDRESS ); balancerv2Executor = new BalancerV2Executor(PERMIT2_ADDRESS); - ekuboV2Executor = - new EkuboExecutor(ekuboV2Core, ekuboV2MevResist, PERMIT2_ADDRESS); - curveExecutor = - new CurveExecutor(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS, STETH_ADDR); - maverickv2Executor = - new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS); + ekuboV2Executor = new EkuboExecutor( + ekuboV2Core, + ekuboV2MevResist, + PERMIT2_ADDRESS + ); + curveExecutor = new CurveExecutor( + ETH_ADDR_FOR_CURVE, + PERMIT2_ADDRESS, + STETH_ADDR + ); + 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); - fluidV1Executor = - new FluidV1Executor(FLUIDV1_LIQUIDITY, PERMIT2_ADDRESS); + hashflowExecutor = new HashflowExecutor( + HASHFLOW_ROUTER, + PERMIT2_ADDRESS + ); + fluidV1Executor = new FluidV1Executor( + FLUIDV1_LIQUIDITY, + PERMIT2_ADDRESS + ); slipstreamsExecutor = new SlipstreamsExecutor( SLIPSTREAMS_FACTORY_BASE, SLIPSTREAMS_NEW_FACTORY_BASE, @@ -168,11 +198,18 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { ); rocketpoolExecutor = new RocketpoolExecutor(PERMIT2_ADDRESS); erc4626Executor = new ERC4626Executor(PERMIT2_ADDRESS); - lidoExecutor = - new LidoExecutor(STETH_ADDR, WSTETH_ADDR, PERMIT2_ADDRESS); + lidoExecutor = new LidoExecutor( + STETH_ADDR, + WSTETH_ADDR, + PERMIT2_ADDRESS + ); ekuboV3Executor = new EkuboV3Executor(PERMIT2_ADDRESS); + liquoriceExecutor = new LiquoriceExecutor( + LIQUORICE_SETTLEMENT, + PERMIT2_ADDRESS + ); - address[] memory executors = new address[](17); + address[] memory executors = new address[](18); executors[0] = address(usv2Executor); executors[1] = address(usv3Executor); executors[2] = address(pancakev3Executor); @@ -190,15 +227,14 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { executors[14] = address(erc4626Executor); executors[15] = address(lidoExecutor); executors[16] = address(ekuboV3Executor); + executors[17] = address(liquoriceExecutor); return executors; } - function pleEncode(bytes[] memory data) - public - pure - returns (bytes memory encoded) - { + function pleEncode( + bytes[] memory data + ) public pure returns (bytes memory encoded) { for (uint256 i = 0; i < data.length; i++) { encoded = bytes.concat( encoded, @@ -207,19 +243,17 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { } } - function encodeSingleSwap(address executor, bytes memory protocolData) - internal - pure - returns (bytes memory) - { + function encodeSingleSwap( + address executor, + bytes memory protocolData + ) internal pure returns (bytes memory) { return abi.encodePacked(executor, protocolData); } - function encodeSequentialSwap(address executor, bytes memory protocolData) - internal - pure - returns (bytes memory) - { + function encodeSequentialSwap( + address executor, + bytes memory protocolData + ) internal pure returns (bytes memory) { return abi.encodePacked(executor, protocolData); } @@ -230,9 +264,14 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { address executor, bytes memory protocolData ) internal pure returns (bytes memory) { - return abi.encodePacked( - tokenInIndex, tokenOutIndex, split, executor, protocolData - ); + return + abi.encodePacked( + tokenInIndex, + tokenOutIndex, + split, + executor, + protocolData + ); } function encodeUniswapV2Swap( @@ -255,15 +294,16 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { RestrictTransferFrom.TransferType transferType ) internal view returns (bytes memory) { IUniswapV3Pool pool = IUniswapV3Pool(target); - return abi.encodePacked( - tokenIn, - tokenOut, - pool.fee(), - receiver, - target, - zero2one, - transferType - ); + return + abi.encodePacked( + tokenIn, + tokenOut, + pool.fee(), + receiver, + target, + zero2one, + transferType + ); } function encodeSlipstreamsSwap( @@ -275,14 +315,15 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { RestrictTransferFrom.TransferType transferType ) internal view returns (bytes memory) { IUniswapV3Pool pool = IUniswapV3Pool(target); - return abi.encodePacked( - tokenIn, - tokenOut, - pool.tickSpacing(), - receiver, - target, - zero2one, - transferType - ); + return + abi.encodePacked( + tokenIn, + tokenOut, + pool.tickSpacing(), + receiver, + target, + zero2one, + transferType + ); } } diff --git a/foundry/test/assets/calldata.txt b/foundry/test/assets/calldata.txt index a8a89e6c..08f1de98 100644 --- a/foundry/test/assets/calldata.txt +++ b/foundry/test/assets/calldata.txt @@ -64,3 +64,5 @@ test_ekubo_v3_encode_swap_multi:01cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000 test_single_encoding_strategy_ekubo_v3:5c4b639c0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000007113aa49bac059d709dd0a18d6bb63290076a702d702cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48517e506700271aea091b02f42756f5e174af5230000000000000000000000000000000000000000000000000000000 test_single_ekubo_v3_grouped_swap:5c4b639c00000000000000000000000000000000000000000000000000000002540be400000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a513aa49bac059d709dd0a18d6bb63290076a702d700cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2dac17f958d2ee523a2206206994597c13d831ec7a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000a7c5ac471b48800000320000000000000000000000000000000000000000517e506700271aea091b02f42756f5e174af5230000000000000000000000000000000000000000000000000000000000000000000000000000000 test_single_encoding_strategy_liquorice:5c4b639c00000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000c00000000000000000000000000000000000000000000000000000000b2d05e0001cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2deadbeef1234567890abcdef1234567890abcdef1234567890abcdef1234567890000000000000000000000000 +test_single_encoding_strategy_liquorice_settle:5c4b639c00000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a1000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000637db25a7b768311de128bbda7b8426c3f9c74f3240a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2002000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0001d2068e04cf586f76eece7ba5beb779d7bb1474a1cba673a700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000448633eb8b0a42efed924c42069e0dcf08fb552000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a10000000000000000000000006bc529dc7b81a031828ddce2bc419d01ff268c66000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f6870580000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000416e4084d38e2d4e334057a124ce1ed667947b4fe6a0b44d6cbd62baa5dd384ff93eba5c03f8ea1f4fec128c1c76ce49eb1aad71f053adf3a4d860ef9fe973374d1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000 +test_single_encoding_strategy_liquorice_settle_single:5c4b639c00000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a1000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000457db25a7b768311de128bbda7b8426c3f9c74f3240a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2006000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0001d2068e04cf586f76eece7ba5beb779d7bb1474a19935c86800000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a10000000000000000000000006bc529dc7b81a031828ddce2bc419d01ff268c66000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f6870580000000000000000000000000000000000000000000000000000000000000001310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000419ab2af25941edb594c4c33388649c35f7bbc545ce0a745f9e6272c0ea0e8b2517939f2747b980419ca5f22129742f65755464928ac9592aff9d16ac4446b18df1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000 diff --git a/foundry/test/protocols/Liquorice.t.sol b/foundry/test/protocols/Liquorice.t.sol index 1c4c0c53..289423b9 100644 --- a/foundry/test/protocols/Liquorice.t.sol +++ b/foundry/test/protocols/Liquorice.t.sol @@ -59,8 +59,6 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { LiquoriceExecutorExposed liquoriceExecutor; address constant AUTH_MANAGER = 0x000438801500c89E225E8D6CB69D9c14dD05e000; - address constant LIQUORICE_SETTLEMENT = - 0x0448633eb8B0A42EfED924C42069E0DcF08fb552; IERC20 WETH = IERC20(WETH_ADDR); IERC20 USDC = IERC20(USDC_ADDR); @@ -412,3 +410,93 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { ); } } + +contract TychoRouterForLiquoriceTest is TychoRouterTestSetup { + using SafeERC20 for IERC20; + + address constant AUTH_MANAGER = 0x000438801500c89E225E8D6CB69D9c14dD05e000; + address constant MAKER = 0x06465bcEEaef280Bb7340A58D75dfc5E1F687058; + + // Override the fork block for Liquorice tests + function getForkBlock() public pure override returns (uint256) { + return 24392845; + } + + function setUp() public override { + super.setUp(); + + // Whitelist executor as solver and MAKER + ILiquoriceSettlement settlement = ILiquoriceSettlement( + LIQUORICE_SETTLEMENT + ); + IAllowListAuthentication authenticator = IAllowListAuthentication( + settlement.AUTHENTICATOR() + ); + + vm.prank(AUTH_MANAGER); + authenticator.addSolver(address(tychoRouter)); + vm.prank(AUTH_MANAGER); + authenticator.addMaker(MAKER); + + // MAKER approves balance manager + address balanceManager = settlement.BALANCE_MANAGER(); + vm.prank(MAKER); + IERC20(WETH_ADDR).approve(balanceManager, type(uint256).max); + } + + function testSettleSingleLiquoriceIntegration() public { + // 3000 USDC -> 1 WETH using settleSingleOrder + address user = 0xd2068e04Cf586f76EEcE7BA5bEB779D7bB1474A1; + deal(USDC_ADDR, user, 3000e6); + deal(WETH_ADDR, MAKER, 1 ether); + uint256 expAmountOut = 1 ether; + + uint256 wethBefore = IERC20(WETH_ADDR).balanceOf(user); + vm.startPrank(user); + IERC20(USDC_ADDR).approve(tychoRouterAddr, type(uint256).max); + + bytes memory callData = loadCallDataFromFile( + "test_single_encoding_strategy_liquorice_settle_single" + ); + + (bool success, ) = tychoRouterAddr.call(callData); + + assertTrue(success, "Call Failed"); + uint256 wethReceived = IERC20(WETH_ADDR).balanceOf(user) - wethBefore; + assertEq(wethReceived, expAmountOut, "Incorrect WETH received"); + assertEq( + IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), + 0, + "USDC left in router" + ); + vm.stopPrank(); + } + + function testSettleLiquoriceIntegration() public { + // 3000 USDC -> 1 WETH using settle + address user = 0xd2068e04Cf586f76EEcE7BA5bEB779D7bB1474A1; + deal(USDC_ADDR, user, 3000e6); + deal(WETH_ADDR, MAKER, 1 ether); + uint256 expAmountOut = 1 ether; + + uint256 wethBefore = IERC20(WETH_ADDR).balanceOf(user); + vm.startPrank(user); + IERC20(USDC_ADDR).approve(tychoRouterAddr, type(uint256).max); + + bytes memory callData = loadCallDataFromFile( + "test_single_encoding_strategy_liquorice_settle" + ); + + (bool success, ) = tychoRouterAddr.call(callData); + + assertTrue(success, "Call Failed"); + uint256 wethReceived = IERC20(WETH_ADDR).balanceOf(user) - wethBefore; + assertEq(wethReceived, expAmountOut, "Incorrect WETH received"); + assertEq( + IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), + 0, + "USDC left in router" + ); + vm.stopPrank(); + } +} diff --git a/tests/protocol_integration_tests.rs b/tests/protocol_integration_tests.rs index 74f77b93..80fa4dde 100644 --- a/tests/protocol_integration_tests.rs +++ b/tests/protocol_integration_tests.rs @@ -1896,30 +1896,34 @@ fn test_single_swap_with_univ4_angstrom() { } #[test] -fn test_single_encoding_strategy_liquorice() { - // Note: This test does not assert anything. It is only used to obtain - // integration test data for our router solidity test. +fn test_single_encoding_strategy_liquorice_settle_single() { + // Note: This test generates calldata for the TychoRouterForLiquoriceTest Solidity integration + // test. // - // Performs a swap from USDC to WETH using Liquorice RFQ + // Performs a swap from USDC to WETH using Liquorice RFQ settleSingleOrder + // Uses real calldata captured at block 24,392,845 // // USDC ───(Liquorice RFQ)──> WETH + let _router = Bytes::from_str("0x6bc529DC7B81A03O1828dDCE2BC419d01FF268C66").unwrap(); + let user = Bytes::from_str("0xd2068e04cf586f76eece7ba5beb779d7bb1474a1").unwrap(); let usdc = usdc(); let weth = weth(); - // USDC -> WETH via Liquorice RFQ using mock order data + // 3000 USDC -> 1 WETH via Liquorice RFQ let quote_amount_out = BigUint::from_str("1000000000000000000").unwrap(); // 1 WETH - // Mock calldata for Liquorice settlement - let mock_calldata = Bytes::from( - hex::decode("deadbeef1234567890abcdef1234567890abcdef1234567890abcdef1234567890") + // Real calldata for Liquorice settleSingleOrder (selector 0x9935c868) + // Captured from testSettleSingle() in Liquorice.t.sol at block 24,392,845 + let liquorice_calldata = Bytes::from( + hex::decode("9935c86800000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a10000000000000000000000006bc529dc7b81a031828ddce2bc419d01ff268c66000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f6870580000000000000000000000000000000000000000000000000000000000000001310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000419ab2af25941edb594c4c33388649c35f7bbc545ce0a745f9e6272c0ea0e8b2517939f2747b980419ca5f22129742f65755464928ac9592aff9d16ac4446b18df1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000") .unwrap(), ); let liquorice_state = MockRFQState { quote_amount_out, quote_data: HashMap::from([ - ("calldata".to_string(), mock_calldata), + ("calldata".to_string(), liquorice_calldata), ( "base_token_amount".to_string(), Bytes::from( @@ -1930,7 +1934,15 @@ fn test_single_encoding_strategy_liquorice() { ), ( "partial_fill_offset".to_string(), - Bytes::from(vec![12u8]), // offset = 12 + Bytes::from(vec![96u8]), // offset = 96 for settleSingleOrder + ), + ( + "min_base_token_amount".to_string(), + Bytes::from( + biguint_to_u256(&BigUint::from(3000000000_u64)) + .to_be_bytes::<32>() + .to_vec(), + ), ), ]), }; @@ -1953,8 +1965,108 @@ fn test_single_encoding_strategy_liquorice() { given_amount: BigUint::from_str("3000000000").unwrap(), checked_token: weth, checked_amount: BigUint::from_str("1000000000000000000").unwrap(), - sender: alice_address(), - receiver: alice_address(), + sender: user.clone(), + receiver: user, + swaps: vec![swap_usdc_weth], + ..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, + ð(), + None, + ) + .unwrap() + .data; + + let hex_calldata = encode(&calldata); + write_calldata_to_file( + "test_single_encoding_strategy_liquorice_settle_single", + hex_calldata.as_str(), + ); +} + +#[test] +fn test_single_encoding_strategy_liquorice_settle() { + // Note: This test generates calldata for the TychoRouterForLiquoriceTest Solidity integration + // test. + // + // Performs a swap from USDC to WETH using Liquorice RFQ settle + // Uses real calldata captured at block 24,392,845 + // + // USDC ───(Liquorice RFQ)──> WETH + + let _router = Bytes::from_str("0x6bc529DC7B81A031828dDCE2BC419d01FF268C66").unwrap(); + let user = Bytes::from_str("0xd2068e04cf586f76eece7ba5beb779d7bb1474a1").unwrap(); + + let usdc = usdc(); + let weth = weth(); + + // 3000 USDC -> 1 WETH via Liquorice RFQ + let quote_amount_out = BigUint::from_str("1000000000000000000").unwrap(); // 1 WETH + + // Real calldata for Liquorice settle (selector 0xcba673a7) + // Captured from testSettle() in Liquorice.t.sol at block 24,392,845 + let liquorice_calldata = Bytes::from( + hex::decode("cba673a700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000448633eb8b0a42efed924c42069e0dcf08fb552000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a10000000000000000000000006bc529dc7b81a031828ddce2bc419d01ff268c66000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f6870580000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000416e4084d38e2d4e334057a124ce1ed667947b4fe6a0b44d6cbd62baa5dd384ff93eba5c03f8ea1f4fec128c1c76ce49eb1aad71f053adf3a4d860ef9fe973374d1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000") + .unwrap(), + ); + + let liquorice_state = MockRFQState { + quote_amount_out, + quote_data: HashMap::from([ + ("calldata".to_string(), liquorice_calldata), + ( + "base_token_amount".to_string(), + Bytes::from( + biguint_to_u256(&BigUint::from(3000000000_u64)) + .to_be_bytes::<32>() + .to_vec(), + ), + ), + ( + "partial_fill_offset".to_string(), + Bytes::from(vec![32u8]), // offset = 32 for settle + ), + ( + "min_base_token_amount".to_string(), + Bytes::from( + biguint_to_u256(&BigUint::from(3000000000_u64)) + .to_be_bytes::<32>() + .to_vec(), + ), + ), + ]), + }; + + let liquorice_component = ProtocolComponent { + id: String::from("liquorice-rfq"), + protocol_system: String::from("rfq:liquorice"), + ..Default::default() + }; + + let swap_usdc_weth = Swap::new(liquorice_component, usdc.clone(), weth.clone()) + .estimated_amount_in(BigUint::from_str("3000000000").unwrap()) + .protocol_state(Arc::new(liquorice_state)); + + let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom); + + let solution = Solution { + exact_out: false, + given_token: usdc, + given_amount: BigUint::from_str("3000000000").unwrap(), + checked_token: weth, + checked_amount: BigUint::from_str("1000000000000000000").unwrap(), + sender: user.clone(), + receiver: user, swaps: vec![swap_usdc_weth], ..Default::default() }; @@ -1976,5 +2088,5 @@ fn test_single_encoding_strategy_liquorice() { .data; let hex_calldata = encode(&calldata); - write_calldata_to_file("test_single_encoding_strategy_liquorice", hex_calldata.as_str()); + write_calldata_to_file("test_single_encoding_strategy_liquorice_settle", hex_calldata.as_str()); } From 918426e4bdfb04c953929ac3f450119909848cbc Mon Sep 17 00:00:00 2001 From: Igor Markin Date: Mon, 9 Feb 2026 18:15:08 +0000 Subject: [PATCH 06/15] Code review fixes --- config/protocol_specific_addresses.json | 2 +- foundry/foundry.lock | 35 ----------- foundry/src/executors/LiquoriceExecutor.sol | 26 ++++---- foundry/test/assets/calldata.txt | 4 +- foundry/test/protocols/Liquorice.t.sol | 14 ++--- src/encoding/evm/swap_encoder/liquorice.rs | 67 ++++++++++----------- tests/protocol_integration_tests.rs | 2 - 7 files changed, 54 insertions(+), 96 deletions(-) delete mode 100644 foundry/foundry.lock diff --git a/config/protocol_specific_addresses.json b/config/protocol_specific_addresses.json index 25a92f8b..9b58e655 100644 --- a/config/protocol_specific_addresses.json +++ b/config/protocol_specific_addresses.json @@ -17,7 +17,7 @@ "hashflow_router_address": "0x55084eE0fEf03f14a305cd24286359A35D735151" }, "rfq:liquorice": { - "balance_manager_address": "0x0000000000000000000000000000000000000000" + "balance_manager_address": "0xb87bAE43a665EB5943A5642F81B26666bC9E5C95" }, "lido": { "st_eth_address": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", diff --git a/foundry/foundry.lock b/foundry/foundry.lock deleted file mode 100644 index 98495f0a..00000000 --- a/foundry/foundry.lock +++ /dev/null @@ -1,35 +0,0 @@ -{ - "lib/balancer-v2-monorepo": { - "rev": "36d282374b457dddea828be7884ee0d185db06ba" - }, - "lib/balancer-v3-monorepo": { - "rev": "84cfb0d9d0bd024f92c7a95c64ab7ab8b4c12f4b" - }, - "lib/ekubo-v3": { - "rev": "81c2c2642afe321e7f5d7de70f8b3be18f6f80b3" - }, - "lib/forge-std": { - "rev": "1801b0541f4fda118a10798fd3486bb7051c5dd6" - }, - "lib/openzeppelin-contracts": { - "rev": "acd4ff74de833399287ed6b31b4debf6b2b35527" - }, - "lib/permit2": { - "rev": "cc56ad0f3439c502c246fc5cfcc3db92bb8b7219" - }, - "lib/solady": { - "rev": "c9e079c0ca836dcc52777a1fa7227ef28e3537b3" - }, - "lib/v2-core": { - "rev": "4dd59067c76dea4a0e8e4bfdda41877a6b16dedc" - }, - "lib/v3-core": { - "rev": "d8b1c635c275d2a9450bd6a78f3fa2484fef73eb" - }, - "lib/v4-core": { - "rev": "59d3ecf53afa9264a16bba0e38f4c5d2231f80bc" - }, - "lib/v4-periphery": { - "rev": "ad04c9f24a170accf5ea1b2836bbafd514537ca6" - } -} \ No newline at end of file diff --git a/foundry/src/executors/LiquoriceExecutor.sol b/foundry/src/executors/LiquoriceExecutor.sol index 40919604..fa96ca12 100644 --- a/foundry/src/executors/LiquoriceExecutor.sol +++ b/foundry/src/executors/LiquoriceExecutor.sol @@ -59,7 +59,7 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { address tokenIn, address tokenOut, TransferType transferType, - uint8 partialFillOffset, + uint32 partialFillOffset, uint256 originalBaseTokenAmount, uint256 minBaseTokenAmount, bool approvalNeeded, @@ -117,7 +117,7 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { address tokenIn, address tokenOut, TransferType transferType, - uint8 partialFillOffset, + uint32 partialFillOffset, uint256 originalBaseTokenAmount, uint256 minBaseTokenAmount, bool approvalNeeded, @@ -126,20 +126,20 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { ) { // Minimum fixed fields: - // tokenIn (20) + tokenOut (20) + transferType (1) + partialFillOffset (1) + + // tokenIn (20) + tokenOut (20) + transferType (1) + partialFillOffset (4) + // originalBaseTokenAmount (32) + minBaseTokenAmount (32) + - // approvalNeeded (1) + receiver (20) = 127 bytes - if (data.length < 127) revert LiquoriceExecutor__InvalidDataLength(); + // approvalNeeded (1) + receiver (20) = 130 bytes + if (data.length < 130) revert LiquoriceExecutor__InvalidDataLength(); tokenIn = address(bytes20(data[0:20])); tokenOut = address(bytes20(data[20:40])); transferType = TransferType(uint8(data[40])); - partialFillOffset = uint8(data[41]); - originalBaseTokenAmount = uint256(bytes32(data[42:74])); - minBaseTokenAmount = uint256(bytes32(data[74:106])); - approvalNeeded = data[106] != 0; - receiver = address(bytes20(data[107:127])); - liquoriceCalldata = data[127:]; + partialFillOffset = uint32(bytes4(data[41:45])); + originalBaseTokenAmount = uint256(bytes32(data[45:77])); + minBaseTokenAmount = uint256(bytes32(data[77:109])); + approvalNeeded = data[109] != 0; + receiver = address(bytes20(data[110:130])); + liquoriceCalldata = data[130:]; } /// @dev Clamps the given amount to be within the valid range for the quote @@ -156,7 +156,7 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { if (givenAmount < minBaseTokenAmount) { revert LiquoriceExecutor__AmountBelowMinimum(); } - // It is possible to have a quote with lesser amount than was requested + // It is possible to have a quote with a smaller amount than was requested if (givenAmount > originalBaseTokenAmount) { return originalBaseTokenAmount; } @@ -171,7 +171,7 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { function _modifyFilledTakerAmount( bytes memory liquoriceCalldata, uint256 givenAmount, - uint8 partialFillOffset + uint32 partialFillOffset ) internal pure returns (bytes memory) { // Use the offset from Liquorice API to locate the fill amount // Position = 4 bytes (selector) + offset bytes diff --git a/foundry/test/assets/calldata.txt b/foundry/test/assets/calldata.txt index 08f1de98..7f9ef71f 100644 --- a/foundry/test/assets/calldata.txt +++ b/foundry/test/assets/calldata.txt @@ -64,5 +64,5 @@ test_ekubo_v3_encode_swap_multi:01cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000 test_single_encoding_strategy_ekubo_v3:5c4b639c0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000007113aa49bac059d709dd0a18d6bb63290076a702d702cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48517e506700271aea091b02f42756f5e174af5230000000000000000000000000000000000000000000000000000000 test_single_ekubo_v3_grouped_swap:5c4b639c00000000000000000000000000000000000000000000000000000002540be400000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a513aa49bac059d709dd0a18d6bb63290076a702d700cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2dac17f958d2ee523a2206206994597c13d831ec7a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000a7c5ac471b48800000320000000000000000000000000000000000000000517e506700271aea091b02f42756f5e174af5230000000000000000000000000000000000000000000000000000000000000000000000000000000 test_single_encoding_strategy_liquorice:5c4b639c00000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000c00000000000000000000000000000000000000000000000000000000b2d05e0001cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2deadbeef1234567890abcdef1234567890abcdef1234567890abcdef1234567890000000000000000000000000 -test_single_encoding_strategy_liquorice_settle:5c4b639c00000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a1000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000637db25a7b768311de128bbda7b8426c3f9c74f3240a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2002000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0001d2068e04cf586f76eece7ba5beb779d7bb1474a1cba673a700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000448633eb8b0a42efed924c42069e0dcf08fb552000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a10000000000000000000000006bc529dc7b81a031828ddce2bc419d01ff268c66000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f6870580000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000416e4084d38e2d4e334057a124ce1ed667947b4fe6a0b44d6cbd62baa5dd384ff93eba5c03f8ea1f4fec128c1c76ce49eb1aad71f053adf3a4d860ef9fe973374d1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000 -test_single_encoding_strategy_liquorice_settle_single:5c4b639c00000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a1000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000457db25a7b768311de128bbda7b8426c3f9c74f3240a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2006000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0001d2068e04cf586f76eece7ba5beb779d7bb1474a19935c86800000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a10000000000000000000000006bc529dc7b81a031828ddce2bc419d01ff268c66000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f6870580000000000000000000000000000000000000000000000000000000000000001310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000419ab2af25941edb594c4c33388649c35f7bbc545ce0a745f9e6272c0ea0e8b2517939f2747b980419ca5f22129742f65755464928ac9592aff9d16ac4446b18df1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000 +test_single_encoding_strategy_liquorice_settle:5c4b639c00000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000063adb25a7b768311de128bbda7b8426c3f9c74f3240a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000002000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0001d2068e04cf586f76eece7ba5beb779d7bb1474a1cba673a700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000448633eb8b0a42efed924c42069e0dcf08fb552000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a10000000000000000000000006bc529dc7b81a031828ddce2bc419d01ff268c66000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f6870580000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000416e4084d38e2d4e334057a124ce1ed667947b4fe6a0b44d6cbd62baa5dd384ff93eba5c03f8ea1f4fec128c1c76ce49eb1aad71f053adf3a4d860ef9fe973374d1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000 +test_single_encoding_strategy_liquorice_settle_single:5c4b639c00000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000045adb25a7b768311de128bbda7b8426c3f9c74f3240a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000006000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0001d2068e04cf586f76eece7ba5beb779d7bb1474a19935c86800000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a10000000000000000000000006bc529dc7b81a031828ddce2bc419d01ff268c66000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f6870580000000000000000000000000000000000000000000000000000000000000001310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000419ab2af25941edb594c4c33388649c35f7bbc545ce0a745f9e6272c0ea0e8b2517939f2747b980419ca5f22129742f65755464928ac9592aff9d16ac4446b18df1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000 diff --git a/foundry/test/protocols/Liquorice.t.sol b/foundry/test/protocols/Liquorice.t.sol index 289423b9..33095197 100644 --- a/foundry/test/protocols/Liquorice.t.sol +++ b/foundry/test/protocols/Liquorice.t.sol @@ -26,7 +26,7 @@ contract LiquoriceExecutorExposed is LiquoriceExecutor { address tokenIn, address tokenOut, TransferType transferType, - uint8 partialFillOffset, + uint32 partialFillOffset, uint256 originalBaseTokenAmount, uint256 minBaseTokenAmount, bool approvalNeeded, @@ -100,7 +100,7 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { RestrictTransferFrom.TransferType transferType = RestrictTransferFrom .TransferType .None; - uint8 partialFillOffset = 96; // PARTIAL_FILL_OFFSET_SETTLE_SINGLE_ORDER + uint32 partialFillOffset = 96; // PARTIAL_FILL_OFFSET_SETTLE_SINGLE_ORDER uint256 amountIn = 3000e6; // 3000 USDC bool approvalNeeded = true; uint256 expectedAmountOut = 1 ether; @@ -152,7 +152,7 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { RestrictTransferFrom.TransferType transferType = RestrictTransferFrom .TransferType .None; - uint8 partialFillOffset = 96; // PARTIAL_FILL_OFFSET_SETTLE_SINGLE_ORDER + uint32 partialFillOffset = 96; // PARTIAL_FILL_OFFSET_SETTLE_SINGLE_ORDER uint256 originalAmountIn = 3000e6; // Original quote: 3000 USDC uint256 amountIn = 1500e6; // Partial fill: 1500 USDC (50%) uint256 minAmountIn = 1500e6; // Minimum allowed @@ -206,7 +206,7 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { RestrictTransferFrom.TransferType transferType = RestrictTransferFrom .TransferType .None; - uint8 partialFillOffset = 32; // PARTIAL_FILL_OFFSET_SETTLE_ORDER + uint32 partialFillOffset = 32; // PARTIAL_FILL_OFFSET_SETTLE_ORDER uint256 amountIn = 3000e6; // 3000 USDC bool approvalNeeded = true; uint256 expectedAmountOut = 1 ether; @@ -258,7 +258,7 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { RestrictTransferFrom.TransferType transferType = RestrictTransferFrom .TransferType .None; - uint8 partialFillOffset = 32; // PARTIAL_FILL_OFFSET_SETTLE_ORDER + uint32 partialFillOffset = 32; // PARTIAL_FILL_OFFSET_SETTLE_ORDER uint256 originalAmountIn = 3000e6; // Original quote: 3000 USDC uint256 amountIn = 1500e6; // Partial fill: 1500 USDC (50%) uint256 minAmountIn = 1500e6; // Minimum allowed @@ -316,7 +316,7 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { USDC_ADDR, // tokenIn (20 bytes) WETH_ADDR, // tokenOut (20 bytes) uint8(RestrictTransferFrom.TransferType.Transfer), // transferType (1 byte) - uint8(5), // partialFillOffset (1 byte) + uint32(5), // partialFillOffset (4 bytes) originalAmount, // originalBaseTokenAmount (32 bytes) minAmount, // minBaseTokenAmount (32 bytes) uint8(0), // approvalNeeded (1 byte) - false @@ -328,7 +328,7 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { address decodedTokenIn, address decodedTokenOut, RestrictTransferFrom.TransferType decodedTransferType, - uint8 decodedPartialFillOffset, + uint32 decodedPartialFillOffset, uint256 decodedOriginalAmount, uint256 decodedMinAmount, bool decodedApprovalNeeded, diff --git a/src/encoding/evm/swap_encoder/liquorice.rs b/src/encoding/evm/swap_encoder/liquorice.rs index 0e6bbc55..a36e0c49 100644 --- a/src/encoding/evm/swap_encoder/liquorice.rs +++ b/src/encoding/evm/swap_encoder/liquorice.rs @@ -28,12 +28,10 @@ use crate::encoding::{ /// # Fields /// * `executor_address` - The address of the executor contract that will perform the swap. /// * `balance_manager_address` - The address of the Liquorice balance manager contract. -/// * `native_token_address` - The chain's native token address. #[derive(Clone)] pub struct LiquoriceSwapEncoder { executor_address: Bytes, balance_manager_address: Bytes, - native_token_address: Bytes, runtime_handle: Handle, #[allow(dead_code)] runtime: Option>, @@ -42,7 +40,7 @@ pub struct LiquoriceSwapEncoder { impl SwapEncoder for LiquoriceSwapEncoder { fn new( executor_address: Bytes, - chain: Chain, + _chain: Chain, config: Option>, ) -> Result { let config = config.ok_or(EncodingError::FatalError( @@ -61,15 +59,9 @@ impl SwapEncoder for LiquoriceSwapEncoder { "Missing liquorice balance manager address in config".to_string(), )) .flatten()?; - let native_token_address = chain.native_token().address; + let (runtime_handle, runtime) = get_runtime()?; - Ok(Self { - executor_address, - balance_manager_address, - native_token_address, - runtime_handle, - runtime, - }) + Ok(Self { executor_address, balance_manager_address, runtime_handle, runtime }) } fn encode_swap( @@ -95,7 +87,7 @@ impl SwapEncoder for LiquoriceSwapEncoder { "Estimated amount in is mandatory for a Liquorice swap".to_string(), ))?; - let sender = encoding_context + let router_address = encoding_context .router_address .clone() .ok_or(EncodingError::FatalError( @@ -106,7 +98,7 @@ impl SwapEncoder for LiquoriceSwapEncoder { amount_in: estimated_amount_in.clone(), token_in: swap.token_in().clone(), token_out: swap.token_out().clone(), - sender, + sender: router_address.clone(), receiver: encoding_context.receiver.clone(), }; @@ -136,16 +128,23 @@ impl SwapEncoder for LiquoriceSwapEncoder { // Get partial fill offset (defaults to 0 if not present, meaning partial fill is not // available for the quote) - let partial_fill_offset: u8 = signed_quote + let partial_fill_offset: Vec = signed_quote .quote_attributes .get("partial_fill_offset") .map(|b| { - // Take the last byte (u8 value) - b.last().copied().unwrap_or(0) + if b.len() == 4 { + b.to_vec() + } else { + // Pad to 4 bytes if needed + let mut padded = vec![0u8; 4]; + let start = 4 - b.len(); + padded[start..].copy_from_slice(b); + padded + } }) - .unwrap_or(0); + .unwrap_or(vec![0u8; 4]); - // Get min base token amount (defaults to original base token amount if partiall fill is not + // Get min base token amount (defaults to original base token amount if partial fill is not // available for the quote) let min_base_token_amount = signed_quote .quote_attributes @@ -173,18 +172,14 @@ impl SwapEncoder for LiquoriceSwapEncoder { padded }; - // Check if approval is needed from executor to balance manager - let approval_needed = if *swap.token_in() == self.native_token_address { - false - } else { - let executor_address = bytes_to_address(&self.executor_address)?; - let balance_manager_address = Address::from_slice(&self.balance_manager_address); - ProtocolApprovalsManager::new()?.approval_needed( - token_in, - executor_address, - balance_manager_address, - )? - }; + // Check if approval is needed from Router to balance manager + let router_address = bytes_to_address(&router_address)?; + let balance_manager_address = Address::from_slice(&self.balance_manager_address); + let approval_needed = ProtocolApprovalsManager::new()?.approval_needed( + token_in, + router_address, + balance_manager_address, + )?; let receiver = bytes_to_address(&encoding_context.receiver)?; @@ -197,7 +192,7 @@ impl SwapEncoder for LiquoriceSwapEncoder { encoded.extend_from_slice(token_in.as_slice()); // 20 bytes encoded.extend_from_slice(token_out.as_slice()); // 20 bytes encoded.push(encoding_context.transfer_type as u8); // 1 byte - encoded.push(partial_fill_offset); // 1 byte + encoded.extend_from_slice(&partial_fill_offset); // 4 bytes encoded.extend_from_slice(&original_base_token_amount); // 32 bytes encoded.extend_from_slice(&min_base_token_amount); // 32 bytes encoded.push(approval_needed as u8); // 1 byte @@ -337,7 +332,7 @@ mod tests { let hex_swap = encode(&encoded_swap); // Expected format: - // token_in (20) | token_out (20) | transfer_type (1) | partial_fill_offset (1) | + // token_in (20) | token_out (20) | transfer_type (1) | partial_fill_offset (4) | // original_base_token_amount (32) | min_base_token_amount (32) | // approval_needed (1) | receiver (20) | calldata (variable) let expected_swap = String::from(concat!( @@ -348,7 +343,7 @@ mod tests { // transfer_type "01", // partial_fill_offset - "0c", + "0000000c", // original_base_token_amount (3000000000 as U256) "00000000000000000000000000000000000000000000000000000000b2d05e00", // min_base_token_amount (2500000000 as U256) @@ -417,8 +412,8 @@ mod tests { .encode_swap(&swap, &encoding_context) .unwrap(); - // Verify approval_needed byte at position 106 - // (20 + 20 + 1 + 1 + 32 + 32 = 106) - assert_eq!(encoded_swap[106], 1); // approval needed (new executor, no prior approval) + // Verify approval_needed byte at position 109 + // (20 + 20 + 1 + 4 + 32 + 32 = 109) + assert_eq!(encoded_swap[109], 1); // approval needed (new executor, no prior approval) } } diff --git a/tests/protocol_integration_tests.rs b/tests/protocol_integration_tests.rs index 80fa4dde..679804c4 100644 --- a/tests/protocol_integration_tests.rs +++ b/tests/protocol_integration_tests.rs @@ -1904,7 +1904,6 @@ fn test_single_encoding_strategy_liquorice_settle_single() { // Uses real calldata captured at block 24,392,845 // // USDC ───(Liquorice RFQ)──> WETH - let _router = Bytes::from_str("0x6bc529DC7B81A03O1828dDCE2BC419d01FF268C66").unwrap(); let user = Bytes::from_str("0xd2068e04cf586f76eece7ba5beb779d7bb1474a1").unwrap(); let usdc = usdc(); @@ -2004,7 +2003,6 @@ fn test_single_encoding_strategy_liquorice_settle() { // // USDC ───(Liquorice RFQ)──> WETH - let _router = Bytes::from_str("0x6bc529DC7B81A031828dDCE2BC419d01FF268C66").unwrap(); let user = Bytes::from_str("0xd2068e04cf586f76eece7ba5beb779d7bb1474a1").unwrap(); let usdc = usdc(); From 2b0e44f93997fb91b842617ec8739086c9d4b0e5 Mon Sep 17 00:00:00 2001 From: Igor Markin Date: Mon, 9 Feb 2026 18:32:35 +0000 Subject: [PATCH 07/15] Fix encoder test --- src/encoding/evm/swap_encoder/liquorice.rs | 101 +-------------------- 1 file changed, 2 insertions(+), 99 deletions(-) diff --git a/src/encoding/evm/swap_encoder/liquorice.rs b/src/encoding/evm/swap_encoder/liquorice.rs index a36e0c49..9165a72e 100644 --- a/src/encoding/evm/swap_encoder/liquorice.rs +++ b/src/encoding/evm/swap_encoder/liquorice.rs @@ -233,46 +233,10 @@ mod tests { fn liquorice_config() -> Option> { Some(HashMap::from([( "balance_manager_address".to_string(), - "0x71D9750ECF0c5081FAE4E3EDC4253E52024b0B59".to_string(), + "0xb87bAE43a665EB5943A5642F81B26666bC9E5C95".to_string(), )])) } - #[test] - fn test_encode_liquorice_single_fails_without_protocol_data() { - let liquorice_component = ProtocolComponent { - id: String::from("liquorice-rfq"), - protocol_system: String::from("rfq:liquorice"), - ..Default::default() - }; - - let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC - let token_out = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); // WETH - - let swap = Swap::new(liquorice_component, token_in.clone(), token_out.clone()) - .estimated_amount_in(BigUint::from_str("3000000000").unwrap()); - - let encoding_context = EncodingContext { - receiver: Bytes::from("0xc5564C13A157E6240659fb81882A28091add8670"), - exact_out: false, - router_address: Some(Bytes::zero(20)), - group_token_in: token_in.clone(), - group_token_out: token_out.clone(), - transfer_type: TransferType::Transfer, - historical_trade: false, - }; - - let encoder = LiquoriceSwapEncoder::new( - Bytes::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), - Chain::Ethereum, - liquorice_config(), - ) - .unwrap(); - - encoder - .encode_swap(&swap, &encoding_context) - .expect_err("Should return an error if the swap has no protocol state"); - } - #[test] fn test_encode_liquorice_single_with_protocol_state() { // 3000 USDC -> 1 WETH using a mocked RFQ state to get a quote @@ -347,7 +311,7 @@ mod tests { // original_base_token_amount (3000000000 as U256) "00000000000000000000000000000000000000000000000000000000b2d05e00", // min_base_token_amount (2500000000 as U256) - "0000000000000000000000000000000000000000000000000000000094f5f200", + "000000000000000000000000000000000000000000000000000000009502f900", // approval_needed "01", // receiver @@ -355,65 +319,4 @@ mod tests { )); assert_eq!(hex_swap, expected_swap + &liquorice_calldata.to_string()[2..]); } - - #[test] - fn test_encode_liquorice_no_approval_needed() { - // Test with no approval needed (approval byte at position 74 should be 0) - let quote_amount_out = BigUint::from_str("1000000000000000000").unwrap(); - let liquorice_calldata = Bytes::from_str("0xabcdef").unwrap(); - let base_token_amount = biguint_to_u256(&BigUint::from(1000000000_u64)) - .to_be_bytes::<32>() - .to_vec(); - - let liquorice_component = ProtocolComponent { - id: String::from("liquorice-rfq"), - protocol_system: String::from("rfq:liquorice"), - ..Default::default() - }; - - let min_base_token_amount = biguint_to_u256(&BigUint::from(800000000_u64)) - .to_be_bytes::<32>() - .to_vec(); - - let liquorice_state = MockRFQState { - quote_amount_out, - quote_data: HashMap::from([ - ("calldata".to_string(), liquorice_calldata.clone()), - ("base_token_amount".to_string(), Bytes::from(base_token_amount)), - ("min_base_token_amount".to_string(), Bytes::from(min_base_token_amount)), - ]), - }; - - let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); - let token_out = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); - - let swap = Swap::new(liquorice_component, token_in.clone(), token_out.clone()) - .estimated_amount_in(BigUint::from_str("1000000000").unwrap()) - .protocol_state(Arc::new(liquorice_state)); - - let encoding_context = EncodingContext { - receiver: Bytes::from("0xc5564C13A157E6240659fb81882A28091add8670"), - exact_out: false, - router_address: Some(Bytes::zero(20)), - group_token_in: token_in.clone(), - group_token_out: token_out.clone(), - transfer_type: TransferType::Transfer, - historical_trade: false, - }; - - let encoder = LiquoriceSwapEncoder::new( - Bytes::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), - Chain::Ethereum, - liquorice_config(), - ) - .unwrap(); - - let encoded_swap = encoder - .encode_swap(&swap, &encoding_context) - .unwrap(); - - // Verify approval_needed byte at position 109 - // (20 + 20 + 1 + 4 + 32 + 32 = 109) - assert_eq!(encoded_swap[109], 1); // approval needed (new executor, no prior approval) - } } From 552257f9f95753f09fb329fc235fe0c6c51d5366 Mon Sep 17 00:00:00 2001 From: Igor Markin Date: Mon, 9 Feb 2026 18:46:28 +0000 Subject: [PATCH 08/15] Formatting fixes --- foundry/src/executors/LiquoriceExecutor.sol | 46 +++-- foundry/test/TychoRouterTestSetup.sol | 162 +++++++----------- foundry/test/protocols/Liquorice.t.sol | 123 ++++++------- .../evm/swap_encoder/swap_encoder_registry.rs | 8 +- 4 files changed, 139 insertions(+), 200 deletions(-) diff --git a/foundry/src/executors/LiquoriceExecutor.sol b/foundry/src/executors/LiquoriceExecutor.sol index fa96ca12..63336e6a 100644 --- a/foundry/src/executors/LiquoriceExecutor.sol +++ b/foundry/src/executors/LiquoriceExecutor.sol @@ -37,10 +37,9 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { /// @notice The Liquorice settlement contract address address public immutable liquoriceSettlement; - constructor( - address _liquoriceSettlement, - address _permit2 - ) RestrictTransferFrom(_permit2) { + constructor(address _liquoriceSettlement, address _permit2) + RestrictTransferFrom(_permit2) + { if (_liquoriceSettlement == address(0)) { revert LiquoriceExecutor__ZeroAddress(); } @@ -51,10 +50,13 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { /// @param givenAmount The amount of input token to swap /// @param data Encoded swap data containing tokens and liquorice calldata /// @return calculatedAmount The amount of output token received - function swap( - uint256 givenAmount, - bytes calldata data - ) external payable virtual override returns (uint256 calculatedAmount) { + function swap(uint256 givenAmount, bytes calldata data) + external + payable + virtual + override + returns (uint256 calculatedAmount) + { ( address tokenIn, address tokenOut, @@ -77,9 +79,7 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { } givenAmount = _clampAmount( - givenAmount, - originalBaseTokenAmount, - minBaseTokenAmount + givenAmount, originalBaseTokenAmount, minBaseTokenAmount ); // Transfer tokens to executor @@ -90,9 +90,7 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { bytes memory finalCalldata = liquoriceCalldata; if (partialFillOffset > 0 && originalBaseTokenAmount > givenAmount) { finalCalldata = _modifyFilledTakerAmount( - liquoriceCalldata, - givenAmount, - partialFillOffset + liquoriceCalldata, givenAmount, partialFillOffset ); } @@ -108,9 +106,7 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { } /// @dev Decodes the packed calldata - function _decodeData( - bytes calldata data - ) + function _decodeData(bytes calldata data) internal pure returns ( @@ -195,14 +191,14 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { /// @param token The token address, or address(0) for ETH /// @param account The account to get the balance of /// @return 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); + 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 diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 573d3621..4084358d 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -50,10 +50,7 @@ contract TychoRouterExposed is TychoRouter { bool transferFromNeeded ) external { _tstoreTransferFromInfo( - tokenIn, - amountIn, - isPermit2, - transferFromNeeded + tokenIn, amountIn, isPermit2, transferFromNeeded ); } @@ -65,10 +62,10 @@ contract TychoRouterExposed is TychoRouter { return _splitSwap(amountIn, nTokens, swaps); } - function exposedSequentialSwap( - uint256 amountIn, - bytes calldata swaps - ) external returns (uint256) { + function exposedSequentialSwap(uint256 amountIn, bytes calldata swaps) + external + returns (uint256) + { return _sequentialSwap(amountIn, swaps); } } @@ -127,8 +124,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { tychoRouter.grantRole(keccak256("PAUSER_ROLE"), PAUSER); tychoRouter.grantRole(keccak256("UNPAUSER_ROLE"), UNPAUSER); tychoRouter.grantRole( - keccak256("EXECUTOR_SETTER_ROLE"), - EXECUTOR_SETTER + keccak256("EXECUTOR_SETTER_ROLE"), EXECUTOR_SETTER ); return tychoRouter; } @@ -145,52 +141,28 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { address ekuboV2MevResist = 0x553a2EFc570c9e104942cEC6aC1c18118e54C091; IPoolManager poolManager = IPoolManager(poolManagerAddress); - usv2Executor = new UniswapV2Executor( - factoryV2, - initCodeV2, - PERMIT2_ADDRESS, - 30 - ); - usv3Executor = new UniswapV3Executor( - factoryV3, - initCodeV3, - PERMIT2_ADDRESS - ); - usv4Executor = new UniswapV4Executor( - poolManager, - ANGSTROM_HOOK, - PERMIT2_ADDRESS - ); + usv2Executor = + new UniswapV2Executor(factoryV2, initCodeV2, PERMIT2_ADDRESS, 30); + usv3Executor = + new UniswapV3Executor(factoryV3, initCodeV3, PERMIT2_ADDRESS); + usv4Executor = + new UniswapV4Executor(poolManager, ANGSTROM_HOOK, PERMIT2_ADDRESS); pancakev3Executor = new UniswapV3Executor( - factoryPancakeV3, - initCodePancakeV3, - PERMIT2_ADDRESS + factoryPancakeV3, initCodePancakeV3, PERMIT2_ADDRESS ); balancerv2Executor = new BalancerV2Executor(PERMIT2_ADDRESS); - ekuboV2Executor = new EkuboExecutor( - ekuboV2Core, - ekuboV2MevResist, - PERMIT2_ADDRESS - ); - curveExecutor = new CurveExecutor( - ETH_ADDR_FOR_CURVE, - PERMIT2_ADDRESS, - STETH_ADDR - ); - maverickv2Executor = new MaverickV2Executor( - MAVERICK_V2_FACTORY, - PERMIT2_ADDRESS - ); + ekuboV2Executor = + new EkuboExecutor(ekuboV2Core, ekuboV2MevResist, PERMIT2_ADDRESS); + curveExecutor = + new CurveExecutor(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS, STETH_ADDR); + 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 - ); - fluidV1Executor = new FluidV1Executor( - FLUIDV1_LIQUIDITY, - PERMIT2_ADDRESS - ); + hashflowExecutor = + new HashflowExecutor(HASHFLOW_ROUTER, PERMIT2_ADDRESS); + fluidV1Executor = + new FluidV1Executor(FLUIDV1_LIQUIDITY, PERMIT2_ADDRESS); slipstreamsExecutor = new SlipstreamsExecutor( SLIPSTREAMS_FACTORY_BASE, SLIPSTREAMS_NEW_FACTORY_BASE, @@ -198,16 +170,11 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { ); rocketpoolExecutor = new RocketpoolExecutor(PERMIT2_ADDRESS); erc4626Executor = new ERC4626Executor(PERMIT2_ADDRESS); - lidoExecutor = new LidoExecutor( - STETH_ADDR, - WSTETH_ADDR, - PERMIT2_ADDRESS - ); + lidoExecutor = + new LidoExecutor(STETH_ADDR, WSTETH_ADDR, PERMIT2_ADDRESS); ekuboV3Executor = new EkuboV3Executor(PERMIT2_ADDRESS); - liquoriceExecutor = new LiquoriceExecutor( - LIQUORICE_SETTLEMENT, - PERMIT2_ADDRESS - ); + liquoriceExecutor = + new LiquoriceExecutor(LIQUORICE_SETTLEMENT, PERMIT2_ADDRESS); address[] memory executors = new address[](18); executors[0] = address(usv2Executor); @@ -232,9 +199,11 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { return executors; } - function pleEncode( - bytes[] memory data - ) public pure returns (bytes memory encoded) { + function pleEncode(bytes[] memory data) + public + pure + returns (bytes memory encoded) + { for (uint256 i = 0; i < data.length; i++) { encoded = bytes.concat( encoded, @@ -243,17 +212,19 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { } } - function encodeSingleSwap( - address executor, - bytes memory protocolData - ) internal pure returns (bytes memory) { + function encodeSingleSwap(address executor, bytes memory protocolData) + internal + pure + returns (bytes memory) + { return abi.encodePacked(executor, protocolData); } - function encodeSequentialSwap( - address executor, - bytes memory protocolData - ) internal pure returns (bytes memory) { + function encodeSequentialSwap(address executor, bytes memory protocolData) + internal + pure + returns (bytes memory) + { return abi.encodePacked(executor, protocolData); } @@ -264,14 +235,9 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { address executor, bytes memory protocolData ) internal pure returns (bytes memory) { - return - abi.encodePacked( - tokenInIndex, - tokenOutIndex, - split, - executor, - protocolData - ); + return abi.encodePacked( + tokenInIndex, tokenOutIndex, split, executor, protocolData + ); } function encodeUniswapV2Swap( @@ -294,16 +260,15 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { RestrictTransferFrom.TransferType transferType ) internal view returns (bytes memory) { IUniswapV3Pool pool = IUniswapV3Pool(target); - return - abi.encodePacked( - tokenIn, - tokenOut, - pool.fee(), - receiver, - target, - zero2one, - transferType - ); + return abi.encodePacked( + tokenIn, + tokenOut, + pool.fee(), + receiver, + target, + zero2one, + transferType + ); } function encodeSlipstreamsSwap( @@ -315,15 +280,14 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { RestrictTransferFrom.TransferType transferType ) internal view returns (bytes memory) { IUniswapV3Pool pool = IUniswapV3Pool(target); - return - abi.encodePacked( - tokenIn, - tokenOut, - pool.tickSpacing(), - receiver, - target, - zero2one, - transferType - ); + return abi.encodePacked( + tokenIn, + tokenOut, + pool.tickSpacing(), + receiver, + target, + zero2one, + transferType + ); } } diff --git a/foundry/test/protocols/Liquorice.t.sol b/foundry/test/protocols/Liquorice.t.sol index 33095197..9df2ca94 100644 --- a/foundry/test/protocols/Liquorice.t.sol +++ b/foundry/test/protocols/Liquorice.t.sol @@ -12,14 +12,11 @@ import { } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; contract LiquoriceExecutorExposed is LiquoriceExecutor { - constructor( - address _liquoriceSettlement, - address _permit2 - ) LiquoriceExecutor(_liquoriceSettlement, _permit2) {} - - function decodeData( - bytes calldata data - ) + constructor(address _liquoriceSettlement, address _permit2) + LiquoriceExecutor(_liquoriceSettlement, _permit2) + {} + + function decodeData(bytes calldata data) external pure returns ( @@ -42,12 +39,9 @@ contract LiquoriceExecutorExposed is LiquoriceExecutor { uint256 originalBaseTokenAmount, uint256 minBaseTokenAmount ) external pure returns (uint256) { - return - _clampAmount( - givenAmount, - originalBaseTokenAmount, - minBaseTokenAmount - ); + return _clampAmount( + givenAmount, originalBaseTokenAmount, minBaseTokenAmount + ); } } @@ -70,15 +64,12 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { function setUp() public { vm.createSelectFork(vm.rpcUrl("mainnet"), FORK_BLOCK); - liquoriceExecutor = new LiquoriceExecutorExposed( - LIQUORICE_SETTLEMENT, - PERMIT2_ADDRESS - ); + liquoriceExecutor = + new LiquoriceExecutorExposed(LIQUORICE_SETTLEMENT, PERMIT2_ADDRESS); liquoriceSettlement = ILiquoriceSettlement(LIQUORICE_SETTLEMENT); - authenticator = IAllowListAuthentication( - liquoriceSettlement.AUTHENTICATOR() - ); + authenticator = + IAllowListAuthentication(liquoriceSettlement.AUTHENTICATOR()); vm.prank(AUTH_MANAGER); authenticator.addSolver(address(liquoriceExecutor)); @@ -92,14 +83,13 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { function testSettleSingle() public { // 3000 USDC -> 1 WETH - bytes - memory liquoriceCalldata = hex"9935c86800000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f687058000000000000000000000000000000000000000000000000000000000000000131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000041883a6506193307eebda0f3adf2cb81f84a073e030749055ebb18cbf98704eef100a03c307266527d706f9a5c3e08ed0988f5b130bc5327e0ad62dde6f3709d251b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; + bytes memory liquoriceCalldata = + hex"9935c86800000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f687058000000000000000000000000000000000000000000000000000000000000000131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000041883a6506193307eebda0f3adf2cb81f84a073e030749055ebb18cbf98704eef100a03c307266527d706f9a5c3e08ed0988f5b130bc5327e0ad62dde6f3709d251b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; address tokenIn = USDC_ADDR; address tokenOut = WETH_ADDR; - RestrictTransferFrom.TransferType transferType = RestrictTransferFrom - .TransferType - .None; + RestrictTransferFrom.TransferType transferType = + RestrictTransferFrom.TransferType.None; uint32 partialFillOffset = 96; // PARTIAL_FILL_OFFSET_SETTLE_SINGLE_ORDER uint256 amountIn = 3000e6; // 3000 USDC bool approvalNeeded = true; @@ -122,16 +112,15 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { liquoriceCalldata ); - uint256 initialTokenOutBalance = IERC20(tokenOut).balanceOf( - address(liquoriceExecutor) - ); + uint256 initialTokenOutBalance = + IERC20(tokenOut).balanceOf(address(liquoriceExecutor)); uint256 amountOut = liquoriceExecutor.swap(amountIn, params); assertEq(amountOut, expectedAmountOut, "Incorrect amount out"); assertEq( - IERC20(tokenOut).balanceOf(address(liquoriceExecutor)) - - initialTokenOutBalance, + IERC20(tokenOut).balanceOf(address(liquoriceExecutor)) + - initialTokenOutBalance, expectedAmountOut, "WETH should be at receiver" ); @@ -144,14 +133,13 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { function testSettleSingle_PartialFill() public { // 1500 USDC -> 0.5 WETH (50% partial fill of 3000 USDC -> 1 WETH quote) - bytes - memory liquoriceCalldata = hex"9935c86800000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f687058000000000000000000000000000000000000000000000000000000000000000131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000041883a6506193307eebda0f3adf2cb81f84a073e030749055ebb18cbf98704eef100a03c307266527d706f9a5c3e08ed0988f5b130bc5327e0ad62dde6f3709d251b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; + bytes memory liquoriceCalldata = + hex"9935c86800000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f687058000000000000000000000000000000000000000000000000000000000000000131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000041883a6506193307eebda0f3adf2cb81f84a073e030749055ebb18cbf98704eef100a03c307266527d706f9a5c3e08ed0988f5b130bc5327e0ad62dde6f3709d251b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; address tokenIn = USDC_ADDR; address tokenOut = WETH_ADDR; - RestrictTransferFrom.TransferType transferType = RestrictTransferFrom - .TransferType - .None; + RestrictTransferFrom.TransferType transferType = + RestrictTransferFrom.TransferType.None; uint32 partialFillOffset = 96; // PARTIAL_FILL_OFFSET_SETTLE_SINGLE_ORDER uint256 originalAmountIn = 3000e6; // Original quote: 3000 USDC uint256 amountIn = 1500e6; // Partial fill: 1500 USDC (50%) @@ -176,16 +164,15 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { liquoriceCalldata ); - uint256 initialTokenOutBalance = IERC20(tokenOut).balanceOf( - address(liquoriceExecutor) - ); + uint256 initialTokenOutBalance = + IERC20(tokenOut).balanceOf(address(liquoriceExecutor)); uint256 amountOut = liquoriceExecutor.swap(amountIn, params); assertEq(amountOut, expectedAmountOut, "Incorrect partial amount out"); assertEq( - IERC20(tokenOut).balanceOf(address(liquoriceExecutor)) - - initialTokenOutBalance, + IERC20(tokenOut).balanceOf(address(liquoriceExecutor)) + - initialTokenOutBalance, expectedAmountOut, "WETH should be at receiver" ); @@ -198,14 +185,13 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { function testSettle() public { // 3000 USDC -> 1 WETH using settle() function - bytes - memory liquoriceCalldata = hex"cba673a700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000448633eb8b0a42efed924c42069e0dcf08fb5520000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f6870580000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000411d85c337d0e071eb601d8a90e2e8dd0afb61db200a1614c4afe5d26ff0c11bd402e018ab5fdbf8386437d7594af4383cf020a75c96e02c0a208f0b06e86115401b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; + bytes memory liquoriceCalldata = + hex"cba673a700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000448633eb8b0a42efed924c42069e0dcf08fb5520000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f6870580000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000411d85c337d0e071eb601d8a90e2e8dd0afb61db200a1614c4afe5d26ff0c11bd402e018ab5fdbf8386437d7594af4383cf020a75c96e02c0a208f0b06e86115401b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; address tokenIn = USDC_ADDR; address tokenOut = WETH_ADDR; - RestrictTransferFrom.TransferType transferType = RestrictTransferFrom - .TransferType - .None; + RestrictTransferFrom.TransferType transferType = + RestrictTransferFrom.TransferType.None; uint32 partialFillOffset = 32; // PARTIAL_FILL_OFFSET_SETTLE_ORDER uint256 amountIn = 3000e6; // 3000 USDC bool approvalNeeded = true; @@ -228,16 +214,15 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { liquoriceCalldata ); - uint256 initialTokenOutBalance = IERC20(tokenOut).balanceOf( - address(liquoriceExecutor) - ); + uint256 initialTokenOutBalance = + IERC20(tokenOut).balanceOf(address(liquoriceExecutor)); uint256 amountOut = liquoriceExecutor.swap(amountIn, params); assertEq(amountOut, expectedAmountOut, "Incorrect amount out"); assertEq( - IERC20(tokenOut).balanceOf(address(liquoriceExecutor)) - - initialTokenOutBalance, + IERC20(tokenOut).balanceOf(address(liquoriceExecutor)) + - initialTokenOutBalance, expectedAmountOut, "WETH should be at receiver" ); @@ -250,14 +235,13 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { function testSettle_PartialFill() public { // 1500 USDC -> 0.5 WETH (50% partial fill) using settle() function - bytes - memory liquoriceCalldata = hex"cba673a700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f687058000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000448633eb8b0a42efed924c42069e0dcf08fb5520000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f6870580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000041e89ad636a6d749213b9339ac5218229adaa53bdf96d457ee2cebfd4fd02909bf678953c976ceca7307ef2e73c5687c33738a6a1e4130e378d220b68d9c59e18b1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; + bytes memory liquoriceCalldata = + hex"cba673a700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f687058000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000448633eb8b0a42efed924c42069e0dcf08fb5520000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f6870580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000041e89ad636a6d749213b9339ac5218229adaa53bdf96d457ee2cebfd4fd02909bf678953c976ceca7307ef2e73c5687c33738a6a1e4130e378d220b68d9c59e18b1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; address tokenIn = USDC_ADDR; address tokenOut = WETH_ADDR; - RestrictTransferFrom.TransferType transferType = RestrictTransferFrom - .TransferType - .None; + RestrictTransferFrom.TransferType transferType = + RestrictTransferFrom.TransferType.None; uint32 partialFillOffset = 32; // PARTIAL_FILL_OFFSET_SETTLE_ORDER uint256 originalAmountIn = 3000e6; // Original quote: 3000 USDC uint256 amountIn = 1500e6; // Partial fill: 1500 USDC (50%) @@ -282,16 +266,15 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { liquoriceCalldata ); - uint256 initialTokenOutBalance = IERC20(tokenOut).balanceOf( - address(liquoriceExecutor) - ); + uint256 initialTokenOutBalance = + IERC20(tokenOut).balanceOf(address(liquoriceExecutor)); uint256 amountOut = liquoriceExecutor.swap(amountIn, params); assertEq(amountOut, expectedAmountOut, "Incorrect partial amount out"); assertEq( - IERC20(tokenOut).balanceOf(address(liquoriceExecutor)) - - initialTokenOutBalance, + IERC20(tokenOut).balanceOf(address(liquoriceExecutor)) + - initialTokenOutBalance, expectedAmountOut, "WETH should be at receiver" ); @@ -345,9 +328,7 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { ); assertEq(decodedPartialFillOffset, 5, "partialFillOffset mismatch"); assertEq( - decodedOriginalAmount, - originalAmount, - "originalAmount mismatch" + decodedOriginalAmount, originalAmount, "originalAmount mismatch" ); assertEq(decodedMinAmount, minAmount, "minAmount mismatch"); assertFalse(decodedApprovalNeeded, "approvalNeeded should be false"); @@ -426,12 +407,10 @@ contract TychoRouterForLiquoriceTest is TychoRouterTestSetup { super.setUp(); // Whitelist executor as solver and MAKER - ILiquoriceSettlement settlement = ILiquoriceSettlement( - LIQUORICE_SETTLEMENT - ); - IAllowListAuthentication authenticator = IAllowListAuthentication( - settlement.AUTHENTICATOR() - ); + ILiquoriceSettlement settlement = + ILiquoriceSettlement(LIQUORICE_SETTLEMENT); + IAllowListAuthentication authenticator = + IAllowListAuthentication(settlement.AUTHENTICATOR()); vm.prank(AUTH_MANAGER); authenticator.addSolver(address(tychoRouter)); @@ -459,7 +438,7 @@ contract TychoRouterForLiquoriceTest is TychoRouterTestSetup { "test_single_encoding_strategy_liquorice_settle_single" ); - (bool success, ) = tychoRouterAddr.call(callData); + (bool success,) = tychoRouterAddr.call(callData); assertTrue(success, "Call Failed"); uint256 wethReceived = IERC20(WETH_ADDR).balanceOf(user) - wethBefore; @@ -487,7 +466,7 @@ contract TychoRouterForLiquoriceTest is TychoRouterTestSetup { "test_single_encoding_strategy_liquorice_settle" ); - (bool success, ) = tychoRouterAddr.call(callData); + (bool success,) = tychoRouterAddr.call(callData); assertTrue(success, "Call Failed"); uint256 wethReceived = IERC20(WETH_ADDR).balanceOf(user) - wethBefore; diff --git a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs index 1ed9a404..c96b206d 100644 --- a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs +++ b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs @@ -11,10 +11,10 @@ use crate::encoding::{ bebop::BebopSwapEncoder, curve::CurveSwapEncoder, ekubo::EkuboSwapEncoder, ekubo_v3::EkuboV3SwapEncoder, erc_4626::ERC4626SwapEncoder, fluid_v1::FluidV1SwapEncoder, hashflow::HashflowSwapEncoder, lido::LidoSwapEncoder, - liquorice::LiquoriceSwapEncoder, - maverick_v2::MaverickV2SwapEncoder, rocketpool::RocketpoolSwapEncoder, - slipstreams::SlipstreamsSwapEncoder, uniswap_v2::UniswapV2SwapEncoder, - uniswap_v3::UniswapV3SwapEncoder, uniswap_v4::UniswapV4SwapEncoder, + liquorice::LiquoriceSwapEncoder, maverick_v2::MaverickV2SwapEncoder, + rocketpool::RocketpoolSwapEncoder, slipstreams::SlipstreamsSwapEncoder, + uniswap_v2::UniswapV2SwapEncoder, uniswap_v3::UniswapV3SwapEncoder, + uniswap_v4::UniswapV4SwapEncoder, }, }, swap_encoder::SwapEncoder, From b513b1f3fc4264c60b2898671ed39ac62c7d9712 Mon Sep 17 00:00:00 2001 From: Igor Markin Date: Mon, 9 Feb 2026 18:49:15 +0000 Subject: [PATCH 09/15] Remove redundand calldata --- foundry/test/assets/calldata.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/foundry/test/assets/calldata.txt b/foundry/test/assets/calldata.txt index 7f9ef71f..a7a3f0b2 100644 --- a/foundry/test/assets/calldata.txt +++ b/foundry/test/assets/calldata.txt @@ -63,6 +63,5 @@ test_encoding_strategy_curve_lido_sequential_swap:e21dd0d30000000000000000000000 test_ekubo_v3_encode_swap_multi:01cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48517e506700271aea091b02f42756f5e174af5230000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000a7c5ac471b4880000032 test_single_encoding_strategy_ekubo_v3:5c4b639c0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000007113aa49bac059d709dd0a18d6bb63290076a702d702cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48517e506700271aea091b02f42756f5e174af5230000000000000000000000000000000000000000000000000000000 test_single_ekubo_v3_grouped_swap:5c4b639c00000000000000000000000000000000000000000000000000000002540be400000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a513aa49bac059d709dd0a18d6bb63290076a702d700cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2dac17f958d2ee523a2206206994597c13d831ec7a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000a7c5ac471b48800000320000000000000000000000000000000000000000517e506700271aea091b02f42756f5e174af5230000000000000000000000000000000000000000000000000000000000000000000000000000000 -test_single_encoding_strategy_liquorice:5c4b639c00000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000c00000000000000000000000000000000000000000000000000000000b2d05e0001cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2deadbeef1234567890abcdef1234567890abcdef1234567890abcdef1234567890000000000000000000000000 test_single_encoding_strategy_liquorice_settle:5c4b639c00000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000063adb25a7b768311de128bbda7b8426c3f9c74f3240a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000002000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0001d2068e04cf586f76eece7ba5beb779d7bb1474a1cba673a700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000003a0000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000448633eb8b0a42efed924c42069e0dcf08fb552000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a10000000000000000000000006bc529dc7b81a031828ddce2bc419d01ff268c66000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f6870580000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000416e4084d38e2d4e334057a124ce1ed667947b4fe6a0b44d6cbd62baa5dd384ff93eba5c03f8ea1f4fec128c1c76ce49eb1aad71f053adf3a4d860ef9fe973374d1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000 test_single_encoding_strategy_liquorice_settle_single:5c4b639c00000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000045adb25a7b768311de128bbda7b8426c3f9c74f3240a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000006000000000000000000000000000000000000000000000000000000000b2d05e0000000000000000000000000000000000000000000000000000000000b2d05e0001d2068e04cf586f76eece7ba5beb779d7bb1474a19935c86800000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f68705800000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d2068e04cf586f76eece7ba5beb779d7bb1474a10000000000000000000000006bc529dc7b81a031828ddce2bc419d01ff268c66000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006985036700000000000000000000000006465bceeaef280bb7340a58d75dfc5e1f6870580000000000000000000000000000000000000000000000000000000000000001310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000419ab2af25941edb594c4c33388649c35f7bbc545ce0a745f9e6272c0ea0e8b2517939f2747b980419ca5f22129742f65755464928ac9592aff9d16ac4446b18df1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000 From 727cb1851177451015253468ed49073c7c59804c Mon Sep 17 00:00:00 2001 From: Igor Markin Date: Fri, 13 Mar 2026 10:07:15 +0000 Subject: [PATCH 10/15] Update contracts to use balance manager from constructor --- config/executor_addresses.json | 1 - foundry/src/executors/LiquoriceExecutor.sol | 23 +++++++++++++-------- foundry/test/Constants.sol | 2 ++ foundry/test/TychoRouterTestSetup.sol | 7 +++++-- foundry/test/protocols/Liquorice.t.sol | 23 ++++++++++++--------- 5 files changed, 34 insertions(+), 22 deletions(-) diff --git a/config/executor_addresses.json b/config/executor_addresses.json index f0bea30b..9d148d20 100644 --- a/config/executor_addresses.json +++ b/config/executor_addresses.json @@ -13,7 +13,6 @@ "vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec", "rfq:bebop": "0xFE42BFb115eD9671011cA52BDD23A52A2e077a7c", "rfq:hashflow": "0x19e49Db786c87F4e46B10aFb21c0C06d34270f98", - "rfq:liquorice": "0x0000000000000000000000000000000000000000", "fluid_v1": "0x7191A6a3006ea020C73aCD7068295B9B3767a3Bb", "rocketpool": "0x2605605cbbCfEC01D15704db1a47a0aC48d1f729", "erc4626": "0x842c7e2dA0781B1e8Ea40c377898EFF4002eaB22", diff --git a/foundry/src/executors/LiquoriceExecutor.sol b/foundry/src/executors/LiquoriceExecutor.sol index 63336e6a..f403f6a6 100644 --- a/foundry/src/executors/LiquoriceExecutor.sol +++ b/foundry/src/executors/LiquoriceExecutor.sol @@ -25,7 +25,6 @@ interface IAllowListAuthentication { /// @dev Handles RFQ swaps through Liquorice settlement contracts with support for /// partial fills and dynamic allowance management contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { - using Math for uint256; using SafeERC20 for IERC20; using Address for address; @@ -37,13 +36,22 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { /// @notice The Liquorice settlement contract address address public immutable liquoriceSettlement; - constructor(address _liquoriceSettlement, address _permit2) - RestrictTransferFrom(_permit2) - { - if (_liquoriceSettlement == address(0)) { + /// @notice The Liquorice balance manager contract address + address public immutable liquoriceBalanceManager; + + constructor( + address _liquoriceSettlement, + address _liquoriceliquoriceBalanceManager, + address _permit2 + ) RestrictTransferFrom(_permit2) { + if ( + _liquoriceSettlement == address(0) + || _liquoriceliquoriceBalanceManager == address(0) + ) { revert LiquoriceExecutor__ZeroAddress(); } liquoriceSettlement = _liquoriceSettlement; + liquoriceBalanceManager = _liquoriceliquoriceBalanceManager; } /// @notice Executes a swap through Liquorice's RFQ system @@ -71,11 +79,8 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { // Grant approval to Liquorice balance manager if needed if (approvalNeeded && tokenIn != address(0)) { - address balanceManager = address( - ILiquoriceSettlement(liquoriceSettlement).BALANCE_MANAGER() - ); // slither-disable-next-line unused-return - IERC20(tokenIn).forceApprove(balanceManager, type(uint256).max); + IERC20(tokenIn).forceApprove(liquoriceBalanceManager, type(uint256).max); } givenAmount = _clampAmount( diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index 2a118f17..d55913db 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -158,6 +158,8 @@ contract Constants is Test, BaseConstants { // Liquorice Settlement address LIQUORICE_SETTLEMENT = 0x0448633eb8B0A42EfED924C42069E0DcF08fb552; + address LIQUORICE_BALANCE_MANAGER = + 0xb87bAE43a665EB5943A5642F81B26666bC9E5C95; // Pool Code Init Hashes bytes32 USV2_POOL_CODE_INIT_HASH = diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index add5476c..5959c93e 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -180,8 +180,11 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee, 0xDadEf1fFBFeaAB4f68A9fD181395F68b4e4E7Ae0 ); - liquoriceExecutor = - new LiquoriceExecutor(LIQUORICE_SETTLEMENT, PERMIT2_ADDRESS); + liquoriceExecutor = new LiquoriceExecutor( + LIQUORICE_SETTLEMENT, + LIQUORICE_BALANCE_MANAGER, + PERMIT2_ADDRESS + ); address[] memory executors = new address[](18); executors[0] = address(usv2Executor); diff --git a/foundry/test/protocols/Liquorice.t.sol b/foundry/test/protocols/Liquorice.t.sol index 9df2ca94..a8fa1caf 100644 --- a/foundry/test/protocols/Liquorice.t.sol +++ b/foundry/test/protocols/Liquorice.t.sol @@ -12,9 +12,11 @@ import { } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; contract LiquoriceExecutorExposed is LiquoriceExecutor { - constructor(address _liquoriceSettlement, address _permit2) - LiquoriceExecutor(_liquoriceSettlement, _permit2) - {} + constructor( + address _liquoriceSettlement, + address _liquoriceBalanceManager, + address _permit2 + ) LiquoriceExecutor(_liquoriceSettlement, _liquoriceBalanceManager, _permit2) {} function decodeData(bytes calldata data) external @@ -64,10 +66,13 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { function setUp() public { vm.createSelectFork(vm.rpcUrl("mainnet"), FORK_BLOCK); - liquoriceExecutor = - new LiquoriceExecutorExposed(LIQUORICE_SETTLEMENT, PERMIT2_ADDRESS); - liquoriceSettlement = ILiquoriceSettlement(LIQUORICE_SETTLEMENT); + + liquoriceExecutor = new LiquoriceExecutorExposed( + LIQUORICE_SETTLEMENT, + LIQUORICE_BALANCE_MANAGER, + PERMIT2_ADDRESS + ); authenticator = IAllowListAuthentication(liquoriceSettlement.AUTHENTICATOR()); @@ -76,9 +81,8 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { vm.prank(AUTH_MANAGER); authenticator.addMaker(MAKER); - address balanceManager = liquoriceSettlement.BALANCE_MANAGER(); vm.prank(MAKER); - WETH.approve(balanceManager, type(uint256).max); + WETH.approve(LIQUORICE_BALANCE_MANAGER, type(uint256).max); } function testSettleSingle() public { @@ -418,9 +422,8 @@ contract TychoRouterForLiquoriceTest is TychoRouterTestSetup { authenticator.addMaker(MAKER); // MAKER approves balance manager - address balanceManager = settlement.BALANCE_MANAGER(); vm.prank(MAKER); - IERC20(WETH_ADDR).approve(balanceManager, type(uint256).max); + IERC20(WETH_ADDR).approve(LIQUORICE_BALANCE_MANAGER, type(uint256).max); } function testSettleSingleLiquoriceIntegration() public { From 2422fe38a807fee04daf118012956fae6c26a55f Mon Sep 17 00:00:00 2001 From: Igor Markin Date: Fri, 13 Mar 2026 10:13:01 +0000 Subject: [PATCH 11/15] Overflow check --- src/encoding/evm/swap_encoder/liquorice.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/encoding/evm/swap_encoder/liquorice.rs b/src/encoding/evm/swap_encoder/liquorice.rs index 9165a72e..e8576ba1 100644 --- a/src/encoding/evm/swap_encoder/liquorice.rs +++ b/src/encoding/evm/swap_encoder/liquorice.rs @@ -137,8 +137,10 @@ impl SwapEncoder for LiquoriceSwapEncoder { } else { // Pad to 4 bytes if needed let mut padded = vec![0u8; 4]; - let start = 4 - b.len(); - padded[start..].copy_from_slice(b); + if b.len() < 4 { + let start = 4 - b.len(); + padded[start..].copy_from_slice(b); + } padded } }) From 99ea59eb2cd6611fe070fb6e108c5f2e74125aec Mon Sep 17 00:00:00 2001 From: Igor Markin Date: Fri, 13 Mar 2026 11:05:07 +0000 Subject: [PATCH 12/15] Fix formatting --- foundry/src/executors/LiquoriceExecutor.sol | 5 +++-- foundry/test/TychoRouterTestSetup.sol | 4 +--- foundry/test/protocols/Liquorice.t.sol | 12 +++++++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/foundry/src/executors/LiquoriceExecutor.sol b/foundry/src/executors/LiquoriceExecutor.sol index f403f6a6..b71212be 100644 --- a/foundry/src/executors/LiquoriceExecutor.sol +++ b/foundry/src/executors/LiquoriceExecutor.sol @@ -42,7 +42,7 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { constructor( address _liquoriceSettlement, address _liquoriceliquoriceBalanceManager, - address _permit2 + address _permit2 ) RestrictTransferFrom(_permit2) { if ( _liquoriceSettlement == address(0) @@ -80,7 +80,8 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { // Grant approval to Liquorice balance manager if needed if (approvalNeeded && tokenIn != address(0)) { // slither-disable-next-line unused-return - IERC20(tokenIn).forceApprove(liquoriceBalanceManager, type(uint256).max); + IERC20(tokenIn) + .forceApprove(liquoriceBalanceManager, type(uint256).max); } givenAmount = _clampAmount( diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 5959c93e..527ff411 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -181,9 +181,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils { 0xDadEf1fFBFeaAB4f68A9fD181395F68b4e4E7Ae0 ); liquoriceExecutor = new LiquoriceExecutor( - LIQUORICE_SETTLEMENT, - LIQUORICE_BALANCE_MANAGER, - PERMIT2_ADDRESS + LIQUORICE_SETTLEMENT, LIQUORICE_BALANCE_MANAGER, PERMIT2_ADDRESS ); address[] memory executors = new address[](18); diff --git a/foundry/test/protocols/Liquorice.t.sol b/foundry/test/protocols/Liquorice.t.sol index a8fa1caf..1d68f118 100644 --- a/foundry/test/protocols/Liquorice.t.sol +++ b/foundry/test/protocols/Liquorice.t.sol @@ -15,8 +15,12 @@ contract LiquoriceExecutorExposed is LiquoriceExecutor { constructor( address _liquoriceSettlement, address _liquoriceBalanceManager, - address _permit2 - ) LiquoriceExecutor(_liquoriceSettlement, _liquoriceBalanceManager, _permit2) {} + address _permit2 + ) + LiquoriceExecutor( + _liquoriceSettlement, _liquoriceBalanceManager, _permit2 + ) + {} function decodeData(bytes calldata data) external @@ -69,9 +73,7 @@ contract LiquoriceExecutorTest is Constants, Permit2TestHelper, TestUtils { liquoriceSettlement = ILiquoriceSettlement(LIQUORICE_SETTLEMENT); liquoriceExecutor = new LiquoriceExecutorExposed( - LIQUORICE_SETTLEMENT, - LIQUORICE_BALANCE_MANAGER, - PERMIT2_ADDRESS + LIQUORICE_SETTLEMENT, LIQUORICE_BALANCE_MANAGER, PERMIT2_ADDRESS ); authenticator = IAllowListAuthentication(liquoriceSettlement.AUTHENTICATOR()); From f56b9dae4b0e185a220108a2580888209a496d58 Mon Sep 17 00:00:00 2001 From: Igor Markin Date: Mon, 16 Mar 2026 19:07:22 +0000 Subject: [PATCH 13/15] Remove unused code --- foundry/src/executors/LiquoriceExecutor.sol | 10 ---------- foundry/test/protocols/Liquorice.t.sol | 10 ++++++++++ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/foundry/src/executors/LiquoriceExecutor.sol b/foundry/src/executors/LiquoriceExecutor.sol index b71212be..d6e2f852 100644 --- a/foundry/src/executors/LiquoriceExecutor.sol +++ b/foundry/src/executors/LiquoriceExecutor.sol @@ -10,16 +10,6 @@ import { } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/Address.sol"; -interface ILiquoriceSettlement { - function BALANCE_MANAGER() external view returns (address); - function AUTHENTICATOR() external view returns (address); -} - -interface IAllowListAuthentication { - function addSolver(address _solver) external; - function addMaker(address _maker) external; -} - /// @title LiquoriceExecutor /// @notice Executor for Liquorice RFQ (Request for Quote) swaps /// @dev Handles RFQ swaps through Liquorice settlement contracts with support for diff --git a/foundry/test/protocols/Liquorice.t.sol b/foundry/test/protocols/Liquorice.t.sol index 1d68f118..fc7a0c87 100644 --- a/foundry/test/protocols/Liquorice.t.sol +++ b/foundry/test/protocols/Liquorice.t.sol @@ -11,6 +11,16 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +interface ILiquoriceSettlement { + function BALANCE_MANAGER() external view returns (address); + function AUTHENTICATOR() external view returns (address); +} + +interface IAllowListAuthentication { + function addSolver(address _solver) external; + function addMaker(address _maker) external; +} + contract LiquoriceExecutorExposed is LiquoriceExecutor { constructor( address _liquoriceSettlement, From 529e4a3e7ed5046340f6a49bc5b93704ea229ee3 Mon Sep 17 00:00:00 2001 From: Igor Markin Date: Thu, 19 Mar 2026 17:28:50 +0000 Subject: [PATCH 14/15] Code review fixes --- foundry/src/executors/LiquoriceExecutor.sol | 1 - src/encoding/evm/swap_encoder/liquorice.rs | 16 +++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/foundry/src/executors/LiquoriceExecutor.sol b/foundry/src/executors/LiquoriceExecutor.sol index d6e2f852..328f525f 100644 --- a/foundry/src/executors/LiquoriceExecutor.sol +++ b/foundry/src/executors/LiquoriceExecutor.sol @@ -78,7 +78,6 @@ contract LiquoriceExecutor is IExecutor, RestrictTransferFrom { givenAmount, originalBaseTokenAmount, minBaseTokenAmount ); - // Transfer tokens to executor _transfer(address(this), transferType, tokenIn, givenAmount); // Modify the fill amount in the calldata if partial fill is supported diff --git a/src/encoding/evm/swap_encoder/liquorice.rs b/src/encoding/evm/swap_encoder/liquorice.rs index e8576ba1..2e494052 100644 --- a/src/encoding/evm/swap_encoder/liquorice.rs +++ b/src/encoding/evm/swap_encoder/liquorice.rs @@ -43,22 +43,20 @@ impl SwapEncoder for LiquoriceSwapEncoder { _chain: Chain, config: Option>, ) -> Result { - let config = config.ok_or(EncodingError::FatalError( - "Missing liquorice specific addresses in config".to_string(), - ))?; let balance_manager_address = config .get("balance_manager_address") - .map(|s| { + .ok_or_else(|| { + EncodingError::FatalError( + "Missing liquorice balance manager address in config".to_string(), + ) + }) + .and_then(|s| { Bytes::from_str(s).map_err(|_| { EncodingError::FatalError( "Invalid liquorice balance manager address".to_string(), ) }) - }) - .ok_or(EncodingError::FatalError( - "Missing liquorice balance manager address in config".to_string(), - )) - .flatten()?; + })?; let (runtime_handle, runtime) = get_runtime()?; Ok(Self { executor_address, balance_manager_address, runtime_handle, runtime }) From 304bb23f89722676b627679e88cdb649e784ba2f Mon Sep 17 00:00:00 2001 From: Igor Markin Date: Thu, 19 Mar 2026 17:32:02 +0000 Subject: [PATCH 15/15] Missing config fix --- src/encoding/evm/swap_encoder/liquorice.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/encoding/evm/swap_encoder/liquorice.rs b/src/encoding/evm/swap_encoder/liquorice.rs index 2e494052..38c81a37 100644 --- a/src/encoding/evm/swap_encoder/liquorice.rs +++ b/src/encoding/evm/swap_encoder/liquorice.rs @@ -43,6 +43,9 @@ impl SwapEncoder for LiquoriceSwapEncoder { _chain: Chain, config: Option>, ) -> Result { + let config = config.ok_or(EncodingError::FatalError( + "Missing liquorice specific addresses in config".to_string(), + ))?; let balance_manager_address = config .get("balance_manager_address") .ok_or_else(|| {