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

feat: apply filtering policy #116

Merged
merged 9 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from 8 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@types/express": "^4.17.18",
"@types/jest": "^29.5.3",
"@types/node": "^18.16.3",
"@types/node-fetch": "^2.6.9",
"@types/node-slack": "^0.0.31",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
Expand Down
2 changes: 2 additions & 0 deletions src/commands/runMulti.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export async function runMulti(options: RunMultiOptions) {
deploymentBlocks,
watchdogTimeouts,
orderBookApis,
filterPolicyConfigFiles,
oneShot,
disableApi,
apiPort,
Expand Down Expand Up @@ -51,6 +52,7 @@ export async function runMulti(options: RunMultiOptions) {
deploymentBlock: deploymentBlocks[index],
watchdogTimeout: watchdogTimeouts[index],
orderBookApi: orderBookApis[index],
filterPolicyConfig: filterPolicyConfigFiles[index],
},
storage
);
Expand Down
27 changes: 25 additions & 2 deletions src/domain/chainContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
reorgsTotal,
} from "../utils/metrics";
import { hexZeroPad } from "ethers/lib/utils";
import { FilterPolicy } from "../utils/filterPolicy";

const WATCHDOG_FREQUENCY = 5 * 1000; // 5 seconds

Expand Down Expand Up @@ -67,6 +68,11 @@ export interface ChainWatcherHealth {
};
}

export interface FilterPolicyConfig {
baseUrl: string;
// authToken: string; // TODO: Implement authToken
}

