Skip to content
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
207 changes: 200 additions & 7 deletions src/BaseConditionalOrder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,28 @@ import {GPv2Order, IERC20} from "cowprotocol/contracts/libraries/GPv2Order.sol";

import {IERC165, IConditionalOrder, IConditionalOrderGenerator} from "./interfaces/IConditionalOrder.sol";

// --- error strings
/**
* @dev This error is returned by the `verify` function if the *generated* order hash does not match
* the hash passed as a parameter.
* @dev The generated order hash does not match the hash passed to `verify`
*/
error InvalidHash();

/**
* @title Base logic for conditional orders.
* @dev Enforces the order verification logic for conditional orders, allowing developers
* to focus on the logic for generating the tradeable order.
* @dev Provides the dual-path plumbing: a lean `verify` for the settlement path and a
* structured, non-reverting `poll` for watch-towers, both derived from one
* `generateOrder` implementation.
* @author mfw78 <mfw78@nxm.rs>
*/
abstract contract BaseConditionalOrder is IConditionalOrderGenerator {
/**
* @dev Signals to poll at `order.validTo + 1` for the next discrete order.
*/
uint256 internal constant POLL_AT_VALIDTO = 0;
/**
* @dev Signals that this is the final discrete order; stop polling after it fills.
*/
uint256 internal constant POLL_NEVER = type(uint256).max;

/**
* @inheritdoc IConditionalOrder
* @dev As an order generator, the `GPv2Order.Data` passed as a parameter is ignored / not validated.
Expand All @@ -37,11 +44,85 @@ abstract contract BaseConditionalOrder is IConditionalOrderGenerator {
GPv2Order.Data memory generatedOrder = generateOrder(owner, sender, ctx, staticInput, offchainInput);

/// @dev Verify that the *generated* order is valid and matches the payload.
if (!(_hash == GPv2Order.hash(generatedOrder, domainSeparator))) {
revert IConditionalOrder.OrderNotValid(InvalidHash.selector);
require(
_hash == GPv2Order.hash(generatedOrder, domainSeparator),
IConditionalOrder.OrderNotValid(InvalidHash.selector)
);
}

/**
* @inheritdoc IConditionalOrderGenerator
* @dev Wraps `generateOrder` in a try/catch, decoding conditional-order errors
* into a structured verdict.
*/
function poll(address owner, address sender, bytes32 ctx, bytes calldata staticInput, bytes calldata offchainInput)
external
view
override
returns (IConditionalOrderGenerator.GeneratorResult memory result)
{
try this.generateOrder(owner, sender, ctx, staticInput, offchainInput) returns (GPv2Order.Data memory order) {
uint256 nextPoll = this.getNextPollTimestamp(owner, ctx, staticInput, order);
return IConditionalOrderGenerator.GeneratorResult({
code: IConditionalOrderGenerator.GeneratorResultCode.POST,
order: order,
nextPollTimestamp: nextPoll,
waitUntil: 0,
reasonCode: bytes4(0)
});
} catch (bytes memory errorData) {
return _decodeErrorToGeneratorResult(errorData);
}
}

/**
* @inheritdoc IConditionalOrderGenerator
* @dev The complete revert data is returned as ordinary return data so a
* consumer can inspect `Panic` sub-codes or unrecognized inner errors
* with their arguments, without any RPC revert-data handling.
*/
function tryGenerateOrder(
address owner,
address sender,
bytes32 ctx,
bytes calldata staticInput,
bytes calldata offchainInput
) external view override returns (bool success, GPv2Order.Data memory order, bytes memory revertData) {
try this.generateOrder(owner, sender, ctx, staticInput, offchainInput) returns (GPv2Order.Data memory o) {
return (true, o, bytes(""));
} catch (bytes memory errorData) {
return (false, _emptyOrder(), errorData);
}
}

/**
* @inheritdoc IConditionalOrderGenerator
* @dev Default: poll at `order.validTo + 1`. Override for multi-part orders.
*/
function getNextPollTimestamp(address, bytes32, bytes calldata, GPv2Order.Data memory)
external
view
virtual
override
returns (uint256)
{
return POLL_AT_VALIDTO;
}

/**
* @inheritdoc IConditionalOrderGenerator
* @dev Default: generic message. Override for better UX.
*/
function describeOrder(address, bytes32, bytes calldata, GPv2Order.Data memory)
external
view
virtual
override
returns (string memory)
{
return "order ready";
}

/**
* @dev Set the visibility of this function to `public` to allow `verify` to call it.
* @inheritdoc IConditionalOrderGenerator
Expand All @@ -60,4 +141,116 @@ abstract contract BaseConditionalOrder is IConditionalOrderGenerator {
function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) {
return interfaceId == type(IConditionalOrderGenerator).interfaceId || interfaceId == type(IERC165).interfaceId;
}

/**
* @dev Decode revert data from `generateOrder` into a `GeneratorResult`.
*
* Only `OrderNotValid` maps to the terminal `INVALID` verdict. 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
* whatever was caught; the complete revert data is retrievable via
* `tryGenerateOrder`.
*/
function _decodeErrorToGeneratorResult(bytes memory errorData)
internal
pure
returns (IConditionalOrderGenerator.GeneratorResult memory)
{
if (errorData.length >= 4) {
bytes4 selector;
assembly {
selector := mload(add(errorData, 32))
}

// OrderNotValid(bytes4): the only permanently terminal verdict
if (selector == IConditionalOrder.OrderNotValid.selector) {
return
_verdict(IConditionalOrderGenerator.GeneratorResultCode.INVALID, 0, _decodeBytes4Error(errorData));
}

// PollTryNextBlock(bytes4)
if (selector == IConditionalOrderGenerator.PollTryNextBlock.selector) {
return _verdict(
IConditionalOrderGenerator.GeneratorResultCode.TRY_NEXT_BLOCK, 0, _decodeBytes4Error(errorData)
);
}

// PollTryAtTimestamp(uint256, bytes4)
if (selector == IConditionalOrderGenerator.PollTryAtTimestamp.selector) {
(uint256 timestamp, bytes4 reasonCode) = _decodeUintBytes4Error(errorData);
return _verdict(IConditionalOrderGenerator.GeneratorResultCode.WAIT_TIMESTAMP, timestamp, reasonCode);
}

// PollTryAtBlock(uint256, bytes4)
if (selector == IConditionalOrderGenerator.PollTryAtBlock.selector) {
(uint256 blockNum, bytes4 reasonCode) = _decodeUintBytes4Error(errorData);
return _verdict(IConditionalOrderGenerator.GeneratorResultCode.WAIT_BLOCK, blockNum, reasonCode);
}

// 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);
}

// Empty or malformed revert data: transient, no reason available
return _verdict(IConditionalOrderGenerator.GeneratorResultCode.TRY_NEXT_BLOCK, 0, bytes4(0));
}

/**
* @dev Build a non-POST verdict with an empty order.
*/
function _verdict(IConditionalOrderGenerator.GeneratorResultCode code, uint256 waitUntil, bytes4 reasonCode)
private
pure
returns (IConditionalOrderGenerator.GeneratorResult memory)
{
return IConditionalOrderGenerator.GeneratorResult({
code: code, order: _emptyOrder(), nextPollTimestamp: 0, waitUntil: waitUntil, reasonCode: reasonCode
});
}

/**
* @dev Decode an error with signature `(bytes4)`.
*/
function _decodeBytes4Error(bytes memory errorData) internal pure returns (bytes4 reasonCode) {
if (errorData.length >= 36) {
assembly {
errorData := add(errorData, 4)
}
reasonCode = abi.decode(errorData, (bytes4));
}
}

/**
* @dev Decode an error with signature `(uint256, bytes4)`.
*/
function _decodeUintBytes4Error(bytes memory errorData) internal pure returns (uint256 value, bytes4 reasonCode) {
if (errorData.length >= 68) {
assembly {
errorData := add(errorData, 4)
}
(value, reasonCode) = abi.decode(errorData, (uint256, bytes4));
}
}

/**
* @dev An all-zero order for non-POST verdicts.
*/
function _emptyOrder() internal pure returns (GPv2Order.Data memory) {
return GPv2Order.Data({
sellToken: IERC20(address(0)),
buyToken: IERC20(address(0)),
receiver: address(0),
sellAmount: 0,
buyAmount: 0,
validTo: 0,
appData: bytes32(0),
feeAmount: 0,
kind: bytes32(0),
partiallyFillable: false,
sellTokenBalance: bytes32(0),
buyTokenBalance: bytes32(0)
});
}
}
122 changes: 110 additions & 12 deletions src/interfaces/IConditionalOrder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
pragma solidity >=0.8.0 <0.9.0;

