Skip to content

Commit

Permalink
Refactor stop-loss create just 1 discrete order (#89)
Browse files Browse the repository at this point in the history
# Description
- Avoid multiple discrete order creations for one stop-loss order
creation.

# Changes
- [x] Replace the `validityBucketTime` parameter of stop loss data with
constant `validTo`.

## How to test
1. Run the modified local test.
2. Try to create multiple discrete orders from the same stop-loss order
using the deployed contract on sepolia:
`0xe6CDbC068654C506424F7747357F51d0e7caB00e`

---------

Co-authored-by: José Ribeiro <[email protected]>
Co-authored-by: Federico Giacon <[email protected]>
  • Loading branch information
3 people authored Aug 6, 2024
1 parent 7c815d4 commit 9666d9d
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 50 deletions.
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(
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
}
}

0 comments on commit 9666d9d

Please sign in to comment.