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

Refactor stop-loss create just 1 discrete order #89

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"solidity.packageDefaultDependenciesContractsDirectory": "src",
"solidity.packageDefaultDependenciesDirectory": "lib"
"solidity.packageDefaultDependenciesDirectory": "lib",
"solidity.compileUsingRemoteVersion": "v0.8.26+commit.8a97fa7a"
}
13 changes: 10 additions & 3 deletions src/types/StopLoss.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ string constant ORACLE_INVALID_PRICE = "oracle invalid price";
string constant ORACLE_STALE_PRICE = "oracle stale price";
/// @dev The strike price has not been reached
string constant STRIKE_NOT_REACHED = "strike not reached";
/// @dev The order is not valid anymore
string constant ORDER_EXPIRED = "order expired";

/**
* @title StopLoss conditional order
Expand All @@ -34,7 +36,7 @@ contract StopLoss is BaseConditionalOrder {
* @param receiver: The account that should receive the proceeds of the trade
* @param isSellOrder: Whether this is a sell or buy order
* @param isPartiallyFillable: Whether solvers are allowed to only fill a fraction of the order (useful if exact sell or buy amount isn't know at time of placement)
* @param validityBucketSeconds: How long the order will be valid. E.g. if the validityBucket is set to 15 minutes and the order is placed at 00:08, it will be valid until 00:15
* @param validTo: The UNIX timestamp before which this order is valid
* @param sellTokenPriceOracle: A chainlink-like oracle returning the current sell token price in a given numeraire
* @param buyTokenPriceOracle: A chainlink-like oracle returning the current buy token price in the same numeraire
* @param strike: The exchange rate (denominated in sellToken/buyToken) which triggers the StopLoss order if the oracle price falls below. Specified in base / quote with 18 decimals.
Expand All @@ -49,7 +51,7 @@ contract StopLoss is BaseConditionalOrder {
address receiver;
bool isSellOrder;
bool isPartiallyFillable;
uint32 validityBucketSeconds;
uint32 validTo;
IAggregatorV3Interface sellTokenPriceOracle;
IAggregatorV3Interface buyTokenPriceOracle;
int256 strike;
Expand All @@ -65,6 +67,11 @@ contract StopLoss is BaseConditionalOrder {
Data memory data = abi.decode(staticInput, (Data));
// scope variables to avoid stack too deep error
{
/// @dev Guard against expired orders
if (data.validTo < block.timestamp) {
revert IConditionalOrder.OrderNotValid(ORDER_EXPIRED);
}

(, int256 basePrice,, uint256 sellUpdatedAt,) = data.sellTokenPriceOracle.latestRoundData();
(, int256 quotePrice,, uint256 buyUpdatedAt,) = data.buyTokenPriceOracle.latestRoundData();

Expand Down Expand Up @@ -100,7 +107,7 @@ contract StopLoss is BaseConditionalOrder {
data.receiver,
data.sellAmount,
data.buyAmount,
Utils.validToBucket(data.validityBucketSeconds),
data.validTo,
data.appData,
0, // use zero fee for limit orders
data.isSellOrder ? GPv2Order.KIND_SELL : GPv2Order.KIND_BUY,
Expand Down
110 changes: 64 additions & 46 deletions test/ComposableCoW.stoploss.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity >=0.8.0 <0.9.0;

import {IERC20, GPv2Order, IConditionalOrder, BaseComposableCoWTest} from "./ComposableCoW.base.t.sol";
import {IAggregatorV3Interface} from "../src/interfaces/IAggregatorV3Interface.sol";
import {StopLoss, STRIKE_NOT_REACHED, ORACLE_STALE_PRICE, ORACLE_INVALID_PRICE} from "../src/types/StopLoss.sol";
import {StopLoss, STRIKE_NOT_REACHED, ORACLE_STALE_PRICE, ORACLE_INVALID_PRICE, ORDER_EXPIRED} from "../src/types/StopLoss.sol";

contract ComposableCoWStopLossTest is BaseComposableCoWTest {
IERC20 immutable SELL_TOKEN = IERC20(address(0x1));
Expand Down Expand Up @@ -57,7 +57,7 @@ contract ComposableCoWStopLossTest is BaseComposableCoWTest {
receiver: address(0x0),
isSellOrder: false,
isPartiallyFillable: false,
validityBucketSeconds: 15 minutes,
validTo: type(uint32).max,
maxTimeSinceLastOracleUpdate: 15 minutes
});

Expand Down Expand Up @@ -85,6 +85,9 @@ contract ComposableCoWStopLossTest is BaseComposableCoWTest {
vm.assume(sellTokenOraclePrice * int256(10 ** 18) / buyTokenOraclePrice > strike);
vm.assume(currentTime > staleTime);

// guard against overflow
vm.assume(currentTime < type(uint32).max);

vm.warp(currentTime);

StopLoss.Data memory data = StopLoss.Data({
Expand All @@ -99,7 +102,7 @@ contract ComposableCoWStopLossTest is BaseComposableCoWTest {
receiver: address(0x0),
isSellOrder: false,
isPartiallyFillable: false,
validityBucketSeconds: 15 minutes,
validTo: type(uint32).max,
maxTimeSinceLastOracleUpdate: staleTime
});

Expand Down Expand Up @@ -153,7 +156,7 @@ contract ComposableCoWStopLossTest is BaseComposableCoWTest {
receiver: address(0x0),
isSellOrder: true,
isPartiallyFillable: false,
validityBucketSeconds: 15 minutes,
validTo: type(uint32).max,
maxTimeSinceLastOracleUpdate: 15 minutes
});

Expand All @@ -164,7 +167,7 @@ contract ComposableCoWStopLossTest is BaseComposableCoWTest {
assertEq(order.sellAmount, 1 ether);
assertEq(order.buyAmount, 1);
assertEq(order.receiver, address(0x0));
assertEq(order.validTo, 1687718700);
assertEq(order.validTo, type(uint32).max);
assertEq(order.appData, APP_DATA);
assertEq(order.feeAmount, 0);
assertEq(order.kind, GPv2Order.KIND_SELL);
Expand All @@ -189,7 +192,7 @@ contract ComposableCoWStopLossTest is BaseComposableCoWTest {
receiver: address(0x0),
isSellOrder: true,
isPartiallyFillable: false,
validityBucketSeconds: 15 minutes,
validTo: type(uint32).max,
maxTimeSinceLastOracleUpdate: 15 minutes
});

Expand All @@ -200,7 +203,7 @@ contract ComposableCoWStopLossTest is BaseComposableCoWTest {
assertEq(order.sellAmount, 1 ether);
assertEq(order.buyAmount, 1);
assertEq(order.receiver, address(0x0));
assertEq(order.validTo, 1687718700);
assertEq(order.validTo, type(uint32).max);
assertEq(order.appData, APP_DATA);
assertEq(order.feeAmount, 0);
assertEq(order.kind, GPv2Order.KIND_SELL);
Expand Down Expand Up @@ -234,7 +237,7 @@ contract ComposableCoWStopLossTest is BaseComposableCoWTest {
receiver: address(0x0),
isSellOrder: false,
isPartiallyFillable: false,
validityBucketSeconds: 15 minutes,
validTo: type(uint32).max,
maxTimeSinceLastOracleUpdate: maxTimeSinceLastOracleUpdate
});

Expand Down Expand Up @@ -268,7 +271,7 @@ contract ComposableCoWStopLossTest is BaseComposableCoWTest {
receiver: address(0x0),
isSellOrder: false,
isPartiallyFillable: false,
validityBucketSeconds: 15 minutes,
validTo: type(uint32).max,
maxTimeSinceLastOracleUpdate: 15 minutes
});

Expand All @@ -284,15 +287,61 @@ contract ComposableCoWStopLossTest is BaseComposableCoWTest {
stopLoss.getTradeableOrder(safe, address(0), bytes32(0), abi.encode(data), bytes(""));
}

function test_strikePriceMet_fuzz(int256 sellTokenOraclePrice, int256 buyTokenOraclePrice, int256 strike) public {
function test_OracleRevertOnExpiredOrder_fuzz(
uint32 currentTime,
uint32 validTo
) public {
// enforce expired order
vm.assume(currentTime > validTo);

vm.warp(currentTime);

StopLoss.Data memory data = StopLoss.Data({
sellToken: mockToken(SELL_TOKEN, DEFAULT_DECIMALS),
buyToken: mockToken(BUY_TOKEN, DEFAULT_DECIMALS),
sellTokenPriceOracle: mockOracle(SELL_ORACLE, 100 ether, block.timestamp, DEFAULT_DECIMALS),
buyTokenPriceOracle: mockOracle(BUY_ORACLE, 100 ether, block.timestamp, DEFAULT_DECIMALS),
strike: 1,
sellAmount: 1 ether,
buyAmount: 1 ether,
appData: APP_DATA,
receiver: address(0x0),
isSellOrder: false,
isPartiallyFillable: false,
validTo: validTo,
maxTimeSinceLastOracleUpdate: 15 minutes
});

vm.expectRevert(
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be better if there was also a test that shows an order is valid if warping to validTo, all other tests use type(uint256).max for the valid to so the positive boundary condition isn't tested on a different number.

abi.encodeWithSelector(
IConditionalOrder.OrderNotValid.selector,
ORDER_EXPIRED
)
);
stopLoss.getTradeableOrder(
safe,
address(0),
bytes32(0),
abi.encode(data),
bytes("")
);
}

function test_strikePriceMet_fuzz(
int256 sellTokenOraclePrice,
int256 buyTokenOraclePrice,
int256 strike,
uint32 validTo
) public {
// 25 June 2023 18:40:51
vm.warp(1687718451);

vm.assume(validTo >= block.timestamp);
vm.assume(buyTokenOraclePrice > 0);
vm.assume(sellTokenOraclePrice > 0 && sellTokenOraclePrice <= type(int256).max / 10 ** 18);
vm.assume(strike > 0);
vm.assume(sellTokenOraclePrice * int256(10 ** 18) / buyTokenOraclePrice <= strike);

// 25 June 2023 18:40:51
vm.warp(1687718451);

StopLoss.Data memory data = StopLoss.Data({
sellToken: mockToken(SELL_TOKEN, DEFAULT_DECIMALS),
buyToken: mockToken(BUY_TOKEN, DEFAULT_DECIMALS),
Expand All @@ -305,7 +354,7 @@ contract ComposableCoWStopLossTest is BaseComposableCoWTest {
receiver: address(0x0),
isSellOrder: false,
isPartiallyFillable: false,
validityBucketSeconds: 15 minutes,
validTo: validTo,
maxTimeSinceLastOracleUpdate: 15 minutes
});

Expand All @@ -316,43 +365,12 @@ contract ComposableCoWStopLossTest is BaseComposableCoWTest {
assertEq(order.sellAmount, 1 ether);
assertEq(order.buyAmount, 1 ether);
assertEq(order.receiver, address(0x0));
assertEq(order.validTo, 1687718700);
assertEq(order.validTo, validTo);
assertEq(order.appData, APP_DATA);
assertEq(order.feeAmount, 0);
assertEq(order.kind, GPv2Order.KIND_BUY);
assertEq(order.partiallyFillable, false);
assertEq(order.sellTokenBalance, GPv2Order.BALANCE_ERC20);
assertEq(order.buyTokenBalance, GPv2Order.BALANCE_ERC20);
}

function test_validTo() public {
uint256 BLOCK_TIMESTAMP = 1687712399;

StopLoss.Data memory data = StopLoss.Data({
sellToken: mockToken(SELL_TOKEN, 18),
buyToken: mockToken(BUY_TOKEN, 18),
sellTokenPriceOracle: mockOracle(SELL_ORACLE, 99 ether, BLOCK_TIMESTAMP, DEFAULT_DECIMALS),
buyTokenPriceOracle: mockOracle(BUY_ORACLE, 100 ether, BLOCK_TIMESTAMP, DEFAULT_DECIMALS),
strike: 1e18, // required as the strike price has 18 decimals
sellAmount: 1 ether,
buyAmount: 1 ether,
appData: APP_DATA,
receiver: address(0x0),
isSellOrder: false,
isPartiallyFillable: false,
validityBucketSeconds: 1 hours,
maxTimeSinceLastOracleUpdate: 15 minutes
});

// 25 June 2023 18:59:59
vm.warp(BLOCK_TIMESTAMP);
GPv2Order.Data memory order =
stopLoss.getTradeableOrder(safe, address(0), bytes32(0), abi.encode(data), bytes(""));
assertEq(order.validTo, BLOCK_TIMESTAMP + 1); // 25 June 2023 19:00:00

// 25 June 2023 19:00:00
vm.warp(BLOCK_TIMESTAMP + 1);
order = stopLoss.getTradeableOrder(safe, address(0), bytes32(0), abi.encode(data), bytes(""));
assertEq(order.validTo, BLOCK_TIMESTAMP + 1 + 1 hours); // 25 June 2023 20:00:00
}
}
Loading