From f0172ecbe165dede1ab2ec2bedb868c0bc6441b0 Mon Sep 17 00:00:00 2001 From: Mark Toda Date: Thu, 14 Dec 2023 15:47:05 -0500 Subject: [PATCH] fix: tests --- ...V2DutchOrder-BaseExecuteSingleWithFee.snap | 1 + .../Base-V2DutchOrder-ExecuteBatch.snap | 1 + ...utchOrder-ExecuteBatchMultipleOutputs.snap | 1 + ...teBatchMultipleOutputsDifferentTokens.snap | 1 + ...V2DutchOrder-ExecuteBatchNativeOutput.snap | 1 + .../Base-V2DutchOrder-ExecuteSingle.snap | 1 + ...2DutchOrder-ExecuteSingleNativeOutput.snap | 1 + ...-V2DutchOrder-ExecuteSingleValidation.snap | 1 + src/lib/V2DutchOrderLib.sol | 89 ++++++++++++ src/reactors/V2DutchOrderReactor.sol | 135 ++++++++++++++++++ test/reactors/V2DutchOrderReactor.t.sol | 108 ++++++++++++++ test/util/PermitSignature.sol | 21 +++ 12 files changed, 361 insertions(+) create mode 100644 .forge-snapshots/Base-V2DutchOrder-BaseExecuteSingleWithFee.snap create mode 100644 .forge-snapshots/Base-V2DutchOrder-ExecuteBatch.snap create mode 100644 .forge-snapshots/Base-V2DutchOrder-ExecuteBatchMultipleOutputs.snap create mode 100644 .forge-snapshots/Base-V2DutchOrder-ExecuteBatchMultipleOutputsDifferentTokens.snap create mode 100644 .forge-snapshots/Base-V2DutchOrder-ExecuteBatchNativeOutput.snap create mode 100644 .forge-snapshots/Base-V2DutchOrder-ExecuteSingle.snap create mode 100644 .forge-snapshots/Base-V2DutchOrder-ExecuteSingleNativeOutput.snap create mode 100644 .forge-snapshots/Base-V2DutchOrder-ExecuteSingleValidation.snap create mode 100644 src/lib/V2DutchOrderLib.sol create mode 100644 src/reactors/V2DutchOrderReactor.sol create mode 100644 test/reactors/V2DutchOrderReactor.t.sol diff --git a/.forge-snapshots/Base-V2DutchOrder-BaseExecuteSingleWithFee.snap b/.forge-snapshots/Base-V2DutchOrder-BaseExecuteSingleWithFee.snap new file mode 100644 index 00000000..e2faebe3 --- /dev/null +++ b/.forge-snapshots/Base-V2DutchOrder-BaseExecuteSingleWithFee.snap @@ -0,0 +1 @@ +189606 \ No newline at end of file diff --git a/.forge-snapshots/Base-V2DutchOrder-ExecuteBatch.snap b/.forge-snapshots/Base-V2DutchOrder-ExecuteBatch.snap new file mode 100644 index 00000000..3d868bd1 --- /dev/null +++ b/.forge-snapshots/Base-V2DutchOrder-ExecuteBatch.snap @@ -0,0 +1 @@ +212188 \ No newline at end of file diff --git a/.forge-snapshots/Base-V2DutchOrder-ExecuteBatchMultipleOutputs.snap b/.forge-snapshots/Base-V2DutchOrder-ExecuteBatchMultipleOutputs.snap new file mode 100644 index 00000000..bcad2798 --- /dev/null +++ b/.forge-snapshots/Base-V2DutchOrder-ExecuteBatchMultipleOutputs.snap @@ -0,0 +1 @@ +222748 \ No newline at end of file diff --git a/.forge-snapshots/Base-V2DutchOrder-ExecuteBatchMultipleOutputsDifferentTokens.snap b/.forge-snapshots/Base-V2DutchOrder-ExecuteBatchMultipleOutputsDifferentTokens.snap new file mode 100644 index 00000000..11bb5f60 --- /dev/null +++ b/.forge-snapshots/Base-V2DutchOrder-ExecuteBatchMultipleOutputsDifferentTokens.snap @@ -0,0 +1 @@ +277211 \ No newline at end of file diff --git a/.forge-snapshots/Base-V2DutchOrder-ExecuteBatchNativeOutput.snap b/.forge-snapshots/Base-V2DutchOrder-ExecuteBatchNativeOutput.snap new file mode 100644 index 00000000..501fb006 --- /dev/null +++ b/.forge-snapshots/Base-V2DutchOrder-ExecuteBatchNativeOutput.snap @@ -0,0 +1 @@ +205714 \ No newline at end of file diff --git a/.forge-snapshots/Base-V2DutchOrder-ExecuteSingle.snap b/.forge-snapshots/Base-V2DutchOrder-ExecuteSingle.snap new file mode 100644 index 00000000..ca5523c0 --- /dev/null +++ b/.forge-snapshots/Base-V2DutchOrder-ExecuteSingle.snap @@ -0,0 +1 @@ +155794 \ No newline at end of file diff --git a/.forge-snapshots/Base-V2DutchOrder-ExecuteSingleNativeOutput.snap b/.forge-snapshots/Base-V2DutchOrder-ExecuteSingleNativeOutput.snap new file mode 100644 index 00000000..99c3d6bd --- /dev/null +++ b/.forge-snapshots/Base-V2DutchOrder-ExecuteSingleNativeOutput.snap @@ -0,0 +1 @@ +141360 \ No newline at end of file diff --git a/.forge-snapshots/Base-V2DutchOrder-ExecuteSingleValidation.snap b/.forge-snapshots/Base-V2DutchOrder-ExecuteSingleValidation.snap new file mode 100644 index 00000000..fcae6ec6 --- /dev/null +++ b/.forge-snapshots/Base-V2DutchOrder-ExecuteSingleValidation.snap @@ -0,0 +1 @@ +165115 \ No newline at end of file diff --git a/src/lib/V2DutchOrderLib.sol b/src/lib/V2DutchOrderLib.sol new file mode 100644 index 00000000..cba1938b --- /dev/null +++ b/src/lib/V2DutchOrderLib.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {OrderInfo} from "../base/ReactorStructs.sol"; +import {DutchOutput, DutchInput, DutchOrderLib} from "./DutchOrderLib.sol"; +import {OrderInfoLib} from "./OrderInfoLib.sol"; + +struct V2DutchOrderInner { + // generic order information + OrderInfo info; + // The address which must cosign the ful order + address cosigner; + // The tokens that the swapper will provide when settling the order + DutchInput input; + // The tokens that must be received to satisfy the order + DutchOutput[] outputs; +} + +struct V2DutchOrder { + // Inner order + V2DutchOrderInner inner; + // The time at which the DutchOutputs start decaying + uint256 decayStartTime; + // The time at which price becomes static + uint256 decayEndTime; + // The address who has exclusive rights to the order until decayStartTime + address exclusiveFiller; + // The amount in bps that a non-exclusive filler needs to improve the outputs by to be able to fill the order + uint256 exclusivityOverrideBps; + // The tokens that the swapper will provide when settling the order + uint256 inputOverride; + // The tokens that must be received to satisfy the order + uint256[] outputOverrides; +} + +struct CosignedV2DutchOrder { + V2DutchOrder order; + bytes signature; +} + +/// @notice helpers for handling v2 dutch order objects +library V2DutchOrderLib { + using DutchOrderLib for DutchOutput[]; + using OrderInfoLib for OrderInfo; + + bytes internal constant V2_DUTCH_ORDER_TYPE = abi.encodePacked( + "V2DutchOrder(", + "OrderInfo info,", + "address cosigner,", + "address inputToken,", + "uint256 inputStartAmount,", + "uint256 inputEndAmount,", + "DutchOutput[] outputs)" + ); + + bytes internal constant ORDER_TYPE = abi.encodePacked( + V2_DUTCH_ORDER_TYPE, DutchOrderLib.DUTCH_OUTPUT_TYPE, OrderInfoLib.ORDER_INFO_TYPE + ); + bytes32 internal constant ORDER_TYPE_HASH = keccak256(ORDER_TYPE); + + /// @dev Note that sub-structs have to be defined in alphabetical order in the EIP-712 spec + string internal constant PERMIT2_ORDER_TYPE = string( + abi.encodePacked( + "V2DutchOrder witness)", + DutchOrderLib.DUTCH_OUTPUT_TYPE, + OrderInfoLib.ORDER_INFO_TYPE, + DutchOrderLib.TOKEN_PERMISSIONS_TYPE, + V2_DUTCH_ORDER_TYPE + ) + ); + + /// @notice hash the given order + /// @param order the order to hash + /// @return the eip-712 order hash + function hash(V2DutchOrder memory order) internal pure returns (bytes32) { + V2DutchOrderInner memory inner = order.inner; + return keccak256( + abi.encode( + ORDER_TYPE_HASH, + inner.info.hash(), + inner.cosigner, + inner.input.token, + inner.input.startAmount, + inner.input.endAmount, + inner.outputs.hash() + ) + ); + } +} diff --git a/src/reactors/V2DutchOrderReactor.sol b/src/reactors/V2DutchOrderReactor.sol new file mode 100644 index 00000000..e9547e05 --- /dev/null +++ b/src/reactors/V2DutchOrderReactor.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {BaseReactor} from "./BaseReactor.sol"; +import {IPermit2} from "permit2/src/interfaces/IPermit2.sol"; +import {ExclusivityOverrideLib} from "../lib/ExclusivityOverrideLib.sol"; +import {Permit2Lib} from "../lib/Permit2Lib.sol"; +import {DutchDecayLib} from "../lib/DutchDecayLib.sol"; +import {V2DutchOrderLib, V2DutchOrder, V2DutchOrderInner, CosignedV2DutchOrder, DutchOutput, DutchInput} from "../lib/V2DutchOrderLib.sol"; +import {SignedOrder, ResolvedOrder} from "../base/ReactorStructs.sol"; + +/// @notice Reactor for v2 dutch orders +/// @dev V2 orders must be cosigned by the specified cosigner to override timings and starting values +/// @dev resolution behavior: +/// - If cosignature is invalid or not from specified cosigner, revert +/// - If inputOverride is 0, then use inner inputs +/// - If inputOverride is nonzero, then ensure it is less than specified input and replace startAmount +/// - For each DutchOutput: +/// - If override is 0, then use inner output +/// - If override is nonzero, then ensure it is greater than specified output and replace startAmount +contract V2DutchOrderReactor is BaseReactor { + using Permit2Lib for ResolvedOrder; + using V2DutchOrderLib for V2DutchOrder; + using DutchDecayLib for DutchOutput[]; + using DutchDecayLib for DutchInput; + using ExclusivityOverrideLib for ResolvedOrder; + + /// @notice thrown when an order's deadline is before its end time + error DeadlineBeforeEndTime(); + + /// @notice thrown when an order's end time is before its start time + error OrderEndTimeBeforeStartTime(); + + /// @notice thrown when an order's inputs and outputs both decay + error InputAndOutputDecay(); + + /// @notice thrown when an order's cosignature does not match the expected cosigner + error InvalidCosignature(); + + /// @notice thrown when an order's input override is greater than the specified + error InvalidInputOverride(); + + /// @notice thrown when an order's output override is greater than the specified + error InvalidOutputOverride(); + + constructor(IPermit2 _permit2, address _protocolFeeOwner) BaseReactor(_permit2, _protocolFeeOwner) {} + + /// @inheritdoc BaseReactor + function resolve(SignedOrder calldata signedOrder) + internal + view + virtual + override + returns (ResolvedOrder memory resolvedOrder) + { + CosignedV2DutchOrder memory cosignedOrder = abi.decode(signedOrder.order, (CosignedV2DutchOrder)); + _validateOrder(cosignedOrder); + V2DutchOrder memory order = cosignedOrder.order; + _updateWithOverrides(order); + + resolvedOrder = ResolvedOrder({ + info: order.inner.info, + input: order.inner.input.decay(order.decayStartTime, order.decayEndTime), + outputs: order.inner.outputs.decay(order.decayStartTime, order.decayEndTime), + sig: signedOrder.sig, + hash: order.hash() + }); + resolvedOrder.handleOverride(order.exclusiveFiller, order.decayStartTime, order.exclusivityOverrideBps); + } + + /// @inheritdoc BaseReactor + function transferInputTokens(ResolvedOrder memory order, address to) internal override { + permit2.permitWitnessTransferFrom( + order.toPermit(), + order.transferDetails(to), + order.info.swapper, + order.hash, + V2DutchOrderLib.PERMIT2_ORDER_TYPE, + order.sig + ); + } + + function _updateWithOverrides(V2DutchOrder memory order) internal pure { + if (order.inputOverride != 0) order.inner.input.startAmount = order.inputOverride; + + for (uint256 i = 0; i < order.inner.outputs.length; i++) { + DutchOutput memory output = order.inner.outputs[i]; + uint256 outputOverride = order.outputOverrides[i]; + if (outputOverride != 0) output.startAmount = outputOverride; + } + } + + /// @notice validate the dutch order fields + /// - deadline must be greater than or equal than decayEndTime + /// - decayEndTime must be greater than or equal to decayStartTime + /// - if there's input decay, outputs must not decay + /// @dev Throws if the order is invalid + function _validateOrder(CosignedV2DutchOrder memory cosigned) internal pure { + V2DutchOrder memory order = cosigned.order; + if (order.inner.info.deadline < order.decayEndTime) { + revert DeadlineBeforeEndTime(); + } + + if (order.decayEndTime < order.decayStartTime) { + revert OrderEndTimeBeforeStartTime(); + } + + (bytes32 r, bytes32 s) = abi.decode(cosigned.signature, (bytes32, bytes32)); + uint8 v = uint8(cosigned.signature[64]); + address signer = ecrecover(keccak256(abi.encode(order)), v, r, s); + if (order.inner.cosigner != signer) { + revert InvalidCosignature(); + } + + if (order.inputOverride != 0 && order.inputOverride > order.inner.input.startAmount) { + revert InvalidInputOverride(); + } + + if (order.inner.input.startAmount != order.inner.input.endAmount) { + unchecked { + for (uint256 i = 0; i < order.inner.outputs.length; i++) { + DutchOutput memory output = order.inner.outputs[i]; + if (output.startAmount != output.endAmount) { + revert InputAndOutputDecay(); + } + + uint256 outputOverride = order.outputOverrides[i]; + if (outputOverride < output.startAmount) { + revert InvalidOutputOverride(); + } + } + } + } + } +} diff --git a/test/reactors/V2DutchOrderReactor.t.sol b/test/reactors/V2DutchOrderReactor.t.sol new file mode 100644 index 00000000..6e6e899d --- /dev/null +++ b/test/reactors/V2DutchOrderReactor.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {DeployPermit2} from "../util/DeployPermit2.sol"; +import { + V2DutchOrder, + V2DutchOrderLib, + V2DutchOrderInner, + V2DutchOrderReactor, + CosignedV2DutchOrder, + ResolvedOrder, + DutchOutput, + DutchInput, + BaseReactor +} from "../../src/reactors/V2DutchOrderReactor.sol"; +import {OrderInfo, InputToken, SignedOrder, OutputToken} from "../../src/base/ReactorStructs.sol"; +import {DutchDecayLib} from "../../src/lib/DutchDecayLib.sol"; +import {CurrencyLibrary, NATIVE} from "../../src/lib/CurrencyLibrary.sol"; +import {OrderInfoBuilder} from "../util/OrderInfoBuilder.sol"; +import {MockERC20} from "../util/mock/MockERC20.sol"; +import {OutputsBuilder} from "../util/OutputsBuilder.sol"; +import {MockFillContract} from "../util/mock/MockFillContract.sol"; +import {MockFillContractWithOutputOverride} from "../util/mock/MockFillContractWithOutputOverride.sol"; +import {PermitSignature} from "../util/PermitSignature.sol"; +import {ReactorEvents} from "../../src/base/ReactorEvents.sol"; +import {BaseReactorTest} from "../base/BaseReactor.t.sol"; + +contract V2DutchOrderTest is PermitSignature, DeployPermit2, BaseReactorTest { + using OrderInfoBuilder for OrderInfo; + using V2DutchOrderLib for V2DutchOrder; + + uint256 constant cosignerPrivateKey = 0x99999999; + + function name() public pure override returns (string memory) { + return "V2DutchOrder"; + } + + function createReactor() public override returns (BaseReactor) { + return new V2DutchOrderReactor(permit2, PROTOCOL_FEE_OWNER); + } + + /// @dev Create and return a basic single Dutch limit order along with its signature, orderHash, and orderInfo + /// TODO: Support creating a single dutch order with multiple outputs + function createAndSignOrder(ResolvedOrder memory request) + public + view + override + returns (SignedOrder memory signedOrder, bytes32 orderHash) + { + DutchOutput[] memory outputs = new DutchOutput[](request.outputs.length); + for (uint256 i = 0; i < request.outputs.length; i++) { + OutputToken memory output = request.outputs[i]; + outputs[i] = DutchOutput({ + token: output.token, + startAmount: output.amount, + endAmount: output.amount, + recipient: output.recipient + }); + } + + V2DutchOrderInner memory inner = V2DutchOrderInner({ + info: request.info, + cosigner: vm.addr(cosignerPrivateKey), + input: DutchInput(request.input.token, request.input.amount, request.input.amount), + outputs: outputs + }); + + uint256[] memory outputOverrides = new uint256[](request.outputs.length); + for (uint256 i = 0; i < request.outputs.length; i++) { + outputOverrides[i] = 0; + } + + V2DutchOrder memory order = V2DutchOrder({ + inner: inner, + decayStartTime: block.timestamp, + decayEndTime: request.info.deadline, + exclusiveFiller: address(0), + exclusivityOverrideBps: 300, + inputOverride: 0, + outputOverrides: outputOverrides + }); + orderHash = order.hash(); + CosignedV2DutchOrder memory cosigned = CosignedV2DutchOrder({ + order: order, + signature: cosignOrder(order) + }); + return (SignedOrder(abi.encode(cosigned), signOrder(swapperPrivateKey, address(permit2), order)), orderHash); + } + + function cosignOrder(V2DutchOrder memory order) private pure returns (bytes memory sig) { + bytes32 msgHash = keccak256(abi.encode(order)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(cosignerPrivateKey, msgHash); + sig = bytes.concat(r, s, bytes1(v)); + } + + function generateSignedOrders(V2DutchOrder[] memory orders) + private + view + returns (SignedOrder[] memory result) + { + result = new SignedOrder[](orders.length); + for (uint256 i = 0; i < orders.length; i++) { + bytes memory sig = signOrder(swapperPrivateKey, address(permit2), orders[i]); + result[i] = SignedOrder(abi.encode(orders[i]), sig); + } + } +} diff --git a/test/util/PermitSignature.sol b/test/util/PermitSignature.sol index bfa016db..cbd9b45d 100644 --- a/test/util/PermitSignature.sol +++ b/test/util/PermitSignature.sol @@ -8,12 +8,14 @@ import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol" import {LimitOrder, LimitOrderLib} from "../../src/lib/LimitOrderLib.sol"; import {DutchOrder, DutchOrderLib} from "../../src/lib/DutchOrderLib.sol"; import {ExclusiveDutchOrder, ExclusiveDutchOrderLib} from "../../src/lib/ExclusiveDutchOrderLib.sol"; +import {V2DutchOrder, V2DutchOrderLib} from "../../src/lib/V2DutchOrderLib.sol"; import {OrderInfo, InputToken} from "../../src/base/ReactorStructs.sol"; contract PermitSignature is Test { using LimitOrderLib for LimitOrder; using DutchOrderLib for DutchOrder; using ExclusiveDutchOrderLib for ExclusiveDutchOrder; + using V2DutchOrderLib for V2DutchOrder; bytes32 public constant NAME_HASH = keccak256("Permit2"); bytes32 public constant TYPE_HASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); @@ -31,6 +33,9 @@ contract PermitSignature is Test { bytes32 constant EXCLUSIVE_DUTCH_LIMIT_ORDER_TYPE_HASH = keccak256(abi.encodePacked(TYPEHASH_STUB, ExclusiveDutchOrderLib.PERMIT2_ORDER_TYPE)); + bytes32 constant V2_DUTCH_ORDER_TYPE_HASH = + keccak256(abi.encodePacked(TYPEHASH_STUB, V2DutchOrderLib.PERMIT2_ORDER_TYPE)); + function getPermitSignature( uint256 privateKey, address permit2, @@ -117,6 +122,22 @@ contract PermitSignature is Test { ); } + function signOrder(uint256 privateKey, address permit2, V2DutchOrder memory order) + internal + view + returns (bytes memory sig) + { + return signOrder( + privateKey, + permit2, + order.inner.info, + address(order.inner.input.token), + order.inner.input.endAmount, + V2_DUTCH_ORDER_TYPE_HASH, + order.hash() + ); + } + function _domainSeparatorV4(address permit2) internal view returns (bytes32) { return keccak256(abi.encode(TYPE_HASH, NAME_HASH, block.chainid, permit2)); }