Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a8925c9
Initial
markin-io Feb 4, 2026
e61af02
Rework target contract and approvals
markin-io Feb 4, 2026
9f75d04
Handle minBaseTokenAmount in partial fills
markin-io Feb 5, 2026
65aa9d2
Implement settle tests
markin-io Feb 6, 2026
53922e6
Implement integration tests with TychoRouter
markin-io Feb 9, 2026
918426e
Code review fixes
markin-io Feb 9, 2026
2b0e44f
Fix encoder test
markin-io Feb 9, 2026
552257f
Formatting fixes
markin-io Feb 9, 2026
b513b1f
Remove redundand calldata
markin-io Feb 9, 2026
bb8ba31
Merge branch 'main' into feat/liquorice-executor
markin-io Feb 9, 2026
b3777a3
Merge branch 'main' into feat/liquorice-executor
markin-io Feb 11, 2026
1dd4a4e
Merge branch 'main' into feat/liquorice-executor
markin-io Feb 23, 2026
7d3371f
Merge branch 'main' into feat/liquorice-executor
markin-io Mar 2, 2026
2a584a3
Merge branch 'main' into feat/liquorice-executor
markin-io Mar 2, 2026
964f581
Merge branch 'main' into feat/liquorice-executor
markin-io Mar 3, 2026
7b7ceea
Merge branch 'main' into feat/liquorice-executor
markin-io Mar 5, 2026
60b5571
Merge branch 'main' into feat/liquorice-executor
markin-io Mar 10, 2026
a4bffea
Merge branch 'main' into feat/liquorice-executor
markin-io Mar 13, 2026
727cb18
Update contracts to use balance manager from constructor
markin-io Mar 13, 2026
2422fe3
Overflow check
markin-io Mar 13, 2026
99ea59e
Fix formatting
markin-io Mar 13, 2026
f56b9da
Remove unused code
markin-io Mar 16, 2026
529e4a3
Code review fixes
markin-io Mar 19, 2026
304bb23
Missing config fix
markin-io Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/protocol_specific_addresses.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"rfq:hashflow": {
"hashflow_router_address": "0x55084eE0fEf03f14a305cd24286359A35D735151"
},
"rfq:liquorice": {
"balance_manager_address": "0xb87bAE43a665EB5943A5642F81B26666bC9E5C95"
},
"etherfi": {
"eeth_address": "0x35fA164735182de50811E8e2E824cFb9B6118ac2",
"weeth_address": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee",
Expand Down
1 change: 1 addition & 0 deletions config/test_executor_addresses.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"vm:balancer_v3": "0x03A6a84cD762D9707A21605b548aaaB891562aAb",
"rfq:bebop": "0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF",
"rfq:hashflow": "0x15cF58144EF33af1e14b5208015d11F9143E27b9",
"rfq:liquorice": "0xDB25A7b768311dE128BBDa7B8426c3f9C74f3240",
"fluid_v1": "0x212224D2F2d262cd093eE13240ca4873fcCBbA3C",
"rocketpool": "0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7",
"erc4626": "0xD16d567549A2a2a2005aEACf7fB193851603dd70",
Expand Down
212 changes: 212 additions & 0 deletions foundry/src/executors/LiquoriceExecutor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// 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";

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
/// partial fills and dynamic allowance management
contract LiquoriceExecutor is IExecutor, RestrictTransferFrom {
using SafeERC20 for IERC20;
using Address for address;

/// @notice Liquorice-specific errors
error LiquoriceExecutor__InvalidDataLength();
error LiquoriceExecutor__ZeroAddress();
error LiquoriceExecutor__AmountBelowMinimum();

/// @notice The Liquorice settlement contract address
address public immutable liquoriceSettlement;

/// @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
/// @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)
{
(
address tokenIn,
address tokenOut,
TransferType transferType,
uint32 partialFillOffset,
uint256 originalBaseTokenAmount,
uint256 minBaseTokenAmount,
bool approvalNeeded,
address receiver,
bytes memory liquoriceCalldata
) = _decodeData(data);

// 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);
}

givenAmount = _clampAmount(
givenAmount, originalBaseTokenAmount, minBaseTokenAmount
);

// Transfer tokens to executor
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when running through the tycho router, address(this) represents the tycho router and not the executor. Can we clean up this comment a bit so it isn't misleading please?

_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 && originalBaseTokenAmount > givenAmount) {
finalCalldata = _modifyFilledTakerAmount(
liquoriceCalldata, givenAmount, partialFillOffset
);
}

uint256 balanceBefore = _balanceOf(tokenOut, receiver);
uint256 ethValue = tokenIn == address(0) ? givenAmount : 0;

// Execute the swap by forwarding calldata to settlement contract
// slither-disable-next-line unused-return
liquoriceSettlement.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,
uint32 partialFillOffset,
uint256 originalBaseTokenAmount,
uint256 minBaseTokenAmount,
bool approvalNeeded,
address receiver,
bytes memory liquoriceCalldata
)
{
// Minimum fixed fields:
// tokenIn (20) + tokenOut (20) + transferType (1) + partialFillOffset (4) +
// originalBaseTokenAmount (32) + minBaseTokenAmount (32) +
// 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 = 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
/// @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 a smaller 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 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,
uint32 partialFillOffset
) internal pure returns (bytes memory) {
// Use the offset from Liquorice API to locate the fill amount
// 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
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, givenAmount)
}

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 {}
}
5 changes: 5 additions & 0 deletions foundry/test/Constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ contract Constants is Test, BaseConstants {
// Hashflow Router
address HASHFLOW_ROUTER = 0x55084eE0fEf03f14a305cd24286359A35D735151;

// Liquorice Settlement
address LIQUORICE_SETTLEMENT = 0x0448633eb8B0A42EfED924C42069E0DcF08fb552;
address LIQUORICE_BALANCE_MANAGER =
0xb87bAE43a665EB5943A5642F81B26666bC9E5C95;

// Pool Code Init Hashes
bytes32 USV2_POOL_CODE_INIT_HASH =
0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f;
Expand Down
8 changes: 7 additions & 1 deletion foundry/test/TychoRouterTestSetup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -84,6 +85,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;
Expand Down Expand Up @@ -178,8 +180,11 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee,
0xDadEf1fFBFeaAB4f68A9fD181395F68b4e4E7Ae0
);
liquoriceExecutor = new LiquoriceExecutor(
LIQUORICE_SETTLEMENT, LIQUORICE_BALANCE_MANAGER, 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);
Expand All @@ -197,6 +202,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
executors[14] = address(erc4626Executor);
executors[15] = address(ekuboV3Executor);
executors[16] = address(etherfiExecutor);
executors[17] = address(liquoriceExecutor);

return executors;
}
Expand Down
Loading
Loading