Skip to content

Commit

Permalink
Conditional Limit Order type
Browse files Browse the repository at this point in the history
  • Loading branch information
fleupold committed Jan 5, 2024
1 parent 074e015 commit 94e59c7
Show file tree
Hide file tree
Showing 2 changed files with 383 additions and 0 deletions.
124 changes: 124 additions & 0 deletions src/types/LimitOrder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import { IERC20 } from "@openzeppelin/interfaces/IERC20.sol";
import "../interfaces/IConditionalOrder.sol";
import "../BaseConditionalOrder.sol";

// --- error strings

/// @dev Invalid sell token asset
string constant INVALID_SELL_TOKEN = "invalid sell token";
/// @dev Invalid buy token asset
string constant INVALID_BUY_TOKEN = "invalid sell amount";
/// @dev Invalid receiver
string constant INVALID_RECEIVER = "invalid receiver";
/// @dev Invalid valid to timestamp
string constant INVALID_VALIDITY = "invalid validity";
/// @dev Either a buy order was attempted to be matched with a sell order or vice versa
string constant INVALID_ORDER_KIND = "invalid order kind";
/// @dev The limit price is not satisfied or the order is trying to be partially filled
string constant INVALID_LIMIT_AMOUNTS = "invalid limit amounts";
/// @dev Only ERC20 balances are supported
string constant INVALID_BALANCE = "invalid balances";

/**
* @title Limit order
* Providing tokens, limit amounts and a recipient, this conditional order type will accept any fill-or-kill trade satisfying these parameters until a certain deadline.
* @dev This order type does not have any replay protection, meaning it may be triggered many times assuming the contract has sufficient funds.
*/
contract LimitOrder is IConditionalOrder {
/**
* Defines the parameters of the limit order
* @param sellToken: the token to be sold
* @param buyToken: the token to be bought
* @param sellAmount: In case of a sell order, the exact amount of tokens the order is willing to sell. In case of a buy order, the maximium amount of tokens it is willing to sell
* @param buyAmount: In case of a sell order, the min amount of tokens the order is wants to receive. In case of a buy order, the exact amount of tokens it is willing to receive
* @param receiver: The account that should receive the proceeds of the trade
* @param validTo: The timestamp (in unix epoch) until which the order is valid
* @param isSellOrder: Whether this is a sell or buy order
*/
struct Data {
IERC20 sellToken;
IERC20 buyToken;
uint256 sellAmount;
uint256 buyAmount;
address receiver;
uint32 validTo;
bool isSellOrder;
}

/**
* @dev Check if the suggested order satisfies the limit order parameters.
*/
function verify(
address,
address,
bytes32 hash,
bytes32 domainSeparator,
bytes32,
bytes calldata staticInput,
bytes calldata,
GPv2Order.Data calldata suggestedOrder
) external pure override {
/// @dev Verify that the *suggested* order matches the payload.
if (!(hash == GPv2Order.hash(suggestedOrder, domainSeparator))) {
revert IConditionalOrder.OrderNotValid(INVALID_HASH);
}

Data memory limitOrder = abi.decode(staticInput, (Data));

/// Verify order parameters
if (suggestedOrder.sellToken != limitOrder.sellToken) {
revert IConditionalOrder.OrderNotValid(INVALID_SELL_TOKEN);
}

if (suggestedOrder.buyToken != limitOrder.buyToken) {
revert IConditionalOrder.OrderNotValid(INVALID_BUY_TOKEN);
}

if (suggestedOrder.receiver != limitOrder.receiver) {
revert IConditionalOrder.OrderNotValid(INVALID_RECEIVER);
}

if (suggestedOrder.validTo > limitOrder.validTo) {
revert IConditionalOrder.OrderNotValid(INVALID_VALIDITY);
}

if (suggestedOrder.kind == GPv2Order.KIND_SELL) {
if (!limitOrder.isSellOrder) {
revert IConditionalOrder.OrderNotValid(INVALID_ORDER_KIND);
}
if (
(suggestedOrder.sellAmount + suggestedOrder.feeAmount) !=
limitOrder.sellAmount
) {
revert IConditionalOrder.OrderNotValid(INVALID_LIMIT_AMOUNTS);
}
if (suggestedOrder.buyAmount < limitOrder.buyAmount) {
revert IConditionalOrder.OrderNotValid(INVALID_LIMIT_AMOUNTS);
}
} else {
// BUY order
if (limitOrder.isSellOrder) {
revert IConditionalOrder.OrderNotValid(INVALID_ORDER_KIND);
}
if (
(suggestedOrder.sellAmount + suggestedOrder.feeAmount) >
limitOrder.sellAmount
) {
revert IConditionalOrder.OrderNotValid(INVALID_LIMIT_AMOUNTS);
}
if (suggestedOrder.buyAmount != limitOrder.buyAmount) {
revert IConditionalOrder.OrderNotValid(INVALID_LIMIT_AMOUNTS);
}
}

if (
suggestedOrder.buyTokenBalance != GPv2Order.BALANCE_ERC20 ||
suggestedOrder.sellTokenBalance != GPv2Order.BALANCE_ERC20
) {
revert IConditionalOrder.OrderNotValid(INVALID_BALANCE);
}
}
}
259 changes: 259 additions & 0 deletions test/ComposableCoW.limit.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import { IERC20 } from "@openzeppelin/interfaces/IERC20.sol";
import "./ComposableCoW.base.t.sol";
import "../src/types/LimitOrder.sol";

