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
5 changes: 3 additions & 2 deletions src/BaseConditionalOrder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {IERC165, IConditionalOrder, IConditionalOrderGenerator} from "./interfac
// --- 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.
string constant INVALID_HASH = "invalid hash";
/// @dev The generated order hash does not match the hash passed to `verify`
error InvalidHash();

/**
* @title Base logic for conditional orders.
Expand All @@ -35,7 +36,7 @@ abstract contract BaseConditionalOrder is IConditionalOrderGenerator {

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

Expand Down
33 changes: 17 additions & 16 deletions src/ComposableCoW.sol
Original file line number Diff line number Diff line change
Expand Up @@ -193,16 +193,17 @@ contract ComposableCoW is ISafeSignatureVerifier {
}

// Proof is valid, guard (if any) is valid, now check the handler
_payload.params.handler.verify(
address(safe),
sender,
_hash,
_domainSeparator,
ctx,
_payload.params.staticInput,
_payload.offchainInput,
order
);
_payload.params.handler
.verify(
address(safe),
sender,
_hash,
_domainSeparator,
ctx,
_payload.params.staticInput,
_payload.offchainInput,
order
);

return ERC1271.isValidSignature.selector;
}
Expand All @@ -228,19 +229,19 @@ contract ComposableCoW is ISafeSignatureVerifier {
bytes32 ctx = _auth(owner, params, proof);

// Make sure the handler supports `IConditionalOrderGenerator`
try IConditionalOrderGenerator(address(params.handler)).supportsInterface(
type(IConditionalOrderGenerator).interfaceId
) returns (bool supported) {
try IConditionalOrderGenerator(address(params.handler))
.supportsInterface(type(IConditionalOrderGenerator).interfaceId) returns (
bool supported
) {
if (!supported) {
revert InterfaceNotSupported();
}
} catch {
revert InterfaceNotSupported();
}

order = IConditionalOrderGenerator(address(params.handler)).generateOrder(
owner, msg.sender, ctx, params.staticInput, offchainInput
);
order = IConditionalOrderGenerator(address(params.handler))
.generateOrder(owner, msg.sender, ctx, params.staticInput, offchainInput);

// Check with the swap guard if the order is restricted or not
if (!(_guardCheck(owner, ctx, params, offchainInput, order))) {
Expand Down
11 changes: 6 additions & 5 deletions src/interfaces/IConditionalOrder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ import {IERC165} from "safe/interfaces/IERC165.sol";
*/
interface IConditionalOrder {
/// @dev This error is returned by the `generateOrder` function if the order condition is not met.
/// A parameter of `string` type is included to allow the caller to specify the reason for the failure.
error OrderNotValid(string);
/// `reasonCode` is the selector of a handler-declared custom error, so any ABI-aware
/// consumer can resolve it to a name without a bespoke table.
error OrderNotValid(bytes4 reasonCode);

// --- errors specific for polling
// Signal to a watch tower that polling should be attempted again.
error PollTryNextBlock(string reason);
error PollTryNextBlock(bytes4 reasonCode);
// Signal to a watch tower that polling should be attempted again at a specific block number.
error PollTryAtBlock(uint256 blockNumber, string reason);
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, string reason);
error PollTryAtTimestamp(uint256 timestamp, bytes4 reasonCode);

