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
140 changes: 140 additions & 0 deletions contracts/ConditionalSwap.sol
Original file line number Diff line number Diff line change
@@ -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;
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
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
);
}
}
42 changes: 42 additions & 0 deletions contracts/interfaces/IConditionalSwap.sol
Original file line number Diff line number Diff line change
@@ -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;
}
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,
keccak256(order.takerTokenPermit),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

EIP712: The dynamic values bytes and string are encoded as a keccak256 hash of their contents.

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
Loading