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
13 changes: 12 additions & 1 deletion src/BaseConditionalOrder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,9 @@ abstract contract BaseConditionalOrder is IConditionalOrderGenerator, IOrderMani
/**
* @dev Decode revert data from `generateOrder` into a `GeneratorResult`.
*
* Only `OrderNotValid` maps to the terminal `INVALID` verdict. Unrecognized
* Only `OrderNotValid` maps to the terminal `INVALID` verdict.
* `PollNeedsOffchainInput` maps to `NEEDS_INPUT` (acquire input or park,
* never a timed retry). Unrecognized
* reverts - including `Panic` and `Error(string)` - map to `TRY_NEXT_BLOCK`
* so a transient or recoverable fault reschedules rather than permanently
* killing the order off-chain. The `reasonCode` carries the selector of
Expand Down Expand Up @@ -238,6 +240,15 @@ abstract contract BaseConditionalOrder is IConditionalOrderGenerator, IOrderMani
return _verdict(IConditionalOrderGenerator.GeneratorResultCode.WAIT_BLOCK, blockNum, reasonCode);
}

// PollNeedsOffchainInput(bytes4): not a timed retry - the caller
// must acquire non-empty offchainInput or park the order
if (selector == IConditionalOrder.PollNeedsOffchainInput.selector) {
return
_verdict(
IConditionalOrderGenerator.GeneratorResultCode.NEEDS_INPUT, 0, _decodeBytes4Error(errorData)
);
}

// Anything else - Panic, Error(string), or an unrecognized custom
// error - is a transient verdict carrying the caught selector
return _verdict(IConditionalOrderGenerator.GeneratorResultCode.TRY_NEXT_BLOCK, 0, selector);
Expand Down
13 changes: 12 additions & 1 deletion src/interfaces/IConditionalOrder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ pragma solidity >=0.8.0 <0.9.0;
import {GPv2Order} from "cowprotocol/contracts/libraries/GPv2Order.sol";
import {IERC165} from "safe/interfaces/IERC165.sol";

/// @dev Canonical reason for `PollNeedsOffchainInput`: the handler cannot
/// generate with empty `offchainInput` and has no more specific reason.
/// Handlers MAY declare their own reason errors naming the missing input.
error OffchainInputRequired();

/**
* @title Conditional Order Interface
* @author CoW Protocol Developers + mfw78 <mfw78@rndlabs.xyz>
Expand All @@ -21,6 +26,11 @@ interface IConditionalOrder {
error PollTryAtBlock(uint256 blockNumber, bytes4 reasonCode);
// Signal to a watch tower that polling should be attempted again at a specific timestamp.
error PollTryAtTimestamp(uint256 timestamp, bytes4 reasonCode);
/// @dev Generation requires non-empty `offchainInput` that the caller did not
/// supply. Unlike the timed poll errors, this is not a retry signal:
/// consumers MUST NOT re-poll with empty input on a schedule - acquire
/// the input (see `docs/discovery.md` on order modules) or park the order.
error PollNeedsOffchainInput(bytes4 reasonCode);

/**
* @dev This struct is used to uniquely identify a conditional order for an owner.
Expand Down Expand Up @@ -110,7 +120,8 @@ interface IConditionalOrderGenerator is IConditionalOrder, IERC165 {
WAIT_TIMESTAMP, // Wait until `waitUntil` (unix timestamp)
WAIT_BLOCK, // Wait until `waitUntil` (block number)
TRY_NEXT_BLOCK, // Transient condition, retry next block
INVALID // Permanently invalid, stop polling
INVALID, // Permanently invalid, stop polling
NEEDS_INPUT // Requires non-empty offchainInput; acquire input or park - never a timed retry
}

/**
Expand Down
33 changes: 33 additions & 0 deletions test/ComposableCoW.base.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,39 @@ contract PollTryNextBlockHandler is BaseConditionalOrder {
}

/// @dev Test handler that throws PollTryAtTimestamp from generateOrder
/// @dev Test handler that requires non-empty offchainInput: reverts
/// PollNeedsOffchainInput when polled empty, generates when supplied.
contract NeedsOffchainInputHandler is BaseConditionalOrder {
bytes4 public reasonCode;

constructor(bytes4 _reasonCode) {
reasonCode = _reasonCode;
}

function generateOrder(address, address, bytes32, bytes calldata, bytes calldata offchainInput)
public
view
override
returns (GPv2Order.Data memory)
{
require(offchainInput.length > 0, IConditionalOrder.PollNeedsOffchainInput(reasonCode));
return GPv2Order.Data({
sellToken: IERC20(address(0)),
buyToken: IERC20(address(0)),
receiver: address(0),
sellAmount: 0,
buyAmount: 0,
validTo: 0,
appData: keccak256(offchainInput),
feeAmount: 0,
kind: GPv2Order.KIND_SELL,
partiallyFillable: false,
sellTokenBalance: GPv2Order.BALANCE_ERC20,
buyTokenBalance: GPv2Order.BALANCE_ERC20
});
}
}

contract PollTryAtTimestampHandler is BaseConditionalOrder {
uint256 public timestamp;
bytes4 public reasonCode;
Expand Down
45 changes: 45 additions & 0 deletions test/ComposableCoW.poll.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
BaseComposableCoWTest,
InvalidHash,
OrderNotValidHandler,
NeedsOffchainInputHandler,
PollTryNextBlockHandler,
PollTryAtTimestampHandler,
PollTryAtBlockHandler,
Expand All @@ -17,12 +18,14 @@ import {
RequireFailHandler,
UnknownErrorHandler
} from "./ComposableCoW.base.t.sol";
import {OffchainInputRequired} from "../src/interfaces/IConditionalOrder.sol";

/// @dev Test reason errors, as a handler would declare them
error TestOrderInvalid();
error TestTryNextBlock();
error TestWaitTimestamp();
error TestWaitBlock();
error TestTriggerPriceRequired();

/// @title Tests for poll() verdicts and error decoding in BaseConditionalOrder
contract ComposableCoWPollTest is BaseComposableCoWTest {
Expand All @@ -44,6 +47,48 @@ contract ComposableCoWPollTest is BaseComposableCoWTest {
assertEq(result.nextPollTimestamp, 0);
}

/// @dev PollNeedsOffchainInput decodes to the NEEDS_INPUT verdict: not a
/// timed retry - the caller must acquire input or park
function test_poll_DecodesPollNeedsOffchainInput() public {
bytes4 expectedReason = TestTriggerPriceRequired.selector;
NeedsOffchainInputHandler handler = new NeedsOffchainInputHandler(expectedReason);

IConditionalOrderGenerator.GeneratorResult memory result =
handler.poll(address(safe1), address(this), bytes32(0), bytes(""), bytes(""));

assertEq(uint256(result.code), uint256(IConditionalOrderGenerator.GeneratorResultCode.NEEDS_INPUT));
assertEq(result.reasonCode, expectedReason);
assertEq(result.waitUntil, 0);
assertEq(result.nextPollTimestamp, 0);
}

/// @dev The same handler generates once offchainInput is supplied - the
/// NEEDS_INPUT verdict is about the missing input, not the order
function test_poll_NeedsInputHandlerPostsWithInput() public {
NeedsOffchainInputHandler handler = new NeedsOffchainInputHandler(TestTriggerPriceRequired.selector);

IConditionalOrderGenerator.GeneratorResult memory result =
handler.poll(address(safe1), address(this), bytes32(0), bytes(""), bytes("trigger"));

assertEq(uint256(result.code), uint256(IConditionalOrderGenerator.GeneratorResultCode.POST));
assertEq(result.reasonCode, bytes4(0));
}

/// @dev The canonical OffchainInputRequired reason round-trips the probe:
/// tryGenerateOrder surfaces the full PollNeedsOffchainInput revert
function test_tryGenerateOrder_NeedsInputRevertData() public {
NeedsOffchainInputHandler handler = new NeedsOffchainInputHandler(OffchainInputRequired.selector);

(bool success,, bytes memory revertData) =
handler.tryGenerateOrder(address(safe1), address(this), bytes32(0), bytes(""), bytes(""));

assertFalse(success);
assertEq(
revertData,
abi.encodeWithSelector(IConditionalOrder.PollNeedsOffchainInput.selector, OffchainInputRequired.selector)
);
}

/// @dev PollTryNextBlock decodes to the TRY_NEXT_BLOCK verdict
function test_poll_DecodesPollTryNextBlock() public {
bytes4 expectedReason = TestTryNextBlock.selector;
Expand Down