Commit d6388be4 authored by Maurelian's avatar Maurelian

feat(c-mon): Add wallet-mon service

parent a1cbc77d
---
'@eth-optimism/chain-mon': minor
'@eth-optimism/core-utils': patch
---
Added a new service wallet-mon to identify unexpected transfers from key accounts
...@@ -89,3 +89,7 @@ ENTRYPOINT ["npm", "run", "start:drippie-mon"] ...@@ -89,3 +89,7 @@ ENTRYPOINT ["npm", "run", "start:drippie-mon"]
FROM base as wd-mon FROM base as wd-mon
WORKDIR /opt/optimism/packages/chain-mon WORKDIR /opt/optimism/packages/chain-mon
ENTRYPOINT ["yarn", "run", "start:wd-mon"] ENTRYPOINT ["yarn", "run", "start:wd-mon"]
FROM base as wallet-mon
WORKDIR /opt/optimism/packages/chain-mon
ENTRYPOINT ["yarn", "run", "start:wallet-mon"]
...@@ -8,6 +8,16 @@ BALANCE_MON__RPC= ...@@ -8,6 +8,16 @@ BALANCE_MON__RPC=
# JSON array in the format [{ "address": <address>, "nickname": <nickname> }, ... ] # JSON array in the format [{ "address": <address>, "nickname": <nickname> }, ... ]
BALANCE_MON__ACCOUNTS= BALANCE_MON__ACCOUNTS=
###############################################################################
# ↓ wallet-mon ↓ #
###############################################################################
# RPC pointing to network to monitor
WALLET_MON__RPC=
# The block number to start monitoring from
WALLET_MON__START_BLOCK_NUMBER=
############################################################################### ###############################################################################
# ↓ drippie-mon ↓ # # ↓ drippie-mon ↓ #
############################################################################### ###############################################################################
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
], ],
"scripts": { "scripts": {
"start:balance-mon": "ts-node ./src/balance-mon/service.ts", "start:balance-mon": "ts-node ./src/balance-mon/service.ts",
"start:wallet-mon": "ts-node ./src/wallet-mon/service.ts",
"start:drippie-mon": "ts-node ./src/drippie-mon/service.ts", "start:drippie-mon": "ts-node ./src/drippie-mon/service.ts",
"start:wd-mon": "ts-node ./src/wd-mon/service.ts", "start:wd-mon": "ts-node ./src/wd-mon/service.ts",
"test:coverage": "echo 'No tests defined.'", "test:coverage": "echo 'No tests defined.'",
...@@ -35,6 +36,7 @@ ...@@ -35,6 +36,7 @@
"dependencies": { "dependencies": {
"@eth-optimism/common-ts": "0.8.1", "@eth-optimism/common-ts": "0.8.1",
"@eth-optimism/contracts-periphery": "1.0.8", "@eth-optimism/contracts-periphery": "1.0.8",
"@eth-optimism/contracts-bedrock": "0.14.0",
"@eth-optimism/core-utils": "0.12.0", "@eth-optimism/core-utils": "0.12.0",
"@eth-optimism/sdk": "2.1.0", "@eth-optimism/sdk": "2.1.0",
"ethers": "^5.7.0", "ethers": "^5.7.0",
......
export * from './balance-mon/service' export * from './balance-mon/service'
export * from './drippie-mon/service' export * from './drippie-mon/service'
export * from './wd-mon/service' export * from './wd-mon/service'
export * from './wallet-mon/service'
import {
BaseServiceV2,
StandardOptions,
Gauge,
Counter,
validators,
waitForProvider,
} from '@eth-optimism/common-ts'
import { getChainId, compareAddrs } from '@eth-optimism/core-utils'
import { Provider } from '@ethersproject/abstract-provider'
import mainnetConfig from '@eth-optimism/contracts-bedrock/deploy-config/mainnet.json'
import goerliConfig from '@eth-optimism/contracts-bedrock/deploy-config/goerli.json'
import l2OutputOracleArtifactsMainnet from '@eth-optimism/contracts-bedrock/deployments/mainnet/L2OutputOracleProxy.json'
import l2OutputOracleArtifactsGoerli from '@eth-optimism/contracts-bedrock/deployments/goerli/L2OutputOracleProxy.json'
import { version } from '../../package.json'
const networks = {
1: 'mainnet',
10: 'goerli',
}
const bedrockAccounts = {
mainnet: [
{
label: 'Proposer',
wallet: mainnetConfig.l2OutputOracleProposer,
target: l2OutputOracleArtifactsMainnet.address,
},
{
label: 'Batcher',
wallet: mainnetConfig.batchSenderAddress,
target: mainnetConfig.batchInboxAddress,
},
],
goerli: [
{
label: 'Proposer',
wallet: goerliConfig.l2OutputOracleProposer,
target: l2OutputOracleArtifactsGoerli.address,
},
{
label: 'Batcher',
wallet: goerliConfig.batchSenderAddress,
target: goerliConfig.batchInboxAddress,
},
],
}
type TransferMonOptions = {
rpc: Provider
startBlockNumber: number
}
type TransferMonMetrics = {
validatedCalls: Counter
unexpectedCalls: Counter
unexpectedRpcErrors: Counter
}
type TransferMonState = {
chainId: number
highestUncheckedBlockNumber: number
}
export class TransferMonService extends BaseServiceV2<
TransferMonOptions,
TransferMonMetrics,
TransferMonState
> {
constructor(options?: Partial<TransferMonOptions & StandardOptions>) {
super({
version,
name: 'transfer-mon',
loop: true,
options: {
loopIntervalMs: 60_000,
...options,
},
optionsSpec: {
rpc: {
validator: validators.provider,
desc: 'Provider for network to monitor balances on',
},
startBlockNumber: {
validator: validators.num,
default: -1,
desc: 'L1 block number to start checking from',
public: true,
},
},
metricsSpec: {
validatedCalls: {
type: Gauge,
desc: 'Transactions from the account checked',
labels: ['wallet', 'target', 'nickname'],
},
unexpectedCalls: {
type: Counter,
desc: 'Number of unexpected transfers',
labels: ['wallet', 'target', 'nickname'],
},
unexpectedRpcErrors: {
type: Counter,
desc: 'Number of unexpected RPC errors',
labels: ['section', 'name'],
},
},
})
}
protected async init(): Promise<void> {
// Connect to L1.
await waitForProvider(this.options.rpc, {
logger: this.logger,
name: 'L1',
})
this.state.chainId = await getChainId(this.options.rpc)
if (this.options.startBlockNumber === -1) {
this.state.highestUncheckedBlockNumber =
await this.options.rpc.getBlockNumber()
} else {
this.state.highestUncheckedBlockNumber = this.options.startBlockNumber
}
}
protected async main(): Promise<void> {
// get the next unchecked block
const network = networks[this.state.chainId]
const accounts = bedrockAccounts[network]
const block = await this.options.rpc.getBlock(
this.state.highestUncheckedBlockNumber
)
for (const txHash of block.transactions) {
for (const account of accounts) {
const tx = await this.options.rpc.getTransaction(txHash)
if (compareAddrs(account.wallet, tx.from)) {
if (compareAddrs(account.target, tx.to)) {
this.metrics.validatedCalls.inc({
label: account.label,
wallet: account.address,
target: account.target,
})
this.logger.info('validated call', {
label: account.label,
wallet: account.address,
target: account.target,
})
} else {
this.metrics.unexpectedCalls.inc({
label: account.label,
wallet: account.address,
target: tx.to,
})
this.logger.error('Unexpected call detected', {
label: account.label,
address: account.address,
target: tx.to,
})
}
}
}
}
}
}
if (require.main === module) {
const service = new TransferMonService()
service.run()
}
...@@ -48,3 +48,14 @@ export const reqenv = (name: string): string => { ...@@ -48,3 +48,14 @@ export const reqenv = (name: string): string => {
export const getenv = (name: string, fallback?: string): string | undefined => { export const getenv = (name: string, fallback?: string): string | undefined => {
return process.env[name] || fallback return process.env[name] || fallback
} }
/**
* Returns true if the given string is a valid address.
*
* @param a First address to check.
* @param b Second address to check.
* @returns True if the given addresses match.
*/
export const compareAddrs = (a: string, b: string): boolean => {
return a.toLowerCase() === b.toLowerCase()
}
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