/**
* @dev This struct is used to uniquely identify a conditional order for an owner.
Expand Down
31 changes: 15 additions & 16 deletions src/types/GoodAfterTime.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import {IERC20, IConditionalOrder, GPv2Order, BaseConditionalOrder} from "../Bas
import {ConditionalOrdersUtilsLib as Utils} from "./ConditionalOrdersUtilsLib.sol";

// --- error strings
/// @dev If the trade is called before the time it becomes valid.
string constant TOO_EARLY = "too early";
/// @dev If the sell token balance is below the minimum.
string constant BALANCE_INSUFFICIENT = "balance insufficient";
/// @dev If the price checker fails.
string constant PRICE_CHECKER_FAILED = "price checker failed";
/// @dev The start time has not been reached
error TooEarly();
/// @dev The sell token balance is below minSellBalance
error BalanceInsufficient();
/// @dev The buy amount is below the price checker minimum
error PriceCheckerFailed();

/**
* @title Good After Time (GAT) Conditional Order - with Milkman price checkers
Expand Down Expand Up @@ -50,24 +50,23 @@ contract GoodAfterTime is BaseConditionalOrder {
uint256 allowedSlippage; // in basis points
}

function generateOrder(
address owner,
address,
bytes32,
bytes calldata staticInput,
bytes calldata offchainInput
) public view override returns (GPv2Order.Data memory order) {
function generateOrder(address owner, address, bytes32, bytes calldata staticInput, bytes calldata offchainInput)
public
view
override
returns (GPv2Order.Data memory order)
{
// Decode the payload into the good after time parameters.
Data memory data = abi.decode(staticInput, (Data));

// Don't allow the order to be placed before it becomes valid.
if (!(block.timestamp >= data.startTime)) {
revert IConditionalOrder.PollTryAtTimestamp(data.startTime, TOO_EARLY);
revert IConditionalOrder.PollTryAtTimestamp(data.startTime, TooEarly.selector);
}

// Require that the sell token balance is above the minimum.
if (!(data.sellToken.balanceOf(owner) >= data.minSellBalance)) {
revert IConditionalOrder.OrderNotValid(BALANCE_INSUFFICIENT);
revert IConditionalOrder.OrderNotValid(BalanceInsufficient.selector);
}

uint256 buyAmount = abi.decode(offchainInput, (uint256));
Expand All @@ -82,7 +81,7 @@ contract GoodAfterTime is BaseConditionalOrder {

// Don't allow the order to be placed if the buyAmount is less than the minimum out.
if (!(buyAmount >= (_expectedOut * (Utils.MAX_BPS - p.allowedSlippage)) / Utils.MAX_BPS)) {
revert IConditionalOrder.PollTryNextBlock(PRICE_CHECKER_FAILED);
revert IConditionalOrder.PollTryNextBlock(PriceCheckerFailed.selector);
}
}

Expand Down
12 changes: 7 additions & 5 deletions src/types/PerpetualStableSwap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
import {ConditionalOrdersUtilsLib as Utils} from "./ConditionalOrdersUtilsLib.sol";

// --- error strings
/// @dev The sell amount is insufficient (ie. not funded).
string constant NOT_FUNDED = "not funded";
/// @dev Neither token balance is non-zero
error NotFunded();

/**
* @title A smart contract that is always willing to trade between tokenA and tokenB 1:1,
Expand Down Expand Up @@ -60,7 +60,7 @@ contract PerpetualStableSwap is BaseConditionalOrder {

// Make sure the order is funded, otherwise it is not valid
if (!(buySellData.sellAmount > 0)) {
revert IConditionalOrder.OrderNotValid(NOT_FUNDED);
revert IConditionalOrder.OrderNotValid(NotFunded.selector);
}

// Unless spread is 0 (and there is no surplus), order collision is not an issue as sell and buy amounts should
Expand Down Expand Up @@ -98,14 +98,16 @@ contract PerpetualStableSwap is BaseConditionalOrder {
sellToken: tokenA,
buyToken: tokenB,
sellAmount: balanceA,
buyAmount: convertAmount(tokenA, balanceA, tokenB) * (Utils.MAX_BPS + data.halfSpreadBps) / Utils.MAX_BPS
buyAmount: convertAmount(tokenA, balanceA, tokenB) * (Utils.MAX_BPS + data.halfSpreadBps)
/ Utils.MAX_BPS
});
} else {
buySellData = BuySellData({
sellToken: tokenB,
buyToken: tokenA,
sellAmount: balanceB,
buyAmount: convertAmount(tokenB, balanceB, tokenA) * (Utils.MAX_BPS + data.halfSpreadBps) / Utils.MAX_BPS
buyAmount: convertAmount(tokenB, balanceB, tokenA) * (Utils.MAX_BPS + data.halfSpreadBps)
/ Utils.MAX_BPS
});
}
}
Expand Down
30 changes: 13 additions & 17 deletions src/types/StopLoss.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import {ConditionalOrdersUtilsLib as Utils} from "./ConditionalOrdersUtilsLib.so

// --- error strings

/// @dev Invalid price data returned by the oracle
string constant ORACLE_INVALID_PRICE = "oracle invalid price";
/// @dev The oracle has returned stale data
string constant ORACLE_STALE_PRICE = "oracle stale price";
/// @dev An oracle returned an invalid (non-positive) price
error OracleInvalidPrice();
/// @dev An oracle price is older than maxTimeSinceLastOracleUpdate
error OracleStalePrice();
/// @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";
error StrikeNotReached();
/// @dev The order validity window has passed
error OrderExpired();

/**
* @title StopLoss conditional order
Expand Down Expand Up @@ -69,25 +69,21 @@ contract StopLoss is BaseConditionalOrder {
{
/// @dev Guard against expired orders
if (data.validTo < block.timestamp) {
revert IConditionalOrder.OrderNotValid(ORDER_EXPIRED);
revert IConditionalOrder.OrderNotValid(OrderExpired.selector);
}

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

/// @dev Guard against invalid price data
if (!(basePrice > 0 && quotePrice > 0)) {
revert IConditionalOrder.OrderNotValid(ORACLE_INVALID_PRICE);
revert IConditionalOrder.OrderNotValid(OracleInvalidPrice.selector);
}

/// @dev Guard against stale data at a user-specified interval. The maxTimeSinceLastOracleUpdate should at least exceed the both oracles' update intervals.
if (
!(
sellUpdatedAt >= block.timestamp - data.maxTimeSinceLastOracleUpdate
&& buyUpdatedAt >= block.timestamp - data.maxTimeSinceLastOracleUpdate
)
) {
revert IConditionalOrder.PollTryNextBlock(ORACLE_STALE_PRICE);
if (!(sellUpdatedAt >= block.timestamp - data.maxTimeSinceLastOracleUpdate
&& buyUpdatedAt >= block.timestamp - data.maxTimeSinceLastOracleUpdate)) {
revert IConditionalOrder.PollTryNextBlock(OracleStalePrice.selector);
}

// Normalize the decimals for basePrice and quotePrice, scaling them to 18 decimals
Expand All @@ -97,7 +93,7 @@ contract StopLoss is BaseConditionalOrder {

/// @dev Scale the strike price to 18 decimals.
if (!(basePrice * SCALING_FACTOR / quotePrice <= data.strike)) {
revert IConditionalOrder.PollTryNextBlock(STRIKE_NOT_REACHED);
revert IConditionalOrder.PollTryNextBlock(StrikeNotReached.selector);
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/types/TradeAboveThreshold.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {ConditionalOrdersUtilsLib as Utils} from "./ConditionalOrdersUtilsLib.so

// --- error strings

/// @dev The sell token balance is below the threshold (ie. threshold not met).
string constant BALANCE_INSUFFICIENT = "balance insufficient";
/// @dev The sell token balance is below the threshold or zero
error BalanceInsufficient();

/**
* @title A smart contract that trades whenever its balance of a certain token exceeds a target threshold
Expand Down Expand Up @@ -45,7 +45,7 @@ contract TradeAboveThreshold is BaseConditionalOrder {
uint256 balance = data.sellToken.balanceOf(owner);
// Don't allow the order to be placed if the balance is less than the threshold.
if (!(balance >= data.threshold)) {
revert IConditionalOrder.PollTryNextBlock(BALANCE_INSUFFICIENT);
revert IConditionalOrder.PollTryNextBlock(BalanceInsufficient.selector);
}
// ensures that orders queried shortly after one another result in the same hash (to avoid spamming the orderbook)
order = GPv2Order.Data(
Expand Down
6 changes: 3 additions & 3 deletions src/types/twap/TWAP.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {TWAPOrder} from "./libraries/TWAPOrder.sol";

// --- error strings

/// @dev The order is not within the TWAP bundle's span.
string constant NOT_WITHIN_SPAN = "not within span";
/// @dev The current time is outside the active part's span
error NotWithinSpan();

/**
* @title TWAP Conditional Order
Expand Down Expand Up @@ -61,7 +61,7 @@ contract TWAP is BaseConditionalOrder {

/// @dev Revert if the order is outside the TWAP bundle's span.
if (!(block.timestamp <= order.validTo)) {
revert IConditionalOrder.OrderNotValid(NOT_WITHIN_SPAN);
revert IConditionalOrder.OrderNotValid(NotWithinSpan.selector);
}
}
}
42 changes: 26 additions & 16 deletions src/types/twap/libraries/TWAPOrder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,22 @@ import {TWAPOrderMathLib} from "./TWAPOrderMathLib.sol";

// --- error strings

string constant INVALID_SAME_TOKEN = "same token";
string constant INVALID_TOKEN = "invalid token";
string constant INVALID_PART_SELL_AMOUNT = "invalid part sell amount";
string constant INVALID_MIN_PART_LIMIT = "invalid min part limit";
string constant INVALID_START_TIME = "invalid start time";
string constant INVALID_NUM_PARTS = "invalid num parts";
string constant INVALID_FREQUENCY = "invalid frequency";
string constant INVALID_SPAN = "invalid span";
/// @dev Sell and buy token are identical
error InvalidSameToken();
/// @dev A token is the zero address
error InvalidToken();
/// @dev The part sell amount is zero
error InvalidPartSellAmount();
/// @dev The minimum part limit is zero
error InvalidMinPartLimit();
/// @dev The start time does not fit uint32
error InvalidStartTime();
/// @dev The number of parts is not in (1, uint32.max]
error InvalidNumParts();
/// @dev The frequency is not in (0, 365 days]
error InvalidFrequency();
/// @dev The span exceeds the frequency
error InvalidSpan();

/**
* @title Time-weighted Average Order Library
Expand Down Expand Up @@ -48,16 +56,18 @@ library TWAPOrder {
* @param self The TWAP order to validate
*/
function validate(Data memory self) internal pure {
if (!(self.sellToken != self.buyToken)) revert IConditionalOrder.OrderNotValid(INVALID_SAME_TOKEN);
if (!(self.sellToken != self.buyToken)) revert IConditionalOrder.OrderNotValid(InvalidSameToken.selector);
if (!(address(self.sellToken) != address(0) && address(self.buyToken) != address(0))) {
revert IConditionalOrder.OrderNotValid(INVALID_TOKEN);
revert IConditionalOrder.OrderNotValid(InvalidToken.selector);
}
if (!(self.partSellAmount > 0)) revert IConditionalOrder.OrderNotValid(INVALID_PART_SELL_AMOUNT);
if (!(self.minPartLimit > 0)) revert IConditionalOrder.OrderNotValid(INVALID_MIN_PART_LIMIT);
if (!(self.t0 < type(uint32).max)) revert IConditionalOrder.OrderNotValid(INVALID_START_TIME);
if (!(self.n > 1 && self.n <= type(uint32).max)) revert IConditionalOrder.OrderNotValid(INVALID_NUM_PARTS);
if (!(self.t > 0 && self.t <= 365 days)) revert IConditionalOrder.OrderNotValid(INVALID_FREQUENCY);
if (!(self.span <= self.t)) revert IConditionalOrder.OrderNotValid(INVALID_SPAN);
if (!(self.partSellAmount > 0)) revert IConditionalOrder.OrderNotValid(InvalidPartSellAmount.selector);
if (!(self.minPartLimit > 0)) revert IConditionalOrder.OrderNotValid(InvalidMinPartLimit.selector);
if (!(self.t0 < type(uint32).max)) revert IConditionalOrder.OrderNotValid(InvalidStartTime.selector);
if (!(self.n > 1 && self.n <= type(uint32).max)) {
revert IConditionalOrder.OrderNotValid(InvalidNumParts.selector);
}
if (!(self.t > 0 && self.t <= 365 days)) revert IConditionalOrder.OrderNotValid(InvalidFrequency.selector);
if (!(self.span <= self.t)) revert IConditionalOrder.OrderNotValid(InvalidSpan.selector);
}