library LimitOrderTest {
bytes32 constant DOMAIN_SEPARATOR =
0x3fd54831f488a22b28398de0c567a3b064b937f54f81739ae9bd545967f3abab;

function fail(
LimitOrder order,
Vm vm,
LimitOrder.Data memory orderData,
GPv2Order.Data memory trade,
string memory reason
) internal {
vm.expectRevert(
abi.encodeWithSelector(IConditionalOrder.OrderNotValid.selector, reason)
);
order.verify(
address(0),
address(0),
GPv2Order.hash(trade, DOMAIN_SEPARATOR),
DOMAIN_SEPARATOR,
bytes32(0),
abi.encode(orderData),
bytes(""),
trade
);
}

function pass(
LimitOrder order,
LimitOrder.Data memory orderData,
GPv2Order.Data memory trade
) internal pure {
order.verify(
address(0),
address(0),
GPv2Order.hash(trade, DOMAIN_SEPARATOR),
DOMAIN_SEPARATOR,
bytes32(0),
abi.encode(orderData),
bytes(""),
trade
);
}
}

contract ComposableCoWLimitOrderTest is Test {
IERC20 immutable SELL_TOKEN = IERC20(address(0x1));
IERC20 immutable BUY_TOKEN = IERC20(address(0x2));
address constant RECEIVER = address(0x3);
uint32 constant VALID_TO = 1687718700;

using LimitOrderTest for LimitOrder;

LimitOrder.Data sell;
LimitOrder.Data buy;

function setUp() public virtual {
sell = LimitOrder.Data({
sellToken: SELL_TOKEN,
buyToken: BUY_TOKEN,
sellAmount: 1 ether,
buyAmount: 1 ether,
receiver: RECEIVER,
validTo: VALID_TO,
isSellOrder: true
});
buy = LimitOrder.Data({
sellToken: SELL_TOKEN,
buyToken: BUY_TOKEN,
sellAmount: 1 ether,
buyAmount: 1 ether,
receiver: RECEIVER,
validTo: VALID_TO,
isSellOrder: false
});
}

function valid_trade(
bytes32 kind
) public view returns (GPv2Order.Data memory) {
return
GPv2Order.Data(
SELL_TOKEN,
BUY_TOKEN,
RECEIVER,
1 ether,
1 ether,
VALID_TO,
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff,
0, // use zero fee for limit orders
kind,
false,
GPv2Order.BALANCE_ERC20,
GPv2Order.BALANCE_ERC20
);
}

function test_valid_order() public {
LimitOrder order = new LimitOrder();
GPv2Order.Data memory valid = valid_trade(GPv2Order.KIND_SELL);
order.pass(sell, valid);

valid.kind = GPv2Order.KIND_BUY;
order.pass(buy, valid);
}

function test_amounts_sell_order() public {
LimitOrder order = new LimitOrder();
GPv2Order.Data memory valid = valid_trade(GPv2Order.KIND_SELL);

// Higer buy amount allowed for sell orders
valid.buyAmount += 1;
order.pass(sell, valid);

// Lower buy amount not allowed
valid.buyAmount -= 2;
order.fail(vm, sell, valid, INVALID_LIMIT_AMOUNTS);

// Different sell amount not allowed
valid = valid_trade(GPv2Order.KIND_SELL);
valid.sellAmount += 1;
order.fail(vm, sell, valid, INVALID_LIMIT_AMOUNTS);

valid.sellAmount -= 2;
order.fail(vm, sell, valid, INVALID_LIMIT_AMOUNTS);
}

function test_amounts_buy_order() public {
LimitOrder order = new LimitOrder();
GPv2Order.Data memory valid = valid_trade(GPv2Order.KIND_BUY);

// Lower sell amount allowed for buy orders
valid.sellAmount -= 1;
order.pass(buy, valid);

// Higher sell amount not allowed
valid.sellAmount += 2;
order.fail(vm, buy, valid, INVALID_LIMIT_AMOUNTS);

// Different buy amount not allowed
valid = valid_trade(GPv2Order.KIND_BUY);
valid.buyAmount += 1;
order.fail(vm, buy, valid, INVALID_LIMIT_AMOUNTS);

valid.buyAmount -= 2;
order.fail(vm, buy, valid, INVALID_LIMIT_AMOUNTS);
}

function test_amounts_fee() public {
LimitOrder order = new LimitOrder();
GPv2Order.Data memory valid = valid_trade(GPv2Order.KIND_SELL);

// Fee exceeds amount
valid.feeAmount = 0.1 ether;
order.fail(vm, sell, valid, INVALID_LIMIT_AMOUNTS);

// Can be taken from sell amount
valid.sellAmount -= 0.1 ether;
order.pass(sell, valid);

// Same for buy orders
valid = valid_trade(GPv2Order.KIND_BUY);
valid.feeAmount = 0.1 ether;
order.fail(vm, buy, valid, INVALID_LIMIT_AMOUNTS);

valid.sellAmount -= 0.1 ether;
order.pass(buy, valid);

// Smaller fee is allowed for buy orders
valid.feeAmount = 0.01 ether;
order.pass(buy, valid);
}

function test_invalid_preimage() public {
LimitOrder order = new LimitOrder();
GPv2Order.Data memory valid = valid_trade(GPv2Order.KIND_SELL);
bytes32 invalid_hash = GPv2Order.hash(
valid,
LimitOrderTest.DOMAIN_SEPARATOR
);

// Changing something about the preimage makes it no longer correspond to the hash
valid.appData = keccak256("other data");

vm.expectRevert(
abi.encodeWithSelector(
IConditionalOrder.OrderNotValid.selector,
INVALID_HASH
)
);
order.verify(
address(0),
address(0),
invalid_hash,
LimitOrderTest.DOMAIN_SEPARATOR,
bytes32(0),
abi.encode(sell),
bytes(""),
valid
);
}

function test_params() public {
LimitOrder order = new LimitOrder();
GPv2Order.Data memory valid = valid_trade(GPv2Order.KIND_SELL);

// Any app data is allowed
valid.appData = keccak256("other data");
order.pass(sell, valid);

// Earlier validTo is allowed
valid.validTo -= 1;
order.pass(sell, valid);

// Later validTo is not allowed
valid.validTo += 2;
order.fail(vm, sell, valid, INVALID_VALIDITY);

// Different balance is not allowed
valid = valid_trade(GPv2Order.KIND_SELL);
valid.sellTokenBalance = GPv2Order.BALANCE_EXTERNAL;
order.fail(vm, sell, valid, INVALID_BALANCE);

valid = valid_trade(GPv2Order.KIND_SELL);
valid.buyTokenBalance = GPv2Order.BALANCE_EXTERNAL;
order.fail(vm, sell, valid, INVALID_BALANCE);

// Different kind is not allowed
valid = valid_trade(GPv2Order.KIND_SELL);
valid.kind = GPv2Order.KIND_BUY;
order.pass(buy, valid);
order.fail(vm, sell, valid, INVALID_ORDER_KIND);

valid.kind = GPv2Order.KIND_SELL;
order.pass(sell, valid);
order.fail(vm, buy, valid, INVALID_ORDER_KIND);

// Different receiver is not allowed
valid = valid_trade(GPv2Order.KIND_SELL);
valid.receiver = address(0xdeadbeef);
order.fail(vm, sell, valid, INVALID_RECEIVER);

// Different sell token is not allowed
valid = valid_trade(GPv2Order.KIND_SELL);
valid.sellToken = IERC20(address(0xdeadbeef));
order.fail(vm, sell, valid, INVALID_SELL_TOKEN);

// Different buy token is not allowed
valid = valid_trade(GPv2Order.KIND_SELL);
valid.buyToken = IERC20(address(0xdeadbeef));
order.fail(vm, sell, valid, INVALID_BUY_TOKEN);
}
}

0 comments on commit 94e59c7

Please sign in to comment.