Skip to content
118 changes: 118 additions & 0 deletions contracts/ConditionalSwap.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// 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:];

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);
uint256 returnedAmount = order.makerToken.getBalance(address(this)) - makerTokenBalanceBefore;

if (returnedAmount < makerTokenAmount) revert InsufficientOutput();
order.makerToken.transferTo(order.recipient, returnedAmount);
} else revert InvalidSettlementType();

_emitConOrderFilled(order, orderHash, takerTokenAmount, makerTokenAmount);
}

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 ExpiredOrder();
error InsufficientTimePassed();
error InvalidSignature();
error ZeroTokenAmount();
error InvalidTakingAmount();
error InvalidMakingAmount();
error InsufficientOutput();
error NotOrderMaker();
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
);

// 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
Loading