/**
* The chain context handles watching a single chain for new conditional orders
* and executing them.
Expand All @@ -85,6 +91,7 @@ export class ChainContext {
chainId: SupportedChainId;
registry: Registry;
orderBook: OrderBookApi;
filterPolicy: FilterPolicy | undefined;
contract: ComposableCoW;
multicall: Multicall3;

Expand All @@ -101,6 +108,7 @@ export class ChainContext {
watchdogTimeout,
owners,
orderBookApi,
filterPolicyConfig,
} = options;
this.deploymentBlock = deploymentBlock;
this.pageSize = pageSize;
Expand All @@ -126,12 +134,19 @@ export class ChainContext {
},
});

this.filterPolicy = filterPolicyConfig
? new FilterPolicy({
configBaseUrl: filterPolicyConfig,
// configAuthToken: filterPolicyConfigAuthToken, // TODO: Implement authToken
})
: undefined;
this.contract = composableCowContract(this.provider, this.chainId);
this.multicall = Multicall3__factory.connect(MULTICALL3, this.provider);
}

/**
* Initialise a chain context.
* Initialize a chain context.
*
* @param options as parsed by commander from the command line arguments.
* @param storage the db singleton that provides persistence.
* @returns A chain context that is monitoring for orders on the chain.
Expand Down Expand Up @@ -444,7 +459,7 @@ async function processBlock(
blockNumberOverride?: number,
blockTimestampOverride?: number
) {
const { provider, chainId } = context;
const { provider, chainId, filterPolicy } = context;
const timer = processBlockDurationSeconds
.labels(context.chainId.toString())
.startTimer();
Expand All @@ -454,6 +469,14 @@ async function processBlock(
block.number.toString()
);

// Get the latest filter policy
if (filterPolicy) {
filterPolicy.reloadPolicies().catch((error) => {
console.log(`Error fetching the filter policy config for chain `, error);
return null;
});
}

// Transaction watcher for adding new contracts
let hasErrors = false;
for (const event of events) {
Expand Down
43 changes: 37 additions & 6 deletions src/domain/checkForAndPlaceOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
PollResultCode,
PollResultErrors,
PollResultSuccess,
SigningScheme,
SupportedChainId,
formatEpoch,
} from "@cowprotocol/cow-sdk";
Expand All @@ -41,6 +42,7 @@ import {
pollingOnChainEthersErrorsTotal,
measureTime,
} from "../utils/metrics";
import { FilterAction } from "../utils/filterPolicy";

const GPV2SETTLEMENT = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41";

Expand Down Expand Up @@ -83,7 +85,7 @@ export async function checkForAndPlaceOrder(
blockNumberOverride?: number,
blockTimestampOverride?: number
) {
const { chainId, registry } = context;
const { chainId, registry, filterPolicy } = context;
const { ownerOrders, numOrders } = registry;

const blockNumber = blockNumberOverride || block.number;
Expand Down Expand Up @@ -127,13 +129,32 @@ export async function checkForAndPlaceOrder(

const { result: lastHint } = conditionalOrder.pollResult || {};

// Apply filtering policy
if (filterPolicy) {
const filterResult = filterPolicy.preFilter({
owner,
conditionalOrderParams: conditionalOrder.params,
});

switch (filterResult) {
case FilterAction.DROP:
log.debug("Dropping conditional order. Reason: AcceptPolicy: DROP");
ordersPendingDelete.push(conditionalOrder);

continue;
case FilterAction.SKIP:
log.debug("Skipping conditional order. Reason: AcceptPolicy: SKIP");
continue;
}
}

// Check if the order is due (by epoch)
if (
lastHint?.result === PollResultCode.TRY_AT_EPOCH &&
blockTimestamp < lastHint.epoch
) {
log.debug(
`Skipping conditional. Reason: Not due yet (TRY_AT_EPOCH=${
`Skipping conditional order. Reason: Not due yet (TRY_AT_EPOCH=${
lastHint.epoch
}, ${formatEpoch(lastHint.epoch)}). ${logOrderDetails}`,
conditionalOrder.params
Expand All @@ -147,7 +168,7 @@ export async function checkForAndPlaceOrder(
blockNumber < lastHint.blockNumber
) {
log.debug(
`Skipping conditional. Reason: Not due yet (TRY_ON_BLOCK=${
`Skipping conditional order. Reason: Not due yet (TRY_ON_BLOCK=${
lastHint.blockNumber
}, in ${
lastHint.blockNumber - blockNumber
Expand Down Expand Up @@ -442,16 +463,26 @@ async function _placeOrder(params: {
const { chainId } = orderBook.context;
try {
const postOrder: OrderCreation = {
...order,
kind: order.kind,
from: order.from,
sellToken: order.sellToken,
buyToken: order.buyToken,
sellAmount: order.sellAmount.toString(),
buyAmount: order.buyAmount.toString(),
receiver: order.receiver,
feeAmount: order.feeAmount.toString(),
signingScheme: "eip1271",
validTo: order.validTo,
appData: order.appData,
partiallyFillable: order.partiallyFillable,
sellTokenBalance: order.sellTokenBalance,
buyTokenBalance: order.buyTokenBalance,
signingScheme: SigningScheme.EIP1271,
signature: order.signature,
mfw78 marked this conversation as resolved.
Show resolved Hide resolved
};

// If the operation is a dry run, don't post to the API
log.info(`Post order ${orderUid} to OrderBook on chain ${chainId}`);
log.debug(`Order`, postOrder);
log.debug(`Post order details`, postOrder);
if (!dryRun) {
const orderUid = await orderBook.sendOrder(postOrder);
orderBookDiscreteOrdersTotal.labels(...metricLabels).inc();
Expand Down
37 changes: 31 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const databasePathOption = new Option(
.default(DEFAULT_DATABASE_PATH)
.env("DATABASE_PATH");

const chainConfigHelp = `Chain configuration in the format of <rpc>,<deploymentBlock>[,<watchdogTimeout>,<orderBookApi>], e.g. http://erigon.dappnode:8545,12345678,30,https://api.cow.fi/mainnet`;
const chainConfigHelp = `Chain configuration in the format of <rpc>,<deploymentBlock>[,<watchdogTimeout>,<orderBookApi>,<filterPolicyConfig>], e.g. http://erigon.dappnode:8545,12345678,30,https://api.cow.fi/mainnet,https://raw.githubusercontent.com/cowprotocol/watch-tower/config/filter-policy-1.json`;
const multiChainConfigOption = new Option(
"--chain-config <chainConfig...>",
chainConfigHelp
Expand Down Expand Up @@ -231,7 +231,7 @@ function parseChainConfigOption(option: string): ChainConfigOptions {
// Ensure there are at least two parts (rpc and deploymentBlock)
if (parts.length < 2) {
throw new InvalidArgumentError(
`Chain configuration must be in the format of <rpc>,<deploymentBlock>[,<watchdogTimeout>,<orderBookApi>], e.g. http://erigon.dappnode:8545,12345678,30,https://api.cow.fi/mainnet`
`Chain configuration must be in the format of <rpc>,<deploymentBlock>[,<watchdogTimeout>,<orderBookApi>,<filterPolicyConfig>], e.g. http://erigon.dappnode:8545,12345678,30,https://api.cow.fi/mainnet,https://raw.githubusercontent.com/cowprotocol/watch-tower/config/filter-policy-1.json`
);
}

Expand All @@ -255,7 +255,8 @@ function parseChainConfigOption(option: string): ChainConfigOptions {

const rawWatchdogTimeout = parts[2];
// If there is a third part, it is the watchdogTimeout
const watchdogTimeout = parts.length > 2 ? Number(rawWatchdogTimeout) : 30;
const watchdogTimeout =
parts.length > 2 && rawWatchdogTimeout ? Number(rawWatchdogTimeout) : 30;
// Ensure that the watchdogTimeout is a positive number
if (isNaN(watchdogTimeout) || watchdogTimeout < 0) {
throw new InvalidArgumentError(
Expand All @@ -264,15 +265,31 @@ function parseChainConfigOption(option: string): ChainConfigOptions {
}

// If there is a fourth part, it is the orderBookApi
const orderBookApi = parts.length > 3 ? parts[3] : undefined;
const orderBookApi = parts.length > 3 && parts[3] ? parts[3] : undefined;
// Ensure that the orderBookApi is a valid URL
if (orderBookApi && !isValidUrl(orderBookApi)) {
throw new InvalidArgumentError(
`${orderBookApi} must be a valid URL (orderBookApi)`
);
}

return { rpc, deploymentBlock, watchdogTimeout, orderBookApi };
// If there is a fifth part, it is the filterPolicyConfig
const filterPolicyConfig =
parts.length > 4 && parts[4] ? parts[4] : undefined;
// Ensure that the orderBookApi is a valid URL
if (filterPolicyConfig && !isValidUrl(filterPolicyConfig)) {
throw new InvalidArgumentError(
`${filterPolicyConfig} must be a valid URL (orderBookApi)`
);
}

return {
rpc,
deploymentBlock,
watchdogTimeout,
orderBookApi,
filterPolicyConfig,
};
}

function parseChainConfigOptions(
Expand All @@ -282,15 +299,23 @@ function parseChainConfigOptions(
deploymentBlocks: [],
watchdogTimeouts: [],
orderBookApis: [],
filterPolicyConfigFiles: [],
}
): MultiChainConfigOptions {
const parsedOption = parseChainConfigOption(option);
const { rpc, deploymentBlock, watchdogTimeout, orderBookApi } = parsedOption;
const {
rpc,
deploymentBlock,
watchdogTimeout,
orderBookApi,
filterPolicyConfig,
} = parsedOption;

previous.rpcs.push(rpc);
previous.deploymentBlocks.push(deploymentBlock);
previous.watchdogTimeouts.push(watchdogTimeout);
previous.orderBookApis.push(orderBookApi);
previous.filterPolicyConfigFiles.push(filterPolicyConfig);
return previous;
}

Expand Down
7 changes: 5 additions & 2 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,17 @@ export type ChainConfigOptions = {
rpc: string;
deploymentBlock: number;
watchdogTimeout: number;
orderBookApi: OrderBookApi;
orderBookApi?: string;
filterPolicyConfig?: string;
// filterPolicyConfigAuthToken?: string; // TODO: Implement authToken
};

export type MultiChainConfigOptions = {
rpcs: string[];
deploymentBlocks: number[];
watchdogTimeouts: number[];
orderBookApis: OrderBookApi[];
orderBookApis: (string | undefined)[];
filterPolicyConfigFiles: (string | undefined)[];
};

export type RunSingleOptions = RunOptions & ChainConfigOptions;
Expand Down
84 changes: 84 additions & 0 deletions src/utils/filterPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { ConditionalOrderParams } from "@cowprotocol/cow-sdk";

export enum FilterAction {
DROP = "DROP",
SKIP = "SKIP",
ACCEPT = "ACCEPT",
}

interface PolicyConfig {
owners: Map<string, FilterAction>;
handlers: Map<string, FilterAction>;
}

export interface FilterParams {
owner: string;
conditionalOrderParams: ConditionalOrderParams;
}

export interface FilterPolicyParams {
configBaseUrl: string;
// configAuthToken: string; // TODO: Implement authToken
}
export class FilterPolicy {
protected configUrl: string;
protected config: PolicyConfig | undefined;

constructor({ configBaseUrl }: FilterPolicyParams) {
this.configUrl = configBaseUrl;
}

/**
* Decide if a conditional order should be processed, ignored, or dropped base in some filtering rules
*
* @param filterParams params required for the pre-filtering, including the conditional order params, chainId and the owner contract
* @returns The action that should be performed with the conditional order
*/
preFilter({ owner, conditionalOrderParams }: FilterParams): FilterAction {
if (!this.config) {
return FilterAction.ACCEPT;
}

const { owners, handlers } = this.config;

const action =
owners.get(owner) || handlers.get(conditionalOrderParams.handler);
Comment on lines +44 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a suggestion, are we sure we want to consider the individual owner case first? This may open up attacks if there's a public list with an owner explicitly whitelisted, in which case, one can create a bad handler and target that owner address with fake events being emitted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now don't make sense, as the default is to ACCEPT. It makes sense if we allow to change the default to DROP or SKIP (by the way, good feature and easy to add)

However, if we change it in the future, we might not remember this, so changing it just in case.


if (action) {
return action;
}

return FilterAction.ACCEPT;
}

/**
* Reloads the policies with their latest version
*/
async reloadPolicies() {
const policyConfig = await this.getConfig();

if (policyConfig) {
this.config = policyConfig;
}
}

protected async getConfig(): Promise<PolicyConfig> {
if (!this.configUrl) {
throw new Error("configUrl must be defined");
}
const configResponse = await fetch(this.configUrl); // TODO: Implement authToken

if (!configResponse.ok) {
throw new Error(
`Failed to fetch policy. Error ${
configResponse.status
}: ${await configResponse.text().catch(() => "")}`
);
}
const config = await configResponse.json();
return {
owners: new Map(Object.entries(config.owners)),
handlers: new Map(Object.entries(config.handlers)),
};
}
}
Loading
Loading