From 7a89258245b0eb7aa862265158f2a9fef41363c9 Mon Sep 17 00:00:00 2001 From: Stepan Chekhovskoi Date: Thu, 11 Jun 2026 15:41:43 +0200 Subject: [PATCH 1/2] refactor timestamps to durations --- src/instructions/PiecewiseLinearScale.sol | 139 ++++--- test/PiecewiseLinearScale.t.sol | 479 +++++++++++++++++++--- 2 files changed, 506 insertions(+), 112 deletions(-) diff --git a/src/instructions/PiecewiseLinearScale.sol b/src/instructions/PiecewiseLinearScale.sol index 2a1217b..6256504 100644 --- a/src/instructions/PiecewiseLinearScale.sol +++ b/src/instructions/PiecewiseLinearScale.sol @@ -5,39 +5,54 @@ pragma solidity 0.8.30; /// @custom:copyright © 2026 Degensoft Ltd import { Context } from "../libs/VM.sol"; +import { Calldata } from "@1inch/solidity-utils/contracts/libraries/Calldata.sol"; library PiecewiseLinearScaleArgsBuilder { error PiecewiseLinearScaleMismatchInputLengths(); error PiecewiseLinearScaleNotEnoughPointsToBuildPiece(); - error PiecewiseLinearScaleUnorderedPoints(); /// @notice Build instruction arguments for PiecewiseLinearScale - /// @param timestamps Point timestamps, strictly ordered - /// @param scales Point values at specified timestamps, `scale = (scales[n] + 1) / 2 ** 24` - /// @return args Packed bytes for inclusion in program bytecode (8 bytes per point) - function build(uint40[] memory timestamps, uint24[] memory scales) internal pure returns (bytes memory) { - require(timestamps.length == scales.length, PiecewiseLinearScaleMismatchInputLengths()); - require(timestamps.length >= 2, PiecewiseLinearScaleNotEnoughPointsToBuildPiece()); - - for (uint256 i = 1; i < timestamps.length; i++) { - require(timestamps[i - 1] < timestamps[i], PiecewiseLinearScaleUnorderedPoints()); + /// @param timestamp First interval start timestamp + /// @param durations Interval durations + /// @param scales Packed scale values at timestamp + sum(durations[0:i]) + /// @return args Packed bytes for inclusion in program bytecode + function build(uint40 timestamp, uint16[] memory durations, uint24[] memory scales) internal pure returns (bytes memory) { + require(scales.length >= 2, PiecewiseLinearScaleNotEnoughPointsToBuildPiece()); + require(durations.length == scales.length - 1, PiecewiseLinearScaleMismatchInputLengths()); + + bytes memory code = abi.encodePacked(timestamp, scales[0]); + for (uint256 i; i < durations.length; i++) { + code = abi.encodePacked(code, durations[i], scales[i + 1]); } - bytes memory code = new bytes(timestamps.length * 8); - for (uint256 i; i < timestamps.length; i++) { - assembly ("memory-safe") { - let ptr := add(code, add(32, mul(i, 8))) - let start := shl(216, mload(add(timestamps, add(32, mul(i, 32))))) - let scale := shl(232, mload(add(scales, add(32, mul(i, 32))))) - mstore(ptr, or(mload(ptr), or(start, shr(40, scale)))) - } + return code; + } + + /// @notice Parse start timestamp + function parseStartTimestamp(bytes calldata args) internal pure returns (uint256 ts) { + assembly ("memory-safe") { + ts := shr(216, calldataload(args.offset)) } + } - return code; + /// @notice Parse specific interval duration + /// @dev Requires args to be shifted by 8 bytes + function parseIntervalDuration(bytes calldata args, uint256 n) internal pure returns (uint256 duration) { + assembly ("memory-safe") { + duration := shr(240, calldataload(add(args.offset, mul(n, 5)))) + } + } + + /// @notice Parse specific point scale + /// @dev Requires args to be shifted by 5 bytes + function parsePointScale(bytes calldata args, uint256 n) internal pure returns (uint256 scale) { + assembly ("memory-safe") { + scale := shr(232, calldataload(add(args.offset, mul(n, 5)))) + } } /// @notice Apply scale to the value - /// @dev Matches the scaling in opcodes, note the `1` is added in `_calcScaleNow` + /// @dev Matches the scaling in opcodes function scaleValue(uint256 value, uint24 scale) internal pure returns (uint256 scaled) { scaled = (value * (uint256(scale) + 1)) >> 24; } @@ -48,20 +63,6 @@ library PiecewiseLinearScaleArgsBuilder { function unscaleValue(uint256 value, uint24 scale) internal pure returns (uint256 unscaled) { unscaled = ((value << 24) + scale) / (uint256(scale) + 1); } - - /// @notice Parse specific point timestamp - function pointTs(bytes calldata args, uint256 n) internal pure returns (uint40 ts) { - assembly ("memory-safe") { - ts := shr(216, calldataload(add(args.offset, mul(n, 8)))) - } - } - - /// @notice Parse specific point scale - function pointScale(bytes calldata args, uint256 n) internal pure returns (uint24 scale) { - assembly ("memory-safe") { - scale := shr(232, calldataload(add(args.offset, add(mul(n, 8), 5)))) - } - } } /** @@ -70,30 +71,31 @@ library PiecewiseLinearScaleArgsBuilder { * - Designed to be used after balances set and before a swap instruction * - Applies time-based scaling to the balances * - Could be used for complex auctions with periods of price increase and decrease - * - Applied at specified time periods, does not affect price out of the boundaries + * - Set scaling before start to first point scale, set scaling after end to last point scale * * Example usage: * 1. Balances set to 1000e18 : 2000e18 - * 2. _piecewiseLinearScaleBalanceIn1D is used with points [(now, 0.5), (now + 1000, 0.7), (now + 2000, 0.3)] - * 3. At start balances would be threated as 500e18 : 2000e18 then linearly go to 700e18 : 2000e18 and later to 300e18 : 2000e18 + * 2. `_piecewiseLinearScaleBalanceIn1D` is used with args (now, [100, 1000], [0.5, 0.7, 0.3]) + * 3. At start balances would be treated as 500e18 : 2000e18 then linearly go to 700e18 : 2000e18 and later to 300e18 : 2000e18 * 4. Swap instruction calculates amounts based on updated balances * * @dev Integration Notes * - Scaling is applied to token balances (reserves), not the amounts, this follows Exact In/Out Symmetry SwapVM Invariant - * - Scaling basis points are 2 ** 24 (comparing to 10 ** 7 in Fusion), this uses the computation field efficiently + * - Scaling basis points are 2 ** 24 (comparing to 10 ** 7 in Fusion), this uses the computation field efficiently, scaling formula: `value * (scale + 1) / 2 ** 24` * - Scaling range is (0; 1] (comparing to (~0.373; 1] in Fusion), this contributes to instruction generalization to be not bounded by case-specific limitations - * - The price adjustment is applied only at specified time ranges to allow multiple adjesment insructions apply at different time ranges without forced overlap * - For dutch auction selling specified amount, the order balance would not equal the amount, the amount should be reached as a result of final scaling, * the `unscaleValue(amount, finalScale)` provides the value which would result in desired amount after scaling - * - The instruction accepts array of points, each point is 8 bytes: 5 bytes (timestamp) + 3 bytes (scale - 1) + * - The instruction accepts start timestamp, arrays of points and interval durations: + * packed [5 bytes timestamp, 3 bytes scales[0], 2 bytes durations[0] ...], `durations.length == scales.length - 1` */ contract PiecewiseLinearScale { using PiecewiseLinearScaleArgsBuilder for bytes; + using Calldata for bytes; error PiecewiseLinearScaleAmountsAlreadyComputed(uint256 amountIn, uint256 amountOut); /// @notice Apply a piecewise-linear scale to grow the amount out by shrinking the balance in - /// @dev Should not be used with _invalidateTokenIn1D because it relies on ctx.swap.balanceIn which is modified here + /// @dev Should not be used with `_invalidateTokenIn1D` because it relies on `ctx.swap.balanceIn` which is modified here function _piecewiseLinearScaleBalanceIn1D(Context memory ctx, bytes calldata points) internal view { require(ctx.swap.amountIn == 0 || ctx.swap.amountOut == 0, PiecewiseLinearScaleAmountsAlreadyComputed(ctx.swap.amountIn, ctx.swap.amountOut)); @@ -101,35 +103,50 @@ contract PiecewiseLinearScale { } /// @notice Apply a piecewise-linear scale to grow the amount in by shrinking the balance out - /// @dev Should not be used with _invalidateTokenOut1D because it relies on ctx.swap.balanceOut which is modified here + /// @dev Should not be used with `_invalidateTokenOut1D` because it relies on `ctx.swap.balanceOut` which is modified here function _piecewiseLinearScaleBalanceOut1D(Context memory ctx, bytes calldata points) internal view { require(ctx.swap.amountIn == 0 || ctx.swap.amountOut == 0, PiecewiseLinearScaleAmountsAlreadyComputed(ctx.swap.amountIn, ctx.swap.amountOut)); ctx.swap.balanceOut = (ctx.swap.balanceOut * _calcScaleNow(points)) >> 24; } - /// @dev The function relies on point are strictly ordered and there are at least two points - function _calcScaleNow(bytes calldata points) private view returns (uint256 scale) { - uint256 max = points.length / 8 - 1; + /// @notice Find the current interval and get linear weighted scale, returns initial or last scale for no matching interval + /// @dev Relies on packing [5 bytes timestamp, 3 bytes scales[k], 2 bytes durations[k] ...], `durations.length == scales.length - 1` + /// - (a) At least two points provided -> `args.length >= 13 bytes` + /// - (b) Scale is 3 bytes -> `scale + 1 <= 2 ** 24` + /// - (c) Scale is 3 bytes and duration is 2 bytes -> `scale * duration < 2 ** 40` + function _calcScaleNow(bytes calldata args) private view returns (uint256 scale) { + unchecked { + uint256 start = args.parseStartTimestamp(); - uint256 blockTs = block.timestamp; - if (blockTs < points.pointTs(0)) return 1 << 24; - if (points.pointTs(max) < blockTs) return 1 << 24; + // Shift for 5 bytes: [5 bytes timestamp], then read 3 bytes at each 5 bytes slot with `parsePointScale` + bytes calldata scales = args.slice(5); + // Shift for 8 bytes: [5 bytes timestamp, 3 bytes scales[0]], then read 2 bytes at each 5 bytes slot with `parseIntervalDuration` + bytes calldata durations = args.slice(8); - // For `num == max`, that's always false and loop exits, guaranteed by if statement above - // For `num == 0` that could be false if `pointTs(0) == blockTs`, so starting from `num == 1` - uint256 num = 1; - while (points.pointTs(num) < blockTs) { - unchecked { num++; } - } + // max == durations.length == scales.length - 1 + uint256 max = args.length / 5 - 1; // No underflow by (a) + + // Decrease time left instead of start and durations summation + uint256 timeLeft = block.timestamp; - uint256 prevPointTs = points.pointTs(num - 1); - uint256 nextPointTs = points.pointTs(num); + // Early exit: scaling starts in future, return initial scale + if (timeLeft <= start) return scales.parsePointScale(0) + 1; // No overflow by (b) + timeLeft -= start; // No underflow by the check above - // scale is in [1; 2 ** 24] range - scale = ( - (blockTs - prevPointTs) * points.pointScale(num) + - (nextPointTs - blockTs) * points.pointScale(num - 1) - ) / (nextPointTs - prevPointTs) + 1; + uint256 num = 0; + while (durations.parseIntervalDuration(num) < timeLeft) { + timeLeft -= durations.parseIntervalDuration(num); // No underflow by the check above and resulting `timeLeft > 0` + + // Exit: durations[max] does not exist, last scaling interval in past, return last scale + if (++num == max) return scales.parsePointScale(max) + 1; // No overflow by (b) + } + + // durations[num] >= timeLeft > 0 -> `duration != 0`, later division is safe + uint256 duration = durations.parseIntervalDuration(num); + + // Scale is in [1; 2 ** 24] range by the averaging property + (b) + scale = (timeLeft * scales.parsePointScale(num + 1) + (duration - timeLeft) * scales.parsePointScale(num)) / duration + 1; // No overflow by (c) + } } } diff --git a/test/PiecewiseLinearScale.t.sol b/test/PiecewiseLinearScale.t.sol index 28f9b2f..6953605 100644 --- a/test/PiecewiseLinearScale.t.sol +++ b/test/PiecewiseLinearScale.t.sol @@ -47,7 +47,8 @@ contract PiecewiseLinearScaleTest is Test, LimitOpcodesDebug { function _buildProgram( uint256 balanceIn, uint256 balanceOut, - uint40[] memory timestamps, + uint40 timestamp, + uint16[] memory durations, uint24[] memory scales, bool scaleIn ) internal view returns (bytes memory) { @@ -55,8 +56,8 @@ contract PiecewiseLinearScaleTest is Test, LimitOpcodesDebug { return bytes.concat( p.build(_staticBalancesXD, BalancesArgsBuilder.build(dynamic([address(tokenA), address(tokenB)]), dynamic([balanceIn, balanceOut]))), scaleIn - ? p.build(_piecewiseLinearScaleBalanceIn1D, PiecewiseLinearScaleArgsBuilder.build(timestamps, scales)) - : p.build(_piecewiseLinearScaleBalanceOut1D, PiecewiseLinearScaleArgsBuilder.build(timestamps, scales)), + ? p.build(_piecewiseLinearScaleBalanceIn1D, PiecewiseLinearScaleArgsBuilder.build(timestamp, durations, scales)) + : p.build(_piecewiseLinearScaleBalanceOut1D, PiecewiseLinearScaleArgsBuilder.build(timestamp, durations, scales)), p.build(_limitSwap1D, LimitSwapArgsBuilder.build(address(tokenA), address(tokenB))) ); } @@ -83,30 +84,30 @@ contract PiecewiseLinearScaleTest is Test, LimitOpcodesDebug { uint256 makingAmount, uint256 takingAmount, uint8 pointsCountSeed, - uint24[7] memory scaleSeed, - uint32[7] memory timestampGapSeed + uint32 timestampSeed, + uint24[17] memory scaleSeed, + uint16[16] memory durationSeed ) public { makingAmount = bound(makingAmount, 2, MAX_AMOUNT); takingAmount = bound(takingAmount, 2, MAX_AMOUNT); - uint256 pointsCount = bound(pointsCountSeed, 2, 7); - uint40[] memory timestamps = new uint40[](pointsCount); + uint256 pointsCount = bound(pointsCountSeed, 2, 17); uint24[] memory scales = new uint24[](pointsCount); + uint16[] memory durations = new uint16[](pointsCount - 1); + uint40 timestamp = timestampSeed; uint256 last = pointsCount - 1; - // Strictly increasing timestamps and descending, scales in [0, 2 ** 24 - 1] - timestamps[0] = uint40(bound(timestampGapSeed[0], 1, 1e9)); scales[0] = type(uint24).max; // Initial scale = 1.0 for (uint256 i = 1; i < pointsCount; i++) { - timestamps[i] = timestamps[i - 1] + uint40(bound(timestampGapSeed[i], 1, 1e9)); - scales[i] = uint24(bound(scaleSeed[i], 0, uint256(scales[i - 1]))); + scales[i] = uint24(bound(scaleSeed[i], 0, uint256(scales[i - 1]))); // Descending scales + durations[i - 1] = uint16(bound(durationSeed[i - 1], 1, type(uint16).max)); // Non-zero durations } // It is important that `balanceIn` is not `takingAmount` and should be calculated uint256 balanceIn = PiecewiseLinearScaleArgsBuilder.unscaleValue(takingAmount, scales[last]); - ISwapVM.Order memory order = _buildOrder(_buildProgram(balanceIn, makingAmount, timestamps, scales, true)); + ISwapVM.Order memory order = _buildOrder(_buildProgram(balanceIn, makingAmount, timestamp, durations, scales, true)); bytes memory takerDataExactIn = _buildTakerData(true); bytes memory takerDataExactOut = _buildTakerData(false); @@ -114,7 +115,7 @@ contract PiecewiseLinearScaleTest is Test, LimitOpcodesDebug { uint256 amountIn; // At the initial point the whole `makingAmount` sells for exactly `balanceIn`, and one wei less in buys strictly less - vm.warp(timestamps[0]); + vm.warp(timestamp); (, amountOut,) = swapVM.quote(order, address(tokenA), address(tokenB), balanceIn, takerDataExactIn); assertEq(amountOut, makingAmount); (, amountOut,) = swapVM.quote(order, address(tokenA), address(tokenB), balanceIn - 1, takerDataExactIn); @@ -127,7 +128,7 @@ contract PiecewiseLinearScaleTest is Test, LimitOpcodesDebug { assertLe(amountIn, balanceIn); // At the final point the whole `makingAmount` sells for exactly `takingAmount`, and one wei less in buys strictly less - vm.warp(timestamps[last]); + vm.warp(timestamp + _sum(durations, last)); (, amountOut,) = swapVM.quote(order, address(tokenA), address(tokenB), takingAmount, takerDataExactIn); assertEq(amountOut, makingAmount); (, amountOut,) = swapVM.quote(order, address(tokenA), address(tokenB), takingAmount - 1, takerDataExactIn); @@ -140,18 +141,18 @@ contract PiecewiseLinearScaleTest is Test, LimitOpcodesDebug { assertLe(amountIn, takingAmount); for (uint256 k = 1; k < pointsCount; k++) { - vm.warp(timestamps[k - 1]); + vm.warp(timestamp + _sum(durations, k - 1)); (uint256 amountInPast,,) = swapVM.quote(order, address(tokenA), address(tokenB), makingAmount, takerDataExactOut); // Predictable at exact point - vm.warp(timestamps[k]); + vm.warp(timestamp + _sum(durations, k)); (uint256 amountInNext,,) = swapVM.quote(order, address(tokenA), address(tokenB), makingAmount, takerDataExactOut); assertEq(amountInNext, PiecewiseLinearScaleArgsBuilder.scaleValue(balanceIn, scales[k])); // Mid point - vm.warp((timestamps[k - 1] + timestamps[k]) / 2); + vm.warp(timestamp + (_sum(durations, k - 1) + _sum(durations, k)) / 2); (uint256 amountInMidLeft,,) = swapVM.quote(order, address(tokenA), address(tokenB), makingAmount, takerDataExactOut); - vm.warp((timestamps[k - 1] + timestamps[k] + 1) / 2); + vm.warp(timestamp + (_sum(durations, k - 1) + _sum(durations, k) + 1) / 2); (uint256 amountInMidRight,,) = swapVM.quote(order, address(tokenA), address(tokenB), makingAmount, takerDataExactOut); assertApproxEqAbs((amountInMidLeft + amountInMidRight) / 2, (amountInPast + amountInNext) / 2, (balanceIn >> 24) + 1); } @@ -163,30 +164,30 @@ contract PiecewiseLinearScaleTest is Test, LimitOpcodesDebug { uint256 makingAmount, uint256 takingAmount, uint8 pointsCountSeed, - uint24[7] memory scaleSeed, - uint32[7] memory timestampGapSeed + uint32 timestampSeed, + uint24[17] memory scaleSeed, + uint16[16] memory durationSeed ) public { makingAmount = bound(makingAmount, 2, MAX_AMOUNT); takingAmount = bound(takingAmount, 2, MAX_AMOUNT); - uint256 pointsCount = bound(pointsCountSeed, 2, 7); - uint40[] memory timestamps = new uint40[](pointsCount); + uint256 pointsCount = bound(pointsCountSeed, 2, 17); uint24[] memory scales = new uint24[](pointsCount); + uint16[] memory durations = new uint16[](pointsCount - 1); + uint40 timestamp = timestampSeed; uint256 last = pointsCount - 1; - // Strictly increasing timestamps and ascending, scales in [0, 2**24 - 1] - timestamps[0] = uint40(bound(timestampGapSeed[0], 1, 1e9)); // Initial scale set so that minimal balanceOut >= 2 scales[0] = uint24(bound(scaleSeed[0], (2 * 2 ** 24 + makingAmount - 1) / makingAmount - 1, type(uint24).max)); for (uint256 i = 1; i < pointsCount; i++) { - timestamps[i] = timestamps[i - 1] + uint40(bound(timestampGapSeed[i], 1, 1e9)); - scales[i] = uint24(bound(scaleSeed[i], uint256(scales[i - 1]), type(uint24).max)); + scales[i] = uint24(bound(scaleSeed[i], uint256(scales[i - 1]), type(uint24).max)); // Ascending scales + durations[i - 1] = uint16(bound(durationSeed[i - 1], 1, type(uint16).max)); // Non-zero durations } scales[last] = type(uint24).max; // Last scale = 1.0 // Here `balanceOut = makingAmount` with last scale 1.0 - ISwapVM.Order memory order = _buildOrder(_buildProgram(takingAmount, makingAmount, timestamps, scales, false)); + ISwapVM.Order memory order = _buildOrder(_buildProgram(takingAmount, makingAmount, timestamp, durations, scales, false)); bytes memory takerDataExactIn = _buildTakerData(true); bytes memory takerDataExactOut = _buildTakerData(false); @@ -197,7 +198,7 @@ contract PiecewiseLinearScaleTest is Test, LimitOpcodesDebug { uint256 balanceOutInitial = PiecewiseLinearScaleArgsBuilder.scaleValue(makingAmount, scales[0]); // At the initial point the whole `takingAmount` sells for exactly `balanceOutInitial`, and one wei less in sells strictly less - vm.warp(timestamps[0]); + vm.warp(timestamp); (, amountOut,) = swapVM.quote(order, address(tokenA), address(tokenB), takingAmount, takerDataExactIn); assertEq(amountOut, balanceOutInitial); (, amountOut,) = swapVM.quote(order, address(tokenA), address(tokenB), takingAmount - 1, takerDataExactIn); @@ -210,7 +211,7 @@ contract PiecewiseLinearScaleTest is Test, LimitOpcodesDebug { assertLe(amountIn, takingAmount); // At the final point the whole `takingAmount` sells for exactly `makingAmount`, and one wei less in sells strictly less - vm.warp(timestamps[last]); + vm.warp(timestamp + _sum(durations, last)); (, amountOut,) = swapVM.quote(order, address(tokenA), address(tokenB), takingAmount, takerDataExactIn); assertEq(amountOut, makingAmount); @@ -224,18 +225,18 @@ contract PiecewiseLinearScaleTest is Test, LimitOpcodesDebug { assertLe(amountIn, takingAmount); for (uint256 k = 1; k < pointsCount; k++) { - vm.warp(timestamps[k - 1]); + vm.warp(timestamp + _sum(durations, k - 1)); (, uint256 amountOutPast,) = swapVM.quote(order, address(tokenA), address(tokenB), takingAmount, takerDataExactIn); // Predictable at exact point - vm.warp(timestamps[k]); + vm.warp(timestamp + _sum(durations, k)); (, uint256 amountOutNext,) = swapVM.quote(order, address(tokenA), address(tokenB), takingAmount, takerDataExactIn); assertEq(amountOutNext, PiecewiseLinearScaleArgsBuilder.scaleValue(makingAmount, scales[k])); // Mid point - vm.warp((timestamps[k - 1] + timestamps[k]) / 2); + vm.warp(timestamp + (_sum(durations, k - 1) + _sum(durations, k)) / 2); (, uint256 amountOutMidLeft,) = swapVM.quote(order, address(tokenA), address(tokenB), takingAmount, takerDataExactIn); - vm.warp((timestamps[k - 1] + timestamps[k] + 1) / 2); + vm.warp(timestamp + (_sum(durations, k - 1) + _sum(durations, k) + 1) / 2); (, uint256 amountOutMidRight,) = swapVM.quote(order, address(tokenA), address(tokenB), takingAmount, takerDataExactIn); assertApproxEqAbs((amountOutMidLeft + amountOutMidRight) / 2, (amountOutPast + amountOutNext) / 2, (makingAmount >> 24) + 1); } @@ -248,18 +249,19 @@ contract PiecewiseLinearScaleTest is Test, LimitOpcodesDebug { uint256 balanceIn = 1000e18; uint256 balanceOut = 4000e18; - for (uint256 length = 2; length < 32; ++length) { - uint40[] memory timestamps = new uint40[](length); + for (uint256 length = 2; length < 51; ++length) { + uint16[] memory durations = new uint16[](length - 1); uint24[] memory scales = new uint24[](length); - timestamps[0] = 50; - scales[0] = type(uint24).max; + uint40 timestamp = 50; + for (uint256 i = 0; i < length; ++i) { + scales[i] = type(uint24).max; + } for (uint256 i = 1; i < length; ++i) { - timestamps[i] = timestamps[i - 1] + 100; - scales[i] = scales[0]; + durations[i - 1] = 100; } - ISwapVM.Order memory order = _buildOrder(_buildProgram(balanceIn, balanceOut, timestamps, scales, true)); + ISwapVM.Order memory order = _buildOrder(_buildProgram(balanceIn, balanceOut, timestamp, durations, scales, true)); bytes memory takerDataExactIn = _buildTakerData(true); uint256 amountIn = 10_000_000; @@ -267,7 +269,7 @@ contract PiecewiseLinearScaleTest is Test, LimitOpcodesDebug { uint256 usage; uint256 worst; - for (uint256 i = 1; i < length; ++i) { + for (uint256 i = 0; i <= length; ++i) { vm.warp(i * 100); uint256 gas = gasleft(); swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); @@ -276,23 +278,24 @@ contract PiecewiseLinearScaleTest is Test, LimitOpcodesDebug { if (worst < temp) worst = temp; } - console.log(usage / (length - 1), worst, length); + console.log(usage / (length + 1), worst, length); } } - function test_PiecewiseLinearScale_ExactIn_5points_ScaleIn_Basic() public { + function test_PiecewiseLinearScale_Basic() public { uint256 balanceIn = 1000e18; uint256 balanceOut = 4000e18; - uint40[] memory timestamps = new uint40[](5); - uint24[] memory scales = new uint24[](5); - timestamps[0] = 1000; scales[0] = uint24(2 ** 24 - 1); - timestamps[1] = 1100; scales[1] = uint24(2 ** 23 - 1); - timestamps[2] = 1300; scales[2] = uint24(2 ** 20 * 5 - 1); - timestamps[3] = 1400; scales[3] = uint24(2 ** 22 - 1); - timestamps[4] = 1500; scales[4] = uint24(2 ** 20 * 3 - 1); + uint16[] memory durations = new uint16[](5); + uint24[] memory scales = new uint24[](6); + uint40 timestamp = 1000; scales[0] = uint24(2 ** 24 - 1); + durations[0] = 100; scales[1] = uint24(2 ** 23 - 1); + durations[1] = 200; scales[2] = uint24(2 ** 20 * 5 - 1); + durations[2] = 100; scales[3] = uint24(2 ** 22 - 1); + durations[3] = 0; scales[4] = uint24(2 ** 21 - 1); + durations[4] = 100; scales[5] = uint24(2 ** 20 * 3 - 1); - ISwapVM.Order memory order = _buildOrder(_buildProgram(balanceIn, balanceOut, timestamps, scales, true)); + ISwapVM.Order memory order = _buildOrder(_buildProgram(balanceIn, balanceOut, timestamp, durations, scales, true)); bytes memory takerDataExactIn = _buildTakerData(true); bytes memory takerDataExactOut = _buildTakerData(false); @@ -376,6 +379,20 @@ contract PiecewiseLinearScaleTest is Test, LimitOpcodesDebug { (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); assertEq(amountInCalc, 6_250_000); } + { + vm.warp(1425); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 284_444_444); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 3_515_625); + } + { + vm.warp(1450); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 256_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 3_906_250); + } { vm.warp(1500); (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); @@ -386,10 +403,370 @@ contract PiecewiseLinearScaleTest is Test, LimitOpcodesDebug { { vm.warp(1501); (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 213_333_333); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 4_687_500); + } + { + vm.warp(100_000); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 213_333_333); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 4_687_500); + } + } + + function test_PiecewiseLinearScale_ZeroDuration_Single() public { + uint256 balanceIn = 1000e18; + uint256 balanceOut = 4000e18; + + uint256 amountIn = 10_000_000; + uint256 amountOut = 100_000_000; + + uint16[] memory durations = new uint16[](1); + uint24[] memory scales = new uint24[](2); + uint40 timestamp = 1000; scales[0] = uint24(2 ** 24 - 1); + durations[0] = 0; scales[1] = uint24(2 ** 23 - 1); + + ISwapVM.Order memory order = _buildOrder(_buildProgram(balanceIn, balanceOut, timestamp, durations, scales, true)); + bytes memory takerDataExactIn = _buildTakerData(true); + bytes memory takerDataExactOut = _buildTakerData(false); + + { + vm.warp(999); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 40_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 25_000_000); + } + { + vm.warp(1000); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 40_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 25_000_000); + } + { + vm.warp(1001); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 80_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 12_500_000); + } + { + vm.warp(1002); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 80_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 12_500_000); + } + } + + function test_PiecewiseLinearScale_ZeroDuration_Double() public { + uint256 balanceIn = 1000e18; + uint256 balanceOut = 4000e18; + + uint256 amountIn = 10_000_000; + uint256 amountOut = 100_000_000; + + uint16[] memory durations = new uint16[](2); + uint24[] memory scales = new uint24[](3); + uint40 timestamp = 1000; scales[0] = uint24(2 ** 24 - 1); + durations[0] = 0; scales[1] = uint24(2 ** 23 - 1); + durations[1] = 0; scales[2] = uint24(2 ** 22 - 1); + + ISwapVM.Order memory order = _buildOrder(_buildProgram(balanceIn, balanceOut, timestamp, durations, scales, true)); + bytes memory takerDataExactIn = _buildTakerData(true); + bytes memory takerDataExactOut = _buildTakerData(false); + + { + vm.warp(999); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); assertEq(amountOutCalc, 40_000_000); (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); assertEq(amountInCalc, 25_000_000); } + { + vm.warp(1000); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 40_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 25_000_000); + } + { + vm.warp(1001); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 160_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 6_250_000); + } + { + vm.warp(1002); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 160_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 6_250_000); + } + } + + function test_PiecewiseLinearScale_ZeroDuration_SingleWrapped() public { + uint256 balanceIn = 1000e18; + uint256 balanceOut = 4000e18; + + uint256 amountIn = 10_000_000; + uint256 amountOut = 100_000_000; + + uint16[] memory durations = new uint16[](3); + uint24[] memory scales = new uint24[](4); + uint40 timestamp = 1000; scales[0] = uint24(2 ** 24 - 1); + durations[0] = 2; scales[1] = uint24(2 ** 23 - 1); + durations[1] = 0; scales[2] = uint24(2 ** 22 - 1); + durations[2] = 2; scales[3] = uint24(2 ** 21 - 1); + + ISwapVM.Order memory order = _buildOrder(_buildProgram(balanceIn, balanceOut, timestamp, durations, scales, true)); + bytes memory takerDataExactIn = _buildTakerData(true); + bytes memory takerDataExactOut = _buildTakerData(false); + + { + vm.warp(999); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 40_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 25_000_000); + } + { + vm.warp(1000); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 40_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 25_000_000); + } + { + vm.warp(1001); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 53_333_333); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 18_750_000); + } + { + vm.warp(1002); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 80_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 12_500_000); + } + { + vm.warp(1003); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 213_333_333); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 4_687_500); + } + { + vm.warp(1004); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 320_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 3_125_000); + } + { + vm.warp(1005); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 320_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 3_125_000); + } + } + + function test_PiecewiseLinearScale_ZeroDuration_DoubleWrapped() public { + uint256 balanceIn = 1000e18; + uint256 balanceOut = 4000e18; + + uint256 amountIn = 10_000_000; + uint256 amountOut = 100_000_000; + + uint16[] memory durations = new uint16[](4); + uint24[] memory scales = new uint24[](5); + uint40 timestamp = 1000; scales[0] = uint24(2 ** 24 - 1); + durations[0] = 2; scales[1] = uint24(2 ** 23 - 1); + durations[1] = 0; scales[2] = uint24(2 ** 21 * 3 - 1); + durations[2] = 0; scales[3] = uint24(2 ** 22 - 1); + durations[3] = 2; scales[4] = uint24(2 ** 21 - 1); + + ISwapVM.Order memory order = _buildOrder(_buildProgram(balanceIn, balanceOut, timestamp, durations, scales, true)); + bytes memory takerDataExactIn = _buildTakerData(true); + bytes memory takerDataExactOut = _buildTakerData(false); + + { + vm.warp(999); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 40_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 25_000_000); + } + { + vm.warp(1000); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 40_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 25_000_000); + } + { + vm.warp(1001); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 53_333_333); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 18_750_000); + } + { + vm.warp(1002); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 80_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 12_500_000); + } + { + vm.warp(1003); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 213_333_333); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 4_687_500); + } + { + vm.warp(1004); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 320_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 3_125_000); + } + { + vm.warp(1005); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 320_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 3_125_000); + } + } + + function test_PiecewiseLinearScale_ZeroDuration_SingleFirst() public { + uint256 balanceIn = 1000e18; + uint256 balanceOut = 4000e18; + + uint256 amountIn = 10_000_000; + uint256 amountOut = 100_000_000; + + uint16[] memory durations = new uint16[](2); + uint24[] memory scales = new uint24[](3); + uint40 timestamp = 1000; scales[0] = uint24(2 ** 24 - 1); + durations[0] = 0; scales[1] = uint24(2 ** 23 - 1); + durations[1] = 2; scales[2] = uint24(2 ** 22 - 1); + + ISwapVM.Order memory order = _buildOrder(_buildProgram(balanceIn, balanceOut, timestamp, durations, scales, true)); + bytes memory takerDataExactIn = _buildTakerData(true); + bytes memory takerDataExactOut = _buildTakerData(false); + + { + vm.warp(999); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 40_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 25_000_000); + } + { + vm.warp(1000); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 40_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 25_000_000); + } + { + vm.warp(1001); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 106_666_666); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 9_375_000); + } + { + vm.warp(1002); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 160_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 6_250_000); + } + { + vm.warp(1003); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 160_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 6_250_000); + } + } + + function test_PiecewiseLinearScale_ZeroDuration_SingleLast() public { + uint256 balanceIn = 1000e18; + uint256 balanceOut = 4000e18; + + uint256 amountIn = 10_000_000; + uint256 amountOut = 100_000_000; + + uint16[] memory durations = new uint16[](2); + uint24[] memory scales = new uint24[](3); + uint40 timestamp = 1000; scales[0] = uint24(2 ** 24 - 1); + durations[0] = 2; scales[1] = uint24(2 ** 23 - 1); + durations[1] = 0; scales[2] = uint24(2 ** 22 - 1); + + ISwapVM.Order memory order = _buildOrder(_buildProgram(balanceIn, balanceOut, timestamp, durations, scales, true)); + bytes memory takerDataExactIn = _buildTakerData(true); + bytes memory takerDataExactOut = _buildTakerData(false); + + { + vm.warp(999); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 40_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 25_000_000); + } + { + vm.warp(1000); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 40_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 25_000_000); + } + { + vm.warp(1001); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 53_333_333); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 18_750_000); + } + { + vm.warp(1002); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 80_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 12_500_000); + } + { + vm.warp(1003); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 160_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 6_250_000); + } + { + vm.warp(1003); + (, uint256 amountOutCalc,) = swapVM.quote(order, address(tokenA), address(tokenB), amountIn, takerDataExactIn); + assertEq(amountOutCalc, 160_000_000); + (uint256 amountInCalc,,) = swapVM.quote(order, address(tokenA), address(tokenB), amountOut, takerDataExactOut); + assertEq(amountInCalc, 6_250_000); + } + } + + function _sum(uint16[] memory durations, uint256 n) internal pure returns (uint40 sum) { + for (uint256 i; i < n; ++i) { + sum += durations[i]; + } } function _buildOrder(bytes memory program) internal view returns (ISwapVM.Order memory) { From 66c26941b9ba8b6d45c40e98f72277ae1dc26057 Mon Sep 17 00:00:00 2001 From: Stepan Chekhovskoi Date: Thu, 11 Jun 2026 15:42:28 +0200 Subject: [PATCH 2/2] gas snapshot --- .gas-snapshot | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 65a7136..278b09a 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -454,8 +454,14 @@ PeggedSwapTest:test_PeggedSwap_SwapAbundantForScarce() (gas: 176597) PeggedSwapTest:test_PeggedSwap_SwapScarceForAbundant() (gas: 176987) PeggedSwapTest:test_PeggedSwap_WithFee() (gas: 318503) PeggedSwapTest:test_PeggedSwap_ZeroBalanceIn_SwapSucceeds() (gas: 177769) -PiecewiseLinearScaleTest:test_PiecewiseLinearScale_ExactIn_5points_ScaleIn_Basic() (gas: 575441) -PiecewiseLinearScaleTest:test_PiecewiseLinearScale_GasBenchmark() (gas: 11618785) +PiecewiseLinearScaleTest:test_PiecewiseLinearScale_Basic() (gas: 680126) +PiecewiseLinearScaleTest:test_PiecewiseLinearScale_GasBenchmark() (gas: 32232558) +PiecewiseLinearScaleTest:test_PiecewiseLinearScale_ZeroDuration_Double() (gas: 214218) +PiecewiseLinearScaleTest:test_PiecewiseLinearScale_ZeroDuration_DoubleWrapped() (gas: 331739) +PiecewiseLinearScaleTest:test_PiecewiseLinearScale_ZeroDuration_Single() (gas: 212670) +PiecewiseLinearScaleTest:test_PiecewiseLinearScale_ZeroDuration_SingleFirst() (gas: 252656) +PiecewiseLinearScaleTest:test_PiecewiseLinearScale_ZeroDuration_SingleLast() (gas: 290725) +PiecewiseLinearScaleTest:test_PiecewiseLinearScale_ZeroDuration_SingleWrapped() (gas: 330932) ProgressiveFeeTest:test_ProgressiveFeeIn_ConsistentForExactInAndExactOut() (gas: 117013) ProgressiveFeeTest:test_ProgressiveFeeIn_ExactIn_DecreasesBySplittingAmount() (gas: 217200) ProgressiveFeeTest:test_ProgressiveFeeIn_ExactIn_IncreasesWithLargerSwaps() (gas: 109950)