-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
383 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |