Plug-in-templates for Defender as Code
This repository contains templates and tooling to setup web3 monitoring on OpenZeppelin Defender without hassle.
Structure of this codebase is made with plug-in-ability in mind, so adding new templates is very easy as well!
Why use it?
- Generate monitoring configuration with current on-chain data as source of truth
- Manage only one configuration file and reuse code with use of templates
- Combine matchiing logic templates with monitoring templates to get what you need.
- Run this in CI/CD pipeline to automatically keep up to date with any new deployments
- Typescript support for configuring Defender
Installing and using as package
pnpm add autodefender
or
yarn add autodefender
then create config file defender.config.js:
const {getProcessEnv, getSlackNotifyChannel} = require('autodefender/utils');
const polygonRPC = getProcessEnv(false, 'POLYGON_RPC_URL');
const mainnerRPC = getProcessEnv(false, 'MAINNET_RPC_URL');
const {
generateOwnableMonitor,
} = require('autodefender/templates/monitors/ownership');
const {all} = require('autodefender/templates/matchers/all');
const config = {
projectName: 'WORKSHOP1',
ssot: false,
path: './deployments',
networks: {
matic: {rpc: polygonRPC, directoryName: 'polygon'},
mainnet: {rpc: mainnerRPC, directoryName: 'mainnet'},
},
monitors: {
Ownership: {
filter: all(),
monitor: generateOwnableMonitor(),
notification: {
channels: [getSlackNotifyChannel(getProcessEnv(false, 'SLACK_URL'))],
},
},
},
outDir: './out',
extractedAccountsMonitoring: {},
excludeDeployments: ['Old_*'],
excludeAccounts: ['0x000000000000AAeB6D7670E522A718067333cd4E'],
};
module.exports = config;
To deploy contracts:
yarn autodefender contracts --config defender.config.js
To deploy monitors:
yarn autodefender monitors --config defender.config.js
Setting up dev enviroment:
Run
pnpm install && pnpm types
Open or create new defender.config.ts in root of this repository.
{
projectName: '',
path:
networks: {
matic: {rpc: polygonRPC, directoryName: 'polygon'},
mainnet: {rpc: mainnerRPC, directoryName: 'mainnet'},
},
monitors: {
'Large-Mint-ERC20': {
notification: {
channels: [getSlackNotifyChannel(getProcessEnv(false, 'SLACK_URL'))],
},
filter: findERC20Contracts(),
monitor: mintMonitor('ERC20', '100'),
},
},
outDir: './out',
extractedAccountsMonitoring: {
EthBalance: {
monitor: accountEthMonitor('10'),
filter: all(),
notification: {
channels: [getSlackNotifyChannel(getProcessEnv(false, 'SLACK_URL'))],
},
},
excludeDeployments: [
'QUICKSWAP_SAND_MATIC',
'FXCHILD*',
'CHILD_CHAIN_MANAGER',
'*_Implementation',
'*_Proxy',
'TRUSTED_FORWARDER',
'PolygonLand_V1',
'FAKE*',
'DAIMedianize',
],
excludeAccounts: ['0x000000000000AAeB6D7670E522A718067333cd4E'],
};
Generating & Deploying
Export env variables
export DEFENDER_SECRET = ""
export DEFENDER_KEY = ""
export POLYGON_RPC_URL="url"
export SLACK_URL="url"
export MAINNET_RPC_URL="url"
NB: Im not a fun of using dotenv. Use export keyword in env files/variables and source <your_env_file> or add sorucing to yarn commands, or add dotenv if you like using it.
yarn contracts
yarn monitors
Kinds of Templates
Create new file in /templates/. Add it to index.
Follow typing definitions of DefenderConfigType
Matchers
Matchers are the templates intended to take some given address space, provider already connected to network and any extra arguments, and it must return array of MatcherFindings. Each finding has address of finding and relatedAccounts[] array (i.e. roles in AccessControl contracts)
Monitors
Findings from Matcher function are passed to Monitor Getter template along with other arguments and generate a monitoring template that consists of Monitor itself, and optional dependencies.
Dependencies might be: (trigger/condition) Functions, connected to them Relayers and Secrets.
Messages
md files that must be parsed in to strings and be returned from Monitor template
Notifications
Templates to generate YNotification
Functions
Functions are javascript templates that are ready to be deployed in Defender scripts. They are intended to run in the Defender Node enviroment and therefore best practice is to rollup or use webpack to genetrate them.
Convinient development way is to create your template in src/templates directory and then add it to the build process in rollup.config.js. Then by simply running yarn prebuild your function will be added to templates/functions.
Creating a new monitor
Monitor getter
Monitor getters are described by
TSentinelGetter<Record<string, string | never>, Record<string, string | never>>;
where inputs are
contractAddresses: string[]
provider: JsonRpcProvider
networkName: Network
and return is Promise of TSentinelOutput which consists of
export interface TSentinelOutput<T, K> {
newMonitor: TSentinel;
defaultMessage: string;
actionsParams?: ActionsParams<T, K>;
}
Adding functions
Simply add in your templates newMonitor properties: autotask-condition or autotask-trigger with path from repo root to compiled template.
Connecting functions to relays
If your functions require relay to operate you can use either default relay for read operations generated automatically, or specify key for custom relay. In second case you must specify relay configuration in your config file with same key.
To add relay return object must contain actionsParams object. I.e. connecting to default relay in same network as Monitor is run for conditional autotask may look like this:
return {
newMonitor,
defaultMessage,
actionsParams: {
condition: {
relayNetwork: sentinelNetwork,
customRelay: 'myRelayer',
},
trigger: trigger
? {relayNetwork: trigger.params.relayNetwork, secrets: trigger.params}
: undefined,
},
};
Specifying secrets
In the example above, monitoring template actually requires LOW_ETH_THRESHOLD secret to exist in Defender stack. In order to add id, pass required key-values as actionsParams.condition or actionsParams.trigger. Having trigger/conditon differentiator will allow that secret to be used in scoped secrets workflow
return {
newMonitor,
defaultMessage,
actionsParams: {
condition: {
relayNetwork: sentinelNetwork,
secrets: {LOW_ETH_THRESHOLD: threshold},
customRelay: 'myRelayer',
},
trigger: trigger
? {relayNetwork: trigger.params.relayNetwork, secrets: trigger.params}
: undefined,
},
};
Using scoped secrets
If you need to pass some argument from the configuration to function, this is possible to do with scoped secrets defined in src/templates/utils
Scoped secrets use secret name and function visible name to combine it to a secret key that can be red from enviroment.
Function getter
Are not yet supported but it's easy to implement. Add your PR ;)