-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Implement a conditional swap #304
base: master
Are you sure you want to change the base?
Changes from 5 commits
1201ae7
374475d
a647fbb
68f223e
4067352
48fbfef
712a687
344d6a4
fad1859
d4347b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
// 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 << 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) {} | ||
|
||
//@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) revert NotOrderMaker(); | ||
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 | ||
_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; | ||
|
||
if (returnedAmount < minMakerTokenAmount) revert InsufficientOutput(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think compare with |
||
order.makerToken.transferTo(order.recipient, returnedAmount); | ||
} else { | ||
revert(); // specific the error message1 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add error here, debug would be easier There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I add a new error |
||
} | ||
|
||
_emitConOrderFilled(order, orderHash, takerTokenAmount, returnedAmount); | ||
} | ||
|
||
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 | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.17; | ||
|
||
import { ConOrder } from "../libraries/ConditionalOrder.sol"; | ||
|
||
interface IConditionalSwap { | ||
// error | ||
error ExpiredOrder(); | ||
error InsufficientTimePassed(); | ||
error ZeroTokenAmount(); | ||
error InvalidSignature(); | ||
error InvalidTakingAmount(); | ||
error InvalidMakingAmount(); | ||
error InvalidRecipient(); | ||
error NotOrderMaker(); | ||
error InsufficientOutput(); | ||
|
||
// event | ||
event ConditionalOrderFilled( | ||
bytes32 indexed orderHash, | ||
address indexed taker, | ||
address indexed maker, | ||
address takerToken, | ||
uint256 takerTokenFilledAmount, | ||
address makerToken, | ||
uint256 makerTokenSettleAmount, | ||
address recipient | ||
); | ||
|
||
// function | ||
function fillConOrder( | ||
ConOrder calldata order, | ||
bytes calldata takerSignature, | ||
uint256 takerTokenAmount, | ||
uint256 makerTokenAmount, | ||
bytes calldata settlementData | ||
) external payable; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
order.takerTokenPermit, | ||
order.flagsAndPeriod, | ||
order.expiry, | ||
order.salt | ||
) | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add comments for different scenario requirement and flags to help reader understanding |
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Define flags constant first and use logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fix |
||
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(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move declaration into the
settlementType == 0x01
blockThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed, this should not be changed.