Commit f7f6d76f authored by Maurelian's avatar Maurelian Committed by GitHub

bmon: Detect at warning and danger level balances (#4571)

* bmon: Detect at warning and danger level balances

* bmon: Simplify by reducing to a single threshold

* bmon: Set amounts in eth rather than wei

* bmon: Add ops genie alert creation

* bmon: Add ops genie heartbeat

* bmon: Prep for goerli
parent d3308176
export L1_RPC_URL=
export OPS_GENIE_KEY=
export OPS_GENIE_TEAM=
export OPS_GENIE_HEARTBEAT_NAME=
# Threshold values are denominated in ETH.
export SEQUENCER_ADDRESS=
export SEQUENCER_WARNING_THRESHOLD=
export SEQUENCER_DANGER_THRESHOLD=
export SEQUENCER_DANGER_THRESHOLD=10
export PROPOSER_ADDRESS=
export PROPOSER_WARNING_THRESHOLD=
export PROPOSER_DANGER_THRESHOLD=
export PROPOSER_DANGER_THRESHOLD=10
......@@ -2,23 +2,22 @@
## Description
A forta agent which detects when a specified account balance is below 0.5 ETH
A forta agent which detects when a specified account balance is below the
specified threshold.
## Installing and building
`yarn && yarn build`
## Running
1. Copy `.env.example` into `.env` and set the values as desired.
1. Copy `.env.example` into `.env` and set the appropriate values.
2. Copy `forta.config.example.json` into `forta.config.json`, and set the RPC endpoint (yes, this is
duplicated in the .env file).
2. `yarn run start:prod`
## Alerts
- `OPTIMISM-BALANCE-WARNING-[ACCOUNT_NAME]`
- `ACCOUNT_NAME` is either `SEQUENCER` or `PROPOSER`
- Fired when the specified account balance is below the configured WARNING threshold
- Severity is always set to "info"
- Type is always set to "info"
- Metadata "balance" field contains amount of wei in account
- `OPTIMISM-BALANCE-DANGER-[ACCOUNT_NAME]`
- Fired when the specified account balance is below the configured DANGER threshold
- Severity is always set to "high"
......
......@@ -9,12 +9,15 @@
"type": "git",
"url": "https://github.com/ethereum-optimism/optimism.git"
},
"chainIds": [
5
],
"scripts": {
"build": "tsc -p tsconfig.json",
"clean": "rimraf dist/ ./tsconfig.tsbuildinfo",
"start": "yarn run start:dev",
"start:dev": "nodemon --watch src --watch forta.config.json -e js,ts,json --exec 'yarn run build && forta-agent run'",
"start:prod": "forta-agent run --prod",
"start:prod": "forta-agent run",
"tx": "yarn run build && forta-agent run --tx",
"block": "yarn run build && forta-agent run --block",
"range": "yarn run build && forta-agent run --range",
......@@ -26,6 +29,7 @@
},
"dependencies": {
"ethers": "^5.7.2",
"node-fetch": "^2.6.1",
"forta-agent": "^0.1.1"
},
"devDependencies": {
......
#!/bin/bash
export SEQUENCER_ADDRESS=0xabba
export SEQUENCER_WARNING_THRESHOLD=1000
export SEQUENCER_DANGER_THRESHOLD=100
export SEQUENCER_DANGER_THRESHOLD=100 # 100 eth
export PROPOSER_ADDRESS=0xacdc
export PROPOSER_WARNING_THRESHOLD=2000
export PROPOSER_DANGER_THRESHOLD=200
export PROPOSER_DANGER_THRESHOLD=200 # 200 eth
yarn ts-mocha src/*.spec.ts
import { HandleBlock, createBlockEvent } from 'forta-agent'
import {
Finding,
FindingSeverity,
FindingType,
HandleBlock,
createBlockEvent,
} from 'forta-agent'
import { BigNumber, utils } from 'ethers'
import { expect } from 'chai'
import agent from './agent'
import agent, { accounts } from './agent'
import { describeFinding } from './utils'
describe('minimum balance agent', async () => {
describe('minimum balance agent', () => {
let handleBlock: HandleBlock
let mockEthersProvider
const blockEvent = createBlockEvent({
......@@ -19,7 +26,18 @@ describe('minimum balance agent', async () => {
return {
getBalance: async (addr: string): Promise<BigNumber> => {
if (addr === '0xabba') {
return utils.parseEther('1001')
return utils.parseEther('101')
}
if (addr === '0xacdc') {
return utils.parseEther('2001')
}
},
} as any
case 'danger':
return {
getBalance: async (addr: string): Promise<BigNumber> => {
if (addr === '0xabba') {
return utils.parseEther('99') // below danger threshold
}
if (addr === '0xacdc') {
return utils.parseEther('2001')
......@@ -35,13 +53,40 @@ describe('minimum balance agent', async () => {
handleBlock = agent.provideHandleBlock(mockEthersProvider)
})
describe('handleBlock', async () => {
describe('handleBlock', () => {
it('returns empty findings if balance is above threshold', async () => {
mockEthersProvider = mockEthersProviderByCase('safe')
handleBlock = agent.provideHandleBlock(mockEthersProvider)
const findings = await handleBlock(blockEvent)
expect(findings).to.deep.equal([])
})
it('returns high severity finding if balance is below danger threshold', async () => {
mockEthersProvider = mockEthersProviderByCase('danger')
handleBlock = agent.provideHandleBlock(mockEthersProvider)
const balance = await mockEthersProvider.getBalance('0xabba')
const findings = await handleBlock(blockEvent)
// Take the second alert in the list, as the first is a warning
expect(findings).to.deep.equal([
Finding.fromObject({
name: 'Minimum Account Balance',
description: describeFinding(
accounts[0].address,
balance,
accounts[0].thresholds.danger
),
alertId: 'OPTIMISM-BALANCE-DANGER-Sequencer',
severity: FindingSeverity.High,
type: FindingType.Info,
metadata: {
balance: balance.toString(),
},
}),
])
})
})
})
import { BlockEvent, Finding, HandleBlock } from 'forta-agent'
import { BigNumber, providers } from 'ethers'
import {
BlockEvent,
Finding,
HandleBlock,
FindingSeverity,
FindingType,
} from 'forta-agent'
import { BigNumber, providers, utils } from 'ethers'
import { createAlert, heartBeat, describeFinding } from './utils'
type AccountAlert = {
name: string
address: string
thresholds: {
warning: BigNumber
danger: BigNumber
}
}
......@@ -15,16 +22,14 @@ export const accounts: AccountAlert[] = [
name: 'Sequencer',
address: process.env.SEQUENCER_ADDRESS,
thresholds: {
warning: BigNumber.from(process.env.SEQUENCER_WARNING_THRESHOLD),
danger: BigNumber.from(process.env.SEQUENCER_DANGER_THRESHOLD),
danger: utils.parseEther(process.env.SEQUENCER_DANGER_THRESHOLD),
},
},
{
name: 'Proposer',
address: process.env.PROPOSER_ADDRESS,
thresholds: {
warning: BigNumber.from(process.env.PROPOSER_WARNING_THRESHOLD),
danger: BigNumber.from(process.env.PROPOSER_DANGER_THRESHOLD),
danger: utils.parseEther(process.env.PROPOSER_DANGER_THRESHOLD),
},
},
]
......@@ -37,21 +42,46 @@ const provideHandleBlock = (
const findings: Finding[] = []
// iterate over accounts with the index
for (const [idx, account] of accounts.entries()) {
for (const [, account] of accounts.entries()) {
const accountBalance = BigNumber.from(
(
await provider.getBalance(account.address, blockEvent.blockNumber)
).toString()
)
if (accountBalance.gte(account.thresholds.warning)) {
// todo: add to the findings array when balances are below the threshold
// return if this is the last account
if (idx === accounts.length - 1) {
return findings
if (accountBalance.lte(account.thresholds.danger)) {
const alertId = `OPTIMISM-BALANCE-DANGER-${account.name}`
const description = describeFinding(
account.address,
accountBalance,
account.thresholds.danger
)
// If an alert is already open with the same alertId, this will have no effect.
// Alerts must be disabled manually in opsgenie. We don't provide a method here
// for closing when the balance is above the threshold again.
if (process.env.OPS_GENIE_KEY !== undefined) {
await createAlert({ alias: alertId, message: description })
}
// Add to the findings array. This will only be meaningful when running on
// public forta nodes.
findings.push(
Finding.fromObject({
name: 'Minimum Account Balance',
description,
alertId,
severity: FindingSeverity.High,
type: FindingType.Info,
metadata: {
balance: accountBalance.toString(),
},
})
)
}
}
// Let ops-genie know that we're still alive.
await heartBeat()
return findings
}
}
......
// import 'ethers'
import { ethers } from 'ethers'
import fetch from 'node-fetch'
// new function to log an account an it's balance
export const describeFinding = (
account: string,
actual: ethers.BigNumber,
threshold: ethers.BigNumber
) => {
return `Balance of account ${account} is (${ethers.utils.formatEther(
actual
)} eth) below threshold (${ethers.utils.formatEther(threshold)} eth)`
}
// Create an alert in ops-genie. The alias will be used an unique identifier for the alert.
// There can only be one open alert per alias. If this is called with an alias which already
// has an alert, it will not be reopened.
export const createAlert = async (alertOpts: {
alias: string
message: string
}) => {
const response = await fetch('https://api.opsgenie.com/v2/alerts', {
method: 'post',
body: JSON.stringify({
message: alertOpts.message,
alias: alertOpts.alias,
responders: [{ id: process.env.OPS_GENIE_TEAM, type: 'team' }],
tags: ['Bedrock-Beta', 'Balance-Low'],
priority: 'P2',
}),
headers: {
'Content-type': 'application/json',
Authorization: `GenieKey ${process.env.OPS_GENIE_KEY}`,
},
})
if (!response.ok) {
console.log(`Error creating alert: ${JSON.stringify(response.body)}`)
}
}
// Send this with every block. If Ops Genie doesn't get this ping for 10 minutes,
// it will trigger a P3 alert.
export const heartBeat = async () => {
const response = await fetch(
`https://api.opsgenie.com/v2/heartbeats/${process.env.OPS_GENIE_HEARTBEAT_NAME}/ping`,
{
method: 'get',
headers: {
Authorization: `GenieKey ${process.env.OPS_GENIE_KEY}`,
},
}
)
if (!response.ok) {
console.log(`Error creating alert: ${JSON.stringify(response.body)}`)
}
}
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist"
"outDir": "./dist",
"rootDir": "./src"
},
"include": [
"src/**/*"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment