Skip to content
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

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
122 changes: 122 additions & 0 deletions contracts/ConditionalSwap.sol
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;
Copy link
Contributor

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 block

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix

Copy link
Contributor

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.

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think compare with makerTokenAmount instead of minMakerTokenAmount is more appropriate here.

order.makerToken.transferTo(order.recipient, returnedAmount);
} else {
revert(); // specific the error message1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add error here, debug would be easier

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I add a new error InvalidSettlementType for this case.

}

_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
);
}
}
38 changes: 38 additions & 0 deletions contracts/interfaces/IConditionalSwap.sol
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;
}
41 changes: 41 additions & 0 deletions contracts/libraries/ConditionalOrder.sol
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
)
);
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
168 changes: 168 additions & 0 deletions test/forkMainnet/ConditionalSwap/Fill.t.sol
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Define flags constant first and use logic OR here to compose flags to have better semantic meaning.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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();
}
}
Loading
Loading