Skip to content
Open
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
33 changes: 32 additions & 1 deletion src/types/GoodAfterTime.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ pragma solidity >=0.8.0 <0.9.0;
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";

import {IExpectedOutCalculator} from "../vendored/Milkman.sol";
import {IERC20, IConditionalOrder, GPv2Order, BaseConditionalOrder} from "../BaseConditionalOrder.sol";
import {
IERC20,
IConditionalOrder,
IConditionalOrderGenerator,
GPv2Order,
BaseConditionalOrder
} from "../BaseConditionalOrder.sol";
import {ConditionalOrdersUtilsLib as Utils} from "./ConditionalOrdersUtilsLib.sol";

// --- error strings
Expand Down Expand Up @@ -100,4 +106,29 @@ contract GoodAfterTime is BaseConditionalOrder {
GPv2Order.BALANCE_ERC20
);
}

/**
* @inheritdoc IConditionalOrderGenerator
* @dev Single-shot order: once it fills there is nothing further to poll for.
*/
function getNextPollTimestamp(address, bytes32, bytes calldata, GPv2Order.Data memory)
external
pure
override
returns (uint256)
{
return POLL_NEVER;
}

/**
* @inheritdoc IConditionalOrderGenerator
*/
function describeOrder(address, bytes32, bytes calldata, GPv2Order.Data memory)
external
pure
override
returns (string memory)
{
return "good-after-time order ready";
}
}
33 changes: 32 additions & 1 deletion src/types/StopLoss.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import {IERC20, GPv2Order, IConditionalOrder, BaseConditionalOrder} from "../BaseConditionalOrder.sol";
import {
IERC20,
GPv2Order,
IConditionalOrder,
IConditionalOrderGenerator,
BaseConditionalOrder
} from "../BaseConditionalOrder.sol";
import {IAggregatorV3Interface} from "../interfaces/IAggregatorV3Interface.sol";
import {ConditionalOrdersUtilsLib as Utils} from "./ConditionalOrdersUtilsLib.sol";

Expand Down Expand Up @@ -112,4 +118,29 @@ contract StopLoss is BaseConditionalOrder {
GPv2Order.BALANCE_ERC20
);
}

/**
* @inheritdoc IConditionalOrderGenerator
* @dev Single-shot order: once it fills there is nothing further to poll for.
*/
function getNextPollTimestamp(address, bytes32, bytes calldata, GPv2Order.Data memory)
external
pure
override
returns (uint256)
{
return POLL_NEVER;
}

/**
* @inheritdoc IConditionalOrderGenerator
*/
function describeOrder(address, bytes32, bytes calldata, GPv2Order.Data memory)
external
pure
override
returns (string memory)
{
return "stop-loss triggered";
}
}
82 changes: 75 additions & 7 deletions src/types/twap/TWAP.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
BaseConditionalOrder
} from "../../BaseConditionalOrder.sol";
import {TWAPOrder} from "./libraries/TWAPOrder.sol";
import {TWAPOrderMathLib, AfterTwapFinish} from "./libraries/TWAPOrderMathLib.sol";

// --- error strings

Expand Down Expand Up @@ -48,20 +49,87 @@ contract TWAP is BaseConditionalOrder {
* valid is filled. This is safe as CoW Protocol ensures that each `orderUid` is only
* settled once.
*/
TWAPOrder.Data memory twap = abi.decode(staticInput, (TWAPOrder.Data));
TWAPOrder.Data memory twap = _resolveTwapData(owner, ctx, staticInput);

order = TWAPOrder.orderFor(twap);

/**
* @dev If outside the current part's span, this is a scheduling gap, not a
* permanent failure: signal when the next part starts, or terminate if
* there is no next part.
*/
if (!(block.timestamp <= order.validTo)) {
uint256 part = _currentPart(twap);
uint256 nextPartStart = twap.t0 + ((part + 1) * twap.t);
uint256 endTime = twap.t0 + (twap.n * twap.t);

require(nextPartStart < endTime, IConditionalOrder.OrderNotValid(AfterTwapFinish.selector));
revert IConditionalOrder.PollTryAtTimestamp(nextPartStart, NotWithinSpan.selector);
}
}

/**
* @inheritdoc IConditionalOrderGenerator
* @dev The next discrete order becomes available when the next part starts;
* after the final part there is nothing left to poll for.
*/
function getNextPollTimestamp(address owner, bytes32 ctx, bytes calldata staticInput, GPv2Order.Data memory)
external
view
override
returns (uint256)
{
TWAPOrder.Data memory twap = _resolveTwapData(owner, ctx, staticInput);
uint256 part = _currentPart(twap);

// Last part - stop polling after this fills. Written as `part + 1 >= n`
// to stay underflow-safe for degenerate bundles.
if (part + 1 >= twap.n) {
return POLL_NEVER;
}

// Next part starts at...
return twap.t0 + ((part + 1) * twap.t);
}