/**
Expand Down
12 changes: 6 additions & 6 deletions src/types/twap/libraries/TWAPOrderMathLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {IConditionalOrder} from "../../../interfaces/IConditionalOrder.sol";

// --- error strings

/// @dev No discrete order is valid before the start of the TWAP conditional order.
string constant BEFORE_TWAP_START = "before twap start";
/// @dev No discrete order is valid after it's last part.
string constant AFTER_TWAP_FINISH = "after twap finish";
/// @dev No discrete order is valid before the start of the TWAP
error BeforeTwapStart();
/// @dev No discrete order is valid after the last part
error AfterTwapFinish();

/**
* @title CoWProtocol TWAP Order Math Library
Expand Down Expand Up @@ -39,7 +39,7 @@ library TWAPOrderMathLib {

unchecked {
/// @dev Order is not valid before the start (order commences at `t0`).
if (!(startTime <= block.timestamp)) revert IConditionalOrder.OrderNotValid(BEFORE_TWAP_START);
if (!(startTime <= block.timestamp)) revert IConditionalOrder.OrderNotValid(BeforeTwapStart.selector);

/**
* @dev Order is expired after the last part (`n` parts, running at `t` time length).
Expand All @@ -51,7 +51,7 @@ library TWAPOrderMathLib {
* `type(uint32).max` so the sum of `startTime + (numParts * frequency)` is ≈ 2⁵⁵.
*/
if (!(block.timestamp < startTime + (numParts * frequency))) {
revert IConditionalOrder.OrderNotValid(AFTER_TWAP_FINISH);
revert IConditionalOrder.OrderNotValid(AfterTwapFinish.selector);
}

/**
Expand Down
Loading