tycho-execution 0.309.0

Provides tools for encoding and executing swaps against Tycho router and protocol executors.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {
    EnumerableSet
} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {FeeRecipient} from "../lib/FeeStructs.sol";
import {IFeeCalculator, CustomFees} from "@interfaces/IFeeCalculator.sol";

error FeeCalculator__FeeTooHigh();
error FeeCalculator__AddressZero();

/**
 * @title FeeCalculator
 * @notice Contract responsible for calculating fees on swap outputs and managing fee configuration
 * @dev This contract is called via staticCall from TychoRouter.
 *      It calculates fees and returns the values - accounting is done by the caller.
 *      It also stores all fee-related configuration.
 *
 *      Router fees use an 8-decimal precision unit: 1 unit = 0.0001 BPS = 0.000001%.
 *      100% = 100_000_000 units. This allows sub-BPS fee rates (e.g. 1.5 BPS = 15_000 units).
 *
 *      The external interface (calculateFee, getEffectiveRouterFeeOnOutput) preserves legacy
 *      BPS semantics (10_000 = 100%) for compatibility with TychoRouter and Dispatcher.
 */
contract FeeCalculator is AccessControl, IFeeCalculator {
    using EnumerableSet for EnumerableSet.AddressSet;

    // 100% expressed in 8-decimal fee units (1 unit = 0.0001 BPS = 0.000001%)
    uint32 public constant MAX_FEE_BPS = 100_000_000;
    // Combined denominator when both fees use the MAX_FEE_BPS scale (MAX_FEE_BPS^2)
    uint64 public constant MAX_FEE_BPS_SQUARED = 10_000_000_000_000_000;

    uint32 private _routerFeeOnOutputBps; // Router fee on output amount in fee units
    uint32 private _routerFeeOnClientFeeBps; // Router fee on client fee in fee units
    address private _routerFeeReceiver; // Address whose vault balance receives router fees

    // Per-client custom router fees (both output and client fees)
    // If set, custom values will override the default router fees for the client
    // Storage-optimized: all custom fee data for a client fits in a single slot
    mapping(address => CustomFees) private _customRouterFees;

    // Tracks all clients that currently have at least one custom fee override
    EnumerableSet.AddressSet private _customFeeClients;

    //keccak256("ROUTER_FEE_SETTER_ROLE")
    bytes32 public constant ROUTER_FEE_SETTER_ROLE =
        0x9939157be7760e9462f1d5a0dcad88b616ddc64138e317108b40b1cf55601348;

    event RouterFeeOnOutputUpdated(uint32 oldFeeBps, uint32 newFeeBps);
    event RouterFeeOnClientFeeUpdated(uint32 oldFeeBps, uint32 newFeeBps);
    event CustomRouterFeeOnOutputUpdated(
        address indexed client, uint32 oldFeeBps, uint32 newFeeBps
    );
    event CustomRouterFeeOnClientFeeUpdated(
        address indexed client, uint32 oldFeeBps, uint32 newFeeBps
    );
    event CustomRouterFeeOnOutputRemoved(address indexed client);
    event CustomRouterFeeOnClientFeeRemoved(address indexed client);
    event RouterFeeReceiverUpdated(
        address indexed oldReceiver, address indexed newReceiver
    );

    constructor(address routerFeeSetter) {
        _routerFeeReceiver = msg.sender;

        // Make the role its own admin so role holders can manage their own role
        _setRoleAdmin(ROUTER_FEE_SETTER_ROLE, ROUTER_FEE_SETTER_ROLE);
        _grantRole(ROUTER_FEE_SETTER_ROLE, routerFeeSetter);
    }

    /**
     * @notice Calculates fees from the swap output amount
     * @dev Called from TychoRouter. Does not perform any accounting.
     *      Router fee parameters are retrieved from contract storage based on the client address.
     *      Client fee parameters are passed as function arguments.
     *      clientFeeBps uses the legacy BPS scale (10000 = 100%). Internally it is scaled to the
     *      same 8-decimal unit system used for router fees (100_000_000 = 100%).
     * @param amountIn The amount before fee deduction
     * @param client The client address to look up custom router fees for and to receive fees
     * @param clientFeeBps Client fee in basis points (10000 = 100%)
     * @return amountOut The amount remaining after all fee deductions
     * @return feeRecipients Array of (address, feeAmount) tuples for fee distribution
     */
    function calculateFee(uint256 amountIn, address client, uint16 clientFeeBps)
        external
        view
        returns (uint256 amountOut, FeeRecipient[] memory feeRecipients)
    {
        (uint32 routerFeeOnOutputBps, uint32 routerFeeOnClientFeeBps) =
            _getFeeInfo(client);

        // Scale clientFeeBps from legacy scale (10_000 = 100%) to internal scale
        // (100_000_000 = 100%) so both fee types can be compared and combined.
        uint32 scaledClientFeeBps = uint32(clientFeeBps) * 10_000;

        if (
            (scaledClientFeeBps + routerFeeOnOutputBps > MAX_FEE_BPS)
                || routerFeeOnClientFeeBps > MAX_FEE_BPS
        ) {
            revert FeeCalculator__FeeTooHigh();
        }

        amountOut = amountIn;
        uint256 routerFeeOnClientFee = 0;
        uint256 clientPortion = 0;

        // Calculate client fee if > 0
        if (scaledClientFeeBps > 0) {
            // Save numerator for later routerFeeOnClientFee calculation to avoid
            // divide-before-multiply precision loss and warning
            uint256 clientFeeNumerator = amountOut * scaledClientFeeBps;
            uint256 totalClientFee = clientFeeNumerator / MAX_FEE_BPS;

            // Calculate router's cut of the client fee
            if (routerFeeOnClientFeeBps > 0) {
                // Both fees use the 100_000_000 scale, so denominator is 100_000_000^2
                routerFeeOnClientFee =
                    (clientFeeNumerator * routerFeeOnClientFeeBps)
                        / MAX_FEE_BPS_SQUARED;
            }

            // Client gets their portion (after router's cut)
            clientPortion = totalClientFee - routerFeeOnClientFee;
        }

        uint256 totalRouterFee = routerFeeOnClientFee;

        // Calculate router fee on output amount if > 0
        if (routerFeeOnOutputBps > 0) {
            uint256 routerFeeOnOutput =
                (amountOut * routerFeeOnOutputBps) / MAX_FEE_BPS;
            totalRouterFee += routerFeeOnOutput;
        }

        // Update amountOut considering both fees
        amountOut -= (clientPortion + totalRouterFee);

        // Build fee recipients array
        feeRecipients = new FeeRecipient[](2);
        feeRecipients[0] = FeeRecipient({
            recipient: _routerFeeReceiver, feeAmount: totalRouterFee
        });
        feeRecipients[1] =
            FeeRecipient({recipient: client, feeAmount: clientPortion});

        return (amountOut, feeRecipients);
    }

    /**
     * @notice Gets fee information for a specific client
     * @dev Returns custom fees if set for the client, otherwise returns default fees
     * @param client The client address to check
     * @return routerFeeOnOutputBps Router fee on output in fee units
     * @return routerFeeOnClientFeeBps Router fee on client fee in fee units
     */
    function _getFeeInfo(address client)
        internal
        view
        returns (uint32 routerFeeOnOutputBps, uint32 routerFeeOnClientFeeBps)
    {
        CustomFees memory customFees = _customRouterFees[client];

        routerFeeOnOutputBps = customFees.hasCustomFeeOnOutput
            ? customFees.feeBpsOnOutput
            : _routerFeeOnOutputBps;

        routerFeeOnClientFeeBps = customFees.hasCustomFeeOnClientFee
            ? customFees.feeBpsOnClientFee
            : _routerFeeOnClientFeeBps;
    }

    /**
     * @dev Sets the router fee on output amount in fee units
     * @param feeBps Fee in fee units (1 unit = 0.0001 BPS; 100_000_000 = 100%)
     */
    function setRouterFeeOnOutput(uint32 feeBps)
        external
        onlyRole(ROUTER_FEE_SETTER_ROLE)
    {
        if (feeBps > MAX_FEE_BPS) revert FeeCalculator__FeeTooHigh();
        uint32 oldFeeBps = _routerFeeOnOutputBps;
        _routerFeeOnOutputBps = feeBps;
        emit RouterFeeOnOutputUpdated(oldFeeBps, feeBps);
    }

    /**
     * @dev Returns the current router fee on output amount in fee units
     */
    function getRouterFeeOnOutput() external view returns (uint32) {
        return _routerFeeOnOutputBps;
    }

    /**
     * @dev Sets a custom router fee on output amount for a specific client
     * @param client The client address to set the custom fee for
     * @param feeBps Fee in fee units (1 unit = 0.0001 BPS; 100_000_000 = 100%)
     */
    function setCustomRouterFeeOnOutput(address client, uint32 feeBps)
        external
        onlyRole(ROUTER_FEE_SETTER_ROLE)
    {
        if (feeBps > MAX_FEE_BPS) revert FeeCalculator__FeeTooHigh();
        CustomFees memory customFees = _customRouterFees[client];
        uint32 oldFeeBps = customFees.hasCustomFeeOnOutput
            ? customFees.feeBpsOnOutput
            : _routerFeeOnOutputBps;

        customFees.feeBpsOnOutput = feeBps;
        customFees.hasCustomFeeOnOutput = true;
        _customRouterFees[client] = customFees;
        // slither-disable-next-line unused-return
        _customFeeClients.add(client);

        emit CustomRouterFeeOnOutputUpdated(client, oldFeeBps, feeBps);
    }

    /**
     * @dev Removes the custom router fee on output amount for a specific client, reverting to
     *      default
     * @param client The client address to remove the custom fee from
     */
    function removeCustomRouterFeeOnOutput(address client)
        external
        onlyRole(ROUTER_FEE_SETTER_ROLE)
    {
        CustomFees memory customFees = _customRouterFees[client];
        customFees.hasCustomFeeOnOutput = false;
        customFees.feeBpsOnOutput = 0;
        _customRouterFees[client] = customFees;

        if (!customFees.hasCustomFeeOnClientFee) {
            // slither-disable-next-line unused-return
            _customFeeClients.remove(client);
        }

        emit CustomRouterFeeOnOutputRemoved(client);
    }

    /**
     * @dev Returns the effective router fee on output for a specific client in legacy BPS scale
     *      (10_000 = 100%), for interface compatibility with TychoRouter and Dispatcher.
     * @dev For full-precision value use getEffectiveRouterFeeOnOutputScaled.
     * @param client The client address to check
     * @return Zero if no fee is set; otherwise the fee in legacy BPS (rounded down, minimum 1).
     */
    function getEffectiveRouterFeeOnOutput(address client)
        external
        view
        returns (uint16)
    {
        CustomFees memory customFees = _customRouterFees[client];
        uint32 fee = customFees.hasCustomFeeOnOutput
            ? customFees.feeBpsOnOutput
            : _routerFeeOnOutputBps;
        if (fee == 0) return 0;
        // Convert from internal scale (100_000_000 = 100%) to legacy BPS scale (10_000 = 100%).
        // Return at least 1 so callers can detect that a fee is active.
        uint32 legacyBps = fee / 10_000;
        return uint16(legacyBps > 0 ? legacyBps : 1);
    }

    /**
     * @dev Returns the effective router fee on output for a specific client in fee units
     *      (100_000_000 = 100%).
     * @param client The client address to check
     * @return The fee in fee units (custom if set, otherwise default)
     */
    function getEffectiveRouterFeeOnOutputScaled(address client)
        external
        view
        returns (uint32)
    {
        CustomFees memory customFees = _customRouterFees[client];
        return customFees.hasCustomFeeOnOutput
            ? customFees.feeBpsOnOutput
            : _routerFeeOnOutputBps;
    }

    /**
     * @dev Sets the router platform fee on client fee in fee units
     * @param feeBps Fee in fee units (1 unit = 0.0001 BPS; 100_000_000 = 100%)
     */
    function setRouterFeeOnClientFee(uint32 feeBps)
        external
        onlyRole(ROUTER_FEE_SETTER_ROLE)
    {
        if (feeBps > MAX_FEE_BPS) revert FeeCalculator__FeeTooHigh();
        uint32 oldFeeBps = _routerFeeOnClientFeeBps;
        _routerFeeOnClientFeeBps = feeBps;
        emit RouterFeeOnClientFeeUpdated(oldFeeBps, feeBps);
    }

    /**
     * @dev Returns the current router platform fee on client fee in fee units
     */
    function getRouterFeeOnClientFee() external view returns (uint32) {
        return _routerFeeOnClientFeeBps;
    }

    /**
     * @dev Sets a custom router fee on client fee for a specific client
     * @param client The client address to set the custom fee for
     * @param feeBps Fee in fee units (1 unit = 0.0001 BPS; 100_000_000 = 100%)
     */
    function setCustomRouterFeeOnClientFee(address client, uint32 feeBps)
        external
        onlyRole(ROUTER_FEE_SETTER_ROLE)
    {
        if (feeBps > MAX_FEE_BPS) revert FeeCalculator__FeeTooHigh();
        CustomFees memory customFees = _customRouterFees[client];
        uint32 oldFeeBps = customFees.hasCustomFeeOnClientFee
            ? customFees.feeBpsOnClientFee
            : _routerFeeOnClientFeeBps;

        customFees.feeBpsOnClientFee = feeBps;
        customFees.hasCustomFeeOnClientFee = true;
        _customRouterFees[client] = customFees;
        // slither-disable-next-line unused-return
        _customFeeClients.add(client);

        emit CustomRouterFeeOnClientFeeUpdated(client, oldFeeBps, feeBps);
    }

    /**
     * @dev Removes the custom router fee on client fee for a specific client, reverting to default
     * @param client The client address to remove the custom fee from
     */
    function removeCustomRouterFeeOnClientFee(address client)
        external
        onlyRole(ROUTER_FEE_SETTER_ROLE)
    {
        CustomFees memory customFees = _customRouterFees[client];
        customFees.hasCustomFeeOnClientFee = false;
        customFees.feeBpsOnClientFee = 0;
        _customRouterFees[client] = customFees;

        if (!customFees.hasCustomFeeOnOutput) {
            // slither-disable-next-line unused-return
            _customFeeClients.remove(client);
        }

        emit CustomRouterFeeOnClientFeeRemoved(client);
    }

    /**
     * @dev Returns the effective router fee on client fee for a specific client in fee units
     * @param client The client address to check
     * @return The fee in fee units (custom if set, otherwise default)
     */
    function getEffectiveRouterFeeOnClientFee(address client)
        external
        view
        returns (uint32)
    {
        CustomFees memory customFees = _customRouterFees[client];
        return customFees.hasCustomFeeOnClientFee
            ? customFees.feeBpsOnClientFee
            : _routerFeeOnClientFeeBps;
    }

    /**
     * @notice Returns a page of clients with custom fee overrides and their current settings
     * @param start Index to start reading from (0-indexed)
     * @param count Maximum number of entries to return
     * @return clients Addresses of clients with at least one custom fee
     * @return fees Custom fee configuration for each client (parallel array)
     */
    function getAllClientFees(uint256 start, uint256 count)
        external
        view
        returns (address[] memory clients, CustomFees[] memory fees)
    {
        uint256 total = _customFeeClients.length();
        if (start >= total) return (new address[](0), new CustomFees[](0));
        uint256 remaining = total - start;
        uint256 size = count < remaining ? count : remaining;
        clients = new address[](size);
        fees = new CustomFees[](size);
        for (uint256 i = 0; i < size; i++) {
            address client = _customFeeClients.at(start + i);
            clients[i] = client;
            fees[i] = _customRouterFees[client];
        }
    }

    /**
     * @dev Sets the address that receives router fees
     * @param routerFeeReceiver The address to receive router fees
     */
    function setRouterFeeReceiver(address routerFeeReceiver)
        external
        onlyRole(ROUTER_FEE_SETTER_ROLE)
    {
        if (routerFeeReceiver == address(0)) {
            revert FeeCalculator__AddressZero();
        }
        address oldReceiver = _routerFeeReceiver;
        _routerFeeReceiver = routerFeeReceiver;
        emit RouterFeeReceiverUpdated(oldReceiver, routerFeeReceiver);
    }

    /**
     * @dev Returns the current router fee receiver address
     */
    function getRouterFeeReceiver() external view returns (address) {
        return _routerFeeReceiver;
    }
}