From 1201ae7093484c77259ea42ea71a088806b4e990 Mon Sep 17 00:00:00 2001 From: alex0207s Date: Tue, 6 Feb 2024 02:17:34 +0800 Subject: [PATCH 01/10] add a conditionalSwap skelton --- contracts/ConditionalSwap.sol | 97 +++++++++++++++++++++++ contracts/interfaces/IConditionalSwap.sol | 35 ++++++++ contracts/libraries/ConditionalOrder.sol | 40 ++++++++++ 3 files changed, 172 insertions(+) create mode 100644 contracts/ConditionalSwap.sol create mode 100644 contracts/interfaces/IConditionalSwap.sol create mode 100644 contracts/libraries/ConditionalOrder.sol diff --git a/contracts/ConditionalSwap.sol b/contracts/ConditionalSwap.sol new file mode 100644 index 00000000..6df9aa6d --- /dev/null +++ b/contracts/ConditionalSwap.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import { TokenCollector } from "./abstracts/TokenCollector.sol"; +import { IConditionalSwap } from "./interfaces/IConditionalSwap.sol"; +import { Ownable } from "./abstracts/Ownable.sol"; +import { EIP712 } from "./abstracts/EIP712.sol"; +import { IStrategy } from "./interfaces/IStrategy.sol"; +import { SignatureValidator } from "./libraries/SignatureValidator.sol"; +import { ConOrder, getConOrderHash } from "./libraries/ConditionalOrder.sol"; + +/// @title ConditionalSwap Contract +/// @author imToken Labs +contract ConditionalSwap is IConditionalSwap, Ownable, TokenCollector, EIP712 { + uint256 private constant FLG_SINGLE_AMOUNT_CAP_MASK = 1 << 255; // ConOrder.amount is the cap of single execution, not total cap + uint256 private constant FLG_PERIODIC_MASK = 1 << 254; // ConOrder can be executed periodically + uint256 private constant FLG_PARTIAL_FILL_MASK = 1 << 253; // ConOrder can be fill partially + + // record how many taker tokens have been filled in an order + mapping(bytes32 => uint256) public orderHashToTakerTokenFilledAmount; + + constructor(address _owner, address _uniswapPermit2, address _allowanceTarget) Ownable(_owner) TokenCollector(_uniswapPermit2, _allowanceTarget) {} + + receive() external {} + + function fillConOrder( + ConOrder calldata order, + bytes calldata takerSignature, + uint256 takerTokenAmount, + uint256 makerTokenAmount, + bytes calldata settlementData + ) external payable override { + if (block.timestamp > order.expiry) revert ExpiredOrder(); + if (msg.sender != order.maker) revert NotOrderMaker(); + if (order.recipient != address(0)) revert InvalidRecipient(); + + // validate takerSignature + bytes32 orderHash = getConOrderHash(order); + if (orderHashToTakerTokenFilledAmount[orderHash] == 0) { + if (!SignatureValidator.validateSignature(order.taker, getEIP712Hash(orderHash), takerSignature)) revert InvalidSignature(); + } + + // validate the takerTokenAmount + if (order.flagsAndPeriod & FLG_SINGLE_AMOUNT_CAP_MASK == 1) { + // single cap amount + if (takerTokenAmount > order.totalTakerTokenAmount) { + revert InvalidTakingAmount(); + } + } else { + // total cap amount + if (orderHashToTakerTokenFilledAmount[orderHash] + takerTokenAmount > order.totalTakerTokenAmount) { + revert InvalidTakingAmount(); + } + } + + //@todo validate price constrain (fillPrice < order.price) + //@todo validate time constrain (duration) + //@todo define order.price + uint256 minMakerAmount = max(order.price * takerTokenAmount, makerTokenAmount); + + bytes1 settlementType = settlementData[0]; + bytes strategyData = settlementData[1:]; + + if (settlementType == 0x0) { + // direct settlement type + _collect(order.takerToken, order.taker, msg.sender, takerTokenAmount, ""); + _collect(order.makerToken, msg.sender, order.recipient, makerTokenAmount, ""); + } else if (settlementType == 0x01) { + // strategy settlement type + (address strategy, bytes memory data) = abi.decode(strategyData, (address, bytes)); + _collect(order.takerToken, order.taker, strategy, takerTokenAmount,""); + IStrategy(strategy).executeStrategy(order.takerToken, order.makerToken, takerTokenAmount, data); + //@todo get the returnedAmount from strategy + //@todo validate returnedAmount > minMakerAmount + order.makerToken.transferTo(order.recipient, returnedAmount); + } else { + revert(); + } + + //@todo get takerTokenSettleAmount + //@todo get makerTokenSettleAmount + _emitConOrderFilled(order, orderHash, takerTokenSettleAmount, makerTokenSettleAmount); + } + + function _emitConOrderFilled(ConOrder calldata order, bytes32 orderHash, uint256 takerTokenSettleAmount, uint256 makerTokenSettleAmount) internal { + emit ConditionalOrderFilled( + orderHash, + order.taker, + order.maker, + order.takerToken, + takerTokenSettleAmount, + order.makerToken, + makerTokenSettleAmount, + order.recipient + ); + } +} diff --git a/contracts/interfaces/IConditionalSwap.sol b/contracts/interfaces/IConditionalSwap.sol new file mode 100644 index 00000000..868f5a9f --- /dev/null +++ b/contracts/interfaces/IConditionalSwap.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import { ConOrder } from "../libraries/ConditionalOrder.sol"; + +interface IConditionalSwap { + // error + error ExpiredOrder(); + error FilledOrder(); + error ZeroAddress(); + error ZeroTokenAmount(); + error InvalidMsgValue(); + error InvalidSignature(); + error InvalidTaker(); + error InvalidTakingAmount(); + error InvalidRecipient(); + error NotOrderMaker(); + + // event + event ConditionalOrderFilled( + bytes32 indexed orderHash, + address indexed taker, + address indexed maker, + address takerToken, + uint256 takerTokenFilledAmount, + address makerToken, + uint256 makerTokenSettleAmount, + address recipient + ); + + // struct + + // function + function fillOrder(ConOrder calldata cd, bytes calldata userSig, uint256 userAmount, uint256 makerAmount, bytes calldata settlementData) external; +} diff --git a/contracts/libraries/ConditionalOrder.sol b/contracts/libraries/ConditionalOrder.sol new file mode 100644 index 00000000..668691f0 --- /dev/null +++ b/contracts/libraries/ConditionalOrder.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +string constant CONORDER_TYPESTRING = "ConOrder(address taker,address maker,address recipient,address takerToken,uint256 totalTakerTokenAmount,address makerToken,uint256 minMakerTokenAmount,uint256 flagsAndPeriod,uint256 expiry,uint256 salt)"; + +bytes32 constant CONORDER_DATA_TYPEHASH = keccak256(bytes(CONORDER_TYPESTRING)); + +// @note remember to modify the CONORDER_TYPESTRING if modify the conOrder struct +struct ConOrder { + address taker; + address payable maker; // only maker can fill this ConOrder + address payable recipient; + address takerToken; // from user to maker + uint256 totalTakerTokenAmount; + address makerToken; // from maker to recipient + uint256 minMakerTokenAmount; + uint256 flagsAndPeriod; // first 16 bytes as flags, rest as period duration + uint256 expiry; + uint256 salt; +} + +// solhint-disable-next-line func-visibility +function getConOrderHash(ConOrder calldata order) pure returns (bytes32) { + return + keccak256( + abi.encode( + CONORDER_DATA_TYPEHASH, + order.taker, + order.maker, + order.recipient, + order.takerToken, + order.totalTakerTokenAmount, + order.makerToken, + order.minMakerTokenAmount, + order.flagsAndPeriod, + order.expiry, + order.salt + ) + ); +} From 374475d490a9779be6e072db13019b91e55dcab4 Mon Sep 17 00:00:00 2001 From: alex0207s Date: Tue, 20 Feb 2024 12:11:12 +0800 Subject: [PATCH 02/10] implement fillConOrder function --- contracts/ConditionalSwap.sol | 75 +++++++++++++++-------- contracts/interfaces/IConditionalSwap.sol | 17 ++--- contracts/libraries/ConditionalOrder.sol | 43 ++++++------- 3 files changed, 82 insertions(+), 53 deletions(-) diff --git a/contracts/ConditionalSwap.sol b/contracts/ConditionalSwap.sol index 6df9aa6d..0ca3fb9b 100644 --- a/contracts/ConditionalSwap.sol +++ b/contracts/ConditionalSwap.sol @@ -1,27 +1,33 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.17; -import { TokenCollector } from "./abstracts/TokenCollector.sol"; import { IConditionalSwap } from "./interfaces/IConditionalSwap.sol"; +import { IStrategy } from "./interfaces/IStrategy.sol"; +import { TokenCollector } from "./abstracts/TokenCollector.sol"; import { Ownable } from "./abstracts/Ownable.sol"; import { EIP712 } from "./abstracts/EIP712.sol"; -import { IStrategy } from "./interfaces/IStrategy.sol"; +import { Asset } from "./libraries/Asset.sol"; import { SignatureValidator } from "./libraries/SignatureValidator.sol"; import { ConOrder, getConOrderHash } from "./libraries/ConditionalOrder.sol"; /// @title ConditionalSwap Contract /// @author imToken Labs contract ConditionalSwap is IConditionalSwap, Ownable, TokenCollector, EIP712 { + using Asset for address; + uint256 private constant FLG_SINGLE_AMOUNT_CAP_MASK = 1 << 255; // ConOrder.amount is the cap of single execution, not total cap uint256 private constant FLG_PERIODIC_MASK = 1 << 254; // ConOrder can be executed periodically uint256 private constant FLG_PARTIAL_FILL_MASK = 1 << 253; // ConOrder can be fill partially + uint256 private constant PERIOD_MASK = (1 << 16) - 1; // record how many taker tokens have been filled in an order mapping(bytes32 => uint256) public orderHashToTakerTokenFilledAmount; + mapping(bytes32 => uint256) public orderHashToLastExecutedTime; constructor(address _owner, address _uniswapPermit2, address _allowanceTarget) Ownable(_owner) TokenCollector(_uniswapPermit2, _allowanceTarget) {} - receive() external {} + //@note if this contract has the ability to transfer out ETH, implement the receive function + // receive() external {} function fillConOrder( ConOrder calldata order, @@ -32,54 +38,73 @@ contract ConditionalSwap is IConditionalSwap, Ownable, TokenCollector, EIP712 { ) external payable override { if (block.timestamp > order.expiry) revert ExpiredOrder(); if (msg.sender != order.maker) revert NotOrderMaker(); - if (order.recipient != address(0)) revert InvalidRecipient(); + if (order.recipient == address(0)) revert InvalidRecipient(); + if (takerTokenAmount == 0) revert ZeroTokenAmount(); // validate takerSignature bytes32 orderHash = getConOrderHash(order); if (orderHashToTakerTokenFilledAmount[orderHash] == 0) { - if (!SignatureValidator.validateSignature(order.taker, getEIP712Hash(orderHash), takerSignature)) revert InvalidSignature(); + if (!SignatureValidator.validateSignature(order.taker, getEIP712Hash(orderHash), takerSignature)) { + revert InvalidSignature(); + } } // validate the takerTokenAmount - if (order.flagsAndPeriod & FLG_SINGLE_AMOUNT_CAP_MASK == 1) { + if (order.flagsAndPeriod & FLG_SINGLE_AMOUNT_CAP_MASK != 0) { // single cap amount - if (takerTokenAmount > order.totalTakerTokenAmount) { - revert InvalidTakingAmount(); - } + if (takerTokenAmount > order.takerTokenAmount) revert InvalidTakingAmount(); } else { // total cap amount - if (orderHashToTakerTokenFilledAmount[orderHash] + takerTokenAmount > order.totalTakerTokenAmount) { + if (orderHashToTakerTokenFilledAmount[orderHash] + takerTokenAmount > order.takerTokenAmount) { revert InvalidTakingAmount(); } } + orderHashToTakerTokenFilledAmount[orderHash] += takerTokenAmount; - //@todo validate price constrain (fillPrice < order.price) - //@todo validate time constrain (duration) - //@todo define order.price - uint256 minMakerAmount = max(order.price * takerTokenAmount, makerTokenAmount); + // validate the makerTokenAmounts + uint256 minMakerTokenAmount; + if (order.flagsAndPeriod & FLG_PARTIAL_FILL_MASK != 0) { + // support partial fill + minMakerTokenAmount = (takerTokenAmount * order.makerTokenAmount) / order.takerTokenAmount; + } else { + if (takerTokenAmount != order.takerTokenAmount) revert InvalidTakingAmount(); + minMakerTokenAmount = order.makerTokenAmount; + } + if (makerTokenAmount < minMakerTokenAmount) revert InvalidMakingAmount(); + + // validate time constrain + if (order.flagsAndPeriod & FLG_PERIODIC_MASK != 0) { + uint256 duration = order.flagsAndPeriod & PERIOD_MASK; + if (block.timestamp - orderHashToLastExecutedTime[orderHash] < duration) revert InsufficientTimePassed(); + orderHashToLastExecutedTime[orderHash] = block.timestamp; + } bytes1 settlementType = settlementData[0]; - bytes strategyData = settlementData[1:]; - + bytes memory strategyData = settlementData[1:]; + + uint256 returnedAmount; if (settlementType == 0x0) { // direct settlement type - _collect(order.takerToken, order.taker, msg.sender, takerTokenAmount, ""); - _collect(order.makerToken, msg.sender, order.recipient, makerTokenAmount, ""); + _collect(order.takerToken, order.taker, msg.sender, takerTokenAmount, order.takerTokenPermit); + _collect(order.makerToken, msg.sender, order.recipient, makerTokenAmount, order.takerTokenPermit); } else if (settlementType == 0x01) { // strategy settlement type + (address strategy, bytes memory data) = abi.decode(strategyData, (address, bytes)); - _collect(order.takerToken, order.taker, strategy, takerTokenAmount,""); + _collect(order.takerToken, order.taker, strategy, takerTokenAmount, order.takerTokenPermit); + + uint256 makerTokenBalanceBefore = order.makerToken.getBalance(address(this)); + //@todo Create a separate strategy contract specifically for conditionalSwap IStrategy(strategy).executeStrategy(order.takerToken, order.makerToken, takerTokenAmount, data); - //@todo get the returnedAmount from strategy - //@todo validate returnedAmount > minMakerAmount + returnedAmount = order.makerToken.getBalance(address(this)) - makerTokenBalanceBefore; + + if (returnedAmount < minMakerTokenAmount) revert InsufficientOutput(); order.makerToken.transferTo(order.recipient, returnedAmount); } else { - revert(); + revert(); // specific the error message1 } - //@todo get takerTokenSettleAmount - //@todo get makerTokenSettleAmount - _emitConOrderFilled(order, orderHash, takerTokenSettleAmount, makerTokenSettleAmount); + _emitConOrderFilled(order, orderHash, takerTokenAmount, returnedAmount); } function _emitConOrderFilled(ConOrder calldata order, bytes32 orderHash, uint256 takerTokenSettleAmount, uint256 makerTokenSettleAmount) internal { diff --git a/contracts/interfaces/IConditionalSwap.sol b/contracts/interfaces/IConditionalSwap.sol index 868f5a9f..8b668ee0 100644 --- a/contracts/interfaces/IConditionalSwap.sol +++ b/contracts/interfaces/IConditionalSwap.sol @@ -6,15 +6,14 @@ import { ConOrder } from "../libraries/ConditionalOrder.sol"; interface IConditionalSwap { // error error ExpiredOrder(); - error FilledOrder(); - error ZeroAddress(); + error InsufficientTimePassed(); error ZeroTokenAmount(); - error InvalidMsgValue(); error InvalidSignature(); - error InvalidTaker(); error InvalidTakingAmount(); + error InvalidMakingAmount(); error InvalidRecipient(); error NotOrderMaker(); + error InsufficientOutput(); // event event ConditionalOrderFilled( @@ -28,8 +27,12 @@ interface IConditionalSwap { address recipient ); - // struct - // function - function fillOrder(ConOrder calldata cd, bytes calldata userSig, uint256 userAmount, uint256 makerAmount, bytes calldata settlementData) external; + function fillConOrder( + ConOrder calldata order, + bytes calldata takerSignature, + uint256 takerTokenAmount, + uint256 makerTokenAmount, + bytes calldata settlementData + ) external payable; } diff --git a/contracts/libraries/ConditionalOrder.sol b/contracts/libraries/ConditionalOrder.sol index 668691f0..1c576676 100644 --- a/contracts/libraries/ConditionalOrder.sol +++ b/contracts/libraries/ConditionalOrder.sol @@ -1,40 +1,41 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -string constant CONORDER_TYPESTRING = "ConOrder(address taker,address maker,address recipient,address takerToken,uint256 totalTakerTokenAmount,address makerToken,uint256 minMakerTokenAmount,uint256 flagsAndPeriod,uint256 expiry,uint256 salt)"; +string constant CONORDER_TYPESTRING = "ConOrder(address taker,address maker,address recipient,address takerToken,uint256 takerTokenAmount,address makerToken,uint256 makerTokenAmount,bytes takerTokenPermit,uint256 flagsAndPeriod,uint256 expiry,uint256 salt)"; bytes32 constant CONORDER_DATA_TYPEHASH = keccak256(bytes(CONORDER_TYPESTRING)); -// @note remember to modify the CONORDER_TYPESTRING if modify the conOrder struct +// @note remember to modify the CONORDER_TYPESTRING if modify the ConOrder struct struct ConOrder { address taker; address payable maker; // only maker can fill this ConOrder address payable recipient; address takerToken; // from user to maker - uint256 totalTakerTokenAmount; + uint256 takerTokenAmount; address makerToken; // from maker to recipient - uint256 minMakerTokenAmount; + uint256 makerTokenAmount; + bytes takerTokenPermit; uint256 flagsAndPeriod; // first 16 bytes as flags, rest as period duration uint256 expiry; uint256 salt; } // solhint-disable-next-line func-visibility -function getConOrderHash(ConOrder calldata order) pure returns (bytes32) { - return - keccak256( - abi.encode( - CONORDER_DATA_TYPEHASH, - order.taker, - order.maker, - order.recipient, - order.takerToken, - order.totalTakerTokenAmount, - order.makerToken, - order.minMakerTokenAmount, - order.flagsAndPeriod, - order.expiry, - order.salt - ) - ); +function getConOrderHash(ConOrder memory order) pure returns (bytes32 conOrderHash) { + conOrderHash = keccak256( + abi.encode( + CONORDER_DATA_TYPEHASH, + order.taker, + order.maker, + order.recipient, + order.takerToken, + order.takerTokenAmount, + order.makerToken, + order.makerTokenAmount, + order.takerTokenPermit, + order.flagsAndPeriod, + order.expiry, + order.salt + ) + ); } From a647fbb37564515205d4c9a707a78696d99f182f Mon Sep 17 00:00:00 2001 From: alex0207s Date: Tue, 27 Feb 2024 11:44:28 +0800 Subject: [PATCH 03/10] add test functions for the conditional swap --- test/forkMainnet/ConditionalSwap/Fill.t.sol | 168 +++++++++++++++++++ test/forkMainnet/ConditionalSwap/Setup.t.sol | 68 ++++++++ 2 files changed, 236 insertions(+) create mode 100644 test/forkMainnet/ConditionalSwap/Fill.t.sol create mode 100644 test/forkMainnet/ConditionalSwap/Setup.t.sol diff --git a/test/forkMainnet/ConditionalSwap/Fill.t.sol b/test/forkMainnet/ConditionalSwap/Fill.t.sol new file mode 100644 index 00000000..ecaf2777 --- /dev/null +++ b/test/forkMainnet/ConditionalSwap/Fill.t.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import { IConditionalSwap } from "contracts/interfaces/IConditionalSwap.sol"; +import { ConditionalOrderSwapTest } from "test/forkMainnet/ConditionalSwap/Setup.t.sol"; +import { BalanceSnapshot, Snapshot } from "test/utils/BalanceSnapshot.sol"; + +contract ConFillTest is ConditionalOrderSwapTest { + using BalanceSnapshot for Snapshot; + + function setUp() public override { + super.setUp(); + } + + function testBestBuyOrder() external { + Snapshot memory takerTakerToken = BalanceSnapshot.take({ owner: taker, token: defaultOrder.takerToken }); + Snapshot memory takerMakerToken = BalanceSnapshot.take({ owner: taker, token: defaultOrder.makerToken }); + Snapshot memory makerTakerToken = BalanceSnapshot.take({ owner: maker, token: defaultOrder.takerToken }); + Snapshot memory makerMakerToken = BalanceSnapshot.take({ owner: maker, token: defaultOrder.makerToken }); + Snapshot memory recTakerToken = BalanceSnapshot.take({ owner: recipient, token: defaultOrder.takerToken }); + Snapshot memory recMakerToken = BalanceSnapshot.take({ owner: recipient, token: defaultOrder.makerToken }); + + // craft the `flagAndPeriod` of the defaultOrder for BestBuy case + uint256 flags = 1 << 253; + defaultOrder.flagsAndPeriod = flags; + + defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + + vm.startPrank(maker); + conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + + takerTakerToken.assertChange(-int256(defaultOrder.takerTokenAmount)); + takerMakerToken.assertChange(int256(0)); + makerTakerToken.assertChange(int256(defaultOrder.takerTokenAmount)); + makerMakerToken.assertChange(-int256(defaultOrder.makerTokenAmount)); + recTakerToken.assertChange(int256(0)); + recMakerToken.assertChange(int256(defaultOrder.makerTokenAmount)); + } + + function testRepayment() external { + Snapshot memory takerTakerToken = BalanceSnapshot.take({ owner: taker, token: defaultOrder.takerToken }); + Snapshot memory takerMakerToken = BalanceSnapshot.take({ owner: taker, token: defaultOrder.makerToken }); + Snapshot memory makerTakerToken = BalanceSnapshot.take({ owner: maker, token: defaultOrder.takerToken }); + Snapshot memory makerMakerToken = BalanceSnapshot.take({ owner: maker, token: defaultOrder.makerToken }); + Snapshot memory recTakerToken = BalanceSnapshot.take({ owner: recipient, token: defaultOrder.takerToken }); + Snapshot memory recMakerToken = BalanceSnapshot.take({ owner: recipient, token: defaultOrder.makerToken }); + + // craft the `flagAndPeriod` of the defaultOrder for BestBuy case + uint256 flags = 7 << 253; + uint256 period = 12 hours; + uint256 numberOfCycles = (defaultExpiry - block.timestamp) / period; + defaultOrder.flagsAndPeriod = flags | period; + + defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + vm.startPrank(maker); + for (uint256 i; i < numberOfCycles; ++i) { + conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.warp(block.timestamp + period); + } + vm.stopPrank(); + + takerTakerToken.assertChange(-int256(defaultOrder.takerTokenAmount) * int256(numberOfCycles)); + takerMakerToken.assertChange(int256(0)); + makerTakerToken.assertChange(int256(defaultOrder.takerTokenAmount) * int256(numberOfCycles)); + makerMakerToken.assertChange(-int256(defaultOrder.makerTokenAmount) * int256(numberOfCycles)); + recTakerToken.assertChange(int256(0)); + recMakerToken.assertChange(int256(defaultOrder.makerTokenAmount) * int256(numberOfCycles)); + } + + function testDCAOrder() public { + // craft the `flagAndPeriod` of the defaultOrder for BestBuy case + uint256 flags = 7 << 253; + uint256 period = 1 days; + + defaultOrder.flagsAndPeriod = flags | period; + } + + function testCannotFillExpiredOrder() public { + vm.warp(defaultOrder.expiry + 1); + + vm.expectRevert(IConditionalSwap.ExpiredOrder.selector); + vm.startPrank(maker); + conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + } + + function testCannotFillOrderByInvalidOderMaker() public { + address invalidOrderMaker = makeAddr("invalidOrderMaker"); + + vm.expectRevert(IConditionalSwap.NotOrderMaker.selector); + vm.startPrank(invalidOrderMaker); + conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + } + + function testCannotFillOrderWithZeroTakerTokenAmount() public { + vm.expectRevert(IConditionalSwap.ZeroTokenAmount.selector); + vm.startPrank(maker); + conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, 0, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + } + + function testCannotFillOrderWithInvalidTotalTakerTokenAmount() public { + // craft the `flagAndPeriod` of the defaultOrder for BestBuy case + uint256 flags = 1 << 253; + defaultOrder.flagsAndPeriod = flags; + + defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + + vm.startPrank(maker); + // the first fill with full takerTokenAmount + conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + + vm.expectRevert(IConditionalSwap.InvalidTakingAmount.selector); + // The second fill with 1 takerTokenAmount would exceed the total cap this time. + conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, 1, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + } + + function testCannotFillOrderWithInvalidSingleTakerTokenAmount() public { + // craft the `flagAndPeriod` of the defaultOrder for BestBuy case + uint256 flags = 7 << 253; + uint256 period = 12 hours; + defaultOrder.flagsAndPeriod = flags | period; + + defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + + vm.expectRevert(IConditionalSwap.InvalidTakingAmount.selector); + vm.startPrank(maker); + conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount + 1, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + } + + function testCannotFillOrderWithInvalidZeroRecipient() public { + defaultOrder.recipient = payable(address(0)); + + vm.expectRevert(IConditionalSwap.InvalidRecipient.selector); + vm.startPrank(maker); + conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + } + + function testCannotFillOrderWithIncorrectSignature() public { + uint256 randomPrivateKey = 1234; + bytes memory randomEOASig = signConOrder(randomPrivateKey, defaultOrder, address(conditionalSwap)); + + vm.expectRevert(IConditionalSwap.InvalidSignature.selector); + vm.startPrank(maker); + conditionalSwap.fillConOrder(defaultOrder, randomEOASig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + } + + function testCannotFillOrderWithinSamePeriod() public { + // craft the `flagAndPeriod` of the defaultOrder for BestBuy case + uint256 flags = 7 << 253; + uint256 period = 12 hours; + defaultOrder.flagsAndPeriod = flags | period; + + defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + vm.startPrank(maker); + conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.warp(block.timestamp + 1); + vm.expectRevert(IConditionalSwap.InsufficientTimePassed.selector); + conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + } +} diff --git a/test/forkMainnet/ConditionalSwap/Setup.t.sol b/test/forkMainnet/ConditionalSwap/Setup.t.sol new file mode 100644 index 00000000..ab94a083 --- /dev/null +++ b/test/forkMainnet/ConditionalSwap/Setup.t.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import { Test } from "forge-std/Test.sol"; +import { IWETH } from "contracts/interfaces/IWETH.sol"; +import { IConditionalSwap } from "contracts/interfaces/IConditionalSwap.sol"; +import { ConditionalSwap } from "contracts/ConditionalSwap.sol"; +import { AllowanceTarget } from "contracts/AllowanceTarget.sol"; +import { ConOrder, getConOrderHash } from "contracts/libraries/ConditionalOrder.sol"; +import { Tokens } from "test/utils/Tokens.sol"; +import { BalanceUtil } from "test/utils/BalanceUtil.sol"; +import { SigHelper } from "test/utils/SigHelper.sol"; +import { computeContractAddress } from "test/utils/Addresses.sol"; +import { Permit2Helper } from "test/utils/Permit2Helper.sol"; + +contract ConditionalOrderSwapTest is Test, Tokens, BalanceUtil, Permit2Helper, SigHelper { + // role + address public conditionalOrderOwner = makeAddr("conditionalOrderOwner"); + address public allowanceTargetOwner = makeAddr("allowanceTargetOwner"); + uint256 public takerPrivateKey = uint256(1); + uint256 public makerPrivateKey = uint256(2); + address public taker = vm.addr(takerPrivateKey); + address payable public maker = payable(vm.addr(makerPrivateKey)); + address payable public recipient = payable(makeAddr("recipient")); + + uint256 public defaultExpiry = block.timestamp + 1 days; + uint256 public defaultSalt = 1234; + bytes public defaultTakerPermit; + bytes public defaultTakerSig; + bytes public defaultSettlementData; + + ConditionalSwap conditionalSwap; + AllowanceTarget allowanceTarget; + ConOrder defaultOrder; + + function setUp() public virtual { + // deploy allowance target + address[] memory trusted = new address[](1); + // pre-compute ConditionalOrderSwap address since the whitelist of allowance target is immutable + // NOTE: this assumes LimitOrderSwap is deployed right next to Allowance Target + trusted[0] = computeContractAddress(address(this), uint8(vm.getNonce(address(this)) + 1)); + + allowanceTarget = new AllowanceTarget(allowanceTargetOwner, trusted); + conditionalSwap = new ConditionalSwap(conditionalOrderOwner, UNISWAP_PERMIT2_ADDRESS, address(allowanceTarget)); + + deal(maker, 100 ether); + deal(taker, 100 ether); + setTokenBalanceAndApprove(maker, address(conditionalSwap), tokens, 100000); + setTokenBalanceAndApprove(taker, address(conditionalSwap), tokens, 100000); + + defaultTakerPermit = hex"01"; + defaultSettlementData = hex"00"; + + defaultOrder = ConOrder({ + taker: taker, + maker: maker, + recipient: recipient, + takerToken: USDT_ADDRESS, + takerTokenAmount: 10 * 1e6, + makerToken: DAI_ADDRESS, + makerTokenAmount: 10 ether, + takerTokenPermit: defaultTakerPermit, + flagsAndPeriod: 0, + expiry: defaultExpiry, + salt: defaultSalt + }); + } +} From 68f223e55a1013c78f1caab71729c71d53942e45 Mon Sep 17 00:00:00 2001 From: alex0207s Date: Tue, 27 Feb 2024 11:56:14 +0800 Subject: [PATCH 04/10] add `--via-ir` parameter to avoid stack too deep error --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index ffdf8468..607f6371 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "format": "prettier --write .", "check-pretty": "prettier --check .", "lint": "solhint \"contracts/**/*.sol\"", - "compile": "forge build --force", - "test-foundry-local": "DEPLOYED=false forge test --no-match-path 'test/forkMainnet/*.t.sol'", - "test-foundry-fork": "DEPLOYED=false forge test --fork-url $MAINNET_NODE_RPC_URL --fork-block-number 17900000 --match-path 'test/forkMainnet/*.t.sol'", + "compile": "forge build --force --via-ir", + "test-foundry-local": "DEPLOYED=false forge test --via-ir --no-match-path 'test/forkMainnet/*.t.sol'", + "test-foundry-fork": "DEPLOYED=false forge test --via-ir --fork-url $MAINNET_NODE_RPC_URL --fork-block-number 17900000 --match-path 'test/forkMainnet/*.t.sol'", "gas-report-local": "yarn test-foundry-local --gas-report", "gas-report-fork": "yarn test-foundry-fork --gas-report" }, From 4067352718652ddff4726d8deaa3410860255f04 Mon Sep 17 00:00:00 2001 From: alex0207s Date: Tue, 27 Feb 2024 13:18:32 +0800 Subject: [PATCH 05/10] add a sign function for conditional order --- test/utils/SigHelper.sol | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/utils/SigHelper.sol b/test/utils/SigHelper.sol index d606f6e5..5fba1d1d 100644 --- a/test/utils/SigHelper.sol +++ b/test/utils/SigHelper.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.17; import { AllowFill, getAllowFillHash } from "contracts/libraries/AllowFill.sol"; import { GenericSwapData, getGSDataHash } from "contracts/libraries/GenericSwapData.sol"; import { LimitOrder, getLimitOrderHash } from "contracts/libraries/LimitOrder.sol"; +import { ConOrder, getConOrderHash } from "contracts/libraries/ConditionalOrder.sol"; import { RFQOffer, getRFQOfferHash } from "contracts/libraries/RFQOffer.sol"; import { RFQTx, getRFQTxHash } from "contracts/libraries/RFQTx.sol"; import { Test } from "forge-std/Test.sol"; @@ -159,4 +160,25 @@ contract SigHelper is Test { (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, EIP712SignDigest); return abi.encodePacked(r, s, v); } + + function signConOrder(uint256 _privateKey, ConOrder memory _order, bytes32 domainSeperator) internal pure returns (bytes memory sig) { + bytes32 orderHash = getConOrderHash(_order); + bytes32 EIP712SignDigest = getEIP712Hash(domainSeperator, orderHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, EIP712SignDigest); + return abi.encodePacked(r, s, v); + } + + function signConOrder(uint256 _privateKey, ConOrder memory _order, address verifyingContract) internal view returns (bytes memory sig) { + bytes32 orderHash = getConOrderHash(_order); + bytes32 EIP712SignDigest = getEIP712Hash(computeEIP712DomainSeparator(block.chainid, verifyingContract), orderHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, EIP712SignDigest); + return abi.encodePacked(r, s, v); + } + + function signConOrder(uint256 _privateKey, ConOrder memory _order, uint256 chainId, address verifyingContract) internal pure returns (bytes memory sig) { + bytes32 orderHash = getConOrderHash(_order); + bytes32 EIP712SignDigest = getEIP712Hash(computeEIP712DomainSeparator(chainId, verifyingContract), orderHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, EIP712SignDigest); + return abi.encodePacked(r, s, v); + } } From 48fbfef7712bc528f54b2fe3a26a9a77fc248d9f Mon Sep 17 00:00:00 2001 From: alex0207s Date: Fri, 1 Mar 2024 11:51:48 +0800 Subject: [PATCH 06/10] refine code based on comment and add some test functions --- contracts/ConditionalSwap.sol | 12 ++-- contracts/interfaces/IConditionalSwap.sol | 10 +-- test/forkMainnet/ConditionalSwap/Fill.t.sol | 69 +++++++++++++++----- test/forkMainnet/ConditionalSwap/Setup.t.sol | 20 +++++- 4 files changed, 80 insertions(+), 31 deletions(-) diff --git a/contracts/ConditionalSwap.sol b/contracts/ConditionalSwap.sol index 0ca3fb9b..e7fb05f2 100644 --- a/contracts/ConditionalSwap.sol +++ b/contracts/ConditionalSwap.sol @@ -82,29 +82,25 @@ contract ConditionalSwap is IConditionalSwap, Ownable, TokenCollector, EIP712 { bytes1 settlementType = settlementData[0]; bytes memory strategyData = settlementData[1:]; - uint256 returnedAmount; if (settlementType == 0x0) { // direct settlement type _collect(order.takerToken, order.taker, msg.sender, takerTokenAmount, order.takerTokenPermit); _collect(order.makerToken, msg.sender, order.recipient, makerTokenAmount, order.takerTokenPermit); } else if (settlementType == 0x01) { // strategy settlement type - (address strategy, bytes memory data) = abi.decode(strategyData, (address, bytes)); _collect(order.takerToken, order.taker, strategy, takerTokenAmount, order.takerTokenPermit); uint256 makerTokenBalanceBefore = order.makerToken.getBalance(address(this)); //@todo Create a separate strategy contract specifically for conditionalSwap IStrategy(strategy).executeStrategy(order.takerToken, order.makerToken, takerTokenAmount, data); - returnedAmount = order.makerToken.getBalance(address(this)) - makerTokenBalanceBefore; + uint256 returnedAmount = order.makerToken.getBalance(address(this)) - makerTokenBalanceBefore; - if (returnedAmount < minMakerTokenAmount) revert InsufficientOutput(); + if (returnedAmount < makerTokenAmount) revert InsufficientOutput(); order.makerToken.transferTo(order.recipient, returnedAmount); - } else { - revert(); // specific the error message1 - } + } else revert InvalidSettlementType(); - _emitConOrderFilled(order, orderHash, takerTokenAmount, returnedAmount); + _emitConOrderFilled(order, orderHash, takerTokenAmount, makerTokenAmount); } function _emitConOrderFilled(ConOrder calldata order, bytes32 orderHash, uint256 takerTokenSettleAmount, uint256 makerTokenSettleAmount) internal { diff --git a/contracts/interfaces/IConditionalSwap.sol b/contracts/interfaces/IConditionalSwap.sol index 8b668ee0..6cd3067d 100644 --- a/contracts/interfaces/IConditionalSwap.sol +++ b/contracts/interfaces/IConditionalSwap.sol @@ -4,18 +4,18 @@ pragma solidity 0.8.17; import { ConOrder } from "../libraries/ConditionalOrder.sol"; interface IConditionalSwap { - // error error ExpiredOrder(); error InsufficientTimePassed(); - error ZeroTokenAmount(); error InvalidSignature(); + error ZeroTokenAmount(); error InvalidTakingAmount(); error InvalidMakingAmount(); - error InvalidRecipient(); - error NotOrderMaker(); error InsufficientOutput(); + error NotOrderMaker(); + error InvalidRecipient(); + error InvalidSettlementType(); - // event + /// @notice Emitted when a conditional order is filled event ConditionalOrderFilled( bytes32 indexed orderHash, address indexed taker, diff --git a/test/forkMainnet/ConditionalSwap/Fill.t.sol b/test/forkMainnet/ConditionalSwap/Fill.t.sol index ecaf2777..be70cbdc 100644 --- a/test/forkMainnet/ConditionalSwap/Fill.t.sol +++ b/test/forkMainnet/ConditionalSwap/Fill.t.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.17; import { IConditionalSwap } from "contracts/interfaces/IConditionalSwap.sol"; +import { getConOrderHash } from "contracts/libraries/ConditionalOrder.sol"; import { ConditionalOrderSwapTest } from "test/forkMainnet/ConditionalSwap/Setup.t.sol"; import { BalanceSnapshot, Snapshot } from "test/utils/BalanceSnapshot.sol"; @@ -21,11 +22,23 @@ contract ConFillTest is ConditionalOrderSwapTest { Snapshot memory recMakerToken = BalanceSnapshot.take({ owner: recipient, token: defaultOrder.makerToken }); // craft the `flagAndPeriod` of the defaultOrder for BestBuy case - uint256 flags = 1 << 253; + uint256 flags = FLG_PARTIAL_FILL_MASK; defaultOrder.flagsAndPeriod = flags; defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + vm.expectEmit(true, true, true, true); + emit ConditionalOrderFilled( + getConOrderHash(defaultOrder), + defaultOrder.taker, + defaultOrder.maker, + defaultOrder.takerToken, + defaultOrder.takerTokenAmount, + defaultOrder.makerToken, + defaultOrder.makerTokenAmount, + recipient + ); + vm.startPrank(maker); conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); vm.stopPrank(); @@ -38,7 +51,7 @@ contract ConFillTest is ConditionalOrderSwapTest { recMakerToken.assertChange(int256(defaultOrder.makerTokenAmount)); } - function testRepayment() external { + function testRepaymentOrDCAOrder() external { Snapshot memory takerTakerToken = BalanceSnapshot.take({ owner: taker, token: defaultOrder.takerToken }); Snapshot memory takerMakerToken = BalanceSnapshot.take({ owner: taker, token: defaultOrder.makerToken }); Snapshot memory makerTakerToken = BalanceSnapshot.take({ owner: maker, token: defaultOrder.takerToken }); @@ -47,12 +60,26 @@ contract ConFillTest is ConditionalOrderSwapTest { Snapshot memory recMakerToken = BalanceSnapshot.take({ owner: recipient, token: defaultOrder.makerToken }); // craft the `flagAndPeriod` of the defaultOrder for BestBuy case - uint256 flags = 7 << 253; + uint256 flags = FLG_SINGLE_AMOUNT_CAP_MASK | FLG_PERIODIC_MASK | FLG_PARTIAL_FILL_MASK; uint256 period = 12 hours; - uint256 numberOfCycles = (defaultExpiry - block.timestamp) / period; defaultOrder.flagsAndPeriod = flags | period; + uint256 numberOfCycles = (defaultExpiry - block.timestamp) / period; + defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + + vm.expectEmit(true, true, true, true); + emit ConditionalOrderFilled( + getConOrderHash(defaultOrder), + defaultOrder.taker, + defaultOrder.maker, + defaultOrder.takerToken, + defaultOrder.takerTokenAmount, + defaultOrder.makerToken, + defaultOrder.makerTokenAmount, + recipient + ); + vm.startPrank(maker); for (uint256 i; i < numberOfCycles; ++i) { conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); @@ -68,14 +95,6 @@ contract ConFillTest is ConditionalOrderSwapTest { recMakerToken.assertChange(int256(defaultOrder.makerTokenAmount) * int256(numberOfCycles)); } - function testDCAOrder() public { - // craft the `flagAndPeriod` of the defaultOrder for BestBuy case - uint256 flags = 7 << 253; - uint256 period = 1 days; - - defaultOrder.flagsAndPeriod = flags | period; - } - function testCannotFillExpiredOrder() public { vm.warp(defaultOrder.expiry + 1); @@ -103,7 +122,7 @@ contract ConFillTest is ConditionalOrderSwapTest { function testCannotFillOrderWithInvalidTotalTakerTokenAmount() public { // craft the `flagAndPeriod` of the defaultOrder for BestBuy case - uint256 flags = 1 << 253; + uint256 flags = FLG_PARTIAL_FILL_MASK; defaultOrder.flagsAndPeriod = flags; defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); @@ -120,7 +139,7 @@ contract ConFillTest is ConditionalOrderSwapTest { function testCannotFillOrderWithInvalidSingleTakerTokenAmount() public { // craft the `flagAndPeriod` of the defaultOrder for BestBuy case - uint256 flags = 7 << 253; + uint256 flags = FLG_SINGLE_AMOUNT_CAP_MASK | FLG_PERIODIC_MASK | FLG_PARTIAL_FILL_MASK; uint256 period = 12 hours; defaultOrder.flagsAndPeriod = flags | period; @@ -153,7 +172,7 @@ contract ConFillTest is ConditionalOrderSwapTest { function testCannotFillOrderWithinSamePeriod() public { // craft the `flagAndPeriod` of the defaultOrder for BestBuy case - uint256 flags = 7 << 253; + uint256 flags = FLG_SINGLE_AMOUNT_CAP_MASK | FLG_PERIODIC_MASK | FLG_PARTIAL_FILL_MASK; uint256 period = 12 hours; defaultOrder.flagsAndPeriod = flags | period; @@ -165,4 +184,24 @@ contract ConFillTest is ConditionalOrderSwapTest { conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); vm.stopPrank(); } + + function testCannotFillOrderWithInvalidSettlementType() public { + bytes memory settlementData = hex"02"; + + defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + + vm.expectRevert(IConditionalSwap.InvalidSettlementType.selector); + vm.startPrank(maker); + conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, settlementData); + vm.stopPrank(); + } + + function testCannotFillOrderWithInvalidMakingAmount() public { + defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + + vm.expectRevert(IConditionalSwap.InvalidMakingAmount.selector); + vm.startPrank(maker); + conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount - 1, defaultSettlementData); + vm.stopPrank(); + } } diff --git a/test/forkMainnet/ConditionalSwap/Setup.t.sol b/test/forkMainnet/ConditionalSwap/Setup.t.sol index ab94a083..7d7a9247 100644 --- a/test/forkMainnet/ConditionalSwap/Setup.t.sol +++ b/test/forkMainnet/ConditionalSwap/Setup.t.sol @@ -2,11 +2,9 @@ pragma solidity 0.8.17; import { Test } from "forge-std/Test.sol"; -import { IWETH } from "contracts/interfaces/IWETH.sol"; -import { IConditionalSwap } from "contracts/interfaces/IConditionalSwap.sol"; import { ConditionalSwap } from "contracts/ConditionalSwap.sol"; import { AllowanceTarget } from "contracts/AllowanceTarget.sol"; -import { ConOrder, getConOrderHash } from "contracts/libraries/ConditionalOrder.sol"; +import { ConOrder } from "contracts/libraries/ConditionalOrder.sol"; import { Tokens } from "test/utils/Tokens.sol"; import { BalanceUtil } from "test/utils/BalanceUtil.sol"; import { SigHelper } from "test/utils/SigHelper.sol"; @@ -14,6 +12,17 @@ import { computeContractAddress } from "test/utils/Addresses.sol"; import { Permit2Helper } from "test/utils/Permit2Helper.sol"; contract ConditionalOrderSwapTest is Test, Tokens, BalanceUtil, Permit2Helper, SigHelper { + event ConditionalOrderFilled( + bytes32 indexed orderHash, + address indexed taker, + address indexed maker, + address takerToken, + uint256 takerTokenFilledAmount, + address makerToken, + uint256 makerTokenSettleAmount, + address recipient + ); + // role address public conditionalOrderOwner = makeAddr("conditionalOrderOwner"); address public allowanceTargetOwner = makeAddr("allowanceTargetOwner"); @@ -29,6 +38,11 @@ contract ConditionalOrderSwapTest is Test, Tokens, BalanceUtil, Permit2Helper, S bytes public defaultTakerSig; bytes public defaultSettlementData; + // mask for triggering different business logic (e.g. BestBuy, Repayment, DCA) + uint256 public constant FLG_SINGLE_AMOUNT_CAP_MASK = 1 << 255; // ConOrder.amount is the cap of single execution, not total cap + uint256 public constant FLG_PERIODIC_MASK = 1 << 254; // ConOrder can be executed periodically + uint256 public constant FLG_PARTIAL_FILL_MASK = 1 << 253; // ConOrder can be fill partially + ConditionalSwap conditionalSwap; AllowanceTarget allowanceTarget; ConOrder defaultOrder; From 712a687d418033a69f8c32851496ea43016aa4e5 Mon Sep 17 00:00:00 2001 From: alex0207s Date: Fri, 1 Mar 2024 15:15:48 +0800 Subject: [PATCH 07/10] refine code based on comments, add partial fill test cases, and refactor test contract --- contracts/ConditionalSwap.sol | 9 +- test/forkMainnet/ConditionalSwap/Fill.t.sol | 263 +++++++++++++------ test/forkMainnet/ConditionalSwap/Setup.t.sol | 2 +- 3 files changed, 192 insertions(+), 82 deletions(-) diff --git a/contracts/ConditionalSwap.sol b/contracts/ConditionalSwap.sol index e7fb05f2..9ca03987 100644 --- a/contracts/ConditionalSwap.sol +++ b/contracts/ConditionalSwap.sol @@ -82,8 +82,11 @@ contract ConditionalSwap is IConditionalSwap, Ownable, TokenCollector, EIP712 { bytes1 settlementType = settlementData[0]; bytes memory strategyData = settlementData[1:]; + uint256 returnedAmount; if (settlementType == 0x0) { // direct settlement type + returnedAmount = makerTokenAmount; + _collect(order.takerToken, order.taker, msg.sender, takerTokenAmount, order.takerTokenPermit); _collect(order.makerToken, msg.sender, order.recipient, makerTokenAmount, order.takerTokenPermit); } else if (settlementType == 0x01) { @@ -94,13 +97,15 @@ contract ConditionalSwap is IConditionalSwap, Ownable, TokenCollector, EIP712 { uint256 makerTokenBalanceBefore = order.makerToken.getBalance(address(this)); //@todo Create a separate strategy contract specifically for conditionalSwap IStrategy(strategy).executeStrategy(order.takerToken, order.makerToken, takerTokenAmount, data); - uint256 returnedAmount = order.makerToken.getBalance(address(this)) - makerTokenBalanceBefore; + returnedAmount = order.makerToken.getBalance(address(this)) - makerTokenBalanceBefore; + // We only compare returnedAmount with makerTokenAmount here + // because we ensure that makerTokenAmount is greater than minMakerTokenAmount before if (returnedAmount < makerTokenAmount) revert InsufficientOutput(); order.makerToken.transferTo(order.recipient, returnedAmount); } else revert InvalidSettlementType(); - _emitConOrderFilled(order, orderHash, takerTokenAmount, makerTokenAmount); + _emitConOrderFilled(order, orderHash, takerTokenAmount, returnedAmount); } function _emitConOrderFilled(ConOrder calldata order, bytes32 orderHash, uint256 takerTokenSettleAmount, uint256 makerTokenSettleAmount) internal { diff --git a/test/forkMainnet/ConditionalSwap/Fill.t.sol b/test/forkMainnet/ConditionalSwap/Fill.t.sol index be70cbdc..3c3866f2 100644 --- a/test/forkMainnet/ConditionalSwap/Fill.t.sol +++ b/test/forkMainnet/ConditionalSwap/Fill.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.17; import { IConditionalSwap } from "contracts/interfaces/IConditionalSwap.sol"; -import { getConOrderHash } from "contracts/libraries/ConditionalOrder.sol"; +import { ConOrder, getConOrderHash } from "contracts/libraries/ConditionalOrder.sol"; import { ConditionalOrderSwapTest } from "test/forkMainnet/ConditionalSwap/Setup.t.sol"; import { BalanceSnapshot, Snapshot } from "test/utils/BalanceSnapshot.sol"; @@ -13,94 +13,190 @@ contract ConFillTest is ConditionalOrderSwapTest { super.setUp(); } - function testBestBuyOrder() external { - Snapshot memory takerTakerToken = BalanceSnapshot.take({ owner: taker, token: defaultOrder.takerToken }); - Snapshot memory takerMakerToken = BalanceSnapshot.take({ owner: taker, token: defaultOrder.makerToken }); - Snapshot memory makerTakerToken = BalanceSnapshot.take({ owner: maker, token: defaultOrder.takerToken }); - Snapshot memory makerMakerToken = BalanceSnapshot.take({ owner: maker, token: defaultOrder.makerToken }); - Snapshot memory recTakerToken = BalanceSnapshot.take({ owner: recipient, token: defaultOrder.takerToken }); - Snapshot memory recMakerToken = BalanceSnapshot.take({ owner: recipient, token: defaultOrder.makerToken }); + function testFullyFillBestBuyOrder() external { + ConOrder memory order = defaultOrder; + + Snapshot memory takerTakerToken = BalanceSnapshot.take({ owner: order.taker, token: order.takerToken }); + Snapshot memory takerMakerToken = BalanceSnapshot.take({ owner: order.taker, token: order.makerToken }); + Snapshot memory makerTakerToken = BalanceSnapshot.take({ owner: order.maker, token: order.takerToken }); + Snapshot memory makerMakerToken = BalanceSnapshot.take({ owner: order.maker, token: order.makerToken }); + Snapshot memory recTakerToken = BalanceSnapshot.take({ owner: order.recipient, token: order.takerToken }); + Snapshot memory recMakerToken = BalanceSnapshot.take({ owner: order.recipient, token: order.makerToken }); + + // craft the `flagAndPeriod` of the order for BestBuy case + uint256 flags = FLG_PARTIAL_FILL_MASK; + order.flagsAndPeriod = flags; + + takerSig = signConOrder(takerPrivateKey, order, address(conditionalSwap)); + + vm.expectEmit(true, true, true, true); + emit ConditionalOrderFilled( + getConOrderHash(order), + order.taker, + order.maker, + order.takerToken, + order.takerTokenAmount, + order.makerToken, + order.makerTokenAmount, + order.recipient + ); + + vm.startPrank(order.maker); + conditionalSwap.fillConOrder(order, takerSig, order.takerTokenAmount, order.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + + takerTakerToken.assertChange(-int256(order.takerTokenAmount)); + takerMakerToken.assertChange(int256(0)); + makerTakerToken.assertChange(int256(order.takerTokenAmount)); + makerMakerToken.assertChange(-int256(order.makerTokenAmount)); + recTakerToken.assertChange(int256(0)); + recMakerToken.assertChange(int256(order.makerTokenAmount)); + } + + function testPartialFillBestBuyOrder() external { + ConOrder memory order = defaultOrder; + + Snapshot memory takerTakerToken = BalanceSnapshot.take({ owner: order.taker, token: order.takerToken }); + Snapshot memory takerMakerToken = BalanceSnapshot.take({ owner: order.taker, token: order.makerToken }); + Snapshot memory makerTakerToken = BalanceSnapshot.take({ owner: order.maker, token: order.takerToken }); + Snapshot memory makerMakerToken = BalanceSnapshot.take({ owner: order.maker, token: order.makerToken }); + Snapshot memory recTakerToken = BalanceSnapshot.take({ owner: order.recipient, token: order.takerToken }); + Snapshot memory recMakerToken = BalanceSnapshot.take({ owner: order.recipient, token: order.makerToken }); // craft the `flagAndPeriod` of the defaultOrder for BestBuy case uint256 flags = FLG_PARTIAL_FILL_MASK; - defaultOrder.flagsAndPeriod = flags; + order.flagsAndPeriod = flags; + + uint256 partialTakerTokenAmount = 5 * 1e6; + uint256 partialMakerTokenAmount = 5 ether; + + takerSig = signConOrder(takerPrivateKey, order, address(conditionalSwap)); + + vm.expectEmit(true, true, true, true); + emit ConditionalOrderFilled( + getConOrderHash(order), + order.taker, + order.maker, + order.takerToken, + partialTakerTokenAmount, + order.makerToken, + partialMakerTokenAmount, + order.recipient + ); + + vm.startPrank(order.maker); + conditionalSwap.fillConOrder(order, takerSig, partialTakerTokenAmount, partialMakerTokenAmount, defaultSettlementData); + vm.stopPrank(); - defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + takerTakerToken.assertChange(-int256(partialTakerTokenAmount)); + takerMakerToken.assertChange(int256(0)); + makerTakerToken.assertChange(int256(partialTakerTokenAmount)); + makerMakerToken.assertChange(-int256(partialMakerTokenAmount)); + recTakerToken.assertChange(int256(0)); + recMakerToken.assertChange(int256(partialMakerTokenAmount)); + } + + function testFullyFillRepaymentOrDCAOrder() external { + ConOrder memory order = defaultOrder; + + Snapshot memory takerTakerToken = BalanceSnapshot.take({ owner: order.taker, token: order.takerToken }); + Snapshot memory takerMakerToken = BalanceSnapshot.take({ owner: order.taker, token: order.makerToken }); + Snapshot memory makerTakerToken = BalanceSnapshot.take({ owner: order.maker, token: order.takerToken }); + Snapshot memory makerMakerToken = BalanceSnapshot.take({ owner: order.maker, token: order.makerToken }); + Snapshot memory recTakerToken = BalanceSnapshot.take({ owner: order.recipient, token: order.takerToken }); + Snapshot memory recMakerToken = BalanceSnapshot.take({ owner: order.recipient, token: order.makerToken }); + + // craft the `flagAndPeriod` of the order for BestBuy case + uint256 flags = FLG_SINGLE_AMOUNT_CAP_MASK | FLG_PERIODIC_MASK | FLG_PARTIAL_FILL_MASK; + uint256 period = 12 hours; + order.flagsAndPeriod = flags | period; + + uint256 numberOfCycles = (defaultExpiry - block.timestamp) / period; + + takerSig = signConOrder(takerPrivateKey, order, address(conditionalSwap)); vm.expectEmit(true, true, true, true); emit ConditionalOrderFilled( - getConOrderHash(defaultOrder), - defaultOrder.taker, - defaultOrder.maker, - defaultOrder.takerToken, - defaultOrder.takerTokenAmount, - defaultOrder.makerToken, - defaultOrder.makerTokenAmount, + getConOrderHash(order), + order.taker, + order.maker, + order.takerToken, + order.takerTokenAmount, + order.makerToken, + order.makerTokenAmount, recipient ); - vm.startPrank(maker); - conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.startPrank(order.maker); + for (uint256 i; i < numberOfCycles; ++i) { + conditionalSwap.fillConOrder(order, takerSig, order.takerTokenAmount, order.makerTokenAmount, defaultSettlementData); + vm.warp(block.timestamp + period); + } vm.stopPrank(); - takerTakerToken.assertChange(-int256(defaultOrder.takerTokenAmount)); + takerTakerToken.assertChange(-int256(order.takerTokenAmount) * int256(numberOfCycles)); takerMakerToken.assertChange(int256(0)); - makerTakerToken.assertChange(int256(defaultOrder.takerTokenAmount)); - makerMakerToken.assertChange(-int256(defaultOrder.makerTokenAmount)); + makerTakerToken.assertChange(int256(order.takerTokenAmount) * int256(numberOfCycles)); + makerMakerToken.assertChange(-int256(order.makerTokenAmount) * int256(numberOfCycles)); recTakerToken.assertChange(int256(0)); - recMakerToken.assertChange(int256(defaultOrder.makerTokenAmount)); + recMakerToken.assertChange(int256(order.makerTokenAmount) * int256(numberOfCycles)); } - function testRepaymentOrDCAOrder() external { - Snapshot memory takerTakerToken = BalanceSnapshot.take({ owner: taker, token: defaultOrder.takerToken }); - Snapshot memory takerMakerToken = BalanceSnapshot.take({ owner: taker, token: defaultOrder.makerToken }); - Snapshot memory makerTakerToken = BalanceSnapshot.take({ owner: maker, token: defaultOrder.takerToken }); - Snapshot memory makerMakerToken = BalanceSnapshot.take({ owner: maker, token: defaultOrder.makerToken }); - Snapshot memory recTakerToken = BalanceSnapshot.take({ owner: recipient, token: defaultOrder.takerToken }); - Snapshot memory recMakerToken = BalanceSnapshot.take({ owner: recipient, token: defaultOrder.makerToken }); + function testPartialFillRepaymentOrDCAOrder() external { + ConOrder memory order = defaultOrder; - // craft the `flagAndPeriod` of the defaultOrder for BestBuy case + Snapshot memory takerTakerToken = BalanceSnapshot.take({ owner: order.taker, token: order.takerToken }); + Snapshot memory takerMakerToken = BalanceSnapshot.take({ owner: order.taker, token: order.makerToken }); + Snapshot memory makerTakerToken = BalanceSnapshot.take({ owner: order.maker, token: order.takerToken }); + Snapshot memory makerMakerToken = BalanceSnapshot.take({ owner: order.maker, token: order.makerToken }); + Snapshot memory recTakerToken = BalanceSnapshot.take({ owner: order.recipient, token: order.takerToken }); + Snapshot memory recMakerToken = BalanceSnapshot.take({ owner: order.recipient, token: order.makerToken }); + + // craft the `flagAndPeriod` of the order for BestBuy case uint256 flags = FLG_SINGLE_AMOUNT_CAP_MASK | FLG_PERIODIC_MASK | FLG_PARTIAL_FILL_MASK; uint256 period = 12 hours; - defaultOrder.flagsAndPeriod = flags | period; + order.flagsAndPeriod = flags | period; uint256 numberOfCycles = (defaultExpiry - block.timestamp) / period; - defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + uint256 partialTakerTokenAmount = 5 * 1e6; + uint256 partialMakerTokenAmount = 5 ether; + + takerSig = signConOrder(takerPrivateKey, order, address(conditionalSwap)); vm.expectEmit(true, true, true, true); emit ConditionalOrderFilled( - getConOrderHash(defaultOrder), - defaultOrder.taker, - defaultOrder.maker, - defaultOrder.takerToken, - defaultOrder.takerTokenAmount, - defaultOrder.makerToken, - defaultOrder.makerTokenAmount, + getConOrderHash(order), + order.taker, + order.maker, + order.takerToken, + partialTakerTokenAmount, + order.makerToken, + partialMakerTokenAmount, recipient ); - vm.startPrank(maker); + vm.startPrank(order.maker); for (uint256 i; i < numberOfCycles; ++i) { - conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + conditionalSwap.fillConOrder(order, takerSig, partialTakerTokenAmount, partialMakerTokenAmount, defaultSettlementData); vm.warp(block.timestamp + period); } vm.stopPrank(); - takerTakerToken.assertChange(-int256(defaultOrder.takerTokenAmount) * int256(numberOfCycles)); + takerTakerToken.assertChange(-int256(partialTakerTokenAmount) * int256(numberOfCycles)); takerMakerToken.assertChange(int256(0)); - makerTakerToken.assertChange(int256(defaultOrder.takerTokenAmount) * int256(numberOfCycles)); - makerMakerToken.assertChange(-int256(defaultOrder.makerTokenAmount) * int256(numberOfCycles)); + makerTakerToken.assertChange(int256(partialTakerTokenAmount) * int256(numberOfCycles)); + makerMakerToken.assertChange(-int256(partialMakerTokenAmount) * int256(numberOfCycles)); recTakerToken.assertChange(int256(0)); - recMakerToken.assertChange(int256(defaultOrder.makerTokenAmount) * int256(numberOfCycles)); + recMakerToken.assertChange(int256(partialMakerTokenAmount) * int256(numberOfCycles)); } function testCannotFillExpiredOrder() public { vm.warp(defaultOrder.expiry + 1); vm.expectRevert(IConditionalSwap.ExpiredOrder.selector); - vm.startPrank(maker); - conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.startPrank(defaultOrder.maker); + conditionalSwap.fillConOrder(defaultOrder, takerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); vm.stopPrank(); } @@ -109,54 +205,61 @@ contract ConFillTest is ConditionalOrderSwapTest { vm.expectRevert(IConditionalSwap.NotOrderMaker.selector); vm.startPrank(invalidOrderMaker); - conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + conditionalSwap.fillConOrder(defaultOrder, takerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); vm.stopPrank(); } function testCannotFillOrderWithZeroTakerTokenAmount() public { vm.expectRevert(IConditionalSwap.ZeroTokenAmount.selector); - vm.startPrank(maker); - conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, 0, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.startPrank(defaultOrder.maker); + conditionalSwap.fillConOrder(defaultOrder, takerSig, 0, defaultOrder.makerTokenAmount, defaultSettlementData); vm.stopPrank(); } function testCannotFillOrderWithInvalidTotalTakerTokenAmount() public { - // craft the `flagAndPeriod` of the defaultOrder for BestBuy case + ConOrder memory order = defaultOrder; + + // craft the `flagAndPeriod` of the order for BestBuy case uint256 flags = FLG_PARTIAL_FILL_MASK; - defaultOrder.flagsAndPeriod = flags; + order.flagsAndPeriod = flags; - defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + takerSig = signConOrder(takerPrivateKey, order, address(conditionalSwap)); - vm.startPrank(maker); + vm.startPrank(order.maker); // the first fill with full takerTokenAmount - conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + conditionalSwap.fillConOrder(order, takerSig, order.takerTokenAmount, order.makerTokenAmount, defaultSettlementData); vm.expectRevert(IConditionalSwap.InvalidTakingAmount.selector); // The second fill with 1 takerTokenAmount would exceed the total cap this time. - conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, 1, defaultOrder.makerTokenAmount, defaultSettlementData); + conditionalSwap.fillConOrder(order, takerSig, 1, order.makerTokenAmount, defaultSettlementData); vm.stopPrank(); } function testCannotFillOrderWithInvalidSingleTakerTokenAmount() public { - // craft the `flagAndPeriod` of the defaultOrder for BestBuy case + ConOrder memory order = defaultOrder; + + // craft the `flagAndPeriod` of the order for BestBuy case uint256 flags = FLG_SINGLE_AMOUNT_CAP_MASK | FLG_PERIODIC_MASK | FLG_PARTIAL_FILL_MASK; uint256 period = 12 hours; - defaultOrder.flagsAndPeriod = flags | period; + order.flagsAndPeriod = flags | period; - defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + takerSig = signConOrder(takerPrivateKey, order, address(conditionalSwap)); vm.expectRevert(IConditionalSwap.InvalidTakingAmount.selector); - vm.startPrank(maker); - conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount + 1, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.startPrank(order.maker); + conditionalSwap.fillConOrder(order, takerSig, order.takerTokenAmount + 1, order.makerTokenAmount, defaultSettlementData); vm.stopPrank(); } - function testCannotFillOrderWithInvalidZeroRecipient() public { - defaultOrder.recipient = payable(address(0)); + function testCannotFillOrderWithZeroRecipient() public { + ConOrder memory order = defaultOrder; + order.recipient = payable(address(0)); + + takerSig = signConOrder(takerPrivateKey, order, address(conditionalSwap)); vm.expectRevert(IConditionalSwap.InvalidRecipient.selector); - vm.startPrank(maker); - conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.startPrank(order.maker); + conditionalSwap.fillConOrder(order, takerSig, order.takerTokenAmount, order.makerTokenAmount, defaultSettlementData); vm.stopPrank(); } @@ -165,43 +268,45 @@ contract ConFillTest is ConditionalOrderSwapTest { bytes memory randomEOASig = signConOrder(randomPrivateKey, defaultOrder, address(conditionalSwap)); vm.expectRevert(IConditionalSwap.InvalidSignature.selector); - vm.startPrank(maker); + vm.startPrank(defaultOrder.maker); conditionalSwap.fillConOrder(defaultOrder, randomEOASig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); vm.stopPrank(); } function testCannotFillOrderWithinSamePeriod() public { - // craft the `flagAndPeriod` of the defaultOrder for BestBuy case + ConOrder memory order = defaultOrder; + // craft the `flagAndPeriod` of the order for BestBuy case uint256 flags = FLG_SINGLE_AMOUNT_CAP_MASK | FLG_PERIODIC_MASK | FLG_PARTIAL_FILL_MASK; uint256 period = 12 hours; - defaultOrder.flagsAndPeriod = flags | period; + order.flagsAndPeriod = flags | period; + + takerSig = signConOrder(takerPrivateKey, order, address(conditionalSwap)); - defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); - vm.startPrank(maker); - conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.startPrank(order.maker); + conditionalSwap.fillConOrder(order, takerSig, order.takerTokenAmount, order.makerTokenAmount, defaultSettlementData); vm.warp(block.timestamp + 1); vm.expectRevert(IConditionalSwap.InsufficientTimePassed.selector); - conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + conditionalSwap.fillConOrder(order, takerSig, order.takerTokenAmount, order.makerTokenAmount, defaultSettlementData); vm.stopPrank(); } function testCannotFillOrderWithInvalidSettlementType() public { bytes memory settlementData = hex"02"; - defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + takerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); vm.expectRevert(IConditionalSwap.InvalidSettlementType.selector); - vm.startPrank(maker); - conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, settlementData); + vm.startPrank(defaultOrder.maker); + conditionalSwap.fillConOrder(defaultOrder, takerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, settlementData); vm.stopPrank(); } function testCannotFillOrderWithInvalidMakingAmount() public { - defaultTakerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + takerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); vm.expectRevert(IConditionalSwap.InvalidMakingAmount.selector); - vm.startPrank(maker); - conditionalSwap.fillConOrder(defaultOrder, defaultTakerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount - 1, defaultSettlementData); + vm.startPrank(defaultOrder.maker); + conditionalSwap.fillConOrder(defaultOrder, takerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount - 1, defaultSettlementData); vm.stopPrank(); } } diff --git a/test/forkMainnet/ConditionalSwap/Setup.t.sol b/test/forkMainnet/ConditionalSwap/Setup.t.sol index 7d7a9247..34bde3e2 100644 --- a/test/forkMainnet/ConditionalSwap/Setup.t.sol +++ b/test/forkMainnet/ConditionalSwap/Setup.t.sol @@ -35,7 +35,7 @@ contract ConditionalOrderSwapTest is Test, Tokens, BalanceUtil, Permit2Helper, S uint256 public defaultExpiry = block.timestamp + 1 days; uint256 public defaultSalt = 1234; bytes public defaultTakerPermit; - bytes public defaultTakerSig; + bytes public takerSig; bytes public defaultSettlementData; // mask for triggering different business logic (e.g. BestBuy, Repayment, DCA) From 344d6a418b697eb5294480a9b279a7ce75ba17dc Mon Sep 17 00:00:00 2001 From: alex0207s Date: Tue, 5 Mar 2024 17:50:50 +0800 Subject: [PATCH 08/10] add a feature for a maker to maintain a relayer list --- contracts/ConditionalSwap.sol | 19 +++++- contracts/interfaces/IConditionalSwap.sol | 6 +- test/forkMainnet/ConditionalSwap/Fill.t.sol | 68 +++++++++++++++++++- test/forkMainnet/ConditionalSwap/Setup.t.sol | 4 ++ 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/contracts/ConditionalSwap.sol b/contracts/ConditionalSwap.sol index 9ca03987..aad563d4 100644 --- a/contracts/ConditionalSwap.sol +++ b/contracts/ConditionalSwap.sol @@ -23,6 +23,7 @@ contract ConditionalSwap is IConditionalSwap, Ownable, TokenCollector, EIP712 { // record how many taker tokens have been filled in an order mapping(bytes32 => uint256) public orderHashToTakerTokenFilledAmount; mapping(bytes32 => uint256) public orderHashToLastExecutedTime; + mapping(address => mapping(address => bool)) public makerToRelayer; constructor(address _owner, address _uniswapPermit2, address _allowanceTarget) Ownable(_owner) TokenCollector(_uniswapPermit2, _allowanceTarget) {} @@ -37,7 +38,7 @@ contract ConditionalSwap is IConditionalSwap, Ownable, TokenCollector, EIP712 { bytes calldata settlementData ) external payable override { if (block.timestamp > order.expiry) revert ExpiredOrder(); - if (msg.sender != order.maker) revert NotOrderMaker(); + if (msg.sender != order.maker && !makerToRelayer[order.maker][msg.sender]) revert NotOrderExecutor(); if (order.recipient == address(0)) revert InvalidRecipient(); if (takerTokenAmount == 0) revert ZeroTokenAmount(); @@ -108,6 +109,22 @@ contract ConditionalSwap is IConditionalSwap, Ownable, TokenCollector, EIP712 { _emitConOrderFilled(order, orderHash, takerTokenAmount, returnedAmount); } + function addRelayers(address[] calldata relayers) external { + // the relayers is stored in calldata, there is no need to cache the relayers length + for (uint256 i; i < relayers.length; ++i) { + makerToRelayer[msg.sender][relayers[i]] = true; + emit AddRelayer(msg.sender, relayers[i]); + } + } + + function removeRelayers(address[] calldata relayers) external { + // the relayers is stored in calldata, there is no need to cache the relayers length + for (uint256 i; i < relayers.length; ++i) { + delete makerToRelayer[msg.sender][relayers[i]]; + emit RemoveRelayer(msg.sender, relayers[i]); + } + } + function _emitConOrderFilled(ConOrder calldata order, bytes32 orderHash, uint256 takerTokenSettleAmount, uint256 makerTokenSettleAmount) internal { emit ConditionalOrderFilled( orderHash, diff --git a/contracts/interfaces/IConditionalSwap.sol b/contracts/interfaces/IConditionalSwap.sol index 6cd3067d..5aff7fa7 100644 --- a/contracts/interfaces/IConditionalSwap.sol +++ b/contracts/interfaces/IConditionalSwap.sol @@ -11,7 +11,7 @@ interface IConditionalSwap { error InvalidTakingAmount(); error InvalidMakingAmount(); error InsufficientOutput(); - error NotOrderMaker(); + error NotOrderExecutor(); error InvalidRecipient(); error InvalidSettlementType(); @@ -27,6 +27,10 @@ interface IConditionalSwap { address recipient ); + event AddRelayer(address indexed maker, address indexed relayer); + + event RemoveRelayer(address indexed maker, address indexed relayer); + // function function fillConOrder( ConOrder calldata order, diff --git a/test/forkMainnet/ConditionalSwap/Fill.t.sol b/test/forkMainnet/ConditionalSwap/Fill.t.sol index 3c3866f2..ff8b3038 100644 --- a/test/forkMainnet/ConditionalSwap/Fill.t.sol +++ b/test/forkMainnet/ConditionalSwap/Fill.t.sol @@ -191,6 +191,57 @@ contract ConFillTest is ConditionalOrderSwapTest { recMakerToken.assertChange(int256(partialMakerTokenAmount) * int256(numberOfCycles)); } + function testExecuteOrderWithRelayer() external { + ConOrder memory order = defaultOrder; + + Snapshot memory takerTakerToken = BalanceSnapshot.take({ owner: order.taker, token: order.takerToken }); + Snapshot memory takerMakerToken = BalanceSnapshot.take({ owner: order.taker, token: order.makerToken }); + Snapshot memory makerTakerToken = BalanceSnapshot.take({ owner: order.maker, token: order.takerToken }); + Snapshot memory makerMakerToken = BalanceSnapshot.take({ owner: order.maker, token: order.makerToken }); + Snapshot memory recTakerToken = BalanceSnapshot.take({ owner: order.recipient, token: order.takerToken }); + Snapshot memory recMakerToken = BalanceSnapshot.take({ owner: order.recipient, token: order.makerToken }); + Snapshot memory relayerTakerToken = BalanceSnapshot.take({ owner: relayer, token: order.takerToken }); + Snapshot memory relayerMakerToken = BalanceSnapshot.take({ owner: relayer, token: order.makerToken }); + + // craft the `flagAndPeriod` of the order for BestBuy case + uint256 flags = FLG_PARTIAL_FILL_MASK; + order.flagsAndPeriod = flags; + + // add relayer + vm.startPrank(order.maker); + address[] memory relayers = new address[](1); + relayers[0] = relayer; + conditionalSwap.addRelayers(relayers); + vm.stopPrank(); + + takerSig = signConOrder(takerPrivateKey, order, address(conditionalSwap)); + + vm.expectEmit(true, true, true, true); + emit ConditionalOrderFilled( + getConOrderHash(order), + order.taker, + order.maker, + order.takerToken, + order.takerTokenAmount, + order.makerToken, + order.makerTokenAmount, + recipient + ); + + vm.startPrank(relayer); + conditionalSwap.fillConOrder(order, takerSig, order.takerTokenAmount, order.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + + takerTakerToken.assertChange(-int256(order.takerTokenAmount)); + takerMakerToken.assertChange(int256(0)); + makerTakerToken.assertChange(0); + makerMakerToken.assertChange(0); + recTakerToken.assertChange(int256(0)); + recMakerToken.assertChange(int256(order.makerTokenAmount)); + relayerTakerToken.assertChange(int256(order.takerTokenAmount)); + relayerMakerToken.assertChange(-int256(order.makerTokenAmount)); + } + function testCannotFillExpiredOrder() public { vm.warp(defaultOrder.expiry + 1); @@ -203,7 +254,7 @@ contract ConFillTest is ConditionalOrderSwapTest { function testCannotFillOrderByInvalidOderMaker() public { address invalidOrderMaker = makeAddr("invalidOrderMaker"); - vm.expectRevert(IConditionalSwap.NotOrderMaker.selector); + vm.expectRevert(IConditionalSwap.NotOrderExecutor.selector); vm.startPrank(invalidOrderMaker); conditionalSwap.fillConOrder(defaultOrder, takerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); vm.stopPrank(); @@ -309,4 +360,19 @@ contract ConFillTest is ConditionalOrderSwapTest { conditionalSwap.fillConOrder(defaultOrder, takerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount - 1, defaultSettlementData); vm.stopPrank(); } + + function testCannotFillOrderWithRemovedRelayer() public { + // add relayer + vm.startPrank(defaultOrder.maker); + address[] memory relayers = new address[](1); + relayers[0] = relayer; + conditionalSwap.addRelayers(relayers); + conditionalSwap.removeRelayers(relayers); + vm.stopPrank(); + + vm.expectRevert(IConditionalSwap.NotOrderExecutor.selector); + vm.startPrank(relayer); + conditionalSwap.fillConOrder(defaultOrder, takerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + } } diff --git a/test/forkMainnet/ConditionalSwap/Setup.t.sol b/test/forkMainnet/ConditionalSwap/Setup.t.sol index 34bde3e2..77c4feef 100644 --- a/test/forkMainnet/ConditionalSwap/Setup.t.sol +++ b/test/forkMainnet/ConditionalSwap/Setup.t.sol @@ -28,8 +28,10 @@ contract ConditionalOrderSwapTest is Test, Tokens, BalanceUtil, Permit2Helper, S address public allowanceTargetOwner = makeAddr("allowanceTargetOwner"); uint256 public takerPrivateKey = uint256(1); uint256 public makerPrivateKey = uint256(2); + uint256 public relayerPrivateKey = uint256(3); address public taker = vm.addr(takerPrivateKey); address payable public maker = payable(vm.addr(makerPrivateKey)); + address payable public relayer = payable(vm.addr(relayerPrivateKey)); address payable public recipient = payable(makeAddr("recipient")); uint256 public defaultExpiry = block.timestamp + 1 days; @@ -59,8 +61,10 @@ contract ConditionalOrderSwapTest is Test, Tokens, BalanceUtil, Permit2Helper, S deal(maker, 100 ether); deal(taker, 100 ether); + deal(relayer, 100 ether); setTokenBalanceAndApprove(maker, address(conditionalSwap), tokens, 100000); setTokenBalanceAndApprove(taker, address(conditionalSwap), tokens, 100000); + setTokenBalanceAndApprove(relayer, address(conditionalSwap), tokens, 100000); defaultTakerPermit = hex"01"; defaultSettlementData = hex"00"; From fad185917aa7bbe8e07fcba58ab3d287961d1223 Mon Sep 17 00:00:00 2001 From: alex0207s Date: Thu, 14 Mar 2024 18:27:35 +0800 Subject: [PATCH 09/10] fix the period mask --- contracts/ConditionalSwap.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/ConditionalSwap.sol b/contracts/ConditionalSwap.sol index aad563d4..28b03eeb 100644 --- a/contracts/ConditionalSwap.sol +++ b/contracts/ConditionalSwap.sol @@ -18,7 +18,7 @@ contract ConditionalSwap is IConditionalSwap, Ownable, TokenCollector, EIP712 { uint256 private constant FLG_SINGLE_AMOUNT_CAP_MASK = 1 << 255; // ConOrder.amount is the cap of single execution, not total cap uint256 private constant FLG_PERIODIC_MASK = 1 << 254; // ConOrder can be executed periodically uint256 private constant FLG_PARTIAL_FILL_MASK = 1 << 253; // ConOrder can be fill partially - uint256 private constant PERIOD_MASK = (1 << 16) - 1; + uint256 private constant PERIOD_MASK = (1 << 128) - 1; // this is a 128-bit mask where all bits are set to 1 // record how many taker tokens have been filled in an order mapping(bytes32 => uint256) public orderHashToTakerTokenFilledAmount; From d4347b2ed2b514eabe7756278fd30e9003f22b3f Mon Sep 17 00:00:00 2001 From: alex0207s Date: Thu, 14 Mar 2024 18:31:34 +0800 Subject: [PATCH 10/10] fix order hash function --- contracts/libraries/ConditionalOrder.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/libraries/ConditionalOrder.sol b/contracts/libraries/ConditionalOrder.sol index 1c576676..8f30643d 100644 --- a/contracts/libraries/ConditionalOrder.sol +++ b/contracts/libraries/ConditionalOrder.sol @@ -32,7 +32,7 @@ function getConOrderHash(ConOrder memory order) pure returns (bytes32 conOrderHa order.takerTokenAmount, order.makerToken, order.makerTokenAmount, - order.takerTokenPermit, + keccak256(order.takerTokenPermit), order.flagsAndPeriod, order.expiry, order.salt