From 59153044b910bb6d920f25cfc25d2469648421ad Mon Sep 17 00:00:00 2001 From: Cody Born Date: Wed, 28 Aug 2024 19:43:39 -0600 Subject: [PATCH] Binary search --- src/lib/NonLinearDutchDecayLib.sol | 84 ++++++++++++++++----- src/reactors/NonLinearDutchOrderReactor.sol | 13 ---- test/lib/NonLinearDutchDecayLib.t.sol | 74 +++++++++++++++++- 3 files changed, 140 insertions(+), 31 deletions(-) diff --git a/src/lib/NonLinearDutchDecayLib.sol b/src/lib/NonLinearDutchDecayLib.sol index d6937dcb..e8f7ee54 100644 --- a/src/lib/NonLinearDutchDecayLib.sol +++ b/src/lib/NonLinearDutchDecayLib.sol @@ -7,40 +7,88 @@ import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; import {sub} from "./MathExt.sol"; import {Uint16Array, fromUnderlying} from "../types/Uint16Array.sol"; +/// @notice thrown when the decay curve is invalid +error InvalidDecayCurve(); + /// @notice helpers for handling non-linear dutch order objects library NonLinearDutchDecayLib { + using FixedPointMathLib for uint256; using {sub} for uint256; /// @notice locates the current position on the curve and calculates the decay + /// @param curve The curve to search + /// @param startAmount The absolute start amount + /// @param decayStartBlock The absolute start block of the decay function decay(NonLinearDecay memory curve, uint256 startAmount, uint256 decayStartBlock) internal view returns (uint256 decayedAmount) { + // mismatch of relativeAmounts and relativeBlocks + if(curve.relativeAmounts.length > 16) { + revert InvalidDecayCurve(); + } + // handle current block before decay or no decay - if (decayStartBlock >= block.number) { + if (decayStartBlock >= block.number || curve.relativeAmounts.length == 0) { return startAmount; } + Uint16Array relativeBlocks = fromUnderlying(curve.relativeBlocks); uint16 blockDelta = uint16(block.number - decayStartBlock); - // iterate through the points and locate the current segment - for (uint16 i = 0; i < curve.relativeAmounts.length; i++) { - // relativeBlocks is an array of uint16 packed one uint256 - Uint16Array relativeBlocks = fromUnderlying(curve.relativeBlocks); - uint16 relativeBlock = relativeBlocks.getElement(i); - if (relativeBlock >= blockDelta) { - uint256 lastAmount = startAmount; - uint16 relativeStartBlock = 0; - if (i != 0) { - lastAmount = startAmount.sub(curve.relativeAmounts[i - 1]); - relativeStartBlock = relativeBlocks.getElement(i - 1); + // Special case for when we need to use the decayStartBlock (0) + if (relativeBlocks.getElement(0) > blockDelta) { + return linearDecay(0, relativeBlocks.getElement(0), blockDelta, startAmount, startAmount.sub(curve.relativeAmounts[0])); + } + // the current pos is within or after the curve + uint16 prev; + uint16 next; + (prev, next) = locateCurvePosition(curve, blockDelta); + uint256 lastAmount = startAmount.sub(curve.relativeAmounts[prev]); + uint256 nextAmount = startAmount.sub(curve.relativeAmounts[next]); + return linearDecay(relativeBlocks.getElement(prev), relativeBlocks.getElement(next), blockDelta, lastAmount, nextAmount); + } + + /// @notice Locates the current position on the curve using a binary search + /// @param curve The curve to search + /// @param currentRelativeBlock The current relative position + /// @return prev The relative block before the current position + /// @return next The relative block after the current position + function locateCurvePosition(NonLinearDecay memory curve, uint16 currentRelativeBlock) + internal + pure + returns (uint16 prev, uint16 next) + { + Uint16Array relativeBlocks = fromUnderlying(curve.relativeBlocks); + uint16 left = 0; + uint16 right = uint16(curve.relativeAmounts.length) - 1; + uint16 mid; + + while (left <= right) { + mid = left + (right - left) / 2; + uint16 midBlock = relativeBlocks.getElement(mid); + + if (midBlock == currentRelativeBlock) { + return (mid, mid); + } else if (midBlock > currentRelativeBlock) { + if (mid == 0) { + return (mid, mid); + } else if (relativeBlocks.getElement(mid - 1) < currentRelativeBlock) { + return (mid - 1, mid); + } else { + right = mid - 1; + } + } else { + if (mid == curve.relativeAmounts.length - 1) { + return (mid, mid); + } else if (relativeBlocks.getElement(mid + 1) > currentRelativeBlock) { + return (mid, mid + 1); + } else { + left = mid + 1; } - uint256 nextAmount = startAmount.sub(curve.relativeAmounts[i]); - return linearDecay(relativeStartBlock, relativeBlock, blockDelta, lastAmount, nextAmount); } } - // handle current block after last decay block - decayedAmount = startAmount.sub(curve.relativeAmounts[curve.relativeAmounts.length - 1]); + revert InvalidDecayCurve(); } /// @notice returns the linear interpolation between the two points @@ -56,6 +104,9 @@ library NonLinearDutchDecayLib { uint256 startAmount, uint256 endAmount ) internal pure returns (uint256) { + if (currentBlock >= endBlock) { + return endAmount; + } unchecked { uint256 elapsed = currentBlock - startBlock; uint256 duration = endBlock - startBlock; @@ -67,7 +118,6 @@ library NonLinearDutchDecayLib { } } - /// @notice returns a decayed output using the given dutch spec and times /// @param output The output to decay /// @param decayStartBlock The block to start decaying diff --git a/src/reactors/NonLinearDutchOrderReactor.sol b/src/reactors/NonLinearDutchOrderReactor.sol index 456922c6..0b02da29 100644 --- a/src/reactors/NonLinearDutchOrderReactor.sol +++ b/src/reactors/NonLinearDutchOrderReactor.sol @@ -31,9 +31,6 @@ contract NonLinearDutchOrderReactor is BaseReactor { using NonLinearDutchDecayLib for NonLinearDutchInput; using ExclusivityLib for ResolvedOrder; - /// @notice thrown when the decay curve is invalid - error InvalidDecayCurve(); - /// @notice thrown when an order's deadline is passed error DeadlineReached(); @@ -116,19 +113,9 @@ contract NonLinearDutchOrderReactor is BaseReactor { } /// @notice validate the dutch order fields - /// - decay curves are defined /// - deadline must have not passed /// @dev Throws if the order is invalid function _validateOrder(bytes32 orderHash, NonLinearDutchOrder memory order) internal pure { - if (order.baseInput.curve.relativeAmounts.length == 0 || order.baseInput.curve.relativeAmounts.length > 16) { - revert InvalidDecayCurve(); - } - for (uint256 i = 0; i < order.baseOutputs.length; i++) { - if (order.baseOutputs[i].curve.relativeAmounts.length == 0) { - revert InvalidDecayCurve(); - } - } - (bytes32 r, bytes32 s) = abi.decode(order.cosignature, (bytes32, bytes32)); uint8 v = uint8(order.cosignature[64]); // cosigner signs over (orderHash || cosignerData) diff --git a/test/lib/NonLinearDutchDecayLib.t.sol b/test/lib/NonLinearDutchDecayLib.t.sol index 2f89539f..83befc1e 100644 --- a/test/lib/NonLinearDutchDecayLib.t.sol +++ b/test/lib/NonLinearDutchDecayLib.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import {Test} from "forge-std/Test.sol"; import {console} from "forge-std/console.sol"; -import {NonLinearDutchDecayLib} from "../../src/lib/NonLinearDutchDecayLib.sol"; +import {NonLinearDutchDecayLib, InvalidDecayCurve} from "../../src/lib/NonLinearDutchDecayLib.sol"; import {NonLinearDutchOutput, NonLinearDutchInput, NonLinearDecay} from "../../src/lib/NonLinearDutchOrderLib.sol"; import {Uint16Array, toUint256} from "../../src/types/Uint16Array.sol"; import {ArrayBuilder} from "../util/ArrayBuilder.sol"; @@ -19,8 +19,68 @@ contract MockNonLinearDutchDecayLibContract { contract NonLinearDutchDecayLibTest is Test { MockNonLinearDutchDecayLibContract mockNonLinearDutchDecayLibContract = new MockNonLinearDutchDecayLibContract(); + function testLocateCurvePositionSingle() public { + NonLinearDecay memory curve = NonLinearDecay({ + relativeBlocks: toUint256(ArrayBuilder.fillUint16(1, 1)), + relativeAmounts: ArrayBuilder.fillInt(1, 0) + }); + uint16 prev; + uint16 next; + (prev, next) = NonLinearDutchDecayLib.locateCurvePosition(curve, 1); + assertEq(prev, 0); + assertEq(next, 0); + + (prev, next) = NonLinearDutchDecayLib.locateCurvePosition(curve, 2); + assertEq(prev, 0); + assertEq(next, 0); + } + + + function testLocateCurvePositionMulti() public { + uint16[] memory blocks = new uint16[](3); + blocks[0] = 100; // block 200 + blocks[1] = 200; // block 300 + blocks[2] = 300; // block 400 + int256[] memory decayAmounts = new int256[](3); + decayAmounts[0] = -1 ether; // 2 ether + decayAmounts[1] = 0 ether; // 1 ether + decayAmounts[2] = 1 ether; // 0 ether + NonLinearDecay memory curve = + NonLinearDecay({relativeBlocks: toUint256(blocks), relativeAmounts: decayAmounts}); + + uint16 prev; + uint16 next; + (prev, next) = NonLinearDutchDecayLib.locateCurvePosition(curve, 150); + assertEq(prev, 0); + assertEq(next, 1); + + (prev, next) = NonLinearDutchDecayLib.locateCurvePosition(curve, 200); + assertEq(prev, 1); + assertEq(next, 1); + + (prev, next) = NonLinearDutchDecayLib.locateCurvePosition(curve, 250); + assertEq(prev, 1); + assertEq(next, 2); + + (prev, next) = NonLinearDutchDecayLib.locateCurvePosition(curve, 300); + assertEq(prev, 2); + assertEq(next, 2); + + (prev, next) = NonLinearDutchDecayLib.locateCurvePosition(curve, 350); + assertEq(prev, 2); + assertEq(next, 2); + } + function testDutchDecayNoDecay(uint256 startAmount, uint256 decayStartBlock) public { + // Empty curve NonLinearDecay memory curve = NonLinearDecay({ + relativeBlocks: toUint256(ArrayBuilder.fillUint16(0, 0)), + relativeAmounts: ArrayBuilder.fillInt(0, 0) + }); + assertEq(NonLinearDutchDecayLib.decay(curve, startAmount, decayStartBlock), startAmount); + + // Single value with 0 amount change + curve = NonLinearDecay({ relativeBlocks: toUint256(ArrayBuilder.fillUint16(1, 1)), relativeAmounts: ArrayBuilder.fillInt(1, 0) }); @@ -279,4 +339,16 @@ contract NonLinearDutchDecayLibTest is Test { vm.expectRevert(); mockNonLinearDutchDecayLibContract.decay(curve, startAmount, decayStartBlock); } + + function testDutchMismatchedDecay() public { + uint256 decayStartBlock = 100; + uint256 startAmount = 1 ether; + NonLinearDecay memory curve = NonLinearDecay({ + relativeBlocks: toUint256(ArrayBuilder.fillUint16(16, 1)), + relativeAmounts: ArrayBuilder.fillInt(17, 0) + }); + vm.roll(150); + vm.expectRevert(InvalidDecayCurve.selector); + mockNonLinearDutchDecayLibContract.decay(curve, startAmount, decayStartBlock); + } }