diff --git a/packages/protocol/.gitignore b/packages/protocol/.gitignore index a8640e16fc1..736dd497a49 100644 --- a/packages/protocol/.gitignore +++ b/packages/protocol/.gitignore @@ -33,3 +33,14 @@ broadcast lcov.info simulation/out/ + +# Serverless directories +.serverless +jspm_packages + +*.log +.DS_Store + +secrets*.yml +.defender +.platform diff --git a/packages/protocol/monitors/README.md b/packages/protocol/monitors/README.md new file mode 100644 index 00000000000..ab913793c2c --- /dev/null +++ b/packages/protocol/monitors/README.md @@ -0,0 +1,236 @@ +# Defender as Code Serverless Plugin + +Defender as Code (DaC) is a Serverless Framework plugin for automated resource management and configuration as code. + +:warning: This plugin is under development and behavior might change. Handle with care. + +## Prerequisites + +Serverless Framework: https://www.serverless.com/framework/docs/getting-started/ + +## Installation + +You can initialise your Serverless project directly using our pre-configured template: + +``` +sls install --url https://github.com/OpenZeppelin/defender-as-code/tree/main/template -n my-service +``` + +Note: for the command above to work correctly you need access to this repo. + +Alternatively, you can install it directly into an existing project with: + +`yarn add @openzeppelin/defender-as-code` + +## Setup + +There are a few ways you can set up the `serverless.yml` configuration: + +- Create it from scratch; +- Use Defender's 2.0 Serverless export capability; +- Leverage the example [template](https://github.com/OpenZeppelin/defender-as-code/blob/main/template/serverless.yml) provided in the `defender-as-code` repository. + +If you already have resources such as contracts, notifications, relayers, actions, etc. in Defender 2.0, you can export a `serverless.yml` configuration file containing these resources from the manage → advanced page. + +NOTE: If you have previously deployed with `defender-as-code` to the same account and subsequently created new resources through the Defender 2.0 user interface, the export function will automatically assign a `stackResourceId` to the new resources based on the name of your latest deployment stack. If you have not deployed using `defender-as-code` before, a default stack name of `mystack` will be used. + +This plugin allows you to define Actions, Monitors, Notifications, Categories, Relayers, Contracts, Policies and Secrets declaratively from a `serverless.yml` and provision them via the CLI using `serverless deploy`. An example template below with an action, a relayer, a policy and a single relayer API key defined: + +```yaml +service: defender-serverless-template +configValidationMode: error +frameworkVersion: "3" + +provider: + name: defender + stage: ${opt:stage, 'dev'} + stackName: "mystack" + ssot: false + +defender: + key: "${env:TEAM_API_KEY}" + secret: "${env:TEAM_API_SECRET}" + +resources: + actions: + action-example-1: + name: "Hello world from serverless" + path: "./actions/hello-world" + relayer: ${self:resources.relayers.relayer-1} + trigger: + type: "schedule" + frequency: 1500 + paused: false + # optional - unencrypted and scoped to the individual action + environment-variables: + hello: "world!" + action-example-2: 2cbc3f58-d962-4be8-a158-1035be4b661c + + policies: + policy-1: + gas-price-cap: 1000 + whitelist-receivers: + - "0x0f06aB75c7DD497981b75CD82F6566e3a5CAd8f2" + eip1559-pricing: true + + relayers: + relayer-1: + name: "Test Relayer 1" + network: "sepolia" + min-balance: 1000 + policy: ${self:resources.policies.policy-1} + api-keys: + - key1 + +plugins: + - "@openzeppelin/defender-as-code" +``` + +This requires setting the `key` and `secret` under the `defender` property of the YAML file. We recommend using environment variables or a secure (gitignored) configuration file to retrieve these values. Modify the `serverless.yml` accordingly. + +Ensure the Defender Team API Keys are setup with all appropriate API capabilities. + +The `stackName` (e.g. mystack) is combined with the resource key (e.g. relayer-1) to uniquely identify each resource. This identifier is called the `stackResourceId` (e.g. mystack.relayer-1) and allows you to manage multiple deployments within the same Defender team. + +You may also reference existing Defender resources directly by their unique ID (e.g. `2cbc3f58-d962-4be8-a158-1035be4b661c`). These resources will not be managed by the plugin and will be ignored during the deploy process. However, you may reference them in other resources to update their configuration accordingly. + +A list of properties that support direct referencing: + +- `relayer` may reference a `relayerId` in Actions +- `action-trigger` may reference an `actionid` in Monitor +- `action-condition` may reference an `actionId` in Monitor +- `address-from-relayer` may reference a `relayerId` in Relayer +- `notify-config.channels` may reference multiple `notificationId` in Monitor +- `contracts` may be used over `addresses` and reference multiple `contractId` in Monitor + +The following is an example of how a direct reference to a Defender contract and relayer can be used in monitor and action respectively: + +```yaml +... +contracts: + contract-1: 'sepolia-0x62034459131329bE4349A9cc322B03c63806Aa11' # contractId of an existing resource in Defender + +relayers: + relayer-2: 'bcb659c6-7e11-4d37-a15b-0fa9f3d3442c' # relayerId of an existing relayer in Defender + +actions: + action-example-1: + name: 'Hello world from serverless' + path: './actions/hello-world' + relayer: ${self:resources.relayers.relayer-2} + trigger: + type: 'schedule' + frequency: 1500 + paused: false + +monitors: + block-example: + name: 'Block Example' + type: 'BLOCK' + network: 'sepolia' + risk-category: 'TECHNICAL' + # optional - either contracts OR addresses should be defined + contracts: + - ${self:resources.contracts.contract-1} + ... +... +``` + +### SSOT mode + +Under the `provider` property in the `serverless.yml` file, you can optionally add a `ssot` boolean. SSOT or Single Source of Truth, ensures that the state of your stack in Defender is perfectly in sync with the `serverless.yml` template. +This means that all Defender resources, that are not defined in your current template file, are removed from Defender, with the exception of Relayers, upon deployment. If SSOT is not defined in the template, it will default to `false`. + +Any resource removed from the `serverless.yml` file does _not_ get automatically deleted in order to prevent inadvertent resource deletion. For this behaviour to be anticipated, SSOT mode must be enabled. + +### Block Explorer Api Keys + +Exported serverless configurations with Block Explorer Api Keys will not contain the `key` field but instead a `key-hash` field which is a keccak256 hash of the key. This must be replaced with the actual `key` field (and `key-hash` removed) before deploying + +### Secrets (Action) + +Action secrets can be defined both globally and per stack. Secrets defined under `global` are not affected by changes to the `stackName` and will retain when redeployed under a new stack. Secrets defined under `stack` will be removed (on the condition that [SSOT mode](#SSOT-mode) is enabled) when the stack is redeployed under a new `stackName`. To reference secrets defined under `stack`, use the following format: `_`, for example `mystack_test`. + +```yaml +secrets: + # optional - global secrets are not affected by stackName changes + global: + foo: ${self:custom.config.secrets.foo} + hello: ${self:custom.config.secrets.hello} + # optional - stack secrets (formatted as _) + stack: + test: ${self:custom.config.secrets.test} +``` + +### Types and Schema validation + +We provide auto-generated documentation based on the JSON schemas: + +- [Defender Property](https://github.com/OpenZeppelin/defender-as-code/blob/main/src/types/docs/defender.md) +- [Provider Property](https://github.com/OpenZeppelin/defender-as-code/blob/main/src/types/docs/provider.md) +- [Resources Property](https://github.com/OpenZeppelin/defender-as-code/blob/main/src/types/docs/resources.md) + +More information on types can be found [here](https://github.com/OpenZeppelin/defender-as-code/blob/main/src/types/index.ts). Specifically, the types preceded with `Y` (e.g. YRelayer). For the schemas, you can check out the [docs-schema](https://github.com/OpenZeppelin/defender-as-code/blob/main/src/types/docs-schemas) folder. + +Additionally, an [example project](https://github.com/OpenZeppelin/defender-as-code/blob/main/examples/defender-test-project/serverless.yml) is available which provides majority of properties that can be defined in the `serverless.yml` file. + +## Commands + +### Deploy + +You can use `sls deploy` to deploy your current stack to Defender. + +The deploy takes in an optional `--stage` flag, which is defaulted to `dev` when installed from the template above. + +Moreover, the `serverless.yml` may contain an `ssot` property. More information can be found in the [SSOT mode](#SSOT-mode) section. + +This command will append a log entry in the `.defender` folder of the current working directory. Additionally, if any new relayer keys are created, these will be stored as JSON objects in the `.defender/relayer-keys` folder. + +> When installed from the template, we ensure the `.defender` folder is ignored from any git commits. However, when installing directly, make sure to add this folder it your `.gitignore` file. + +### Info + +You can use `sls info` to retrieve information on every resource defined in the `serverless.yml` file, including unique identifiers, and properties unique to each Defender component. + +### Remove + +You can use `sls remove` to remove all Defender resources defined in the `serverless.yml` file. + +> To avoid potential loss of funds, Relayers can only be deleted from the Defender UI directly. + +### Logs + +You can use `sls logs --function ` to retrieve the latest action logs for a given action identifier (e.g. mystack.action-example-1). This command will run continuously and retrieve logs every 2 seconds. + +### Invoke + +You can use `sls invoke --function ` to manually run an action, given its identifier (e.g. mystack.action-example-1). + +> Each command has a standard output to a JSON object. + +More information can be found on our documentation page [here](https://docs.openzeppelin.com/defender/serverless-plugin.html) + +## Caveats + +Errors thrown during the `deploy` process, will not revert any prior changes. Common errors are: + +- Not having set the API key and secret +- Insufficient permissions for the API key +- Validation error of the `serverless.yml` file (see [Types and Schema validation](#Types-and-Schema-validation)) + +Usually, fixing the error and retrying the deploy should suffice as any existing resources will fall within the `update` clause of the deployment. However, if unsure, you can always call `sls remove` to remove the entire stack, and retry. + +Action secrets are encrypted key-value pairs and injected at runtime into the lambda environment. Secrets are scoped to all actions automatically. Alternatively, you may use environment-variables to define key-value pairs that are scoped to the individual action, and available at runtime through `process.env`. Note that these values are not encrypted. + +## Publish a new release + +```bash +npm login +git checkout main +git pull origin main +# increment version in package.json +npm publish +git add package.json +git commit -m 'v{version here}' +git push origin main +``` diff --git a/packages/protocol/monitors/actions/Bridge-MessageProcessed.js b/packages/protocol/monitors/actions/Bridge-MessageProcessed.js new file mode 100644 index 00000000000..09f4227e481 --- /dev/null +++ b/packages/protocol/monitors/actions/Bridge-MessageProcessed.js @@ -0,0 +1,312 @@ +const { Defender } = require("@openzeppelin/defender-sdk"); +const ethers = require("ethers"); + +const bridgeAbi = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "bytes32", + name: "msgHash", + type: "bytes32", + }, + { + components: [ + { + internalType: "uint64", + name: "id", + type: "uint64", + }, + { + internalType: "uint64", + name: "fee", + type: "uint64", + }, + { + internalType: "uint32", + name: "gasLimit", + type: "uint32", + }, + { + internalType: "address", + name: "from", + type: "address", + }, + { + internalType: "uint64", + name: "srcChainId", + type: "uint64", + }, + { + internalType: "address", + name: "srcOwner", + type: "address", + }, + { + internalType: "uint64", + name: "destChainId", + type: "uint64", + }, + { + internalType: "address", + name: "destOwner", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + indexed: false, + internalType: "struct IBridge.Message", + name: "message", + type: "tuple", + }, + { + components: [ + { + internalType: "uint32", + name: "gasUsedInFeeCalc", + type: "uint32", + }, + { + internalType: "uint32", + name: "proofSize", + type: "uint32", + }, + { + internalType: "uint32", + name: "numCacheOps", + type: "uint32", + }, + ], + indexed: false, + internalType: "struct Bridge.ProcessingStats", + name: "stats", + type: "tuple", + }, + ], + name: "MessageProcessed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "bytes32", + name: "msgHash", + type: "bytes32", + }, + { + components: [ + { + internalType: "uint64", + name: "id", + type: "uint64", + }, + { + internalType: "uint64", + name: "fee", + type: "uint64", + }, + { + internalType: "uint32", + name: "gasLimit", + type: "uint32", + }, + { + internalType: "address", + name: "from", + type: "address", + }, + { + internalType: "uint64", + name: "srcChainId", + type: "uint64", + }, + { + internalType: "address", + name: "srcOwner", + type: "address", + }, + { + internalType: "uint64", + name: "destChainId", + type: "uint64", + }, + { + internalType: "address", + name: "destOwner", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + indexed: false, + internalType: "struct IBridge.Message", + name: "message", + type: "tuple", + }, + ], + name: "MessageSent", + type: "event", + }, +]; + +async function getLogsByTopic(notificationClient, l1provider, l2provider) { + const [ + processedMessagesL1, + processedMessagesL2, + sentMessagesL1, + sentMessagesL2, + ] = await Promise.all([ + fetchLogs( + "MessageProcessed", + 1000, + "0xd60247c6848B7Ca29eDdF63AA924E53dB6Ddd8EC", + bridgeAbi, + l1provider, + ), + fetchLogs( + "MessageProcessed", + 1000, + "0x1670000000000000000000000000000000000001", + bridgeAbi, + l2provider, + ), + fetchLogs( + "MessageSent", + 1000, + "0xd60247c6848B7Ca29eDdF63AA924E53dB6Ddd8EC", + bridgeAbi, + l1provider, + ), + fetchLogs( + "MessageSent", + 1000, + "0x1670000000000000000000000000000000000001", + bridgeAbi, + l2provider, + ), + ]); + + const unmatchedL1 = findUnmatchedMessages( + processedMessagesL1, + sentMessagesL2, + ); + const unmatchedL2 = findUnmatchedMessages( + processedMessagesL2, + sentMessagesL1, + ); + + if (unmatchedL1.length > 0 || unmatchedL2.length > 0) { + const missingCount = unmatchedL1.length + unmatchedL2.length; + alertOrg(notificationClient, missingCount); + } else { + console.log("All messages are matched."); + } +} + +async function fetchLogs(eventName, limit, address, abi, provider) { + const currentBlock = await provider.getBlock("latest"); + const fromBlock = currentBlock.number - limit; + + const iface = new ethers.utils.Interface(abi); + const eventTopic = iface.getEventTopic(eventName); + + try { + const logs = await provider.getLogs({ + address, + fromBlock, + toBlock: currentBlock.number, + topics: [eventTopic], + }); + + return logs.map( + (log) => iface.decodeEventLog(eventName, log.data, log.topics).msgHash, + ); + } catch (error) { + console.error(`Error fetching ${eventName} logs:`, error); + return []; + } +} + +function findUnmatchedMessages(processedMessages, sentMessages) { + return processedMessages.filter((msgHash) => !sentMessages.includes(msgHash)); +} + +function alertOrg(notificationClient, missingCount) { + const outputMessage = `Bridge Health Alert! \nThere are ${missingCount} missing MessageSent events for the processed messages on the other chain.`; + + notificationClient.send({ + channelAlias: "discord_bridging", + subject: "🚨 Bridge: MessageProcessed", + message: outputMessage, + }); +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +exports.handler = async function (event, context) { + const { notificationClient } = context; + const { + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + taikoL2ApiKey, + taikoL2ApiSecret, + } = event.secrets; + + const l1Provider = createProvider( + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + ); + const l2Provider = createProvider( + apiKey, + apiSecret, + taikoL2ApiKey, + taikoL2ApiSecret, + ); + + await getLogsByTopic(notificationClient, l1Provider, l2Provider); + + return true; +}; diff --git a/packages/protocol/monitors/actions/ER20Vault-BridgedTokenChanged.js b/packages/protocol/monitors/actions/ER20Vault-BridgedTokenChanged.js new file mode 100644 index 00000000000..ba04f4bcfb2 --- /dev/null +++ b/packages/protocol/monitors/actions/ER20Vault-BridgedTokenChanged.js @@ -0,0 +1,210 @@ +const { ethers } = require("ethers"); +const { Defender } = require("@openzeppelin/defender-sdk"); + +const ABI = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "srcChainId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "ctoken", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "btokenOld", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "btokenNew", + type: "address", + }, + { + indexed: false, + internalType: "string", + name: "ctokenSymbol", + type: "string", + }, + { + indexed: false, + internalType: "string", + name: "ctokenName", + type: "string", + }, + { + indexed: false, + internalType: "uint8", + name: "ctokenDecimal", + type: "uint8", + }, + ], + name: "BridgedTokenChanged", + type: "event", + }, +]; + +function alertOrg(notificationClient, message) { + notificationClient.send({ + channelAlias: "discord_bridging", + subject: "⚠️ ER20Vault: BridgedTokenChanged Alert", + message, + }); +} + +async function getLatestBlockNumber(provider) { + const currentBlock = await provider.getBlock("latest"); + return currentBlock.number; +} + +async function fetchLogs( + eventName, + fromBlock, + toBlock, + address, + abi, + provider, + networkName, +) { + const iface = new ethers.utils.Interface(abi); + const eventTopic = iface.getEventTopic(eventName); + + try { + const logs = await provider.getLogs({ + address, + fromBlock, + toBlock, + topics: [eventTopic], + }); + + return logs.map((log) => { + const parsedLog = iface.decodeEventLog(eventName, log.data, log.topics); + return { ...parsedLog, network: networkName }; + }); + } catch (error) { + console.error( + `Error fetching logs for ${eventName} on ${networkName}:`, + error, + ); + return []; + } +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +async function calculateBlockTime(provider) { + const latestBlock = await provider.getBlock("latest"); + const previousBlock = await provider.getBlock(latestBlock.number - 100); + + const timeDiff = latestBlock.timestamp - previousBlock.timestamp; + const blockDiff = latestBlock.number - previousBlock.number; + + const blockTime = timeDiff / blockDiff; + return blockTime; +} + +function calculateBlockRange( + currentBlockNumber, + blockTimeInSeconds, + minutes = 5, +) { + const blocksInGivenMinutes = Math.floor((minutes * 60) / blockTimeInSeconds); + const fromBlock = currentBlockNumber - blocksInGivenMinutes; + const toBlock = currentBlockNumber; + + return { fromBlock, toBlock }; +} + +exports.handler = async function (event, context) { + const { notificationClient } = context; + const { + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + taikoL2ApiKey, + taikoL2ApiSecret, + } = event.secrets; + + const taikoL1Provider = createProvider( + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + ); + const taikoL2Provider = createProvider( + apiKey, + apiSecret, + taikoL2ApiKey, + taikoL2ApiSecret, + ); + + const currentBlockNumberL1 = await getLatestBlockNumber(taikoL1Provider); + const currentBlockNumberL2 = await getLatestBlockNumber(taikoL2Provider); + + const blockTimeInSecondsL1 = await calculateBlockTime(taikoL1Provider); + const blockTimeInSecondsL2 = await calculateBlockTime(taikoL2Provider); + + const { fromBlock: fromBlockL1, toBlock: toBlockL1 } = calculateBlockRange( + currentBlockNumberL1, + blockTimeInSecondsL1, + ); + const { fromBlock: fromBlockL2, toBlock: toBlockL2 } = calculateBlockRange( + currentBlockNumberL2, + blockTimeInSecondsL2, + ); + + const logsL1 = await fetchLogs( + "BridgedTokenChanged", + fromBlockL1, + toBlockL1, + "0x996282cA11E5DEb6B5D122CC3B9A1FcAAD4415Ab", + ABI, + taikoL1Provider, + "L1", + ); + const logsL2 = await fetchLogs( + "BridgedTokenChanged", + fromBlockL2, + toBlockL2, + "0x1670000000000000000000000000000000000002", + ABI, + taikoL2Provider, + "L2", + ); + + const logs = [...logsL1, ...logsL2]; + + if (logs.length > 0) { + const logDetails = logs + .map( + (log) => + `Network: ${log.network}, Token: ${log.ctoken}, Old Token: ${log.btokenOld}, New Token: ${log.btokenNew}`, + ) + .join("\n"); + alertOrg( + notificationClient, + `BridgedTokenChanged event detected!\nDetails:\n${logDetails}`, + ); + } + + return true; +}; diff --git a/packages/protocol/monitors/actions/ERC1155Vault-BridgedTokenDeployed.js b/packages/protocol/monitors/actions/ERC1155Vault-BridgedTokenDeployed.js new file mode 100644 index 00000000000..bfac4d6be94 --- /dev/null +++ b/packages/protocol/monitors/actions/ERC1155Vault-BridgedTokenDeployed.js @@ -0,0 +1,162 @@ +const {ethers} = require("ethers"); +const {Defender} = require("@openzeppelin/defender-sdk"); + +const ABI = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint64", + name: "chainId", + type: "uint64", + }, + { + indexed: true, + internalType: "address", + name: "ctoken", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "btoken", + type: "address", + }, + { + indexed: false, + internalType: "string", + name: "ctokenSymbol", + type: "string", + }, + { + indexed: false, + internalType: "string", + name: "ctokenName", + type: "string", + }, + ], + name: "BridgedTokenDeployed", + type: "event", + }, +]; + +function alertOrg(notificationClient, message) { + notificationClient.send({ + channelAlias: "discord_bridging", + subject: "ERC1155Vault BridgedTokenDeployed Event Count", + message: message, + }); +} + +async function getLatestBlockNumber(provider) { + const currentBlock = await provider.getBlock("latest"); + return currentBlock.number; +} + +async function calculateBlockTime(provider) { + const latestBlock = await provider.getBlock("latest"); + const previousBlock = await provider.getBlock(latestBlock.number - 100); + + const timeDiff = latestBlock.timestamp - previousBlock.timestamp; + const blockDiff = latestBlock.number - previousBlock.number; + + const blockTime = timeDiff / blockDiff; + return blockTime; +} + +async function calculateBlockRange(provider) { + const currentBlockNumber = await getLatestBlockNumber(provider); + const blockTimeInSeconds = await calculateBlockTime(provider); + const blocksIn24Hours = Math.floor((24 * 60 * 60) / blockTimeInSeconds); + + const fromBlock = currentBlockNumber - blocksIn24Hours; + const toBlock = currentBlockNumber; + + console.log(`Calculated block range: from ${fromBlock} to ${toBlock}`); + + return {fromBlock, toBlock}; +} + +async function fetchLogs( + eventName, + fromBlock, + toBlock, + address, + abi, + provider +) { + const iface = new ethers.utils.Interface(abi); + const eventTopic = iface.getEventTopic(eventName); + console.log(`eventTopic: ${eventTopic}`); + try { + const logs = await provider.getLogs({ + address, + fromBlock, + toBlock, + topics: [eventTopic], + }); + console.log(`Fetched logs: ${logs.length}`); + return logs.map((log) => { + const parsedLog = iface.parseLog(log); + console.log(`Parsed log: ${JSON.stringify(parsedLog)}`); + return parsedLog; + }); + } catch (error) { + console.error("Error fetching logs:", error); + return []; + } +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +exports.handler = async function (event, context) { + const {notificationClient} = context; + const {apiKey, apiSecret, l1ApiKey, l1ApiSecret, l2ApiKey, l2ApiSecret} = + event.secrets; + + const l1Provider = createProvider(apiKey, apiSecret, l1ApiKey, l1ApiSecret); + const l2Provider = createProvider(apiKey, apiSecret, l2ApiKey, l2ApiSecret); + + const {fromBlock: l1FromBlock, toBlock: l1ToBlock} = + await calculateBlockRange(l1Provider); + const {fromBlock: l2FromBlock, toBlock: l2ToBlock} = + await calculateBlockRange(l2Provider); + + const l1Logs = await fetchLogs( + "BridgedTokenDeployed", + l1FromBlock, + l1ToBlock, + "0xaf145913EA4a56BE22E120ED9C24589659881702", + ABI, + l1Provider + ); + + const l2Logs = await fetchLogs( + "BridgedTokenDeployed", + l2FromBlock, + l2ToBlock, + "0x1670000000000000000000000000000000000004", + ABI, + l2Provider + ); + + const l1EventCount = l1Logs.length; + const l2EventCount = l2Logs.length; + + if (l1EventCount > 0 || l2EventCount > 0) { + const alertMessage = `Detected ${l1EventCount} ERC1155Vault BridgedTokenDeployed events on L1 and ${l2EventCount} events on L2 in the last 24 hours!`; + alertOrg(notificationClient, alertMessage); + } + + return true; +}; diff --git a/packages/protocol/monitors/actions/ERC20Vault-BalanceDrop.js b/packages/protocol/monitors/actions/ERC20Vault-BalanceDrop.js new file mode 100644 index 00000000000..dd6920e4c23 --- /dev/null +++ b/packages/protocol/monitors/actions/ERC20Vault-BalanceDrop.js @@ -0,0 +1,210 @@ +const { ethers } = require("ethers"); +const { Defender } = require("@openzeppelin/defender-sdk"); + +const ERC20_ABI = [ + { + constant: true, + inputs: [{ name: "_owner", type: "address" }], + name: "balanceOf", + outputs: [{ name: "balance", type: "uint256" }], + payable: false, + stateMutability: "view", + type: "function", + }, +]; + +async function getERC20Balance(provider, tokenAddress, vaultAddress) { + const contract = new ethers.Contract(tokenAddress, ERC20_ABI, provider); + return await contract.balanceOf(vaultAddress); +} + +async function getNativeTokenBalance(provider, vaultAddress) { + return await provider.getBalance(vaultAddress); +} + +async function monitorTokenBalance( + provider, + tokenAddress, + vaultAddress, + previousBalanceKey, + notificationClient, + secrets, + client, + tokenName, + networkName, +) { + console.log(`Monitoring ${tokenName} balance on ${networkName}`); + const previousBalance = ethers.BigNumber.from( + secrets[previousBalanceKey] || "0", + ); + console.log( + `Previous ${tokenName} Balance: ${ethers.utils.formatUnits( + previousBalance, + 18, + )}`, + ); + + let currentBalance; + if (tokenName === "ETH") { + currentBalance = await getNativeTokenBalance(provider, vaultAddress); + } else { + currentBalance = await getERC20Balance( + provider, + tokenAddress, + vaultAddress, + ); + } + console.log( + `Current ${tokenName} Balance: ${ethers.utils.formatUnits( + currentBalance, + 18, + )}`, + ); + + if (!previousBalance.isZero()) { + const dropPercentage = previousBalance + .sub(currentBalance) + .mul(100) + .div(previousBalance) + .toNumber(); + console.log( + `Calculated drop percentage for ${tokenName}: ${dropPercentage}%`, + ); + + if (dropPercentage >= 5) { + const message = `Alert: ${tokenName} balance has dropped by ${dropPercentage}% on ${networkName}.\nPrevious Balance: ${ethers.utils.formatUnits( + previousBalance, + 18, + )}\nCurrent Balance: ${ethers.utils.formatUnits(currentBalance, 18)}`; + alertOrg( + notificationClient, + `⚠️ ${networkName}: ${tokenName} Balance Drop Alert`, + message, + ); + } else { + console.log( + `No significant ${tokenName} balance drop detected on ${networkName}`, + ); + } + } else { + console.log( + `No previous ${tokenName} balance to compare on ${networkName}`, + ); + } + + await storePreviousBalance(client, previousBalanceKey, currentBalance); +} + +function alertOrg(notificationClient, subject, message) { + notificationClient.send({ + channelAlias: "discord_bridging", + subject, + message, + }); +} + +async function storePreviousBalance(client, key, newBalance) { + console.log( + `Storing previous balance under key: ${key}, value: ${newBalance.toString()}`, + ); + const body = { + deletes: [], + secrets: { + [key]: newBalance.toString(), + }, + }; + await client.action.createSecrets(body); +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + console.log(`Creating provider with API keys`); + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +exports.handler = async function (event, context) { + const { notificationClient } = context; + const { apiKey, apiSecret, taikoL1ApiKey, taikoL1ApiSecret } = event.secrets; + + console.log(`Starting balance monitoring for L1`); + + const l1Provider = createProvider( + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + ); + + const l1VaultAddress = "0x996282cA11E5DEb6B5D122CC3B9A1FcAAD4415Ab"; + + const l1TokenAddresses = { + ETH: null, + TAIKO: ethers.utils.getAddress( + "0x10dea67478c5F8C5E2D90e5E9B26dBe60c54d800", + ), + USDC: ethers.utils.getAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), + USDT: ethers.utils.getAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7"), + }; + + const client = new Defender({ + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + }); + + await monitorTokenBalance( + l1Provider, + l1TokenAddresses.ETH, + l1VaultAddress, + "previousBalance_L1_ETH", + notificationClient, + event.secrets, + client, + "ETH", + "L1", + ); + await monitorTokenBalance( + l1Provider, + l1TokenAddresses.TAIKO, + l1VaultAddress, + "previousBalance_L1_TAIKO", + notificationClient, + event.secrets, + client, + "TAIKO", + "L1", + ); + await monitorTokenBalance( + l1Provider, + l1TokenAddresses.USDC, + l1VaultAddress, + "previousBalance_L1_USDC", + notificationClient, + event.secrets, + client, + "USDC", + "L1", + ); + await monitorTokenBalance( + l1Provider, + l1TokenAddresses.USDT, + l1VaultAddress, + "previousBalance_L1_USDT", + notificationClient, + event.secrets, + client, + "USDT", + "L1", + ); + + console.log(`Balance monitoring completed`); + + return true; +}; diff --git a/packages/protocol/monitors/actions/ERC20Vault-BridgedTokenDeployed.js b/packages/protocol/monitors/actions/ERC20Vault-BridgedTokenDeployed.js new file mode 100644 index 00000000000..969f16000e0 --- /dev/null +++ b/packages/protocol/monitors/actions/ERC20Vault-BridgedTokenDeployed.js @@ -0,0 +1,168 @@ +const {ethers} = require("ethers"); +const {Defender} = require("@openzeppelin/defender-sdk"); + +const ABI = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "srcChainId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "ctoken", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "btoken", + type: "address", + }, + { + indexed: false, + internalType: "string", + name: "ctokenSymbol", + type: "string", + }, + { + indexed: false, + internalType: "string", + name: "ctokenName", + type: "string", + }, + { + indexed: false, + internalType: "uint8", + name: "ctokenDecimal", + type: "uint8", + }, + ], + name: "BridgedTokenDeployed", + type: "event", + }, +]; + +function alertOrg(notificationClient, message) { + notificationClient.send({ + channelAlias: "discord_bridging", + subject: "BridgedTokenDeployed Event Count", + message: message, + }); +} + +async function getLatestBlockNumber(provider) { + const currentBlock = await provider.getBlock("latest"); + return currentBlock.number; +} + +async function calculateBlockTime(provider) { + const latestBlock = await provider.getBlock("latest"); + const previousBlock = await provider.getBlock(latestBlock.number - 100); + + const timeDiff = latestBlock.timestamp - previousBlock.timestamp; + const blockDiff = latestBlock.number - previousBlock.number; + + const blockTime = timeDiff / blockDiff; + return blockTime; +} + +async function calculateBlockRange(provider) { + const currentBlockNumber = await getLatestBlockNumber(provider); + const blockTimeInSeconds = await calculateBlockTime(provider); + const blocksIn24Hours = Math.floor((24 * 60 * 60) / blockTimeInSeconds); + + const fromBlock = currentBlockNumber - blocksIn24Hours; + const toBlock = currentBlockNumber; + + console.log(`Calculated block range: from ${fromBlock} to ${toBlock}`); + + return {fromBlock, toBlock}; +} + +async function fetchLogs( + eventName, + fromBlock, + toBlock, + address, + abi, + provider +) { + const iface = new ethers.utils.Interface(abi); + const eventTopic = iface.getEventTopic(eventName); + console.log(`eventTopic: ${eventTopic}`); + try { + const logs = await provider.getLogs({ + address, + fromBlock, + toBlock, + topics: [eventTopic], + }); + console.log(`Fetched logs: ${logs.length}`); + return logs.map((log) => { + const parsedLog = iface.parseLog(log); + console.log(`Parsed log: ${JSON.stringify(parsedLog)}`); + return parsedLog; + }); + } catch (error) { + console.error("Error fetching logs:", error); + return []; + } +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +exports.handler = async function (event, context) { + const {notificationClient} = context; + const {apiKey, apiSecret, l1ApiKey, l1ApiSecret, l2ApiKey, l2ApiSecret} = + event.secrets; + + const l1Provider = createProvider(apiKey, apiSecret, l1ApiKey, l1ApiSecret); + const l2Provider = createProvider(apiKey, apiSecret, l2ApiKey, l2ApiSecret); + + const {fromBlock: l1FromBlock, toBlock: l1ToBlock} = + await calculateBlockRange(l1Provider); + const {fromBlock: l2FromBlock, toBlock: l2ToBlock} = + await calculateBlockRange(l2Provider); + + const l1Logs = await fetchLogs( + "BridgedTokenDeployed", + l1FromBlock, + l1ToBlock, + "0x996282cA11E5DEb6B5D122CC3B9A1FcAAD4415Ab", + ABI, + l1Provider + ); + + const l2Logs = await fetchLogs( + "BridgedTokenDeployed", + l2FromBlock, + l2ToBlock, + "0x1670000000000000000000000000000000000002", + ABI, + l2Provider + ); + + const l1EventCount = l1Logs.length; + const l2EventCount = l2Logs.length; + + if (l1EventCount > 0 || l2EventCount > 0) { + const alertMessage = `Detected ${l1EventCount} BridgedTokenDeployed events on L1 and ${l2EventCount} events on L2 in the last 24 hours!`; + alertOrg(notificationClient, alertMessage); + } + + return true; +}; diff --git a/packages/protocol/monitors/actions/ERC721Vault-BridgedTokenDeployed.js b/packages/protocol/monitors/actions/ERC721Vault-BridgedTokenDeployed.js new file mode 100644 index 00000000000..c111fd73d75 --- /dev/null +++ b/packages/protocol/monitors/actions/ERC721Vault-BridgedTokenDeployed.js @@ -0,0 +1,162 @@ +const {ethers} = require("ethers"); +const {Defender} = require("@openzeppelin/defender-sdk"); + +const ABI = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint64", + name: "chainId", + type: "uint64", + }, + { + indexed: true, + internalType: "address", + name: "ctoken", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "btoken", + type: "address", + }, + { + indexed: false, + internalType: "string", + name: "ctokenSymbol", + type: "string", + }, + { + indexed: false, + internalType: "string", + name: "ctokenName", + type: "string", + }, + ], + name: "BridgedTokenDeployed", + type: "event", + }, +]; + +function alertOrg(notificationClient, message) { + notificationClient.send({ + channelAlias: "discord_bridging", + subject: "ERC721Vault BridgedTokenDeployed Event Count", + message: message, + }); +} + +async function getLatestBlockNumber(provider) { + const currentBlock = await provider.getBlock("latest"); + return currentBlock.number; +} + +async function calculateBlockTime(provider) { + const latestBlock = await provider.getBlock("latest"); + const previousBlock = await provider.getBlock(latestBlock.number - 100); + + const timeDiff = latestBlock.timestamp - previousBlock.timestamp; + const blockDiff = latestBlock.number - previousBlock.number; + + const blockTime = timeDiff / blockDiff; + return blockTime; +} + +async function calculateBlockRange(provider) { + const currentBlockNumber = await getLatestBlockNumber(provider); + const blockTimeInSeconds = await calculateBlockTime(provider); + const blocksIn24Hours = Math.floor((24 * 60 * 60) / blockTimeInSeconds); + + const fromBlock = currentBlockNumber - blocksIn24Hours; + const toBlock = currentBlockNumber; + + console.log(`Calculated block range: from ${fromBlock} to ${toBlock}`); + + return {fromBlock, toBlock}; +} + +async function fetchLogs( + eventName, + fromBlock, + toBlock, + address, + abi, + provider +) { + const iface = new ethers.utils.Interface(abi); + const eventTopic = iface.getEventTopic(eventName); + console.log(`eventTopic: ${eventTopic}`); + try { + const logs = await provider.getLogs({ + address, + fromBlock, + toBlock, + topics: [eventTopic], + }); + console.log(`Fetched logs: ${logs.length}`); + return logs.map((log) => { + const parsedLog = iface.parseLog(log); + console.log(`Parsed log: ${JSON.stringify(parsedLog)}`); + return parsedLog; + }); + } catch (error) { + console.error("Error fetching logs:", error); + return []; + } +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +exports.handler = async function (event, context) { + const {notificationClient} = context; + const {apiKey, apiSecret, l1ApiKey, l1ApiSecret, l2ApiKey, l2ApiSecret} = + event.secrets; + + const l1Provider = createProvider(apiKey, apiSecret, l1ApiKey, l1ApiSecret); + const l2Provider = createProvider(apiKey, apiSecret, l2ApiKey, l2ApiSecret); + + const {fromBlock: l1FromBlock, toBlock: l1ToBlock} = + await calculateBlockRange(l1Provider); + const {fromBlock: l2FromBlock, toBlock: l2ToBlock} = + await calculateBlockRange(l2Provider); + + const l1Logs = await fetchLogs( + "BridgedTokenDeployed", + l1FromBlock, + l1ToBlock, + "0x0b470dd3A0e1C41228856Fb319649E7c08f419Aa", + ABI, + l1Provider + ); + + const l2Logs = await fetchLogs( + "BridgedTokenDeployed", + l2FromBlock, + l2ToBlock, + "0x1670000000000000000000000000000000000003", + ABI, + l2Provider + ); + + const l1EventCount = l1Logs.length; + const l2EventCount = l2Logs.length; + + if (l1EventCount > 0 || l2EventCount > 0) { + const alertMessage = `Detected ${l1EventCount} ERC721Vault BridgedTokenDeployed events on L1 and ${l2EventCount} events on L2 in the last 24 hours!`; + alertOrg(notificationClient, alertMessage); + } + + return true; +}; diff --git a/packages/protocol/monitors/actions/GuardianProver-ApprovedCount.js b/packages/protocol/monitors/actions/GuardianProver-ApprovedCount.js new file mode 100644 index 00000000000..919e2db9d7a --- /dev/null +++ b/packages/protocol/monitors/actions/GuardianProver-ApprovedCount.js @@ -0,0 +1,140 @@ +const { ethers } = require("ethers"); +const { Defender } = require("@openzeppelin/defender-sdk"); + +const ABI = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "operationId", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "approvalBits", + type: "uint256", + }, + { + indexed: false, + internalType: "bool", + name: "minGuardiansReached", + type: "bool", + }, + ], + name: "Approved", + type: "event", + }, +]; + +function alertOrg(notificationClient, message) { + notificationClient.send({ + channelAlias: "discord_configs", + subject: "⚠️ GuardianProver: Approved Count", + message, + }); +} + +async function getLatestBlockNumber(provider) { + const currentBlock = await provider.getBlock("latest"); + return currentBlock.number; +} + +async function calculateBlockTime(provider) { + const latestBlock = await provider.getBlock("latest"); + const previousBlock = await provider.getBlock(latestBlock.number - 100); + + const timeDiff = latestBlock.timestamp - previousBlock.timestamp; + const blockDiff = latestBlock.number - previousBlock.number; + + const blockTime = timeDiff / blockDiff; + return blockTime; +} + +async function calculateBlockRange(provider) { + const currentBlockNumber = await getLatestBlockNumber(provider); + const blockTimeInSeconds = await calculateBlockTime(provider); + const blocksInOneHour = Math.floor((16 * 60) / blockTimeInSeconds); + + const fromBlock = currentBlockNumber - blocksInOneHour; + const toBlock = currentBlockNumber; + + console.log(`Calculated block range: from ${fromBlock} to ${toBlock}`); + + return { fromBlock, toBlock }; +} + +async function fetchLogsFromL1( + eventName, + fromBlock, + toBlock, + address, + abi, + provider, +) { + const iface = new ethers.utils.Interface(abi); + const eventTopic = iface.getEventTopic(eventName); + console.log(`eventTopic: ${eventTopic}`); + try { + const logs = await provider.getLogs({ + address, + fromBlock, + toBlock, + topics: [eventTopic], + }); + console.log(`Fetched logs: ${logs.length}`); + return logs.map((log) => { + const parsedLog = iface.parseLog(log); + console.log(`Parsed log: ${JSON.stringify(parsedLog)}`); + return parsedLog; + }); + } catch (error) { + console.error("Error fetching L1 logs:", error); + return []; + } +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +exports.handler = async function (event, context) { + const { notificationClient } = context; + const { apiKey, apiSecret, taikoL1ApiKey, taikoL1ApiSecret } = event.secrets; + + const taikoL1Provider = createProvider( + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + ); + + const { fromBlock, toBlock } = await calculateBlockRange(taikoL1Provider); + + const logs = await fetchLogsFromL1( + "Approved", + fromBlock, + toBlock, + "0xE3D777143Ea25A6E031d1e921F396750885f43aC", + ABI, + taikoL1Provider, + ); + + if (logs.length > 0) { + alertOrg( + notificationClient, + `Detected ${logs.length} Approved events in the last 15 mins on Guardian!`, + ); + } + + return true; +}; diff --git a/packages/protocol/monitors/actions/GuardianProver-ConflictingProofs.js b/packages/protocol/monitors/actions/GuardianProver-ConflictingProofs.js new file mode 100644 index 00000000000..913fde80ff3 --- /dev/null +++ b/packages/protocol/monitors/actions/GuardianProver-ConflictingProofs.js @@ -0,0 +1,144 @@ +const { ethers } = require("ethers"); +const { Defender } = require("@openzeppelin/defender-sdk"); + +const ABI = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "blockId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "guardian", + type: "address", + }, + { + indexed: false, + internalType: "bytes32", + name: "currentProofHash", + type: "bytes32", + }, + { + indexed: false, + internalType: "bytes32", + name: "newProofHash", + type: "bytes32", + }, + { + indexed: false, + internalType: "bool", + name: "provingPaused", + type: "bool", + }, + ], + name: "ConflictingProofs", + type: "event", + }, +]; + +function alertOrg(notificationClient, message) { + notificationClient.send({ + channelAlias: "discord_configs", + subject: "🚨 GuardianProver: ConflictingProofs Alert", + message, + }); +} + +async function getLatestBlockNumber(provider) { + const currentBlock = await provider.getBlock("latest"); + return currentBlock.number; +} + +async function fetchLogsFromL1( + eventName, + fromBlock, + toBlock, + address, + abi, + provider, +) { + const iface = new ethers.utils.Interface(abi); + const eventTopic = iface.getEventTopic(eventName); + + try { + const logs = await provider.getLogs({ + address, + fromBlock, + toBlock, + topics: [eventTopic], + }); + + return logs.map((log) => + iface.decodeEventLog(eventName, log.data, log.topics), + ); + } catch (error) { + console.error(`Error fetching logs for ${eventName}:`, error); + return []; + } +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +async function calculateBlockTime(provider) { + const latestBlock = await provider.getBlock("latest"); + const previousBlock = await provider.getBlock(latestBlock.number - 100); + + const timeDiff = latestBlock.timestamp - previousBlock.timestamp; + const blockDiff = latestBlock.number - previousBlock.number; + + const blockTime = timeDiff / blockDiff; + return blockTime; +} + +exports.handler = async function (event, context) { + const { notificationClient } = context; + const { apiKey, apiSecret, taikoL1ApiKey, taikoL1ApiSecret } = event.secrets; + + const taikoL1Provider = createProvider( + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + ); + + const currentBlockNumber = await getLatestBlockNumber(taikoL1Provider); + const blockTimeInSeconds = await calculateBlockTime(taikoL1Provider); + const blocksInFiveMinutes = Math.floor((5 * 60) / blockTimeInSeconds); + + const fromBlock = currentBlockNumber - blocksInFiveMinutes; + const toBlock = currentBlockNumber; + + const logs = await fetchLogsFromL1( + "ConflictingProofs", + fromBlock, + toBlock, + "0xE3D777143Ea25A6E031d1e921F396750885f43aC", + ABI, + taikoL1Provider, + ); + + console.log(`Logs found: ${logs.length}`); + + if (logs.length > 0) { + alertOrg( + notificationClient, + `ConflictingProofs event detected! Details: ${JSON.stringify(logs)}`, + ); + } + + return true; +}; diff --git a/packages/protocol/monitors/actions/GuardianProver-GuardiansUpdated.js b/packages/protocol/monitors/actions/GuardianProver-GuardiansUpdated.js new file mode 100644 index 00000000000..b5be10015e7 --- /dev/null +++ b/packages/protocol/monitors/actions/GuardianProver-GuardiansUpdated.js @@ -0,0 +1,126 @@ +const { ethers } = require("ethers"); +const { Defender } = require("@openzeppelin/defender-sdk"); + +const ABI = [ + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint32", + name: "version", + type: "uint32", + }, + { + indexed: false, + internalType: "address[]", + name: "guardians", + type: "address[]", + }, + ], + name: "GuardiansUpdated", + type: "event", + }, +]; + +function alertOrg(notificationClient, message) { + notificationClient.send({ + channelAlias: "discord_configs", + subject: "⚠️ GuardianProver: GuardiansUpdated Alert", + message, + }); +} + +async function getLatestBlockNumber(provider) { + const currentBlock = await provider.getBlock("latest"); + return currentBlock.number; +} + +async function fetchLogsFromL1( + eventName, + fromBlock, + toBlock, + address, + abi, + provider, +) { + const iface = new ethers.utils.Interface(abi); + const eventTopic = iface.getEventTopic(eventName); + + try { + const logs = await provider.getLogs({ + address, + fromBlock, + toBlock, + topics: [eventTopic], + }); + + return logs.map((log) => + iface.decodeEventLog(eventName, log.data, log.topics), + ); + } catch (error) { + console.error(`Error fetching logs for ${eventName}:`, error); + return []; + } +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +async function calculateBlockTime(provider) { + const latestBlock = await provider.getBlock("latest"); + const previousBlock = await provider.getBlock(latestBlock.number - 100); + + const timeDiff = latestBlock.timestamp - previousBlock.timestamp; + const blockDiff = latestBlock.number - previousBlock.number; + + const blockTime = timeDiff / blockDiff; + return blockTime; +} + +exports.handler = async function (event, context) { + const { notificationClient } = context; + const { apiKey, apiSecret, taikoL1ApiKey, taikoL1ApiSecret } = event.secrets; + + const taikoL1Provider = createProvider( + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + ); + + const currentBlockNumber = await getLatestBlockNumber(taikoL1Provider); + const blockTimeInSeconds = await calculateBlockTime(taikoL1Provider); + const blocksInFiveMinutes = Math.floor((5 * 60) / blockTimeInSeconds); + + const fromBlock = currentBlockNumber - blocksInFiveMinutes; + const toBlock = currentBlockNumber; + + const logs = await fetchLogsFromL1( + "GuardiansUpdated", + fromBlock, + toBlock, + "0xE3D777143Ea25A6E031d1e921F396750885f43aC", + ABI, + taikoL1Provider, + ); + + console.log(`Logs found: ${logs.length}`); + + if (logs.length > 0) { + alertOrg( + notificationClient, + `GuardiansUpdated event detected! Details: ${JSON.stringify(logs)}`, + ); + } + + return true; +}; diff --git a/packages/protocol/monitors/actions/GuardianProver-ProvingAutoPauseEnabled.js b/packages/protocol/monitors/actions/GuardianProver-ProvingAutoPauseEnabled.js new file mode 100644 index 00000000000..c76d7dedbf4 --- /dev/null +++ b/packages/protocol/monitors/actions/GuardianProver-ProvingAutoPauseEnabled.js @@ -0,0 +1,122 @@ +const { ethers } = require("ethers"); +const { Defender } = require("@openzeppelin/defender-sdk"); + +const ABI = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "bool", + name: "enabled", + type: "bool", + }, + ], + name: "ProvingAutoPauseEnabled", + type: "event", + }, +]; + +function alertOrg(notificationClient, message) { + notificationClient.send({ + channelAlias: "discord_configs", + subject: "⚠️ GuardianProver: ProvingAutoPauseEnabled Alert", + message, + }); +} + +async function getLatestBlockNumber(provider) { + const currentBlock = await provider.getBlock("latest"); + return currentBlock.number; +} + +async function fetchLogsFromL1( + eventName, + fromBlock, + toBlock, + address, + abi, + provider, +) { + const iface = new ethers.utils.Interface(abi); + const eventTopic = iface.getEventTopic(eventName); + + try { + const logs = await provider.getLogs({ + address, + fromBlock, + toBlock, + topics: [eventTopic], + }); + + return logs.map((log) => + iface.decodeEventLog(eventName, log.data, log.topics), + ); + } catch (error) { + console.error(`Error fetching logs for ${eventName}:`, error); + return []; + } +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +async function calculateBlockTime(provider) { + const latestBlock = await provider.getBlock("latest"); + const previousBlock = await provider.getBlock(latestBlock.number - 100); + + const timeDiff = latestBlock.timestamp - previousBlock.timestamp; + const blockDiff = latestBlock.number - previousBlock.number; + + const blockTime = timeDiff / blockDiff; + return blockTime; +} + +exports.handler = async function (event, context) { + const { notificationClient } = context; + const { apiKey, apiSecret, taikoL1ApiKey, taikoL1ApiSecret } = event.secrets; + + const taikoL1Provider = createProvider( + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + ); + + const currentBlockNumber = await getLatestBlockNumber(taikoL1Provider); + const blockTimeInSeconds = await calculateBlockTime(taikoL1Provider); + const blocksInFiveMinutes = Math.floor((5 * 60) / blockTimeInSeconds); + + const fromBlock = currentBlockNumber - blocksInFiveMinutes; + const toBlock = currentBlockNumber; + + const logs = await fetchLogsFromL1( + "ProvingAutoPauseEnabled", + fromBlock, + toBlock, + "0xE3D777143Ea25A6E031d1e921F396750885f43aC", + ABI, + taikoL1Provider, + ); + + console.log(`Logs found: ${logs.length}`); + + if (logs.length > 0) { + logs.forEach((log) => { + const enabled = log.enabled; + const status = enabled ? "ENABLED" : "DISABLED"; + const message = `Proving Auto-Pause has been ${status}.\n\nDetails:\n- Enabled: ${enabled}\n- Block Number: ${log.blockNumber}`; + alertOrg(notificationClient, message); + }); + } + + return true; +}; diff --git a/packages/protocol/monitors/actions/SGXVerifier-verifyProofFailure.js b/packages/protocol/monitors/actions/SGXVerifier-verifyProofFailure.js new file mode 100644 index 00000000000..7eaa5bf7dc4 --- /dev/null +++ b/packages/protocol/monitors/actions/SGXVerifier-verifyProofFailure.js @@ -0,0 +1,107 @@ +const { ethers } = require("ethers"); +const { Defender } = require("@openzeppelin/defender-sdk"); + +const verifyProofSignature = "verifyProof(address,bytes32,bytes32)"; +const verifyProofSelector = ethers.utils + .keccak256(ethers.utils.toUtf8Bytes(verifyProofSignature)) + .substring(0, 10); + +function alertOrg(notificationClient, message) { + notificationClient.send({ + channelAlias: "discord_blocks", + subject: "⚠️ SGXVerifier: verifyProof Failure Alert", + message, + }); +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + return client.relaySigner.getProvider(); +} + +async function getLatestBlockNumber(provider) { + const currentBlock = await provider.getBlock("latest"); + return currentBlock.number; +} + +async function calculateBlockTime(provider) { + const latestBlock = await provider.getBlock("latest"); + const previousBlock = await provider.getBlock(latestBlock.number - 100); + + const timeDiff = latestBlock.timestamp - previousBlock.timestamp; + const blockDiff = latestBlock.number - previousBlock.number; + + const blockTime = timeDiff / blockDiff; + return blockTime; +} + +async function calculateBlockRange(provider, hours = 24) { + const currentBlockNumber = await getLatestBlockNumber(provider); + const blockTimeInSeconds = await calculateBlockTime(provider); + const blocksInTimeFrame = Math.floor((hours * 60 * 60) / blockTimeInSeconds); + + const fromBlock = currentBlockNumber - blocksInTimeFrame; + const toBlock = currentBlockNumber; + + console.log(`Calculated block range: from ${fromBlock} to ${toBlock}`); + + return { fromBlock, toBlock }; +} + +async function monitorTransactions( + provider, + contractAddress, + notificationClient, + hours, +) { + const { fromBlock, toBlock } = await calculateBlockRange(provider, hours); + + const logs = await provider.getLogs({ + fromBlock, + toBlock, + address: contractAddress, + }); + + for (const log of logs) { + const tx = await provider.getTransaction(log.transactionHash); + + if (tx.data.startsWith(verifyProofSelector)) { + const txReceipt = await provider.getTransactionReceipt( + log.transactionHash, + ); + + if (txReceipt && txReceipt.status === 0) { + const message = ` + A failed verifyProof transaction was detected. + - Contract Address: ${log.address} + - Transaction Hash: ${log.transactionHash} + - Block Number: ${txReceipt.blockNumber} + `; + alertOrg(notificationClient, message); + } + } + } +} + +exports.handler = async function (event, context) { + const { notificationClient } = context; + const { apiKey, apiSecret, taikoL1ApiKey, taikoL1ApiSecret } = event.secrets; + + const contractAddress = "0xb0f3186FC1963f774f52ff455DC86aEdD0b31F81"; + + const provider = createProvider( + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + ); + + await monitorTransactions(provider, contractAddress, notificationClient, 24); + + return true; +}; diff --git a/packages/protocol/monitors/actions/TaikoL1-BlockProposed.js b/packages/protocol/monitors/actions/TaikoL1-BlockProposed.js new file mode 100644 index 00000000000..9baa19db62d --- /dev/null +++ b/packages/protocol/monitors/actions/TaikoL1-BlockProposed.js @@ -0,0 +1,367 @@ +const { ethers } = require("ethers"); +const { Defender } = require("@openzeppelin/defender-sdk"); + +const ABI = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "blockId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "assignedProver", + type: "address", + }, + { + indexed: false, + internalType: "uint96", + name: "livenessBond", + type: "uint96", + }, + { + components: [ + { + internalType: "bytes32", + name: "l1Hash", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "difficulty", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "blobHash", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "extraData", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "depositsHash", + type: "bytes32", + }, + { + internalType: "address", + name: "coinbase", + type: "address", + }, + { + internalType: "uint64", + name: "id", + type: "uint64", + }, + { + internalType: "uint32", + name: "gasLimit", + type: "uint32", + }, + { + internalType: "uint64", + name: "timestamp", + type: "uint64", + }, + { + internalType: "uint64", + name: "l1Height", + type: "uint64", + }, + { + internalType: "uint16", + name: "minTier", + type: "uint16", + }, + { + internalType: "bool", + name: "blobUsed", + type: "bool", + }, + { + internalType: "bytes32", + name: "parentMetaHash", + type: "bytes32", + }, + { + internalType: "address", + name: "sender", + type: "address", + }, + ], + indexed: false, + internalType: "struct TaikoData.BlockMetadata", + name: "meta", + type: "tuple", + }, + { + components: [ + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint96", + name: "amount", + type: "uint96", + }, + { + internalType: "uint64", + name: "id", + type: "uint64", + }, + ], + indexed: false, + internalType: "struct TaikoData.EthDeposit[]", + name: "depositsProcessed", + type: "tuple[]", + }, + ], + name: "BlockProposed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "blockId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "assignedProver", + type: "address", + }, + { + indexed: false, + internalType: "uint96", + name: "livenessBond", + type: "uint96", + }, + { + components: [ + { + internalType: "bytes32", + name: "l1Hash", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "difficulty", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "blobHash", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "extraData", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "depositsHash", + type: "bytes32", + }, + { + internalType: "address", + name: "coinbase", + type: "address", + }, + { + internalType: "uint64", + name: "id", + type: "uint64", + }, + { + internalType: "uint32", + name: "gasLimit", + type: "uint32", + }, + { + internalType: "uint64", + name: "timestamp", + type: "uint64", + }, + { + internalType: "uint64", + name: "l1Height", + type: "uint64", + }, + { + internalType: "uint16", + name: "minTier", + type: "uint16", + }, + { + internalType: "bool", + name: "blobUsed", + type: "bool", + }, + { + internalType: "bytes32", + name: "parentMetaHash", + type: "bytes32", + }, + { + internalType: "address", + name: "sender", + type: "address", + }, + ], + indexed: false, + internalType: "struct TaikoData.BlockMetadata", + name: "meta", + type: "tuple", + }, + { + components: [ + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint96", + name: "amount", + type: "uint96", + }, + { + internalType: "uint64", + name: "id", + type: "uint64", + }, + ], + indexed: false, + internalType: "struct TaikoData.EthDeposit[]", + name: "depositsProcessed", + type: "tuple[]", + }, + ], + name: "BlockProposedV2", + type: "event", + }, +]; + +function alertOrg(notificationClient, message) { + notificationClient.send({ + channelAlias: "discord_blocks", + subject: "🚨 TaikoL1: BlockProposed Alert", + message, + }); +} + +async function getLatestBlockNumber(provider) { + const currentBlock = await provider.getBlock("latest"); + return currentBlock.number; +} + +async function fetchLogsFromL1( + eventNames, + fromBlock, + toBlock, + address, + abi, + provider, +) { + const iface = new ethers.utils.Interface(abi); + + const allLogs = []; + + for (const eventName of eventNames) { + const eventTopic = iface.getEventTopic(eventName); + + try { + const logs = await provider.getLogs({ + address, + fromBlock, + toBlock, + topics: [eventTopic], + }); + + allLogs.push( + ...logs.map((log) => + iface.decodeEventLog(eventName, log.data, log.topics), + ), + ); + } catch (error) { + console.error(`Error fetching logs for ${eventName}:`, error); + } + } + + return allLogs; +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +async function calculateBlockTime(provider) { + const latestBlock = await provider.getBlock("latest"); + const previousBlock = await provider.getBlock(latestBlock.number - 100); + + const timeDiff = latestBlock.timestamp - previousBlock.timestamp; + const blockDiff = latestBlock.number - previousBlock.number; + + const blockTime = timeDiff / blockDiff; + return blockTime; +} + +exports.handler = async function (event, context) { + const { notificationClient } = context; + const { apiKey, apiSecret, taikoL1ApiKey, taikoL1ApiSecret } = event.secrets; + + const taikoL1Provider = createProvider( + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + ); + + const currentBlockNumber = await getLatestBlockNumber(taikoL1Provider); + const blockTimeInSeconds = await calculateBlockTime(taikoL1Provider); + const blocksInFiveMinutes = Math.floor((5 * 60) / blockTimeInSeconds); + + const fromBlock = currentBlockNumber - blocksInFiveMinutes; + const toBlock = currentBlockNumber; + + const logs = await fetchLogsFromL1( + ["BlockProposed", "BlockProposedV2"], + fromBlock, + toBlock, + "0x06a9Ab27c7e2255df1815E6CC0168d7755Feb19a", + ABI, + taikoL1Provider, + ); + + console.log(`Logs found: ${logs.length}`); + + if (logs.length === 0) { + alertOrg( + notificationClient, + `No BlockProposed event detected in the last 5 mins on TaikoL1!`, + ); + } + + return true; +}; diff --git a/packages/protocol/monitors/actions/TaikoL1-BlockVerified.js b/packages/protocol/monitors/actions/TaikoL1-BlockVerified.js new file mode 100644 index 00000000000..ee1dee30301 --- /dev/null +++ b/packages/protocol/monitors/actions/TaikoL1-BlockVerified.js @@ -0,0 +1,181 @@ +const { ethers } = require("ethers"); +const { Defender } = require("@openzeppelin/defender-sdk"); + +const ABI = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "blockId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "prover", + type: "address", + }, + { + indexed: false, + internalType: "bytes32", + name: "blockHash", + type: "bytes32", + }, + { + indexed: false, + internalType: "bytes32", + name: "stateRoot", + type: "bytes32", + }, + { + indexed: false, + internalType: "uint16", + name: "tier", + type: "uint16", + }, + ], + name: "BlockVerified", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "blockId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "prover", + type: "address", + }, + { + indexed: false, + internalType: "bytes32", + name: "blockHash", + type: "bytes32", + }, + { + indexed: false, + internalType: "bytes32", + name: "stateRoot", + type: "bytes32", + }, + { + indexed: false, + internalType: "uint16", + name: "tier", + type: "uint16", + }, + ], + name: "BlockVerifiedV2", + type: "event", + }, +]; + +function alertOrg(notificationClient, message) { + notificationClient.send({ + channelAlias: "discord_blocks", + subject: "🚨 TaikoL1: BlockVerified Alert", + message, + }); +} + +async function getLatestBlockNumber(provider) { + const currentBlock = await provider.getBlock("latest"); + return currentBlock.number; +} + +async function fetchLogsFromL1( + eventNames, + fromBlock, + toBlock, + address, + abi, + provider, +) { + const iface = new ethers.utils.Interface(abi); + const eventTopics = eventNames.map((eventName) => + iface.getEventTopic(eventName), + ); + + console.log(`eventTopics: ${eventTopics}`); + + try { + const logs = await provider.getLogs({ + address, + fromBlock, + toBlock, + topics: [eventTopics], + }); + + return logs.map((log) => iface.parseLog(log)); + } catch (error) { + console.error("Error fetching L1 logs:", error); + return []; + } +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +async function calculateBlockTime(provider) { + const latestBlock = await provider.getBlock("latest"); + const previousBlock = await provider.getBlock(latestBlock.number - 100); + + const timeDiff = latestBlock.timestamp - previousBlock.timestamp; + const blockDiff = latestBlock.number - previousBlock.number; + + const blockTime = timeDiff / blockDiff; + return blockTime; +} + +exports.handler = async function (event, context) { + const { notificationClient } = context; + const { apiKey, apiSecret, taikoL1ApiKey, taikoL1ApiSecret } = event.secrets; + + const taikoL1Provider = createProvider( + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + ); + + const currentBlockNumber = await getLatestBlockNumber(taikoL1Provider); + const blockTimeInSeconds = await calculateBlockTime(taikoL1Provider); + const blocksInFiveMinutes = Math.floor((5 * 60) / blockTimeInSeconds); + + const fromBlock = currentBlockNumber - blocksInFiveMinutes; + const toBlock = currentBlockNumber; + + const logs = await fetchLogsFromL1( + ["BlockVerified", "BlockVerifiedV2"], + fromBlock, + toBlock, + "0x06a9Ab27c7e2255df1815E6CC0168d7755Feb19a", + ABI, + taikoL1Provider, + ); + + if (logs.length === 0) { + alertOrg( + notificationClient, + `No BlockVerified event detected in the last 5 mins in TaikoL1!`, + ); + } + + return true; +}; diff --git a/packages/protocol/monitors/actions/TaikoL1-CalldataTxListCount.js b/packages/protocol/monitors/actions/TaikoL1-CalldataTxListCount.js new file mode 100644 index 00000000000..8c515fd22b4 --- /dev/null +++ b/packages/protocol/monitors/actions/TaikoL1-CalldataTxListCount.js @@ -0,0 +1,138 @@ +const { ethers } = require("ethers"); +const { Defender } = require("@openzeppelin/defender-sdk"); + +const ABI = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "blockId", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "txList", + type: "bytes", + }, + ], + name: "CalldataTxList", + type: "event", + }, +]; + +function alertOrg(notificationClient, message) { + notificationClient.send({ + channelAlias: "discord_blocks", + subject: "ℹ️ TaikoL1: CalldataTxList Count", + message, + }); +} + +async function getLatestBlockNumber(provider) { + const currentBlock = await provider.getBlock("latest"); + return currentBlock.number; +} + +async function calculateBlockTime(provider) { + const latestBlock = await provider.getBlock("latest"); + const previousBlock = await provider.getBlock(latestBlock.number - 100); + + const timeDiff = latestBlock.timestamp - previousBlock.timestamp; + const blockDiff = latestBlock.number - previousBlock.number; + + const blockTime = timeDiff / blockDiff; + return blockTime; +} + +async function calculateBlockRange(provider) { + const currentBlockNumber = await getLatestBlockNumber(provider); + const blockTimeInSeconds = await calculateBlockTime(provider); + const blocksIn24Hours = Math.floor((24 * 60 * 60) / blockTimeInSeconds); // 24 hours in seconds + + const fromBlock = currentBlockNumber - blocksIn24Hours; + const toBlock = currentBlockNumber; + + console.log(`Calculated block range: from ${fromBlock} to ${toBlock}`); + + return { fromBlock, toBlock }; +} + +async function fetchLogsFromL1( + eventNames, + fromBlock, + toBlock, + address, + abi, + provider, +) { + const iface = new ethers.utils.Interface(abi); + const eventTopics = eventNames.map((eventName) => + iface.getEventTopic(eventName), + ); + + console.log(`eventTopics: ${eventTopics}`); + + try { + const logs = await provider.getLogs({ + address, + fromBlock, + toBlock, + topics: [eventTopics], + }); + console.log(`Fetched logs: ${logs.length}`); + return logs.map((log) => { + const parsedLog = iface.parseLog(log); + console.log(`Parsed log: ${JSON.stringify(parsedLog)}`); + return parsedLog; + }); + } catch (error) { + console.error("Error fetching L1 logs:", error); + return []; + } +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +exports.handler = async function (event, context) { + const { notificationClient } = context; + const { apiKey, apiSecret, taikoL1ApiKey, taikoL1ApiSecret } = event.secrets; + + const taikoL1Provider = createProvider( + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + ); + + const { fromBlock, toBlock } = await calculateBlockRange(taikoL1Provider); + + const logs = await fetchLogsFromL1( + ["CalldataTxList"], + fromBlock, + toBlock, + "0x06a9Ab27c7e2255df1815E6CC0168d7755Feb19a", + ABI, + taikoL1Provider, + ); + + if (logs.length > 0) { + alertOrg( + notificationClient, + `Detected ${logs.length} CalldataTxList events in the last 24 hours on TaikoL1!`, + ); + } + + return true; +}; diff --git a/packages/protocol/monitors/actions/TaikoL1-ProvingPaused.js b/packages/protocol/monitors/actions/TaikoL1-ProvingPaused.js new file mode 100644 index 00000000000..2e0eeb5cbf6 --- /dev/null +++ b/packages/protocol/monitors/actions/TaikoL1-ProvingPaused.js @@ -0,0 +1,120 @@ +const { ethers } = require("ethers"); +const { Defender } = require("@openzeppelin/defender-sdk"); + +const ABI = [ + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "bool", + name: "paused", + type: "bool", + }, + ], + name: "ProvingPaused", + type: "event", + }, +]; + +function alertOrg(notificationClient, message) { + notificationClient.send({ + channelAlias: "discord_configs", + subject: "⚠️ TaikoL1: ProvingPaused Alert", + message, + }); +} + +async function getLatestBlockNumber(provider) { + const currentBlock = await provider.getBlock("latest"); + return currentBlock.number; +} + +async function fetchLogsFromL1( + eventName, + fromBlock, + toBlock, + address, + abi, + provider, +) { + const iface = new ethers.utils.Interface(abi); + const eventTopic = iface.getEventTopic(eventName); + + try { + const logs = await provider.getLogs({ + address, + fromBlock, + toBlock, + topics: [eventTopic], + }); + + return logs.map((log) => + iface.decodeEventLog(eventName, log.data, log.topics), + ); + } catch (error) { + console.error(`Error fetching logs for ${eventName}:`, error); + return []; + } +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +async function calculateBlockTime(provider) { + const latestBlock = await provider.getBlock("latest"); + const previousBlock = await provider.getBlock(latestBlock.number - 100); + + const timeDiff = latestBlock.timestamp - previousBlock.timestamp; + const blockDiff = latestBlock.number - previousBlock.number; + + const blockTime = timeDiff / blockDiff; + return blockTime; +} + +exports.handler = async function (event, context) { + const { notificationClient } = context; + const { apiKey, apiSecret, taikoL1ApiKey, taikoL1ApiSecret } = event.secrets; + + const taikoL1Provider = createProvider( + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + ); + + const currentBlockNumber = await getLatestBlockNumber(taikoL1Provider); + const blockTimeInSeconds = await calculateBlockTime(taikoL1Provider); + const blocksInFiveMinutes = Math.floor((5 * 60) / blockTimeInSeconds); + + const fromBlock = currentBlockNumber - blocksInFiveMinutes; + const toBlock = currentBlockNumber; + + const logs = await fetchLogsFromL1( + "ProvingPaused", + fromBlock, + toBlock, + "0x06a9Ab27c7e2255df1815E6CC0168d7755Feb19a", + ABI, + taikoL1Provider, + ); + + console.log(`Logs found: ${logs.length}`); + + if (logs.length > 0) { + alertOrg( + notificationClient, + `ProvingPaused event detected! Details: ${JSON.stringify(logs)}`, + ); + } + + return true; +}; diff --git a/packages/protocol/monitors/actions/TaikoL1-TransitionContestedCount.js b/packages/protocol/monitors/actions/TaikoL1-TransitionContestedCount.js new file mode 100644 index 00000000000..d38ed5579fb --- /dev/null +++ b/packages/protocol/monitors/actions/TaikoL1-TransitionContestedCount.js @@ -0,0 +1,237 @@ +const { ethers } = require("ethers"); +const { Defender } = require("@openzeppelin/defender-sdk"); + +const ABI = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "blockId", + type: "uint256", + }, + { + components: [ + { + internalType: "bytes32", + name: "parentHash", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "blockHash", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "stateRoot", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "graffiti", + type: "bytes32", + }, + ], + indexed: false, + internalType: "struct TaikoData.Transition", + name: "tran", + type: "tuple", + }, + { + indexed: false, + internalType: "address", + name: "contester", + type: "address", + }, + { + indexed: false, + internalType: "uint96", + name: "contestBond", + type: "uint96", + }, + { + indexed: false, + internalType: "uint16", + name: "tier", + type: "uint16", + }, + ], + name: "TransitionContested", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "blockId", + type: "uint256", + }, + { + components: [ + { + internalType: "bytes32", + name: "parentHash", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "blockHash", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "stateRoot", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "graffiti", + type: "bytes32", + }, + ], + indexed: false, + internalType: "struct TaikoData.Transition", + name: "tran", + type: "tuple", + }, + { + indexed: false, + internalType: "address", + name: "contester", + type: "address", + }, + { + indexed: false, + internalType: "uint96", + name: "contestBond", + type: "uint96", + }, + { + indexed: false, + internalType: "uint16", + name: "tier", + type: "uint16", + }, + ], + name: "TransitionContestedV2", + type: "event", + }, +]; + +function alertOrg(notificationClient, message) { + notificationClient.send({ + channelAlias: "discord_blocks", + subject: "ℹ️ TaikoL1: TransitionContested Count", + message, + }); +} + +async function getLatestBlockNumber(provider) { + const currentBlock = await provider.getBlock("latest"); + return currentBlock.number; +} + +async function calculateBlockTime(provider) { + const latestBlock = await provider.getBlock("latest"); + const previousBlock = await provider.getBlock(latestBlock.number - 100); + + const timeDiff = latestBlock.timestamp - previousBlock.timestamp; + const blockDiff = latestBlock.number - previousBlock.number; + + const blockTime = timeDiff / blockDiff; + return blockTime; +} + +async function calculateBlockRange(provider) { + const currentBlockNumber = await getLatestBlockNumber(provider); + const blockTimeInSeconds = await calculateBlockTime(provider); + const blocksInOneHour = Math.floor((60 * 60) / blockTimeInSeconds); + + const fromBlock = currentBlockNumber - blocksInOneHour; + const toBlock = currentBlockNumber; + + console.log(`Calculated block range: from ${fromBlock} to ${toBlock}`); + + return { fromBlock, toBlock }; +} + +async function fetchLogsFromL1( + eventNames, + fromBlock, + toBlock, + address, + abi, + provider, +) { + const iface = new ethers.utils.Interface(abi); + const eventTopics = eventNames.map((eventName) => + iface.getEventTopic(eventName), + ); + + console.log(`eventTopics: ${eventTopics}`); + + try { + const logs = await provider.getLogs({ + address, + fromBlock, + toBlock, + topics: [eventTopics], + }); + console.log(`Fetched logs: ${logs.length}`); + return logs.map((log) => { + const parsedLog = iface.parseLog(log); + console.log(`Parsed log: ${JSON.stringify(parsedLog)}`); + return parsedLog; + }); + } catch (error) { + console.error("Error fetching L1 logs:", error); + return []; + } +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +exports.handler = async function (event, context) { + const { notificationClient } = context; + const { apiKey, apiSecret, taikoL1ApiKey, taikoL1ApiSecret } = event.secrets; + + const taikoL1Provider = createProvider( + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + ); + + const { fromBlock, toBlock } = await calculateBlockRange(taikoL1Provider); + + const logs = await fetchLogsFromL1( + ["TransitionContested", "TransitionContestedV2"], + fromBlock, + toBlock, + "0x06a9Ab27c7e2255df1815E6CC0168d7755Feb19a", + ABI, + taikoL1Provider, + ); + + if (logs.length > 0) { + alertOrg( + notificationClient, + `Detected ${logs.length} TransitionContested and TransitionContestedV2 events in the last hour on TaikoL1!`, + ); + } + + return true; +}; diff --git a/packages/protocol/monitors/actions/TaikoL1-TransitionProved.js b/packages/protocol/monitors/actions/TaikoL1-TransitionProved.js new file mode 100644 index 00000000000..b391f3d880b --- /dev/null +++ b/packages/protocol/monitors/actions/TaikoL1-TransitionProved.js @@ -0,0 +1,223 @@ +const { ethers } = require("ethers"); +const { Defender } = require("@openzeppelin/defender-sdk"); + +const ABI = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "blockId", + type: "uint256", + }, + { + components: [ + { + internalType: "bytes32", + name: "parentHash", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "blockHash", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "stateRoot", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "graffiti", + type: "bytes32", + }, + ], + indexed: false, + internalType: "struct TaikoData.Transition", + name: "tran", + type: "tuple", + }, + { + indexed: false, + internalType: "address", + name: "prover", + type: "address", + }, + { + indexed: false, + internalType: "uint96", + name: "validityBond", + type: "uint96", + }, + { + indexed: false, + internalType: "uint16", + name: "tier", + type: "uint16", + }, + ], + name: "TransitionProved", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "blockId", + type: "uint256", + }, + { + components: [ + { + internalType: "bytes32", + name: "parentHash", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "blockHash", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "stateRoot", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "graffiti", + type: "bytes32", + }, + ], + indexed: false, + internalType: "struct TaikoData.Transition", + name: "tran", + type: "tuple", + }, + { + indexed: false, + internalType: "address", + name: "prover", + type: "address", + }, + { + indexed: false, + internalType: "uint96", + name: "validityBond", + type: "uint96", + }, + { + indexed: false, + internalType: "uint16", + name: "tier", + type: "uint16", + }, + ], + name: "TransitionProvedV2", + type: "event", + }, +]; + +function alertOrg(notificationClient, message) { + notificationClient.send({ + channelAlias: "discord_blocks", + subject: "🚨 TaikoL1: TransitionProved Alert", + message, + }); +} + +async function getLatestBlockNumber(provider) { + const currentBlock = await provider.getBlock("latest"); + return currentBlock.number; +} + +async function fetchLogsFromL1( + eventNames, + fromBlock, + toBlock, + address, + abi, + provider, +) { + const iface = new ethers.utils.Interface(abi); + const eventTopics = eventNames.map((eventName) => + iface.getEventTopic(eventName), + ); + + try { + const logs = await provider.getLogs({ + address, + fromBlock, + toBlock, + topics: [eventTopics], + }); + console.log("Raw logs fetched:", logs); + return logs.map((log) => iface.parseLog(log)); + } catch (error) { + console.error("Error fetching L1 logs:", error); + return []; + } +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +async function calculateBlockTime(provider) { + const latestBlock = await provider.getBlock("latest"); + const previousBlock = await provider.getBlock(latestBlock.number - 100); + + const timeDiff = latestBlock.timestamp - previousBlock.timestamp; + const blockDiff = latestBlock.number - previousBlock.number; + + const blockTime = timeDiff / blockDiff; + return blockTime; +} + +exports.handler = async function (event, context) { + const { notificationClient } = context; + const { apiKey, apiSecret, taikoL1ApiKey, taikoL1ApiSecret } = event.secrets; + + const taikoL1Provider = createProvider( + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + ); + + const currentBlockNumber = await getLatestBlockNumber(taikoL1Provider); + const blockTimeInSeconds = await calculateBlockTime(taikoL1Provider); + const blocksInThirtyMinutes = Math.floor((30 * 60) / blockTimeInSeconds); + + const fromBlock = currentBlockNumber - blocksInThirtyMinutes; + const toBlock = currentBlockNumber; + + const logs = await fetchLogsFromL1( + ["TransitionProved", "TransitionProvedV2"], + fromBlock, + toBlock, + "0x06a9Ab27c7e2255df1815E6CC0168d7755Feb19a", + ABI, + taikoL1Provider, + ); + + if (logs.length === 0) { + alertOrg( + notificationClient, + `No TransitionProved event detected in the last 30 mins on TaikoL1!`, + ); + } + + return true; +}; diff --git a/packages/protocol/monitors/actions/Vaults-BridgedTokenDeployed.js b/packages/protocol/monitors/actions/Vaults-BridgedTokenDeployed.js new file mode 100644 index 00000000000..c1da2a0a806 --- /dev/null +++ b/packages/protocol/monitors/actions/Vaults-BridgedTokenDeployed.js @@ -0,0 +1,333 @@ +const { ethers } = require("ethers"); +const { Defender } = require("@openzeppelin/defender-sdk"); + +const ABIs = { + ERC1155Vault: [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint64", + name: "chainId", + type: "uint64", + }, + { + indexed: true, + internalType: "address", + name: "ctoken", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "btoken", + type: "address", + }, + { + indexed: false, + internalType: "string", + name: "ctokenSymbol", + type: "string", + }, + { + indexed: false, + internalType: "string", + name: "ctokenName", + type: "string", + }, + ], + name: "BridgedTokenDeployed", + type: "event", + }, + ], + ERC721Vault: [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint64", + name: "chainId", + type: "uint64", + }, + { + indexed: true, + internalType: "address", + name: "ctoken", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "btoken", + type: "address", + }, + { + indexed: false, + internalType: "string", + name: "ctokenSymbol", + type: "string", + }, + { + indexed: false, + internalType: "string", + name: "ctokenName", + type: "string", + }, + ], + name: "BridgedTokenDeployed", + type: "event", + }, + ], + ERC20Vault: [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "srcChainId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "ctoken", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "btoken", + type: "address", + }, + { + indexed: false, + internalType: "string", + name: "ctokenSymbol", + type: "string", + }, + { + indexed: false, + internalType: "string", + name: "ctokenName", + type: "string", + }, + { + indexed: false, + internalType: "uint8", + name: "ctokenDecimal", + type: "uint8", + }, + ], + name: "BridgedTokenDeployed", + type: "event", + }, + ], +}; + +function alertOrg(notificationClient, subject, message) { + notificationClient.send({ + channelAlias: "discord_bridging", + subject, + message, + }); +} + +async function getLatestBlockNumber(provider) { + const currentBlock = await provider.getBlock("latest"); + return currentBlock.number; +} + +async function calculateBlockTime(provider) { + const latestBlock = await provider.getBlock("latest"); + const previousBlock = await provider.getBlock(latestBlock.number - 100); + + const timeDiff = latestBlock.timestamp - previousBlock.timestamp; + const blockDiff = latestBlock.number - previousBlock.number; + + const blockTime = timeDiff / blockDiff; + return blockTime; +} + +async function calculateBlockRange(provider) { + const currentBlockNumber = await getLatestBlockNumber(provider); + const blockTimeInSeconds = await calculateBlockTime(provider); + const blocksIn24Hours = Math.floor((24 * 60 * 60) / blockTimeInSeconds); + + const fromBlock = currentBlockNumber - blocksIn24Hours; + const toBlock = currentBlockNumber; + + console.log(`Calculated block range: from ${fromBlock} to ${toBlock}`); + + return { fromBlock, toBlock }; +} + +async function fetchLogs( + eventName, + fromBlock, + toBlock, + address, + abi, + provider, +) { + const iface = new ethers.utils.Interface(abi); + const eventTopic = iface.getEventTopic(eventName); + console.log(`eventTopic: ${eventTopic}`); + try { + const logs = await provider.getLogs({ + address, + fromBlock, + toBlock, + topics: [eventTopic], + }); + console.log(`Fetched logs: ${logs.length}`); + return logs.map((log) => { + const parsedLog = iface.parseLog(log); + console.log(`Parsed log: ${JSON.stringify(parsedLog)}`); + return parsedLog; + }); + } catch (error) { + console.error("Error fetching logs:", error); + return []; + } +} + +function createProvider(apiKey, apiSecret, relayerApiKey, relayerApiSecret) { + const client = new Defender({ + apiKey, + apiSecret, + relayerApiKey, + relayerApiSecret, + }); + + return client.relaySigner.getProvider(); +} + +async function monitorEvent( + provider, + eventName, + fromBlock, + toBlock, + contractAddress, + abi, + subject, + notificationClient, +) { + const logs = await fetchLogs( + eventName, + fromBlock, + toBlock, + contractAddress, + abi, + provider, + ); + const eventCount = logs.length; + + if (eventCount > 0) { + const alertMessage = `Detected ${eventCount} ${subject} events on ${provider.network.name} in the last 24 hours!`; + alertOrg(notificationClient, subject, alertMessage); + } +} + +exports.handler = async function (event, context) { + const { notificationClient } = context; + const { + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + taikoL2ApiKey, + taikoL2ApiSecret, + } = event.secrets; + + const l1Provider = createProvider( + apiKey, + apiSecret, + taikoL1ApiKey, + taikoL1ApiSecret, + ); + const l2Provider = createProvider( + apiKey, + apiSecret, + taikoL2ApiKey, + taikoL2ApiSecret, + ); + + const { fromBlock: l1FromBlock, toBlock: l1ToBlock } = + await calculateBlockRange(l1Provider); + const { fromBlock: l2FromBlock, toBlock: l2ToBlock } = + await calculateBlockRange(l2Provider); + + // Monitor ERC1155Vault events + await monitorEvent( + l1Provider, + "BridgedTokenDeployed", + l1FromBlock, + l1ToBlock, + "0xaf145913EA4a56BE22E120ED9C24589659881702", // L1 + ABIs.ERC1155Vault, + "ℹ️ ERC1155Vault BridgedTokenDeployed", + notificationClient, + ); + + await monitorEvent( + l2Provider, + "BridgedTokenDeployed", + l2FromBlock, + l2ToBlock, + "0x1670000000000000000000000000000000000004", // L2 + ABIs.ERC1155Vault, + "ℹ️ ERC1155Vault BridgedTokenDeployed", + notificationClient, + ); + + // Monitor ERC721Vault events + await monitorEvent( + l1Provider, + "BridgedTokenDeployed", + l1FromBlock, + l1ToBlock, + "0x0b470dd3A0e1C41228856Fb319649E7c08f419Aa", // L1 + ABIs.ERC721Vault, + "ℹ️ ERC721Vault BridgedTokenDeployed", + notificationClient, + ); + + await monitorEvent( + l2Provider, + "BridgedTokenDeployed", + l2FromBlock, + l2ToBlock, + "0x1670000000000000000000000000000000000003", // L2 + ABIs.ERC721Vault, + "ℹ️ ERC721Vault BridgedTokenDeployed", + notificationClient, + ); + + // Monitor ERC20Vault events + await monitorEvent( + l1Provider, + "BridgedTokenDeployed", + l1FromBlock, + l1ToBlock, + "0x996282cA11E5DEb6B5D122CC3B9A1FcAAD4415Ab", // L1 + ABIs.ERC20Vault, + "ℹ️ ERC20 BridgedTokenDeployed", + notificationClient, + ); + + await monitorEvent( + l2Provider, + "BridgedTokenDeployed", + l2FromBlock, + l2ToBlock, + "0x1670000000000000000000000000000000000002", // L2 + ABIs.ERC20Vault, + "ℹ️ ERC20Vault BridgedTokenDeployed", + notificationClient, + ); + + return true; +}; diff --git a/packages/protocol/monitors/serverless.yml b/packages/protocol/monitors/serverless.yml new file mode 100644 index 00000000000..fd6459a34b0 --- /dev/null +++ b/packages/protocol/monitors/serverless.yml @@ -0,0 +1,655 @@ +service: defender-as-code-test-project +configValidationMode: error +frameworkVersion: "3" + +provider: + name: defender + stage: ${opt:stage, 'dev'} + stackName: mystack + ssot: false + +custom: + config: ${file(secrets.${self:provider.stage}.yml)} + +defender: + key: ${self:custom.config.keys.api} + secret: ${self:custom.config.keys.secret} + +resources: + actions: + erc-20-vault-balance-drop: + name: "ERC20Vault: Balance Drop" + trigger: + type: schedule + frequency: 15 + paused: false + path: ./actions/ERC20Vault-BalanceDrop.js + + sgx-verifier-verify-proof-failure-alert: + name: "SGXVerifier: VerifyProof failure Alert" + trigger: + type: schedule + frequency: 5 + paused: false + path: ./actions/SGXVerifier-verifyProofFailure.js + + vaults-bridged-token-deployed: + name: "Vaults: BridgedTokenDeployed" + trigger: + type: schedule + frequency: 1440 + paused: false + path: ./actions/Vaults-BridgedTokenDeployed.js + + er-20-vault-bridged-token-changed: + name: "ER20Vault : BridgedTokenChanged" + trigger: + type: schedule + frequency: 4 + paused: false + path: ./actions/ER20Vault-BridgedTokenChanged.js + + bridge-message-processed: + name: "Bridge: MessageProcessed" + trigger: + type: schedule + frequency: 15 + paused: false + path: ./actions/Bridge-MessageProcessed.js + + taiko-l-1-calldata-tx-list-count: + name: "TaikoL1: CalldataTxList Count" + trigger: + type: schedule + frequency: 1440 + paused: false + path: ./actions/TaikoL1-CalldataTxListCount.js + + guardian-prover-proving-auto-pause-enabled-alert: + name: "GuardianProver: ProvingAutoPauseEnabled Alert" + trigger: + type: schedule + frequency: 4 + paused: false + path: ./actions/GuardianProver-ProvingAutoPauseEnabled.js + + guardian-prover-conflicting-proofs-alert: + name: "GuardianProver: ConflictingProofs Alert" + trigger: + type: schedule + frequency: 4 + paused: false + path: ./actions/GuardianProver-ConflictingProofs.js + + guardian-prover-approved-count: + name: "GuardianProver: Approved Count" + trigger: + type: schedule + frequency: 15 + paused: false + path: ./actions/GuardianProver-ApprovedCount.js + + guardian-prover-guardians-updated-alert: + name: "GuardianProver: GuardiansUpdated Alert" + trigger: + type: schedule + frequency: 4 + paused: false + path: ./actions/GuardianProver-GuardiansUpdated.js + + taiko-l-1-proving-paused-alert: + name: "TaikoL1: ProvingPaused Alert" + trigger: + type: schedule + frequency: 4 + paused: false + path: ./actions/TaikoL1-ProvingPaused.js + + taiko-l-1-transition-contested-count: + name: "TaikoL1: TransitionContested Count" + trigger: + type: schedule + frequency: 60 + paused: false + path: ./actions/TaikoL1-TransitionContestedCount.js + + taiko-l-1-transition-proved-alert: + name: "TaikoL1: TransitionProved Alert" + trigger: + type: schedule + frequency: 4 + paused: false + path: ./actions/TaikoL1-TransitionProved.js + + taiko-l-1-block-verified-alert: + name: "TaikoL1: BlockVerified Alert" + trigger: + type: schedule + frequency: 4 + paused: false + path: ./actions/TaikoL1-BlockVerified.js + + taiko-l-1-block-proposed-alert: + name: "TaikoL1: BlockProposed Alert" + trigger: + type: schedule + frequency: 4 + paused: false + path: ./actions/TaikoL1-BlockProposed.js + + policies: + policy-Qe: + eip1559-pricing: true + private-transactions: false + policy-hk: + eip1559-pricing: true + private-transactions: false + contracts: + pem-cert-chain-lib: + name: pem_cert_chain_lib + address: "0x02772b7B3a5Bea0141C993Dbb8D0733C19F46169" + network: mainnet + taiko-l-1: + name: taikoL1 + address: "0x06a9Ab27c7e2255df1815E6CC0168d7755Feb19a" + network: mainnet + erc-721-vault: + name: erc721_vault + address: "0x1670000000000000000000000000000000000003" + network: taikol2 + taiko-token: + name: taiko_token + address: "0x10dea67478c5F8C5E2D90e5E9B26dBe60c54d800" + network: mainnet + p-256-verifier: + name: p256_verifier + address: "0x11A9ebA17EbF92b40fcf9a640Ebbc47Db6fBeab0" + network: mainnet + tier-provider: + name: tierProvider + address: "0x33879cDF01121dc7bCe011b461e64d791aE931F2" + network: mainnet + bridged-erc-1155: + name: bridged_erc1155 + address: "0x39E4C1214e733639d059979079A151911e42791d" + network: mainnet + sig-verify-lib: + name: sig_verify_lib + address: "0x47bB416ee947fE4a4b655011aF7d6E3A1B80E6e9" + network: mainnet + assignment-hook: + name: assignmentHook + address: "0x537a2f0D3a5879b41BCb5A2afE2EA5c4961796F6" + network: mainnet + guardian-prover-minority: + name: guardian_prover_minority + address: "0x579A8d63a2Db646284CBFE31FE5082c9989E985c" + network: mainnet + rollup-address-manager: + name: rollup_address_manager + address: "0x1670000000000000000000000000000000010002" + network: taikol2 + automata-dcap-attestation: + name: automata_dcap_attestation + address: "0x8d7C954960a36a7596d7eA4945dDf891967ca8A3" + network: mainnet + erc-20-vault: + name: erc20_vault + address: "0x1670000000000000000000000000000000000002" + network: taikol2 + signal-service: + name: signal_service + address: "0x1670000000000000000000000000000000000005" + network: taikol2 + guardian-prover: + name: guardian_prover + address: "0xE3D777143Ea25A6E031d1e921F396750885f43aC" + network: mainnet + shared-address-manager: + name: shared_address_manager + address: "0x1670000000000000000000000000000000000006" + network: taikol2 + erc-1155-vault: + name: erc1155_vault + address: "0x1670000000000000000000000000000000000004" + network: taikol2 + tier-sgx: + name: tier_sgx + address: "0xb0f3186FC1963f774f52ff455DC86aEdD0b31F81" + network: mainnet + bridged-erc-721: + name: bridged_erc721 + address: "0xc4096E9ff1526Bd1840B65e9f45695135aC12De7" + network: mainnet + bridged-erc-20: + name: bridged_erc20 + address: "0xcc5d488073FA918cBbd73B9A523F3858C4de7372" + network: mainnet + bridge: + name: bridge + address: "0x1670000000000000000000000000000000000001" + network: taikol2 + taiko-l-2: + name: taikoL2 + address: "0x1670000000000000000000000000000000010001" + network: taikol2 + relayers: + taiko-l-2-relayer: + name: TaikoL2Relayer + network: taikol2 + min-balance: "100000000000000000" + policy: ${self:resources.policies.policy-Qe} + api-keys: + - key-1 + taiko-l-1-relayer: + name: TaikoL1Relayer + network: mainnet + min-balance: "100000000000000000" + policy: ${self:resources.policies.policy-hk} + api-keys: + - key-1 + notifications: + discord-bridging: + type: discord + name: discord_bridging + config: + url: >- + https://discord.com/api/webhooks/1235610195586187285/pT1ZoqTmEKcjtAt0JY6KxOeHP-_YP7mC3SG1janVZyuf99RE7XwymIFQ9iNlFiyxm41w + paused: false + discord-blocks: + type: discord + name: discord_blocks + config: + url: >- + https://discord.com/api/webhooks/1235610046080221214/tEHgmjlZzZsuxGyv3bEPc1I_SGZ69ZmZ7JsF8ey5VEQadylYePXLjRyGCup1I96rTbRk + paused: false + discord-configs: + type: discord + name: discord_configs + config: + url: >- + https://discord.com/api/webhooks/1235609859660185744/d6ygohFcXEJEj6zHlxGhLFqXq4lDfieLrFweae3SnsustZLF0q4aTKKHeuzmrgZBIcvy + paused: false + monitors: + address-manager-l-2-address-set: + name: "Address Manager (L2): AddressSet" + type: BLOCK + network: taikol2 + addresses: + - "0x1670000000000000000000000000000000000006" + - "0x1670000000000000000000000000000000010002" + skip-abi-validation: false + paused: false + confirm-level: 1 + notify-config: + timeout: 60000 + message: |- + **Defender Monitor {{ sentinel.name }} Triggered** + + **Network** + + {{ sentinel.network }} + + **Block Hash** + + {{ blockHash }} + + **Transaction Hash** + + {{ transaction.transactionHash }} + + **Explorer Link** + + {{ transaction.link }} + + **Match Reasons** + + {{ matchReasonsFormatted }} + + **Metadata** + + {{ metadataFormatted }} + message-subject: "Defender Monitor: Address Manager (L2): AddressSet triggered" + channels: + - ${self:resources.notifications.discord-configs} + severity-level: MEDIUM + conditions: + event: + - expression: null + signature: AddressSet(uint64,bytes32,address,address) + address-manager-l-1-address-set: + name: "Address Manager (L1): AddressSet" + type: BLOCK + network: mainnet + addresses: + - "0x579f40D0BE111b823962043702cabe6Aaa290780" + - "0xEf9EaA1dd30a9AA1df01c36411b5F082aA65fBaa" + skip-abi-validation: false + paused: false + confirm-level: 9007199254740991 + notify-config: + timeout: 60000 + message: |- + **Defender Monitor {{ sentinel.name }} Triggered** + + **Network** + + {{ sentinel.network }} + + **Block Hash** + + {{ blockHash }} + + **Transaction Hash** + + {{ transaction.transactionHash }} + + **Explorer Link** + + {{ transaction.link }} + + **Match Reasons** + + {{ matchReasonsFormatted }} + + **Metadata** + + {{ metadataFormatted }} + message-subject: "Defender Monitor: Address Manager (L1): AddressSet triggered" + channels: + - ${self:resources.notifications.discord-configs} + severity-level: MEDIUM + conditions: + event: + - expression: null + signature: AddressSet(uint64,bytes32,address,address) + taiko-l-2-pause-unpause: + name: "TaikoL2: Pause/Unpause" + type: BLOCK + network: taikol2 + addresses: + - "0xbd0999f42742ed29d3311fc5fb5c609be008e9e5" + - "0x1670000000000000000000000000000000000001" + - "0x1670000000000000000000000000000000000002" + - "0x1670000000000000000000000000000000000003" + - "0x1670000000000000000000000000000000000004" + - "0x1670000000000000000000000000000000000005" + - "0x1670000000000000000000000000000000000006" + - "0x1670000000000000000000000000000000010001" + - "0x1670000000000000000000000000000000010002" + skip-abi-validation: true + paused: false + confirm-level: 1 + notify-config: + timeout: 60000 + message: |- + **Defender Monitor {{ sentinel.name }} Triggered** + + **Network** + + {{ sentinel.network }} + + **Block Hash** + + {{ blockHash }} + + **Transaction Hash** + + {{ transaction.transactionHash }} + + **Explorer Link** + + {{ transaction.link }} + + **Match Reasons** + + {{ matchReasonsFormatted }} + + **Metadata** + + {{ metadataFormatted }} + message-subject: "Defender Monitor: TaikoL2: Pause/Unpause triggered" + channels: + - ${self:resources.notifications.discord-configs} + severity-level: MEDIUM + conditions: + event: + - expression: null + signature: Paused(address) + - expression: null + signature: Unpaused(address) + taiko-l-1-pause-unpause: + name: "TaikoL1: Pause/Unpause" + type: BLOCK + network: mainnet + addresses: + - "0x02772b7B3a5Bea0141C993Dbb8D0733C19F46169" + - "0x06a9Ab27c7e2255df1815E6CC0168d7755Feb19a" + - "0x0b470dd3A0e1C41228856Fb319649E7c08f419Aa" + - "0x10dea67478c5F8C5E2D90e5E9B26dBe60c54d800" + - "0x11A9ebA17EbF92b40fcf9a640Ebbc47Db6fBeab0" + - "0x33879cDF01121dc7bCe011b461e64d791aE931F2" + - "0x39E4C1214e733639d059979079A151911e42791d" + - "0x47bB416ee947fE4a4b655011aF7d6E3A1B80E6e9" + - "0x537a2f0D3a5879b41BCb5A2afE2EA5c4961796F6" + - "0x579A8d63a2Db646284CBFE31FE5082c9989E985c" + - "0x579f40D0BE111b823962043702cabe6Aaa290780" + - "0x67281b15aee4d6b805bc755e439abd524dd8da8d" + - "0x8d7C954960a36a7596d7eA4945dDf891967ca8A3" + - "0x996282cA11E5DEb6B5D122CC3B9A1FcAAD4415Ab" + - "0x9e0a24964e5397B566c1ed39258e21aB5E35C77C" + - "0xE3D777143Ea25A6E031d1e921F396750885f43aC" + - "0xEf9EaA1dd30a9AA1df01c36411b5F082aA65fBaa" + - "0xa25e645ff9897b0282e5f17d36de5bca4ec21d6e" + - "0xaf145913EA4a56BE22E120ED9C24589659881702" + - "0xb0f3186FC1963f774f52ff455DC86aEdD0b31F81" + - "0xc4096E9ff1526Bd1840B65e9f45695135aC12De7" + - "0xcc5d488073FA918cBbd73B9A523F3858C4de7372" + - "0xd60247c6848B7Ca29eDdF63AA924E53dB6Ddd8EC" + skip-abi-validation: true + paused: false + confirm-level: 6 + notify-config: + timeout: 60000 + message: |- + **Defender Monitor {{ sentinel.name }} Triggered** + + **Network** + + {{ sentinel.network }} + + **Block Hash** + + {{ blockHash }} + + **Transaction Hash** + + {{ transaction.transactionHash }} + + **Explorer Link** + + {{ transaction.link }} + + **Match Reasons** + + {{ matchReasonsFormatted }} + + **Metadata** + + {{ metadataFormatted }} + message-subject: "Defender Monitor: TaikoL1: Pause/Unpause triggered" + channels: + - ${self:resources.notifications.discord-configs} + severity-level: MEDIUM + conditions: + event: + - expression: null + signature: Paused(address) + - expression: null + signature: Unpaused(address) + taiko-mainnet-sgx-prover: + name: Taiko Mainnet SGX Prover + type: BLOCK + network: mainnet + addresses: + - "0xb0f3186FC1963f774f52ff455DC86aEdD0b31F81" + skip-abi-validation: false + paused: false + confirm-level: 1 + notify-config: + timeout: 0 + message: |- + **Defender Monitor {{ sentinel.name }} Triggered** + + **Network** + + {{ sentinel.network }} + + **Block Hash** + + {{ blockHash }} + + **Transaction Hash** + + {{ transaction.transactionHash }} + + **Explorer Link** + + {{ transaction.link }} + + {{ matchReasonsFormatted }} + message-subject: "Defender Monitor: Taiko Mainnet SGX Prover triggered" + channels: + - ${self:resources.notifications.discord-blocks} + severity-level: LOW + conditions: + event: + - expression: null + signature: InstanceAdded(uint256,address,address,uint256) + - expression: null + signature: InstanceDeleted(uint256,address) + function: + - expression: null + signature: >- + verifyProof((bytes32,bytes32,address,uint64,bool,bool,address),(bytes32,bytes32,bytes32,bytes32),(uint16,bytes)) + transaction: status == "failed" + taiko-mainnet-address-managers: + name: Taiko Mainnet Address Managers + type: BLOCK + network: mainnet + addresses: + - "0xEf9EaA1dd30a9AA1df01c36411b5F082aA65fBaa" + - "0x579f40D0BE111b823962043702cabe6Aaa290780" + skip-abi-validation: false + paused: false + confirm-level: 1 + notify-config: + timeout: 0 + message: |- + **Defender Monitor {{ sentinel.name }} Triggered** + + **Network** + + {{ sentinel.network }} + + **Block Hash** + + {{ blockHash }} + + **Transaction Hash** + + {{ transaction.transactionHash }} + + **Explorer Link** + + {{ transaction.link }} + + {{ matchReasonsFormatted }} + message-subject: "Defender Monitor: Taiko Mainnet Address Managers triggered" + channels: + - ${self:resources.notifications.discord-configs} + severity-level: HIGH + conditions: + event: + - expression: null + signature: AddressSet(uint64,bytes32,address,address) + taiko-mainnet-essential-contracts: + name: Taiko Mainnet Essential Contracts + type: BLOCK + network: mainnet + addresses: + - "0xEf9EaA1dd30a9AA1df01c36411b5F082aA65fBaa" + - "0x10dea67478c5F8C5E2D90e5E9B26dBe60c54d800" + - "0x9e0a24964e5397B566c1ed39258e21aB5E35C77C" + - "0xd60247c6848B7Ca29eDdF63AA924E53dB6Ddd8EC" + - "0x996282cA11E5DEb6B5D122CC3B9A1FcAAD4415Ab" + - "0x0b470dd3A0e1C41228856Fb319649E7c08f419Aa" + - "0x0b470dd3A0e1C41228856Fb319649E7c08f419Aa" + - "0xaf145913EA4a56BE22E120ED9C24589659881702" + - "0x579f40D0BE111b823962043702cabe6Aaa290780" + - "0x06a9Ab27c7e2255df1815E6CC0168d7755Feb19a" + - "0x537a2f0D3a5879b41BCb5A2afE2EA5c4961796F6" + - "0x33879cDF01121dc7bCe011b461e64d791aE931F2" + - "0xb0f3186FC1963f774f52ff455DC86aEdD0b31F81" + - "0x579A8d63a2Db646284CBFE31FE5082c9989E985c" + - "0xE3D777143Ea25A6E031d1e921F396750885f43aC" + - "0x8d7C954960a36a7596d7eA4945dDf891967ca8A3" + skip-abi-validation: false + paused: false + confirm-level: 1 + notify-config: + timeout: 0 + message: |- + **Defender Monitor {{ sentinel.name }} Triggered** + + **Network** + + {{ sentinel.network }} + + **Block Hash** + + {{ blockHash }} + + **Transaction Hash** + + {{ transaction.transactionHash }} + + **Explorer Link** + + {{ transaction.link }} + + {{ matchReasonsFormatted }} + message-subject: "Defender Monitor: Taiko Mainnet Essential Contracts triggered" + channels: + - ${self:resources.notifications.discord-configs} + severity-level: HIGH + conditions: + event: + - expression: null + signature: AdminChanged(address,address) + - expression: null + signature: BeaconUpgraded(address) + - expression: null + signature: Initialized(uint8) + - expression: null + signature: OwnershipTransferStarted(address,address) + - expression: null + signature: OwnershipTransferred(address,address) + - expression: null + signature: Paused(address) + - expression: null + signature: Unpaused(address) + - expression: null + signature: Upgraded(address) + forked-networks: {} + private-networks: + taikol-2: + name: taikol2 + rpc-url: https://rpc.mainnet.taiko.xyz + configuration: + symbol: ETH + eips: + isEIP1559: true + block-explorer-api-keys: {} +plugins: + - "@openzeppelin/defender-as-code"