import {GPv2Order} from "cowprotocol/contracts/libraries/GPv2Order.sol";
import {GPv2Interaction} from "cowprotocol/contracts/libraries/GPv2Interaction.sol";
import {IERC165} from "safe/interfaces/IERC165.sol";

/**
Expand All @@ -17,14 +16,6 @@ interface IConditionalOrder {
*/
error OrderNotValid(bytes4 reasonCode);

// --- errors specific for polling
// Signal to a watch tower that polling should be attempted again.
error PollTryNextBlock(bytes4 reasonCode);
// Signal to a watch tower that polling should be attempted again at a specific block number.
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 This struct is used to uniquely identify a conditional order for an owner.
* H(handler || salt || staticInput) **MUST** be unique for an owner.
Expand Down Expand Up @@ -66,6 +57,10 @@ interface IConditionalOrder {
/**
* @title Conditional Order Generator Interface
* @author mfw78 <mfw78@nxm.rs>
* @notice Adds structured, non-reverting polling for watch-tower integration.
* @dev The generator surface carries the handler's *verdict* only. Observed fill
* state is orthogonal and is composed by the registry (`ComposableCow`),
* never by a handler.
*/
interface IConditionalOrderGenerator is IConditionalOrder, IERC165 {
/**
Expand All @@ -75,15 +70,26 @@ interface IConditionalOrderGenerator is IConditionalOrder, IERC165 {
*/
event ConditionalOrderCreated(address indexed owner, IConditionalOrder.ConditionalOrderParams params);

// --- errors specific for polling
// Signal to a watch tower that polling should be attempted again.
error PollTryNextBlock(bytes4 reasonCode);
// Signal to a watch tower that polling should be attempted again at a specific block number.
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 Get a tradeable order that can be posted to the CoW Protocol API and would pass signature validation.
* **MUST** revert if the order condition is not met.
* Generate the discrete order for the current conditions.
* @dev The single source of truth for order derivation; `poll` is defined in
* terms of it. **MUST** revert with one of the conditional-order errors
* above if the order condition is not met. A generator's `verify` **MUST**
* accept exactly the orders this function produces for the same arguments.
* @param owner the contract who is the owner of the order
* @param sender the `msg.sender` of the parent `isValidSignature` call
* @param ctx the context of the order (bytes32(0) if merkle tree is used, otherwise the H(params))
* @param staticInput the static input for all discrete orders cut from this conditional order
* @param offchainInput dynamic off-chain input for a discrete order cut from this conditional order
* @return the tradeable order for submission to the CoW Protocol API
* @return the discrete order for submission to the CoW Protocol API
*/
function generateOrder(
address owner,
Expand All @@ -92,4 +98,96 @@ interface IConditionalOrderGenerator is IConditionalOrder, IERC165 {
bytes calldata staticInput,
bytes calldata offchainInput
) external view returns (GPv2Order.Data memory);

/**
* @dev The handler's verdict on whether a discrete order can be posted.
* These are the only values a handler can produce; fill state is
* deliberately not representable here.
*/
enum GeneratorResultCode {
POST, // A discrete order is ready to be posted
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
}

/**
* @dev Structured result from `poll`.
* @param code the handler's verdict
* @param order the discrete order; only valid when `code == POST`
* @param nextPollTimestamp when to poll for the next discrete order after this
* one is posted; only meaningful when `code == POST`. `0` means poll at
* `order.validTo + 1`; `type(uint256).max` means this is the final order.
* @param waitUntil the timestamp (`WAIT_TIMESTAMP`) or block number
* (`WAIT_BLOCK`) to wait for; `0` otherwise
* @param reasonCode selector of the handler-declared error behind a non-POST
* verdict; `bytes4(0)` for POST. For a caught `Panic` or `Error(string)`
* this is that built-in's selector - use `tryGenerateOrder` to retrieve
* the complete revert data
*/
struct GeneratorResult {
GeneratorResultCode code;
GPv2Order.Data order;
uint256 nextPollTimestamp;
uint256 waitUntil;
bytes4 reasonCode;
}

/**
* Poll for a discrete order with scheduling metadata.
* @dev Called by watch-towers. **MUST NOT** revert for order conditions: the
* conditional-order errors raised by `generateOrder` are decoded into the
* returned verdict instead.
* @param owner the contract who is the owner of the order
* @param sender the `msg.sender` of the poll call
* @param ctx the context of the order (bytes32(0) if merkle tree is used, otherwise the H(params))
* @param staticInput the static input for all discrete orders cut from this conditional order
* @param offchainInput dynamic off-chain input for a discrete order cut from this conditional order
* @return result structured verdict with the order (if postable) and scheduling hints
*/
function poll(address owner, address sender, bytes32 ctx, bytes calldata staticInput, bytes calldata offchainInput)
external
view
returns (GeneratorResult memory result);

/**
* Probe order generation and return the full revert data on failure.
* @dev Diagnostic surface for off-chain consumers: unlike `poll`, the complete
* ABI-encoded inner error (including `Panic` sub-codes and unrecognized
* custom errors with their arguments) is returned as ordinary return
* data, avoiding RPC revert-data handling entirely. Never consulted on
* the settlement path.
* @return success whether a discrete order was generated
* @return order the discrete order (zeroed when `success` is false)
* @return revertData the complete ABI-encoded revert data (empty when
* `success` is true)
*/
function tryGenerateOrder(
address owner,
address sender,
bytes32 ctx,
bytes calldata staticInput,
bytes calldata offchainInput
) external view returns (bool success, GPv2Order.Data memory order, bytes memory revertData);

/**
* Get the scheduling hint for the next poll after a successful order.
* @dev Only consulted after `generateOrder` succeeds.
* @return nextPollTimestamp `0` = poll at `order.validTo + 1`; `type(uint256).max` = stop
* polling after this order; any other value = poll at that timestamp
*/
function getNextPollTimestamp(address owner, bytes32 ctx, bytes calldata staticInput, GPv2Order.Data memory order)
external
view
returns (uint256 nextPollTimestamp);

/**
* Get a human-readable description of the discrete order.
* @dev Off-chain UX only; never consulted during settlement.
*/
function describeOrder(address owner, bytes32 ctx, bytes calldata staticInput, GPv2Order.Data memory order)
external
view
returns (string memory description);
}
Loading