Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: drop incompatible orders #94

Merged
merged 7 commits into from
Oct 9, 2023
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
188 changes: 39 additions & 149 deletions src/domain/checkForAndPlaceOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import { BytesLike } from "ethers/lib/utils";

import { ConditionalOrder, OrderStatus } from "../types";
import {
LowLevelError,
formatStatus,
getLogger,
pollConditionalOrder,
customErrorDecode,
handleOnChainCustomError,
} from "../utils";
import {
OrderBookApi,
Expand Down Expand Up @@ -510,175 +509,66 @@ async function _pollLegacy(
offchainInput: string,
orderRef: string
): Promise<PollResult> {
const { chainId, contract, multicall } = context;
const { contract, multicall, chainId } = context;
const logPrefix = `checkForAndPlaceOrder:_pollLegacy:${orderRef}`;
const log = getLogger(logPrefix);
// as we going to use multicall, with `aggregate3Value`, there is no need to do any simulation as the
// calls are guaranteed to pass, and will return the results, or the reversion within the ABI-encoded data.
// By not using `populateTransaction`, we avoid an `eth_estimateGas` RPC call.
const log = getLogger(`checkForAndPlaceOrder:_pollLegacy:${orderRef}`);
const to = contract.address;
const data = contract.interface.encodeFunctionData(
const target = contract.address;
const callData = contract.interface.encodeFunctionData(
"getTradeableOrderWithSignature",
[owner, conditionalOrder.params, offchainInput, proof]
);

try {
const lowLevelCall = await multicall.callStatic.aggregate3Value([
{
target: to,
target,
allowFailure: true,
value: 0,
callData: data,
callData,
},
]);

const [{ success, returnData }] = lowLevelCall;

// If the call failed, there may be a custom error to provide hints. We wrap the error in a LowLevelError
// so that it can be handled in the catch.
if (!success) {
throw new LowLevelError("low-level call failed", returnData);
}

// Decode the result to get the order and signature
const { order, signature } = contract.interface.decodeFunctionResult(
"getTradeableOrderWithSignature",
returnData
);
return {
result: PollResultCode.SUCCESS,
order,
signature,
};
} catch (error: any) {
log.error(
anxolin marked this conversation as resolved.
Show resolved Hide resolved
`Error on CALL to getTradeableOrderWithSignature. Simulate: https://dashboard.tenderly.co/gp-v2/watch-tower-prod/simulator/new?network=${chainId}&contractAddress=${to}&rawFunctionInput=${data}`
);

// An error of some type occurred. It may or may not be a hint. We pass it to the handler to decide.
return _handleGetTradableOrderWithSignatureCall(error, owner, orderRef);
}
}

function _handleGetTradableOrderWithSignatureCall(
error: any,
owner: string,
orderRef: string
): PollResultErrors {
const logPrefix = `checkForAndPlaceOrder:_handleGetTradableOrderCall:${orderRef}`;
const log = getLogger(logPrefix);

// If the error is a LowLevelError, we extract the selector, and any parameters.
if (error instanceof LowLevelError) {
try {
// The below will throw if:
// - the error is not a custom error (ie. the selector is not in the map)
// - the error is a custom error, but the parameters are not as expected
const { selector, message, blockNumberOrEpoch } = customErrorDecode(
error.data
if (success) {
// Decode the returnData to get the order and signature tuple
const { order, signature } = contract.interface.decodeFunctionResult(
"getTradeableOrderWithSignature",
returnData
);
switch (selector) {
case "SINGLE_ORDER_NOT_AUTHED":
case "PROOF_NOT_AUTHED":
// If there's no authorization we delete the order
// - One reason could be, because the user CANCELLED the order
// - for now it doesn't support more advanced cases where the order is auth during a pre-interaction
log.info(
`${selector}: Order on safe ${owner} not authed. Deleting order...`
);
return {
result: PollResultCode.DONT_TRY_AGAIN,
reason: `${selector}: The owner has not authorized the order`,
};
case "INTERFACE_NOT_SUPPORTED":
log.info(
`${selector}: Order on safe ${owner} attempted to use a handler that is not supported. Deleting order...`
);
return {
result: PollResultCode.DONT_TRY_AGAIN,
reason: `${selector}: The handler is not supported`,
};
case "INVALID_FALLBACK_HANDLER":
log.info(
`${selector}: Order for safe ${owner} where the Safe does not have ExtensibleFallbackHandler set. Deleting order...`
);
return {
result: PollResultCode.DONT_TRY_AGAIN,
reason: `${selector}: The safe does not have ExtensibleFallbackHandler set`,
};
case "SWAP_GUARD_RESTRICTED":
log.info(
`${selector}: Order for safe ${owner} where the Safe has swap guard enabled. Deleting order...`
);
return {
result: PollResultCode.DONT_TRY_AGAIN,
reason: `${selector}: The safe has swap guard enabled`,
};
// TODO: Add metrics to track this
case "ORDER_NOT_VALID":
case "POLL_TRY_NEXT_BLOCK":
// OrderNotValid: With the revised custom errors, `OrderNotValid` is generally returned when elements
// of the data struct are invalid. For example, if the `sellAmount` is zero, or the `validTo` is in
// the past.
// PollTryNextBlock: The conditional order has signalled that it should be polled again on the next block.
log.info(
`${selector}: Order on safe ${owner} not valid/signalled to try next block.`
);
return {
result: PollResultCode.TRY_NEXT_BLOCK,
reason: `${selector}: ${message}`,
};
case "POLL_TRY_AT_BLOCK":
// The conditional order has signalled that it should be polled again on a specific block.
if (!blockNumberOrEpoch) {
throw new Error(
`Expected blockNumberOrEpoch to be defined for ${selector}`
);
}
return {
result: PollResultCode.TRY_ON_BLOCK,
blockNumber: blockNumberOrEpoch,
reason: `PollTryAtBlock: ${message}`,
};
case "POLL_TRY_AT_EPOCH":
// The conditional order has signalled that it should be polled again on a specific epoch.
if (!blockNumberOrEpoch) {
throw new Error(
`Expected blockNumberOrEpoch to be defined for ${selector}`
);
}
return {
result: PollResultCode.TRY_AT_EPOCH,
epoch: blockNumberOrEpoch,
reason: `PollTryAtEpoch: ${message}`,
};
case "POLL_NEVER":
// The conditional order has signalled that it should never be polled again.
return {
result: PollResultCode.DONT_TRY_AGAIN,
reason: `PollNever: ${message}`,
};
}
} catch (err: any) {
log.error(`${logPrefix} Unexpected error`, err);
return {
result: PollResultCode.UNEXPECTED_ERROR,
reason:
"UnexpectedErrorName: Unexpected error" +
(err.message ? `: ${err.message}` : ""),
error: err,
result: PollResultCode.SUCCESS,
order,
signature,
};
}
}

log.error(`${logPrefix} ethers/call Unexpected error`, error);
// If we don't know the reason, is better to not delete the order
// TODO: Add metrics to track this
return {
result: PollResultCode.TRY_NEXT_BLOCK,
reason:
"UnexpectedErrorName: Unexpected error" +
(error.message ? `: ${error.message}` : ""),
};
// If the low-level call failed, per the `ComposableCoW` interface, the contract is attempting to
// provide hints to the watch-tower. But, we can't trust all the data returned as there may be
// order types created that are _not_ adhering to the interface (and are therefore invalid).
return handleOnChainCustomError({
owner,
orderRef,
chainId,
target,
callData,
revertData: returnData,
});
} catch (error: any) {
// We can only get here from some provider / ethers failure. As the contract hasn't had it's say
// we will defer to try again.
// TODO: Add metrics to track this
log.error(`${logPrefix} ethers/call Unexpected error`, error);
return {
result: PollResultCode.TRY_NEXT_BLOCK,
reason:
"UnexpectedErrorName: Unexpected error" +
(error.message ? `: ${error.message}` : ""),
};
}
}

/**
Expand Down
Loading