Skip to content

Commit

Permalink
refactor: custom error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
mfw78 committed Oct 6, 2023
1 parent a92023b commit 7cadfe9
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 146 deletions.
266 changes: 129 additions & 137 deletions src/domain/checkForAndPlaceOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { BytesLike } from "ethers/lib/utils";

import { ConditionalOrder, OrderStatus } from "../types";
import {
LowLevelError,
formatStatus,
getLogger,
pollConditionalOrder,
Expand Down Expand Up @@ -511,6 +510,8 @@ async function _pollLegacy(
orderRef: string
): Promise<PollResult> {
const { contract, multicall } = 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.
Expand All @@ -532,164 +533,155 @@ async function _pollLegacy(

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);
if (success) {
// Decode the result to get the order and signature
const { order, signature } = contract.interface.decodeFunctionResult(
"getTradeableOrderWithSignature",
returnData
);
return {
result: PollResultCode.SUCCESS,
order,
signature,
};
}

// 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) {
// 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,
// If the call failed, there may be a custom error to provide hints. Let's try.
return _handleOnChainCustomError(
owner,
orderRef,
context,
to,
data
data,
returnData
);
} catch (error: any) {
log.error(`${logPrefix} ethers/call Unexpected error`, error);
// 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
return {
result: PollResultCode.TRY_NEXT_BLOCK,
reason:
"UnexpectedErrorName: Unexpected error" +
(error.message ? `: ${error.message}` : ""),
};
}
}

function _handleGetTradableOrderWithSignatureCall(
error: any,
function _handleOnChainCustomError(
owner: string,
orderRef: string,
context: ChainContext,
to: string,
data: string
data: string,
returnData: string
): PollResultErrors {
const logPrefix = `checkForAndPlaceOrder:_handleGetTradableOrderCall:${orderRef}`;
const logPrefix = `checkForAndPlaceOrder:_handleOnChainCustomError:${orderRef}`;
const log = getLogger(logPrefix);
const { chainId } = context;

// 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
);
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...`
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(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.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_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_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) {
// Any errors thrown here can _ONLY_ come from non-compliant interfaces (ie. bad revert ABI encoding).
// We log the error, and return a DONT_TRY_AGAIN result.
// TODO: Add metrics to track this
log.error(
`Contract returned non-interface compliant revert via getTradeableOrderWithSignature. Simulate: https://dashboard.tenderly.co/gp-v2/watch-tower-prod/simulator/new?network=${chainId}&contractAddress=${to}&rawFunctionInput=${data}`
);
return {
result: PollResultCode.DONT_TRY_AGAIN,
reason:
"Non-compliant interface error" +
(err.message ? `: ${err.message}` : ""),
};
}
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}`,
};
}
} finally {
// We use finally here to ensure that we always log the error, even if we hit an unexpected error
// Any errors thrown here can _ONLY_ come from non-compliant interfaces (ie. bad revert ABI encoding).
// We log the error, and return a DONT_TRY_AGAIN result.
// TODO: Add metrics to track this
log.debug(
`Contract returned non-interface compliant revert via getTradeableOrderWithSignature. Simulate: https://dashboard.tenderly.co/gp-v2/watch-tower-prod/simulator/new?network=${chainId}&contractAddress=${to}&rawFunctionInput=${data}`
);
return {
result: PollResultCode.DONT_TRY_AGAIN,
reason: "Non-compliant interface",
};
}

log.error(`${logPrefix} ethers/call Unexpected error`, error);
// 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
return {
result: PollResultCode.TRY_NEXT_BLOCK,
reason:
"UnexpectedErrorName: Unexpected error" +
(error.message ? `: ${error.message}` : ""),
};
}

/**
Expand Down
9 changes: 0 additions & 9 deletions src/utils/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,6 @@ export function formatStatus(status: OrderStatus) {
}
}

export class LowLevelError extends Error {
data: string;
constructor(msg: string, data: string) {
super(msg);
this.data = data;
Object.setPrototypeOf(this, LowLevelError.prototype);
}
}

/**
* Converts the typechain conditional order params to the sdk conditional order params (simpler version of the same thing)
*
Expand Down

0 comments on commit 7cadfe9

Please sign in to comment.