diff --git a/contracts/ConditionalSwap.sol b/contracts/ConditionalSwap.sol new file mode 100644 index 00000000..28b03eeb --- /dev/null +++ b/contracts/ConditionalSwap.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +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 { 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 << 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; + mapping(bytes32 => uint256) public orderHashToLastExecutedTime; + mapping(address => mapping(address => bool)) public makerToRelayer; + + constructor(address _owner, address _uniswapPermit2, address _allowanceTarget) Ownable(_owner) TokenCollector(_uniswapPermit2, _allowanceTarget) {} + + //@note if this contract has the ability to transfer out ETH, implement the receive function + // 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 && !makerToRelayer[order.maker][msg.sender]) revert NotOrderExecutor(); + 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(); + } + } + + // validate the takerTokenAmount + if (order.flagsAndPeriod & FLG_SINGLE_AMOUNT_CAP_MASK != 0) { + // single cap amount + if (takerTokenAmount > order.takerTokenAmount) revert InvalidTakingAmount(); + } else { + // total cap amount + if (orderHashToTakerTokenFilledAmount[orderHash] + takerTokenAmount > order.takerTokenAmount) { + revert InvalidTakingAmount(); + } + } + orderHashToTakerTokenFilledAmount[orderHash] += takerTokenAmount; + + // 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 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) { + // 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; + + // 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, 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, + 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..5aff7fa7 --- /dev/null +++ b/contracts/interfaces/IConditionalSwap.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import { ConOrder } from "../libraries/ConditionalOrder.sol"; + +interface IConditionalSwap { + error ExpiredOrder(); + error InsufficientTimePassed(); + error InvalidSignature(); + error ZeroTokenAmount(); + error InvalidTakingAmount(); + error InvalidMakingAmount(); + error InsufficientOutput(); + error NotOrderExecutor(); + error InvalidRecipient(); + error InvalidSettlementType(); + + /// @notice Emitted when a conditional order is filled + event ConditionalOrderFilled( + bytes32 indexed orderHash, + address indexed taker, + address indexed maker, + address takerToken, + uint256 takerTokenFilledAmount, + address makerToken, + uint256 makerTokenSettleAmount, + address recipient + ); + + event AddRelayer(address indexed maker, address indexed relayer); + + event RemoveRelayer(address indexed maker, address indexed relayer); + + // function + 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 new file mode 100644 index 00000000..8f30643d --- /dev/null +++ b/contracts/libraries/ConditionalOrder.sol @@ -0,0 +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 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 +struct ConOrder { + address taker; + address payable maker; // only maker can fill this ConOrder + address payable recipient; + address takerToken; // from user to maker + uint256 takerTokenAmount; + address makerToken; // from maker to recipient + 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 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, + keccak256(order.takerTokenPermit), + order.flagsAndPeriod, + order.expiry, + order.salt + ) + ); +} 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" }, diff --git a/test/forkMainnet/ConditionalSwap/Fill.t.sol b/test/forkMainnet/ConditionalSwap/Fill.t.sol new file mode 100644 index 00000000..ff8b3038 --- /dev/null +++ b/test/forkMainnet/ConditionalSwap/Fill.t.sol @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import { IConditionalSwap } from "contracts/interfaces/IConditionalSwap.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"; + +contract ConFillTest is ConditionalOrderSwapTest { + using BalanceSnapshot for Snapshot; + + function setUp() public override { + super.setUp(); + } + + 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; + 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(); + + 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(order), + order.taker, + order.maker, + order.takerToken, + order.takerTokenAmount, + order.makerToken, + order.makerTokenAmount, + recipient + ); + + 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(order.takerTokenAmount) * int256(numberOfCycles)); + takerMakerToken.assertChange(int256(0)); + makerTakerToken.assertChange(int256(order.takerTokenAmount) * int256(numberOfCycles)); + makerMakerToken.assertChange(-int256(order.makerTokenAmount) * int256(numberOfCycles)); + recTakerToken.assertChange(int256(0)); + recMakerToken.assertChange(int256(order.makerTokenAmount) * int256(numberOfCycles)); + } + + function testPartialFillRepaymentOrDCAOrder() 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; + + 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, + recipient + ); + + vm.startPrank(order.maker); + for (uint256 i; i < numberOfCycles; ++i) { + conditionalSwap.fillConOrder(order, takerSig, partialTakerTokenAmount, partialMakerTokenAmount, defaultSettlementData); + vm.warp(block.timestamp + period); + } + vm.stopPrank(); + + takerTakerToken.assertChange(-int256(partialTakerTokenAmount) * int256(numberOfCycles)); + takerMakerToken.assertChange(int256(0)); + makerTakerToken.assertChange(int256(partialTakerTokenAmount) * int256(numberOfCycles)); + makerMakerToken.assertChange(-int256(partialMakerTokenAmount) * int256(numberOfCycles)); + recTakerToken.assertChange(int256(0)); + 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); + + vm.expectRevert(IConditionalSwap.ExpiredOrder.selector); + vm.startPrank(defaultOrder.maker); + conditionalSwap.fillConOrder(defaultOrder, takerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + } + + function testCannotFillOrderByInvalidOderMaker() public { + address invalidOrderMaker = makeAddr("invalidOrderMaker"); + + vm.expectRevert(IConditionalSwap.NotOrderExecutor.selector); + vm.startPrank(invalidOrderMaker); + conditionalSwap.fillConOrder(defaultOrder, takerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + } + + function testCannotFillOrderWithZeroTakerTokenAmount() public { + vm.expectRevert(IConditionalSwap.ZeroTokenAmount.selector); + vm.startPrank(defaultOrder.maker); + conditionalSwap.fillConOrder(defaultOrder, takerSig, 0, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + } + + function testCannotFillOrderWithInvalidTotalTakerTokenAmount() public { + ConOrder memory order = defaultOrder; + + // 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.startPrank(order.maker); + // the first fill with full takerTokenAmount + 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(order, takerSig, 1, order.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + } + + function testCannotFillOrderWithInvalidSingleTakerTokenAmount() public { + 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; + order.flagsAndPeriod = flags | period; + + takerSig = signConOrder(takerPrivateKey, order, address(conditionalSwap)); + + vm.expectRevert(IConditionalSwap.InvalidTakingAmount.selector); + vm.startPrank(order.maker); + conditionalSwap.fillConOrder(order, takerSig, order.takerTokenAmount + 1, order.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + } + + 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(order.maker); + conditionalSwap.fillConOrder(order, takerSig, order.takerTokenAmount, order.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(defaultOrder.maker); + conditionalSwap.fillConOrder(defaultOrder, randomEOASig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + } + + function testCannotFillOrderWithinSamePeriod() public { + 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; + order.flagsAndPeriod = flags | period; + + takerSig = signConOrder(takerPrivateKey, order, address(conditionalSwap)); + + 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(order, takerSig, order.takerTokenAmount, order.makerTokenAmount, defaultSettlementData); + vm.stopPrank(); + } + + function testCannotFillOrderWithInvalidSettlementType() public { + bytes memory settlementData = hex"02"; + + takerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + + vm.expectRevert(IConditionalSwap.InvalidSettlementType.selector); + vm.startPrank(defaultOrder.maker); + conditionalSwap.fillConOrder(defaultOrder, takerSig, defaultOrder.takerTokenAmount, defaultOrder.makerTokenAmount, settlementData); + vm.stopPrank(); + } + + function testCannotFillOrderWithInvalidMakingAmount() public { + takerSig = signConOrder(takerPrivateKey, defaultOrder, address(conditionalSwap)); + + vm.expectRevert(IConditionalSwap.InvalidMakingAmount.selector); + vm.startPrank(defaultOrder.maker); + 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 new file mode 100644 index 00000000..77c4feef --- /dev/null +++ b/test/forkMainnet/ConditionalSwap/Setup.t.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import { Test } from "forge-std/Test.sol"; +import { ConditionalSwap } from "contracts/ConditionalSwap.sol"; +import { AllowanceTarget } from "contracts/AllowanceTarget.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"; +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"); + 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; + uint256 public defaultSalt = 1234; + bytes public defaultTakerPermit; + bytes public takerSig; + 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; + + 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); + 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"; + + 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 + }); + } +} 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); + } }