/**
* @inheritdoc IConditionalOrderGenerator
*/
function describeOrder(address owner, bytes32 ctx, bytes calldata staticInput, GPv2Order.Data memory)
external
view
override
returns (string memory)
{
TWAPOrder.Data memory twap = _resolveTwapData(owner, ctx, staticInput);
uint256 part = _currentPart(twap);

if (part + 1 >= twap.n) {
return "final twap part";
}
return "twap part ready";
}

// --- internal helpers

/// @dev Decode staticInput and resolve t0 from the cabinet if needed
function _resolveTwapData(address owner, bytes32 ctx, bytes calldata staticInput)
internal
view
returns (TWAPOrder.Data memory twap)
{
twap = abi.decode(staticInput, (TWAPOrder.Data));

/**
* @dev If `twap.t0` is set to 0, then get the start time from the context.
*/
if (twap.t0 == 0) {
twap.t0 = uint256(composableCow.cabinet(owner, ctx));
}
}

order = TWAPOrder.orderFor(twap);

/// @dev Revert if the order is outside the TWAP bundle's span.
if (!(block.timestamp <= order.validTo)) {
revert IConditionalOrder.OrderNotValid(NotWithinSpan.selector);
}
/// @dev Get the current part index from block.timestamp
function _currentPart(TWAPOrder.Data memory twap) internal view returns (uint256) {
return TWAPOrderMathLib.currentPart(twap.t0, twap.t);
}
}
24 changes: 19 additions & 5 deletions src/types/twap/libraries/TWAPOrderMathLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ library TWAPOrderMathLib {
assert(span <= frequency);

unchecked {
/// @dev Order is not valid before the start (order commences at `t0`).
if (!(startTime <= block.timestamp)) revert IConditionalOrder.OrderNotValid(BeforeTwapStart.selector);
/// @dev Order is not yet valid before the start (order commences at `t0`);
/// a watch tower should try again at the start time.
require(
startTime <= block.timestamp, IConditionalOrder.PollTryAtTimestamp(startTime, BeforeTwapStart.selector)
);

/**
* @dev Order is expired after the last part (`n` parts, running at `t` time length).
Expand All @@ -50,9 +53,10 @@ library TWAPOrderMathLib {
* Addition overflow: `startTime` is bounded by `block.timestamp` which is reasonably bounded by
* `type(uint32).max` so the sum of `startTime + (numParts * frequency)` is ≈ 2⁵⁵.
*/
if (!(block.timestamp < startTime + (numParts * frequency))) {
revert IConditionalOrder.OrderNotValid(AfterTwapFinish.selector);
}
require(
block.timestamp < startTime + (numParts * frequency),
IConditionalOrder.OrderNotValid(AfterTwapFinish.selector)
);

/**
* @dev We use integer division to get the part number as we want to round down to the nearest part.
Expand Down Expand Up @@ -102,4 +106,14 @@ library TWAPOrderMathLib {
*/
}
}

/**
* @dev Calculate the current part number for a TWAP order.
* @param startTime The start time of the TWAP order.
* @param frequency The frequency of each part (in seconds).
*/
function currentPart(uint256 startTime, uint256 frequency) internal view returns (uint256) {
if (block.timestamp < startTime) return 0;
return (block.timestamp - startTime) / frequency;
}
}
6 changes: 6 additions & 0 deletions test/ComposableCoW.gat.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,12 @@ contract ComposableCoWGatTest is BaseComposableCoWTest {
appData: keccak256("GoodAfterTime")
});
}

/// @dev Single-shot: stop polling after fill, and a descriptive reason
function test_pollHints_SingleShot() public {
assertEq(gat.getNextPollTimestamp(address(0), bytes32(0), bytes(""), getBlankOrder()), type(uint256).max);
assertEq(gat.describeOrder(address(0), bytes32(0), bytes(""), getBlankOrder()), "good-after-time order ready");
}
}

/// @dev A test implementation that returns what we tell it to..
Expand Down
6 changes: 6 additions & 0 deletions test/ComposableCoW.stoploss.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -345,4 +345,10 @@ contract ComposableCoWStopLossTest is BaseComposableCoWTest {
assertEq(order.sellTokenBalance, GPv2Order.BALANCE_ERC20);
assertEq(order.buyTokenBalance, GPv2Order.BALANCE_ERC20);
}

/// @dev Single-shot: stop polling after fill, and a descriptive reason
function test_pollHints_SingleShot() public {
assertEq(stopLoss.getNextPollTimestamp(address(0), bytes32(0), bytes(""), getBlankOrder()), type(uint256).max);
assertEq(stopLoss.describeOrder(address(0), bytes32(0), bytes(""), getBlankOrder()), "stop-loss triggered");
}
}
59 changes: 52 additions & 7 deletions test/ComposableCoW.twap.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@ contract ComposableCoWTwapTest is BaseComposableCoWTest {
// Warp to current time
vm.warp(currentTime);

vm.expectRevert(abi.encodeWithSelector(IConditionalOrder.OrderNotValid.selector, BeforeTwapStart.selector));
vm.expectRevert(
abi.encodeWithSelector(IConditionalOrder.PollTryAtTimestamp.selector, startTime, BeforeTwapStart.selector)
);
twap.generateOrder(address(0), address(0), bytes32(0), abi.encode(o), bytes(""));
}

Expand Down Expand Up @@ -253,7 +255,16 @@ contract ComposableCoWTwapTest is BaseComposableCoWTest {
// Warp to outside of the span
vm.warp(currentTime);

vm.expectRevert(abi.encodeWithSelector(IConditionalOrder.OrderNotValid.selector, NotWithinSpan.selector));
uint256 nextPartStart = startTime + (((currentTime - startTime) / FREQUENCY + 1) * FREQUENCY);
if (nextPartStart < startTime + (FREQUENCY * NUM_PARTS)) {
vm.expectRevert(
abi.encodeWithSelector(
IConditionalOrder.PollTryAtTimestamp.selector, nextPartStart, NotWithinSpan.selector
)
);
} else {
vm.expectRevert(abi.encodeWithSelector(IConditionalOrder.OrderNotValid.selector, AfterTwapFinish.selector));
}
twap.generateOrder(address(0), address(0), bytes32(0), abi.encode(o), bytes(""));
}

Expand All @@ -280,12 +291,15 @@ contract ComposableCoWTwapTest is BaseComposableCoWTest {
// Warp to the current time
vm.warp(currentTime);

// The below should return the terminal INVALID verdict
(ComposableCoW.PollResult memory invalidRes, bytes memory invalidSig) =
// The below should return a WAIT_TIMESTAMP verdict pointing at the start
(ComposableCoW.PollResult memory waitRes, bytes memory waitSig) =
composableCow.getTradeableOrderWithSignature(address(safe1), params, bytes(""), new bytes32[](0));
assertEq(uint256(invalidRes.generator.code), uint256(IConditionalOrderGenerator.GeneratorResultCode.INVALID));
assertEq(invalidRes.generator.reasonCode, BeforeTwapStart.selector);
assertEq(invalidSig.length, 0);
assertEq(
uint256(waitRes.generator.code), uint256(IConditionalOrderGenerator.GeneratorResultCode.WAIT_TIMESTAMP)
);
assertEq(waitRes.generator.waitUntil, ctxBlockTimestamp);
assertEq(waitRes.generator.reasonCode, BeforeTwapStart.selector);
assertEq(waitSig.length, 0);
}

function test_generateOrder_FuzzRevertIfOrderAfterBlocktimestampValidity(
Expand Down Expand Up @@ -685,4 +699,35 @@ contract ComposableCoWTwapTest is BaseComposableCoWTest {
appData: keccak256("test.twap")
});
}

/// @dev getNextPollTimestamp points at the next part while parts remain
function test_getNextPollTimestamp_PointsAtNextPart() public {
TWAPOrder.Data memory o = _twapTestBundle(block.timestamp);

// Within the first part
vm.warp(o.t0);
uint256 nextPoll = twap.getNextPollTimestamp(address(0), bytes32(0), abi.encode(o), getBlankOrder());
assertEq(nextPoll, o.t0 + o.t);
}

/// @dev getNextPollTimestamp signals POLL_NEVER on the final part
function test_getNextPollTimestamp_FinalPartStopsPolling() public {
TWAPOrder.Data memory o = _twapTestBundle(block.timestamp);

// Warp into the final part
vm.warp(o.t0 + (o.n - 1) * o.t);
uint256 nextPoll = twap.getNextPollTimestamp(address(0), bytes32(0), abi.encode(o), getBlankOrder());
assertEq(nextPoll, type(uint256).max);
}

/// @dev describeOrder distinguishes the final part
function test_describeOrder_TwapParts() public {
TWAPOrder.Data memory o = _twapTestBundle(block.timestamp);

vm.warp(o.t0);
assertEq(twap.describeOrder(address(0), bytes32(0), abi.encode(o), getBlankOrder()), "twap part ready");

vm.warp(o.t0 + (o.n - 1) * o.t);
assertEq(twap.describeOrder(address(0), bytes32(0), abi.encode(o), getBlankOrder()), "final twap part");
}
}