Skip to content

Commit

Permalink
Drop ComposableCoW bytecode check (#151)
Browse files Browse the repository at this point in the history
# Description
Removes the janky bytecode check that was being used to enforce that the
contract being indexed was `ComposableCoW` compatible. This is possible
given that there are exhaustive switch statements that were published
for the revert messages.

# Changes

- [x] Add a try/catch around the decode function result to catch bad
`ComposableCoW`-like implementations.
- [x] Remove bytecode checks.

## How to test
1. Remove the sepolia database.
2. Restart
3. Observe new CoW AMMs 🚀

---------

Co-authored-by: Federico Giacon <[email protected]>
  • Loading branch information
mfw78 and fedgiac authored May 10, 2024
1 parent 4f86cb4 commit cd466da
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 96 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@cowprotocol/watch-tower",
"license": "GPL-3.0-or-later",
"version": "1.2.0",
"version": "1.3.0",
"description": "A standalone watch tower, keeping an eye on Composable Cows 👀🐮",
"author": {
"name": "Cow Protocol"
Expand Down
9 changes: 1 addition & 8 deletions src/domain/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
toConditionalOrderParams,
getLogger,
handleExecutionError,
isComposableCowCompatible,
metrics,
} from "../../utils";
import { BytesLike, ethers } from "ethers";
Expand Down Expand Up @@ -50,19 +49,13 @@ async function _addContract(
) {
const log = getLogger("addContract:_addContract");
const composableCow = ComposableCoW__factory.createInterface();
const { provider, registry } = context;
const { registry } = context;
const { transactionHash: tx, blockNumber } = event;

// Process the logs
let hasErrors = false;
let numContractsAdded = 0;

// Do not process logs that are not from a `ComposableCoW`-compatible contract
// This is a *normal* case, if the contract is not `ComposableCoW`-compatible
// then we do not need to do anything, and therefore don't flag as an error.
if (!isComposableCowCompatible(await provider.getCode(event.address))) {
return;
}
const { error, added } = await _registerNewOrder(
event,
composableCow,
Expand Down
33 changes: 22 additions & 11 deletions src/domain/polling/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,11 +665,11 @@ async function _pollLegacy(
): Promise<PollResult> {
const { contract, multicall, chainId } = context;
const log = getLogger("checkForAndPlaceOrder:_pollLegacy", orderRef);
const { composableCow: target } = conditionalOrder;
const { handler } = conditionalOrder.params;
// 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 target = contract.address;
const callData = contract.interface.encodeFunctionData(
"getTradeableOrderWithSignature",
[owner, conditionalOrder.params, offchainInput, proof]
Expand All @@ -690,16 +690,27 @@ async function _pollLegacy(
const [{ success, returnData }] = lowLevelCall;

if (success) {
// Decode the returnData to get the order and signature tuple
const { order, signature } = contract.interface.decodeFunctionResult(
"getTradeableOrderWithSignature",
returnData
);
return {
result: PollResultCode.SUCCESS,
order,
signature,
};
try {
// Decode the returnData to get the order and signature tuple
const { order, signature } = contract.interface.decodeFunctionResult(
"getTradeableOrderWithSignature",
returnData
);
return {
result: PollResultCode.SUCCESS,
order,
signature,
};
} catch (error: any) {
log.error(`ethers/decodeFunctionResult Unexpected error`, error);
metrics.pollingOnChainEthersErrorsTotal.labels(...metricLabels).inc();
return {
result: PollResultCode.DONT_TRY_AGAIN,
reason:
"UnexpectedErrorName: Data decoding failure" +
(error.message ? `: ${error.message}` : ""),
};
}
}

// If the low-level call failed, per the `ComposableCoW` interface, the contract is attempting to
Expand Down
40 changes: 30 additions & 10 deletions src/services/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@ import {
} from "@cowprotocol/cow-sdk";
import { addContract } from "../domain/events";
import { checkForAndPlaceOrder } from "../domain/polling";
import { EventFilter, providers } from "ethers";
import { ethers, providers } from "ethers";
import {
composableCowContract,
getLogger,
isRunningInKubernetesPod,
metrics,
} from "../utils";
import { DBService } from ".";
import { hexZeroPad } from "ethers/lib/utils";
import { policy } from "../domain/polling/filtering";

const WATCHDOG_FREQUENCY_SECS = 5; // 5 seconds
Expand Down Expand Up @@ -506,22 +505,43 @@ async function processBlock(
}
}

function pollContractForEvents(
async function pollContractForEvents(
fromBlock: number,
toBlock: number | "latest",
context: ChainContext
): Promise<ConditionalOrderCreatedEvent[]> {
const { provider, chainId, addresses } = context;
const composableCow = composableCowContract(provider, chainId);
const filter = composableCow.filters.ConditionalOrderCreated() as EventFilter;
const eventName = "ConditionalOrderCreated(address,(address,bytes32,bytes))";
const topic = ethers.utils.id(eventName);

if (addresses) {
filter.topics?.push(
addresses.map((address) => hexZeroPad(address.toLowerCase(), 32))
);
}
const logs = await provider.getLogs({
fromBlock,
toBlock,
topics: [topic],
});

return composableCow.queryFilter(filter, fromBlock, toBlock);
return logs
.map((event) => {
try {
const decoded = composableCow.interface.decodeEventLog(
topic,
event.data,
event.topics
) as unknown as ConditionalOrderCreatedEvent;

return {
...decoded,
...event,
};
} catch {
return null;
}
})
.filter((e): e is ConditionalOrderCreatedEvent => e !== null)
.filter((e): e is ConditionalOrderCreatedEvent => {
return addresses ? addresses.includes(e.args.owner) : true;
});
}

function _formatResult(result: boolean) {
Expand Down
30 changes: 0 additions & 30 deletions src/utils/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ import {
import { getLogger } from "./logging";
import { metrics } from ".";

// Selectors that are required to be part of the contract's bytecode in order to be considered compatible
const REQUIRED_SELECTORS = [
"cabinet(address,bytes32)",
"getTradeableOrderWithSignature(address,(address,bytes32,bytes),bytes,bytes32[])",
];

// Define an enum for the custom error revert hints that can be returned by the ComposableCoW's interfaces.
export enum CustomErrorSelectors {
/**
Expand Down Expand Up @@ -117,30 +111,6 @@ export function abiToSelector(abi: string) {
return ethers.utils.id(abi).slice(0, 10);
}

/**
* Attempts to verify that the contract at the given address implements the interface of the `ComposableCoW`
* contract. This is done by checking that the contract contains the selectors of the functions that are
* required to be implemented by the interface.
*
* @remarks This is not a foolproof way of verifying that the contract implements the interface, but it is
* a good enough heuristic to filter out most of the contracts that do not implement the interface.
*
* @dev The selectors are:
* - `cabinet(address,bytes32)`: `1c7662c8`
* - `getTradeableOrderWithSignature(address,(address,bytes32,bytes),bytes,bytes32[])`: `26e0a196`
*
* @param code the contract's deployed bytecode as a hex string
* @returns A boolean indicating if the contract likely implements the interface
*/
export function isComposableCowCompatible(code: string): boolean {
const composableCow = ComposableCoW__factory.createInterface();

return REQUIRED_SELECTORS.every((signature) => {
const sighash = composableCow.getSighash(signature);
return code.includes(sighash.slice(2));
});
}

export function composableCowContract(
provider: ethers.providers.Provider,
chainId: SupportedChainId
Expand Down
36 changes: 0 additions & 36 deletions src/utils/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,13 @@
import * as composableCow from "../../abi/ComposableCoW.json";
import * as extensibleFallbackHandler from "../../abi/ExtensibleFallbackHandler.json";
import {
CUSTOM_ERROR_ABI_MAP,
CustomErrorSelectors,
abiToSelector,
handleOnChainCustomError,
initLogging,
isComposableCowCompatible,
parseCustomError,
} from ".";
import { COMPOSABLE_COW_CONTRACT_ADDRESS } from "@cowprotocol/cow-sdk";

// consts for readability
const composableCowBytecode = composableCow.deployedBytecode.object;
const failBytecode = extensibleFallbackHandler.deployedBytecode.object;

describe("test supports composable cow interface from bytecode", () => {
it("should pass", () => {
expect(isComposableCowCompatible(composableCowBytecode)).toBe(true);
});

it("should fail", () => {
expect(isComposableCowCompatible(failBytecode)).toBe(false);
});
});

describe("test against concrete examples", () => {
const signatures = ["0x1c7662c8", "0x26e0a196"];

it("should pass with both selectors", () => {
expect(isComposableCowCompatible("0x1c7662c826e0a196")).toBe(true);
});

// using `forEach` here, be careful not to do async tests.
signatures.forEach((s) => {
it(`should fail with only selector ${s}`, () => {
expect(isComposableCowCompatible(s)).toBe(false);
});
});

it("should fail with no selectors", () => {
expect(isComposableCowCompatible("0xdeadbeefdeadbeef")).toBe(false);
});
});

describe("parse custom errors (reversions)", () => {
it("should pass the SingleOrderNotAuthed selector correctly", () => {
expect(parseCustomError(SINGLE_ORDER_NOT_AUTHED_ERROR)).toMatchObject({
Expand Down

0 comments on commit cd466da

Please sign in to comment.