diff --git a/csm-alerts/.prettierrc.json b/csm-alerts/.prettierrc.json index 852349dd9..28667141a 100644 --- a/csm-alerts/.prettierrc.json +++ b/csm-alerts/.prettierrc.json @@ -2,6 +2,6 @@ "semi": false, "trailingComma": "all", "singleQuote": true, - "printWidth": 120, - "tabWidth": 2 + "printWidth": 100, + "tabWidth": 4 } diff --git a/csm-alerts/README.md b/csm-alerts/README.md index aa6b33d6a..68cb77ead 100644 --- a/csm-alerts/README.md +++ b/csm-alerts/README.md @@ -2,133 +2,133 @@ ## Supported chains -- Holesky testnet +- Holesky testnet ## Alerts 1. **CSModule** - 1. General - 1. 🔴 HIGH: EL rewards stealing penalty reported/settled/cancelled for an operator. - 2. 🟠 MEDIUM: targetLimitMode was set for an operator. - 3. 🟢 LOW: Module's share is close to the targetShare. - 4. 🟢 LOW: More than N "empty" batches in the queue. (N = 30) - 5. 🟢 LOW: More than N validators in the queue. (N = 200) - 6. 🔵 INFO: Operator X was unvetted. - 7. 🔵 INFO: Public release is activated. - 8. 🔵 INFO: Every 100 new operators created (69th as well). - 2. Roles monitoring - 1. 🚨 CRITICAL: role change: DEFAULT_ADMIN_ROLE - 2. 🚨 CRITICAL: role change: PAUSE_ROLE - 3. 🚨 CRITICAL: role change: RESUME_ROLE - 4. 🚨 CRITICAL: role change: MODULE_MANAGER_ROLE - 5. 🚨 CRITICAL: role change: STAKING_ROUTER_ROLE - 6. 🚨 CRITICAL: role change: REPORT_EL_REWARDS_STEALING_PENALTY_ROLE - 7. 🚨 CRITICAL: role change: SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE - 8. 🚨 CRITICAL: role change: VERIFIER_ROLE - 9. 🚨 CRITICAL: role change: RECOVERER_ROLE + 1. General + 1. 🔴 HIGH: EL rewards stealing penalty reported/settled/cancelled for an operator. + 2. 🟠 MEDIUM: targetLimitMode was set for an operator. + 3. 🟢 LOW: Module's share is close to the targetShare. + 4. 🟢 LOW: More than N "empty" batches in the queue. (N = 30) + 5. 🟢 LOW: More than N validators in the queue. (N = 200) + 6. 🔵 INFO: Operator X was unvetted. + 7. 🔵 INFO: Public release is activated. + 8. 🔵 INFO: Every 100 new operators created (69th as well). + 2. Roles monitoring + 1. 🚨 CRITICAL: role change: DEFAULT_ADMIN_ROLE + 2. 🚨 CRITICAL: role change: PAUSE_ROLE + 3. 🚨 CRITICAL: role change: RESUME_ROLE + 4. 🚨 CRITICAL: role change: MODULE_MANAGER_ROLE + 5. 🚨 CRITICAL: role change: STAKING_ROUTER_ROLE + 6. 🚨 CRITICAL: role change: REPORT_EL_REWARDS_STEALING_PENALTY_ROLE + 7. 🚨 CRITICAL: role change: SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE + 8. 🚨 CRITICAL: role change: VERIFIER_ROLE + 9. 🚨 CRITICAL: role change: RECOVERER_ROLE 2. **CSAccounting** - 1. General - 1. 🟢 LOW: Average bond value for a validator is below some threshold. - 2. 🟢 LOW: Total bond lock more than some value. - 3. 🟢 LOW: sharesOf(CSAccounting.address) - CSBondCoreStorage.totalBondShares > 0.1 ether - 2. Events monitoring - 1. 🚨 CRITICAL: ChargePenaltyRecipientSet(address chargeRecipient) - 2. 🚨 CRITICAL: BondCurveUpdated(uint256 indexed curveId, uint256[] bondCurve) - 3. 🔴 HIGH: BondCurveAdded(uint256[] bondCurve) - 4. 🔴 HIGH: BondCurveSet(uint256 indexed nodeOperatorId, uint256 curveId) - 5. 🔵 INFO: Approval(address owner, address spender, uint256 value) (stETH contract) - 3. Roles monitoring - 1. 🚨 CRITICAL: DEFAULT_ADMIN_ROLE - 2. 🚨 CRITICAL: PAUSE_ROLE - 3. 🚨 CRITICAL: RESUME_ROLE - 4. 🚨 CRITICAL: ACCOUNTING_MANAGER_ROLE - 5. 🚨 CRITICAL: MANAGE_BOND_CURVES_ROLE - 6. 🚨 CRITICAL: SET_BOND_CURVE_ROLE - 7. 🚨 CRITICAL: RESET_BOND_CURVE_ROLE - 8. 🚨 CRITICAL: RECOVERER_ROLE + 1. General + 1. 🟢 LOW: Average bond value for a validator is below some threshold. + 2. 🟢 LOW: Total bond lock more than some value. + 3. 🟢 LOW: sharesOf(CSAccounting.address) - CSBondCoreStorage.totalBondShares > 0.1 ether + 2. Events monitoring + 1. 🚨 CRITICAL: ChargePenaltyRecipientSet(address chargeRecipient) + 2. 🚨 CRITICAL: BondCurveUpdated(uint256 indexed curveId, uint256[] bondCurve) + 3. 🔴 HIGH: BondCurveAdded(uint256[] bondCurve) + 4. 🔴 HIGH: BondCurveSet(uint256 indexed nodeOperatorId, uint256 curveId) + 5. 🔵 INFO: Approval(address owner, address spender, uint256 value) (stETH contract) + 3. Roles monitoring + 1. 🚨 CRITICAL: DEFAULT_ADMIN_ROLE + 2. 🚨 CRITICAL: PAUSE_ROLE + 3. 🚨 CRITICAL: RESUME_ROLE + 4. 🚨 CRITICAL: ACCOUNTING_MANAGER_ROLE + 5. 🚨 CRITICAL: MANAGE_BOND_CURVES_ROLE + 6. 🚨 CRITICAL: SET_BOND_CURVE_ROLE + 7. 🚨 CRITICAL: RESET_BOND_CURVE_ROLE + 8. 🚨 CRITICAL: RECOVERER_ROLE 3. **CSFeeOracle** - 1. General - 1. 🚨 CRITICAL: ConsensusHashContractSet(address indexed addr, address indexed prevAddr) - 2. 🔴 HIGH: PerfLeewaySet(uint256 valueBP) - 3. 🔴 HIGH: FeeDistributorContractSet(address feeDistributorContract) - 4. 🔴 HIGH: ConsensusVersionSet(uint256 indexed version, uint256 indexed prevVersion) - 5. 🔵 INFO: WarnProcessingMissed(uint256 indexed refSlot) - 6. 🔵 INFO: ReportSubmitted(uint256 indexed refSlot, bytes32 hash, uint256 processingDeadlineTime) - 7. 🔵 INFO: ProcessingStarted(uint256 indexed refSlot, bytes32 hash) - 2. Roles monitoring - 1. 🚨 CRITICAL: DEFAULT_ADMIN_ROLE - 2. 🚨 CRITICAL: CONTRACT_MANAGER_ROLE - 3. 🚨 CRITICAL: SUBMIT_DATA_ROLE - 4. 🚨 CRITICAL: PAUSE_ROLE - 5. 🚨 CRITICAL: RESUME_ROLE - 6. 🚨 CRITICAL: RECOVERER_ROLE - 3. HashConsensus (for CSFeeOracle) - 1. Events monitoring - 1. 🔴 HIGH: MemberAdded(address indexed addr, uint256 newTotalMembers, uint256 newQuorum) - 2. 🔴 HIGH: MemberRemoved(address indexed addr, uint256 newTotalMembers, uint256 newQuorum) - 3. 🔴 HIGH: QuorumSet(uint256 newQuorum, uint256 totalMembers, uint256 prevQuorum) - 4. 🔴 HIGH: FastLaneConfigSet(uint256 fastLaneLengthSlots) - 5. 🔴 HIGH: FrameConfigSet(uint256 newInitialEpoch, uint256 newEpochsPerFrame) - 6. 🔴 HIGH: ReportProcessorSet(address indexed processor, address indexed prevProcessor) - 7. 🔴 HIGH: another report variant appeared (alternative hash) event ReportReceived(uint256 indexed refSlot, address indexed member, bytes32 report) - 8. 🔴 HIGH: ConsensusLost(uint256 indexed refSlot) - 9. 🔵 INFO: ConsensusReached(uint256 indexed refSlot, bytes32 report, uint256 support) - 2. Roles monitoring - 1. 🚨 CRITICAL: DEFAULT_ADMIN_ROLE - 2. 🚨 CRITICAL: DISABLE_CONSENSUS_ROLE - 3. 🚨 CRITICAL: MANAGE_MEMBERS_AND_QUORUM_ROLE - 4. 🚨 CRITICAL: MANAGE_FRAME_CONFIG_ROLE - 5. 🚨 CRITICAL: MANAGE_FAST_LANE_CONFIG_ROLE - 6. 🚨 CRITICAL: MANAGE_REPORT_PROCESSOR_ROLE + 1. General + 1. 🚨 CRITICAL: ConsensusHashContractSet(address indexed addr, address indexed prevAddr) + 2. 🔴 HIGH: PerfLeewaySet(uint256 valueBP) + 3. 🔴 HIGH: FeeDistributorContractSet(address feeDistributorContract) + 4. 🔴 HIGH: ConsensusVersionSet(uint256 indexed version, uint256 indexed prevVersion) + 5. 🔵 INFO: WarnProcessingMissed(uint256 indexed refSlot) + 6. 🔵 INFO: ReportSubmitted(uint256 indexed refSlot, bytes32 hash, uint256 processingDeadlineTime) + 7. 🔵 INFO: ProcessingStarted(uint256 indexed refSlot, bytes32 hash) + 2. Roles monitoring + 1. 🚨 CRITICAL: DEFAULT_ADMIN_ROLE + 2. 🚨 CRITICAL: CONTRACT_MANAGER_ROLE + 3. 🚨 CRITICAL: SUBMIT_DATA_ROLE + 4. 🚨 CRITICAL: PAUSE_ROLE + 5. 🚨 CRITICAL: RESUME_ROLE + 6. 🚨 CRITICAL: RECOVERER_ROLE + 3. HashConsensus (for CSFeeOracle) + 1. Events monitoring + 1. 🔴 HIGH: MemberAdded(address indexed addr, uint256 newTotalMembers, uint256 newQuorum) + 2. 🔴 HIGH: MemberRemoved(address indexed addr, uint256 newTotalMembers, uint256 newQuorum) + 3. 🔴 HIGH: QuorumSet(uint256 newQuorum, uint256 totalMembers, uint256 prevQuorum) + 4. 🔴 HIGH: FastLaneConfigSet(uint256 fastLaneLengthSlots) + 5. 🔴 HIGH: FrameConfigSet(uint256 newInitialEpoch, uint256 newEpochsPerFrame) + 6. 🔴 HIGH: ReportProcessorSet(address indexed processor, address indexed prevProcessor) + 7. 🔴 HIGH: another report variant appeared (alternative hash) event ReportReceived(uint256 indexed refSlot, address indexed member, bytes32 report) + 8. 🔴 HIGH: ConsensusLost(uint256 indexed refSlot) + 9. 🔵 INFO: ConsensusReached(uint256 indexed refSlot, bytes32 report, uint256 support) + 2. Roles monitoring + 1. 🚨 CRITICAL: DEFAULT_ADMIN_ROLE + 2. 🚨 CRITICAL: DISABLE_CONSENSUS_ROLE + 3. 🚨 CRITICAL: MANAGE_MEMBERS_AND_QUORUM_ROLE + 4. 🚨 CRITICAL: MANAGE_FRAME_CONFIG_ROLE + 5. 🚨 CRITICAL: MANAGE_FAST_LANE_CONFIG_ROLE + 6. 🚨 CRITICAL: MANAGE_REPORT_PROCESSOR_ROLE 4. **CSFeeDistributor** - 1. Events monitoring - 1. 🚨 CRITICAL: Receiver of TransferShares is NOT CSAccounting, if from is CSFeeDistributor - 2. 🔴 HIGH: No fees distributed for X days (repeat every 1 day). - 3. 🔵 INFO: DistributionDataUpdated -> Oracle settled a new report. - 2. Roles monitoring - 1. 🚨 CRITICAL: DEFAULT_ADMIN_ROLE - 2. 🚨 CRITICAL: RECOVERER_ROLE + 1. Events monitoring + 1. 🚨 CRITICAL: Receiver of TransferShares is NOT CSAccounting, if from is CSFeeDistributor + 2. 🔴 HIGH: No fees distributed for X days (repeat every 1 day). + 3. 🔵 INFO: DistributionDataUpdated -> Oracle settled a new report. + 2. Roles monitoring + 1. 🚨 CRITICAL: DEFAULT_ADMIN_ROLE + 2. 🚨 CRITICAL: RECOVERER_ROLE 5. **CSEarlyAdoption** - - _To be added_ + - _To be added_ 6. **OssifiableProxy** For the following contracts: - - CSModule - - CSAccounting - - CSFeeOracle - - CSFeeDistributor + - CSModule + - CSAccounting + - CSFeeOracle + - CSFeeDistributor - 1. 🚨 CRITICAL: event ProxyOssified() - 2. 🚨 CRITICAL: event Upgraded(address indexed implementation) - 3. 🚨 CRITICAL: event AdminChanged(address previousAdmin, address newAdmin) + 1. 🚨 CRITICAL: event ProxyOssified() + 2. 🚨 CRITICAL: event Upgraded(address indexed implementation) + 3. 🚨 CRITICAL: event AdminChanged(address previousAdmin, address newAdmin) 7. **PausableUntil** For the following contracts: - - CSModule - - CSAccounting - - CSFeeOracle + - CSModule + - CSAccounting + - CSFeeOracle - 1. 🚨 CRITICAL: Paused(uint256 duration); - 2. 🚨 CRITICAL: Resumed(); + 1. 🚨 CRITICAL: Paused(uint256 duration); + 2. 🚨 CRITICAL: Resumed(); 8. **AssetRecoverer** For the following contracts: - - CSModule - - CSAccounting - - CSFeeOracle - - CSFeeDistributor + - CSModule + - CSAccounting + - CSFeeOracle + - CSFeeDistributor - 1. 🔴 HIGH: EtherRecovered() - 2. 🔴 HIGH: ERC20Recovered() - 3. 🔴 HIGH: StETHSharesRecovered() - 4. 🔴 HIGH: ERC721Recovered() - 5. 🔴 HIGH: ERC1155Recovered() + 1. 🔴 HIGH: EtherRecovered() + 2. 🔴 HIGH: ERC20Recovered() + 3. 🔴 HIGH: StETHSharesRecovered() + 4. 🔴 HIGH: ERC721Recovered() + 5. 🔴 HIGH: ERC1155Recovered() ## Development (Forta specific) @@ -150,12 +150,12 @@ docker-compose up -d 1. For testing alerts you have to install promtool on your machine. - ``` - make tools - ``` + ``` + make tools + ``` 2. Check alerts - ``` - make test_alerts - ``` + ``` + make test_alerts + ``` diff --git a/csm-alerts/src/entity/events.ts b/csm-alerts/src/entity/events.ts index 29d447ae5..5aea48089 100644 --- a/csm-alerts/src/entity/events.ts +++ b/csm-alerts/src/entity/events.ts @@ -3,35 +3,40 @@ import { Finding, TransactionEvent, ethers } from '@fortanetwork/forta-bot' import { EventOfNotice } from '../shared/types' import { sourceFromEvent } from '../utils/findings' -export function handleEventsOfNotice(txEvent: TransactionEvent, eventsOfNotice: EventOfNotice[]): Finding[] { - const out: Finding[] = [] +export function handleEventsOfNotice( + txEvent: TransactionEvent, + eventsOfNotice: EventOfNotice[], +): Finding[] { + const out: Finding[] = [] - for (const log of txEvent.logs) { - for (const eventInfo of eventsOfNotice) { - if (log.address.toLowerCase() != eventInfo.address.toLowerCase()) { - continue - } + for (const log of txEvent.logs) { + for (const eventInfo of eventsOfNotice) { + if (log.address.toLowerCase() != eventInfo.address.toLowerCase()) { + continue + } - const parser = new ethers.Interface(typeof eventInfo.abi === 'string' ? [eventInfo.abi] : eventInfo.abi) - const logDesc = parser.parseLog(log) - if (!logDesc) { - continue - } + const parser = new ethers.Interface( + typeof eventInfo.abi === 'string' ? [eventInfo.abi] : eventInfo.abi, + ) + const logDesc = parser.parseLog(log) + if (!logDesc) { + continue + } - const f = Finding.fromObject({ - name: eventInfo.name, - description: eventInfo.description(logDesc.args), - alertId: eventInfo.alertId, - severity: eventInfo.severity, - type: eventInfo.type, - source: sourceFromEvent(txEvent), - addresses: Object.keys(txEvent.addresses), - metadata: { args: String(logDesc.args) }, - }) + const f = Finding.fromObject({ + name: eventInfo.name, + description: eventInfo.description(logDesc.args), + alertId: eventInfo.alertId, + severity: eventInfo.severity, + type: eventInfo.type, + source: sourceFromEvent(txEvent), + addresses: Object.keys(txEvent.addresses), + metadata: { args: String(logDesc.args) }, + }) - out.push(f) + out.push(f) + } } - } - return out + return out } diff --git a/csm-alerts/src/logger.ts b/csm-alerts/src/logger.ts index 2b4747892..fd7305393 100644 --- a/csm-alerts/src/logger.ts +++ b/csm-alerts/src/logger.ts @@ -3,12 +3,12 @@ import * as Winston from 'winston' import { LOG_FORMAT, LOG_LEVEL } from './config' export function getLogger(service: string) { - return Winston.createLogger({ - format: Winston.format.combine( - Winston.format.label({ label: service, message: true }), - LOG_FORMAT === 'simple' ? Winston.format.simple() : Winston.format.json(), - ), - transports: [new Winston.transports.Console()], - level: LOG_LEVEL, - }) + return Winston.createLogger({ + format: Winston.format.combine( + Winston.format.label({ label: service, message: true }), + LOG_FORMAT === 'simple' ? Winston.format.simple() : Winston.format.json(), + ), + transports: [new Winston.transports.Console()], + level: LOG_LEVEL, + }) } diff --git a/csm-alerts/src/main.ts b/csm-alerts/src/main.ts index a2b8450cc..e999b8427 100644 --- a/csm-alerts/src/main.ts +++ b/csm-alerts/src/main.ts @@ -1,11 +1,11 @@ import { - BlockEvent, - Finding, - TransactionEvent, - ethers, - getProvider, - isProduction, - scanEthereum, + BlockEvent, + Finding, + TransactionEvent, + ethers, + getProvider, + isProduction, + scanEthereum, } from '@fortanetwork/forta-bot' import { RPC_URL } from './config' @@ -20,126 +20,126 @@ import { errorAlert, launchAlert } from './utils/findings' const logger = getLogger('main') async function main() { - const { handleTransaction, handleBlock } = await getHandlers() + const { handleTransaction, handleBlock } = await getHandlers() - scanEthereum({ - handleTransaction, - handleBlock, - rpcUrl: RPC_URL, - }) + scanEthereum({ + handleTransaction, + handleBlock, + rpcUrl: RPC_URL, + }) - if (!isProduction) { - return - } + if (!isProduction) { + return + } - // Run metrics server here if needed. + // Run metrics server here if needed. } if (require.main === module) { - main().catch(logger.error) + main().catch(logger.error) } async function getHandlers() { - const { blockIdentifier } = await parseArgs() - - const services = [ - new CSFeeDistributorSrv(), - new EventsWatcherSrv(), - new CSAccountingSrv(), - new CSFeeOracleSrv(), - new CSModuleSrv(), - ] - - for (const srv of services) { - if ('initialize' in srv) { - await srv.initialize( - blockIdentifier ?? 'latest', - await getProvider({ - rpcUrl: RPC_URL, - }), - ) - } - } - - logger.debug('Initialization complete') - let isLaunchReported = false - - async function handleTransaction(txEvent: TransactionEvent, provider: ethers.Provider) { - // prettier-ignore - const results = await Promise.allSettled( - services.filter((srv) => 'handleTransaction' in srv) - .map((srv) => srv.handleTransaction(txEvent, provider)), - ) - - const out: Finding[] = [] - for (const r of results) { - if (r.status === 'fulfilled') { - out.push(...r.value) - } else { - // TODO: Some exceptions should crash an application in fact. - // out.push(errorAlert(`Error processing tx ${txEvent.transaction.hash}`, r.reason)) - logger.error(r.reason) - } - } - return out - } - - async function handleBlock(blockEvent: BlockEvent, provider: ethers.Provider) { - logger.debug(`Running handlers for block ${blockEvent.blockNumber}`) - - // prettier-ignore - const results = await Promise.allSettled( - services.filter((srv) => 'handleBlock' in srv) - .map((srv) => srv.handleBlock(blockEvent, provider)), - ) - - const out: Finding[] = [] - for (const r of results) { - if (r.status === 'fulfilled') { - out.push(...r.value) - } else { - // TODO: Some exceptions should crash an application in fact. - // out.push(errorAlert(`Error processing block ${blockEvent.block.hash}`, r.reason)) - logger.error(r.reason) - } + const { blockIdentifier } = await parseArgs() + + const services = [ + new CSFeeDistributorSrv(), + new EventsWatcherSrv(), + new CSAccountingSrv(), + new CSFeeOracleSrv(), + new CSModuleSrv(), + ] + + for (const srv of services) { + if ('initialize' in srv) { + await srv.initialize( + blockIdentifier ?? 'latest', + await getProvider({ + rpcUrl: RPC_URL, + }), + ) + } } - if (!isLaunchReported) { - out.push(launchAlert()) - isLaunchReported = true + logger.debug('Initialization complete') + let isLaunchReported = false + + async function handleTransaction(txEvent: TransactionEvent, provider: ethers.Provider) { + const results = await Promise.allSettled( + services + .filter((srv) => 'handleTransaction' in srv) + .map((srv) => srv.handleTransaction(txEvent, provider)), + ) + + const out: Finding[] = [] + for (const r of results) { + if (r.status === 'fulfilled') { + out.push(...r.value) + } else { + // TODO: Some exceptions should crash an application in fact. + // out.push(errorAlert(`Error processing tx ${txEvent.transaction.hash}`, r.reason)) + logger.error(r.reason) + } + } + return out } - return out - } + async function handleBlock(blockEvent: BlockEvent, provider: ethers.Provider) { + logger.debug(`Running handlers for block ${blockEvent.blockNumber}`) + + const results = await Promise.allSettled( + services + .filter((srv) => 'handleBlock' in srv) + .map((srv) => srv.handleBlock(blockEvent, provider)), + ) + + const out: Finding[] = [] + for (const r of results) { + if (r.status === 'fulfilled') { + out.push(...r.value) + } else { + // TODO: Some exceptions should crash an application in fact. + // out.push(errorAlert(`Error processing block ${blockEvent.block.hash}`, r.reason)) + logger.error(r.reason) + } + } + + if (!isLaunchReported) { + out.push(launchAlert()) + isLaunchReported = true + } + + return out + } - return { - handleTransaction, - handleBlock, - } + return { + handleTransaction, + handleBlock, + } } async function parseArgs() { - let blockIdentifier: string | number | undefined = process.env['FORTA_CLI_BLOCK'] - if (blockIdentifier && !blockIdentifier.startsWith('0x')) { - blockIdentifier = parseInt(blockIdentifier) - } - - const txIdentifier = process.env['FORTA_CLI_TX'] - if (txIdentifier) { - const provider = await getProvider({ - rpcUrl: RPC_URL, - }) - - const tx = await provider.getTransaction(txIdentifier) - if (!tx?.blockHash) { - throw Error(`Transaction ${txIdentifier} not mined`) + let blockIdentifier: string | number | undefined = process.env['FORTA_CLI_BLOCK'] + if (blockIdentifier && !blockIdentifier.startsWith('0x')) { + blockIdentifier = parseInt(blockIdentifier) } - blockIdentifier = tx.blockHash - } + const txIdentifier = process.env['FORTA_CLI_TX'] + if (txIdentifier) { + const provider = await getProvider({ + rpcUrl: RPC_URL, + }) + + const tx = await provider.getTransaction(txIdentifier) + if (!tx?.blockHash) { + throw Error(`Transaction ${txIdentifier} not mined`) + } - return { - blockIdentifier, - txIdentifier, - } + blockIdentifier = tx.blockHash + } + + return { + blockIdentifier, + txIdentifier, + } } diff --git a/csm-alerts/src/services/CSAccounting/CSAccounting.srv.spec.ts b/csm-alerts/src/services/CSAccounting/CSAccounting.srv.spec.ts index 71344945c..a103fc514 100644 --- a/csm-alerts/src/services/CSAccounting/CSAccounting.srv.spec.ts +++ b/csm-alerts/src/services/CSAccounting/CSAccounting.srv.spec.ts @@ -2,10 +2,10 @@ import { DeploymentAddress, DeploymentAddresses } from '../../utils/constants.ho import { expect } from '@jest/globals' import { TransactionDto } from '../../entity/events' import { - CSModule__factory, - CSAccounting__factory, - CSFeeDistributor__factory, - CSFeeOracle__factory, + CSModule__factory, + CSAccounting__factory, + CSFeeDistributor__factory, + CSFeeOracle__factory, } from '../../generated/typechain' import { getCSAccountingEvents } from '../../utils/events/cs_accounting_events' import { CSAccountingSrv, ICSAccountingClient } from './CSAccounting.srv' @@ -19,147 +19,156 @@ import { Metrics } from '../../utils/metrics/metrics' const TEST_TIMEOUT = 120_000 // ms describe('CSAccounting event tests', () => { - const chainId = 17000 - - const logger: Winston.Logger = Winston.createLogger({ - format: Winston.format.simple(), - transports: [new Winston.transports.Console()], - }) - - const address: DeploymentAddress = DeploymentAddresses - - const fortaEthersProvider = new ethers.providers.JsonRpcProvider(getFortaConfig().jsonRpcUrl, chainId) - const csModuleRunner = CSModule__factory.connect(address.CS_MODULE_ADDRESS, fortaEthersProvider) - const csAccountingRunner = CSAccounting__factory.connect(address.CS_ACCOUNTING_ADDRESS, fortaEthersProvider) - const csFeeDistributorRunner = CSFeeDistributor__factory.connect( - address.CS_FEE_DISTRIBUTOR_ADDRESS, - fortaEthersProvider, - ) - const csFeeOracleRunner = CSFeeOracle__factory.connect(address.CS_FEE_ORACLE_ADDRESS, fortaEthersProvider) - - const registry = new promClient.Registry() - const m = new Metrics(registry, 'test_') - - const csAccountingClient: ICSAccountingClient = new ETHProvider( - logger, - m, - fortaEthersProvider, - csModuleRunner, - csAccountingRunner, - csFeeDistributorRunner, - csFeeOracleRunner, - ) - - const csAccountingSrv = new CSAccountingSrv( - logger, - csAccountingClient, - getCSAccountingEvents(address.CS_ACCOUNTING_ADDRESS), - address.CS_ACCOUNTING_ADDRESS, - address.LIDO_STETH_ADDRESS, - address.CS_MODULE_ADDRESS, - ) - - test( - '🚨 CSAccounting: Bond Curve Updated', - async () => { - const txHash = '0x8b904da83d58e520c778cc562b10fa4e0943a9f991b9050d93481fdabf2da9c2' - - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + const chainId = 17000 + + const logger: Winston.Logger = Winston.createLogger({ + format: Winston.format.simple(), + transports: [new Winston.transports.Console()], + }) + + const address: DeploymentAddress = DeploymentAddresses + + const fortaEthersProvider = new ethers.providers.JsonRpcProvider( + getFortaConfig().jsonRpcUrl, + chainId, + ) + const csModuleRunner = CSModule__factory.connect(address.CS_MODULE_ADDRESS, fortaEthersProvider) + const csAccountingRunner = CSAccounting__factory.connect( + address.CS_ACCOUNTING_ADDRESS, + fortaEthersProvider, + ) + const csFeeDistributorRunner = CSFeeDistributor__factory.connect( + address.CS_FEE_DISTRIBUTOR_ADDRESS, + fortaEthersProvider, + ) + const csFeeOracleRunner = CSFeeOracle__factory.connect( + address.CS_FEE_ORACLE_ADDRESS, + fortaEthersProvider, + ) + + const registry = new promClient.Registry() + const m = new Metrics(registry, 'test_') + + const csAccountingClient: ICSAccountingClient = new ETHProvider( + logger, + m, + fortaEthersProvider, + csModuleRunner, + csAccountingRunner, + csFeeDistributorRunner, + csFeeOracleRunner, + ) + + const csAccountingSrv = new CSAccountingSrv( + logger, + csAccountingClient, + getCSAccountingEvents(address.CS_ACCOUNTING_ADDRESS), + address.CS_ACCOUNTING_ADDRESS, + address.LIDO_STETH_ADDRESS, + address.CS_MODULE_ADDRESS, + ) + + test( + '🚨 CSAccounting: Bond Curve Updated', + async () => { + const txHash = '0x8b904da83d58e520c778cc562b10fa4e0943a9f991b9050d93481fdabf2da9c2' + + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const results = await csAccountingSrv.handleTransaction(transactionDto) + + expect(results).toMatchSnapshot() + expect(results.length).toBe(1) }, - hash: trx.hash, - } - - const results = await csAccountingSrv.handleTransaction(transactionDto) - - expect(results).toMatchSnapshot() - expect(results.length).toBe(1) - }, - TEST_TIMEOUT, - ) - - test( - '🔴 CSAccounting: Bond Curve added, stETH Approval', - async () => { - const txHash = '0xcc92653babec3b1748d8e04de777796cab2d1ae40fbe926db857e9103a9b74a5' - - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + TEST_TIMEOUT, + ) + + test( + '🔴 CSAccounting: Bond Curve added, stETH Approval', + async () => { + const txHash = '0xcc92653babec3b1748d8e04de777796cab2d1ae40fbe926db857e9103a9b74a5' + + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const results = await csAccountingSrv.handleTransaction(transactionDto) + + expect(results).toMatchSnapshot() + expect(results.length).toBe(4) }, - hash: trx.hash, - } - - const results = await csAccountingSrv.handleTransaction(transactionDto) - - expect(results).toMatchSnapshot() - expect(results.length).toBe(4) - }, - TEST_TIMEOUT, - ) - - test( - '🚨 CSAccounting: Charge Penalty Recipient Set', - async () => { - const txHash = '0xf62269919009e1cb9c6ea8c29cb6c83f9c1d113d97d401ed5ff2b696cee6d82f' - - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + TEST_TIMEOUT, + ) + + test( + '🚨 CSAccounting: Charge Penalty Recipient Set', + async () => { + const txHash = '0xf62269919009e1cb9c6ea8c29cb6c83f9c1d113d97d401ed5ff2b696cee6d82f' + + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const results = await csAccountingSrv.handleTransaction(transactionDto) + + expect(results).toMatchSnapshot() + expect(results.length).toBe(1) }, - hash: trx.hash, - } - - const results = await csAccountingSrv.handleTransaction(transactionDto) - - expect(results).toMatchSnapshot() - expect(results.length).toBe(1) - }, - TEST_TIMEOUT, - ) - - test( - '🔴 CSAccounting: Bond Curve Set', - async () => { - const txHash = '0xcea4d214c8f6e4f3415fc941fdb6802f4243a7b3e12ba5288cf7e7df39d457a0' - - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + TEST_TIMEOUT, + ) + + test( + '🔴 CSAccounting: Bond Curve Set', + async () => { + const txHash = '0xcea4d214c8f6e4f3415fc941fdb6802f4243a7b3e12ba5288cf7e7df39d457a0' + + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const results = await csAccountingSrv.handleTransaction(transactionDto) + + expect(results).toMatchSnapshot() + expect(results.length).toBe(1) }, - hash: trx.hash, - } - - const results = await csAccountingSrv.handleTransaction(transactionDto) - - expect(results).toMatchSnapshot() - expect(results.length).toBe(1) - }, - TEST_TIMEOUT, - ) + TEST_TIMEOUT, + ) }) diff --git a/csm-alerts/src/services/CSAccounting/CSAccounting.srv.ts b/csm-alerts/src/services/CSAccounting/CSAccounting.srv.ts index 604dd404c..e4c63a066 100644 --- a/csm-alerts/src/services/CSAccounting/CSAccounting.srv.ts +++ b/csm-alerts/src/services/CSAccounting/CSAccounting.srv.ts @@ -1,11 +1,11 @@ import { - BlockEvent, - Finding, - FindingSeverity, - FindingType, - TransactionEvent, - ethers, - filterLog, + BlockEvent, + Finding, + FindingSeverity, + FindingType, + TransactionEvent, + ethers, + filterLog, } from '@fortanetwork/forta-bot' import { Logger } from 'winston' @@ -18,7 +18,11 @@ import { RedefineMode, requireWithTier } from '../../utils/require' import { formatEther } from '../../utils/string' import * as Constants from '../constants' -const { DEPLOYED_ADDRESSES } = requireWithTier(module, '../constants', RedefineMode.Merge) +const { DEPLOYED_ADDRESSES } = requireWithTier( + module, + '../constants', + RedefineMode.Merge, +) const CHECK_ACCOUNTING_INTERVAL_BLOCKS = 301 // ~ every hour const CHECK_OPERATORS_INTERVAL_BLOCKS = 2401 // ~ 3 times a day @@ -29,216 +33,228 @@ const CURVE_EARLY_ADOPTION_ID = 1n const CURVE_DEFAULT_ID = 0n export class CSAccountingSrv { - private readonly logger: Logger - - private lastFiredAt = { - accountingExcessShares: 0, - totalLockAlert: 0, - avgBondAlert: 0, - } - - constructor() { - this.logger = getLogger(CSAccountingSrv.name) - } - - async handleBlock(blockEvent: BlockEvent, provider: ethers.Provider): Promise { - return [ - ...(await this.checkAccountingSharesDiscrepancy(blockEvent, provider)), - ...(await this.handleBondAndLockValues(blockEvent, provider)), - ] - } - - public async handleTransaction(txEvent: TransactionEvent, provider: ethers.Provider): Promise { - return [...this.handleStETHApprovalEvents(txEvent), ...this.handleSetBondCurveEvent(txEvent)] - } - - public async handleBondAndLockValues(blockEvent: BlockEvent, provider: ethers.Provider): Promise { - if (blockEvent.blockNumber % CHECK_OPERATORS_INTERVAL_BLOCKS !== 0 && !IS_CLI) { - return [] - } - - const accounting = CSAccounting__factory.connect(DEPLOYED_ADDRESSES.CS_ACCOUNTING, provider) - const csm = CSModule__factory.connect(DEPLOYED_ADDRESSES.CS_MODULE, provider) - const ethCallOpts = { blockTag: blockEvent.blockHash } - - const operatorsCount = await csm.getNodeOperatorsCount(ethCallOpts) - this.logger.debug(`Total operators count: ${operatorsCount}`) - - let totalValidators = 0n - let totalBondWei = 0n - let totalLockWei = 0n + private readonly logger: Logger - for (let noId = 0; noId < operatorsCount; noId++) { - totalValidators += await csm.getNodeOperatorNonWithdrawnKeys(noId, ethCallOpts) - totalBondWei += (await accounting.getBondSummary(noId, ethCallOpts)).current - totalLockWei += await accounting.getActualLockedBond(noId, ethCallOpts) - } - this.logger.debug(`Read ${operatorsCount} operators info`) - - const avgBondWei = totalBondWei / totalValidators - - this.logger.debug(`Averave bond is ${formatEther(avgBondWei)}`) - this.logger.debug(`Total bond is ${formatEther(totalBondWei)}`) - this.logger.debug(`Total lock is ${formatEther(totalLockWei)}`) - - const now = blockEvent.block.timestamp - const out: Finding[] = [] - - if (now - this.lastFiredAt.avgBondAlert > SECONDS_PER_DAY) { - if (avgBondWei < BOND_AVG_WEI_MIN) { - const f = Finding.fromObject({ - name: `🟢 CSAccounting: Average bond value for a validator is below threshold.`, - description: `Average bond value for a validator is less than ${formatEther(BOND_AVG_WEI_MIN)}`, - alertId: 'CS-ACCOUNTING-AVERAGE-BOND-VALUE', - // source: sourceFromEvent(blockEvent), - severity: FindingSeverity.Low, - type: FindingType.Info, - }) - - out.push(f) - this.lastFiredAt.avgBondAlert = now - } + private lastFiredAt = { + accountingExcessShares: 0, + totalLockAlert: 0, + avgBondAlert: 0, } - if (now - this.lastFiredAt.totalLockAlert > SECONDS_PER_DAY) { - if (totalLockWei > LOCK_TOTAL_WEI_MAX) { - const f = Finding.fromObject({ - name: `🟢 Total bond lock exceeds threshold.`, - description: `Total bond lock is more than ${formatEther(LOCK_TOTAL_WEI_MAX)}`, - alertId: 'CS-ACCOUNTING-TOTAL-BOND-LOCK', - // NOTE: Do not include the source to reach quorum. - // source: sourceFromEvent(blockEvent), - severity: FindingSeverity.Low, - type: FindingType.Info, - }) - - out.push(f) - this.lastFiredAt.totalLockAlert = now - } + constructor() { + this.logger = getLogger(CSAccountingSrv.name) } - return out - } - - async checkAccountingSharesDiscrepancy(blockEvent: BlockEvent, provider: ethers.Provider): Promise { - if (blockEvent.blockNumber % CHECK_ACCOUNTING_INTERVAL_BLOCKS !== 0 && !IS_CLI) { - return [] + async handleBlock(blockEvent: BlockEvent, provider: ethers.Provider): Promise { + return [ + ...(await this.checkAccountingSharesDiscrepancy(blockEvent, provider)), + ...(await this.handleBondAndLockValues(blockEvent, provider)), + ] } - const accounting = CSAccounting__factory.connect(DEPLOYED_ADDRESSES.CS_ACCOUNTING, provider) - const steth = Lido__factory.connect(DEPLOYED_ADDRESSES.LIDO_STETH, provider) - - const totalBondShares = await accounting.totalBondShares({ blockTag: blockEvent.blockHash }) - const actualBalance = await steth.sharesOf(accounting, { blockTag: blockEvent.blockHash }) - const diff = totalBondShares - actualBalance - - const now = blockEvent.block.timestamp - const out: Finding[] = [] - - if (now - this.lastFiredAt.accountingExcessShares > SECONDS_PER_DAY) { - if (diff > ACCOUNTING_BALANCE_EXCESS_SHARES_MAX) { - const f = Finding.fromObject({ - name: `🟢 Shares to recover on CSAccounting.`, - description: `There's a valuable amount of shares to recover on CSAccounting.`, - alertId: 'CS-ACCOUNTING-EXCESS-SHARES', - // NOTE: Do not include the source to reach quorum. - // source: sourceFromEvent(blockEvent), - severity: FindingSeverity.Low, - type: FindingType.Info, - }) - out.push(f) - this.lastFiredAt.accountingExcessShares = now - } + public async handleTransaction( + txEvent: TransactionEvent, + provider: ethers.Provider, + ): Promise { + return [ + ...this.handleStETHApprovalEvents(txEvent), + ...this.handleSetBondCurveEvent(txEvent), + ] } - // NOTE: This is a critical invariant, so we fire every CHECK_ACCOUNTING_INTERVAL_BLOCKS. - if (diff < 0n) { - const f = Finding.fromObject({ - name: '🚨 Not enough shares on CSAccounting', - description: 'sharesOf(CSAccounting) < CSAccounting.totalBondShares', - alertId: 'CS-ACCOUNTING-NOT-ENOUGH-SHARES', - // NOTE: Do not include the source to reach quorum. - // source: sourceFromEvent(blockEvent), - severity: FindingSeverity.Critical, - type: FindingType.Info, - }) - out.push(f) + public async handleBondAndLockValues( + blockEvent: BlockEvent, + provider: ethers.Provider, + ): Promise { + if (blockEvent.blockNumber % CHECK_OPERATORS_INTERVAL_BLOCKS !== 0 && !IS_CLI) { + return [] + } + + const accounting = CSAccounting__factory.connect(DEPLOYED_ADDRESSES.CS_ACCOUNTING, provider) + const csm = CSModule__factory.connect(DEPLOYED_ADDRESSES.CS_MODULE, provider) + const ethCallOpts = { blockTag: blockEvent.blockHash } + + const operatorsCount = await csm.getNodeOperatorsCount(ethCallOpts) + this.logger.debug(`Total operators count: ${operatorsCount}`) + + let totalValidators = 0n + let totalBondWei = 0n + let totalLockWei = 0n + + for (let noId = 0; noId < operatorsCount; noId++) { + totalValidators += await csm.getNodeOperatorNonWithdrawnKeys(noId, ethCallOpts) + totalBondWei += (await accounting.getBondSummary(noId, ethCallOpts)).current + totalLockWei += await accounting.getActualLockedBond(noId, ethCallOpts) + } + this.logger.debug(`Read ${operatorsCount} operators info`) + + const avgBondWei = totalBondWei / totalValidators + + this.logger.debug(`Averave bond is ${formatEther(avgBondWei)}`) + this.logger.debug(`Total bond is ${formatEther(totalBondWei)}`) + this.logger.debug(`Total lock is ${formatEther(totalLockWei)}`) + + const now = blockEvent.block.timestamp + const out: Finding[] = [] + + if (now - this.lastFiredAt.avgBondAlert > SECONDS_PER_DAY) { + if (avgBondWei < BOND_AVG_WEI_MIN) { + const f = Finding.fromObject({ + name: `🟢 CSAccounting: Average bond value for a validator is below threshold.`, + description: `Average bond value for a validator is less than ${formatEther(BOND_AVG_WEI_MIN)}`, + alertId: 'CS-ACCOUNTING-AVERAGE-BOND-VALUE', + // source: sourceFromEvent(blockEvent), + severity: FindingSeverity.Low, + type: FindingType.Info, + }) + + out.push(f) + this.lastFiredAt.avgBondAlert = now + } + } + + if (now - this.lastFiredAt.totalLockAlert > SECONDS_PER_DAY) { + if (totalLockWei > LOCK_TOTAL_WEI_MAX) { + const f = Finding.fromObject({ + name: `🟢 Total bond lock exceeds threshold.`, + description: `Total bond lock is more than ${formatEther(LOCK_TOTAL_WEI_MAX)}`, + alertId: 'CS-ACCOUNTING-TOTAL-BOND-LOCK', + // NOTE: Do not include the source to reach quorum. + // source: sourceFromEvent(blockEvent), + severity: FindingSeverity.Low, + type: FindingType.Info, + }) + + out.push(f) + this.lastFiredAt.totalLockAlert = now + } + } + + return out } - return out - } - - // TODO: Does it makes sense for us at all? - handleStETHApprovalEvents(txEvent: TransactionEvent): Finding[] { - const approvalEvents = filterLog( - txEvent.logs, - Lido__factory.createInterface().getEvent('TransferShares').format('full'), - DEPLOYED_ADDRESSES.LIDO_STETH, - ) - - const out: Finding[] = [] - for (const event of approvalEvents) { - if (event.args.owner === DEPLOYED_ADDRESSES.CS_ACCOUNTING) { - const f = Finding.fromObject({ - name: `🔵 Lido stETH: Approval`, - description: `${event.args.spender} received allowance from ${event.args.owner} to ${event.args.value}`, - alertId: 'STETH-APPROVAL', - source: sourceFromEvent(txEvent), - severity: FindingSeverity.Low, - type: FindingType.Info, - }) - out.push(f) - } + async checkAccountingSharesDiscrepancy( + blockEvent: BlockEvent, + provider: ethers.Provider, + ): Promise { + if (blockEvent.blockNumber % CHECK_ACCOUNTING_INTERVAL_BLOCKS !== 0 && !IS_CLI) { + return [] + } + + const accounting = CSAccounting__factory.connect(DEPLOYED_ADDRESSES.CS_ACCOUNTING, provider) + const steth = Lido__factory.connect(DEPLOYED_ADDRESSES.LIDO_STETH, provider) + + const totalBondShares = await accounting.totalBondShares({ blockTag: blockEvent.blockHash }) + const actualBalance = await steth.sharesOf(accounting, { blockTag: blockEvent.blockHash }) + const diff = totalBondShares - actualBalance + + const now = blockEvent.block.timestamp + const out: Finding[] = [] + + if (now - this.lastFiredAt.accountingExcessShares > SECONDS_PER_DAY) { + if (diff > ACCOUNTING_BALANCE_EXCESS_SHARES_MAX) { + const f = Finding.fromObject({ + name: `🟢 Shares to recover on CSAccounting.`, + description: `There's a valuable amount of shares to recover on CSAccounting.`, + alertId: 'CS-ACCOUNTING-EXCESS-SHARES', + // NOTE: Do not include the source to reach quorum. + // source: sourceFromEvent(blockEvent), + severity: FindingSeverity.Low, + type: FindingType.Info, + }) + out.push(f) + this.lastFiredAt.accountingExcessShares = now + } + } + + // NOTE: This is a critical invariant, so we fire every CHECK_ACCOUNTING_INTERVAL_BLOCKS. + if (diff < 0n) { + const f = Finding.fromObject({ + name: '🚨 Not enough shares on CSAccounting', + description: 'sharesOf(CSAccounting) < CSAccounting.totalBondShares', + alertId: 'CS-ACCOUNTING-NOT-ENOUGH-SHARES', + // NOTE: Do not include the source to reach quorum. + // source: sourceFromEvent(blockEvent), + severity: FindingSeverity.Critical, + type: FindingType.Info, + }) + out.push(f) + } + + return out } - return out - } - - handleSetBondCurveEvent(txEvent: TransactionEvent): Finding[] { - const events = filterLog( - txEvent.logs, - CSAccounting__factory.createInterface().getEvent('BondCurveSet').format('full'), - DEPLOYED_ADDRESSES.CS_ACCOUNTING, - ) - - const out: Finding[] = [] - - for (const e of events) { - if (e.args.curveId == CURVE_EARLY_ADOPTION_ID) { - out.push( - Finding.fromObject({ - alertId: 'CS-ACCOUNTING-BOND-CURVE-SET', - name: '🔵 CSAccounting: Bond curve set', - description: `Early adoption bond curve set for Node Operator #${e.args.nodeOperatorId}`, - source: sourceFromEvent(txEvent), - severity: FindingSeverity.Info, - type: FindingType.Info, - }), - ) - } else if (e.args.curveId == CURVE_DEFAULT_ID) { - out.push( - Finding.fromObject({ - alertId: 'CS-ACCOUNTING-BOND-CURVE-SET', - name: '🔵 CSAccounting: Bond curve set', - description: `Bond curve was reset for Node Operator #${e.args.nodeOperatorId}`, - source: sourceFromEvent(txEvent), - severity: FindingSeverity.Info, - type: FindingType.Info, - }), - ) - } else { - out.push( - Finding.fromObject({ - alertId: 'CS-ACCOUNTING-BOND-CURVE-SET', - name: '🔵 CSAccounting: Bond curve set', - description: `Bond curve set for Node Operator #${e.args.nodeOperatorId} with curve ID ${e.args.curveId}`, - source: sourceFromEvent(txEvent), - severity: FindingSeverity.Info, - type: FindingType.Info, - }), + + // TODO: Does it makes sense for us at all? + handleStETHApprovalEvents(txEvent: TransactionEvent): Finding[] { + const approvalEvents = filterLog( + txEvent.logs, + Lido__factory.createInterface().getEvent('TransferShares').format('full'), + DEPLOYED_ADDRESSES.LIDO_STETH, ) - } + + const out: Finding[] = [] + for (const event of approvalEvents) { + if (event.args.owner === DEPLOYED_ADDRESSES.CS_ACCOUNTING) { + const f = Finding.fromObject({ + name: `🔵 Lido stETH: Approval`, + description: `${event.args.spender} received allowance from ${event.args.owner} to ${event.args.value}`, + alertId: 'STETH-APPROVAL', + source: sourceFromEvent(txEvent), + severity: FindingSeverity.Low, + type: FindingType.Info, + }) + out.push(f) + } + } + return out } - return out - } + handleSetBondCurveEvent(txEvent: TransactionEvent): Finding[] { + const events = filterLog( + txEvent.logs, + CSAccounting__factory.createInterface().getEvent('BondCurveSet').format('full'), + DEPLOYED_ADDRESSES.CS_ACCOUNTING, + ) + + const out: Finding[] = [] + + for (const e of events) { + if (e.args.curveId == CURVE_EARLY_ADOPTION_ID) { + out.push( + Finding.fromObject({ + alertId: 'CS-ACCOUNTING-BOND-CURVE-SET', + name: '🔵 CSAccounting: Bond curve set', + description: `Early adoption bond curve set for Node Operator #${e.args.nodeOperatorId}`, + source: sourceFromEvent(txEvent), + severity: FindingSeverity.Info, + type: FindingType.Info, + }), + ) + } else if (e.args.curveId == CURVE_DEFAULT_ID) { + out.push( + Finding.fromObject({ + alertId: 'CS-ACCOUNTING-BOND-CURVE-SET', + name: '🔵 CSAccounting: Bond curve set', + description: `Bond curve was reset for Node Operator #${e.args.nodeOperatorId}`, + source: sourceFromEvent(txEvent), + severity: FindingSeverity.Info, + type: FindingType.Info, + }), + ) + } else { + out.push( + Finding.fromObject({ + alertId: 'CS-ACCOUNTING-BOND-CURVE-SET', + name: '🔵 CSAccounting: Bond curve set', + description: `Bond curve set for Node Operator #${e.args.nodeOperatorId} with curve ID ${e.args.curveId}`, + source: sourceFromEvent(txEvent), + severity: FindingSeverity.Info, + type: FindingType.Info, + }), + ) + } + } + + return out + } } diff --git a/csm-alerts/src/services/CSFeeDistributor/CSFeeDistributor.srv.spec.ts b/csm-alerts/src/services/CSFeeDistributor/CSFeeDistributor.srv.spec.ts index b006e740b..c36ebab3b 100644 --- a/csm-alerts/src/services/CSFeeDistributor/CSFeeDistributor.srv.spec.ts +++ b/csm-alerts/src/services/CSFeeDistributor/CSFeeDistributor.srv.spec.ts @@ -2,10 +2,10 @@ import { DeploymentAddress, DeploymentAddresses } from '../../utils/constants.ho import { expect } from '@jest/globals' import { TransactionDto } from '../../entity/events' import { - CSModule__factory, - CSAccounting__factory, - CSFeeDistributor__factory, - CSFeeOracle__factory, + CSModule__factory, + CSAccounting__factory, + CSFeeDistributor__factory, + CSFeeOracle__factory, } from '../../generated/typechain' import { getCSFeeDistributorEvents } from '../../utils/events/cs_fee_distributor_events' import { CSFeeDistributorSrv, ICSFeeDistributorClient } from './CSFeeDistributor.srv' @@ -19,70 +19,79 @@ import { Metrics } from '../../utils/metrics/metrics' const TEST_TIMEOUT = 120_000 // ms describe('CsFeeDistributor event tests', () => { - const chainId = 17000 + const chainId = 17000 - const logger: Winston.Logger = Winston.createLogger({ - format: Winston.format.simple(), - transports: [new Winston.transports.Console()], - }) + const logger: Winston.Logger = Winston.createLogger({ + format: Winston.format.simple(), + transports: [new Winston.transports.Console()], + }) - const address: DeploymentAddress = DeploymentAddresses + const address: DeploymentAddress = DeploymentAddresses - const fortaEthersProvider = new ethers.providers.JsonRpcProvider(getFortaConfig().jsonRpcUrl, chainId) - const csModuleRunner = CSModule__factory.connect(address.CS_MODULE_ADDRESS, fortaEthersProvider) - const csAccountingRunner = CSAccounting__factory.connect(address.CS_ACCOUNTING_ADDRESS, fortaEthersProvider) - const csFeeDistributorRunner = CSFeeDistributor__factory.connect( - address.CS_FEE_DISTRIBUTOR_ADDRESS, - fortaEthersProvider, - ) - const csFeeOracleRunner = CSFeeOracle__factory.connect(address.CS_FEE_ORACLE_ADDRESS, fortaEthersProvider) + const fortaEthersProvider = new ethers.providers.JsonRpcProvider( + getFortaConfig().jsonRpcUrl, + chainId, + ) + const csModuleRunner = CSModule__factory.connect(address.CS_MODULE_ADDRESS, fortaEthersProvider) + const csAccountingRunner = CSAccounting__factory.connect( + address.CS_ACCOUNTING_ADDRESS, + fortaEthersProvider, + ) + const csFeeDistributorRunner = CSFeeDistributor__factory.connect( + address.CS_FEE_DISTRIBUTOR_ADDRESS, + fortaEthersProvider, + ) + const csFeeOracleRunner = CSFeeOracle__factory.connect( + address.CS_FEE_ORACLE_ADDRESS, + fortaEthersProvider, + ) - const registry = new promClient.Registry() - const m = new Metrics(registry, 'test_') + const registry = new promClient.Registry() + const m = new Metrics(registry, 'test_') - const csFeeDistributorClient: ICSFeeDistributorClient = new ETHProvider( - logger, - m, - fortaEthersProvider, - csModuleRunner, - csAccountingRunner, - csFeeDistributorRunner, - csFeeOracleRunner, - ) + const csFeeDistributorClient: ICSFeeDistributorClient = new ETHProvider( + logger, + m, + fortaEthersProvider, + csModuleRunner, + csAccountingRunner, + csFeeDistributorRunner, + csFeeOracleRunner, + ) - const csFeeDistributorSrv = new CSFeeDistributorSrv( - logger, - csFeeDistributorClient, - getCSFeeDistributorEvents(address.CS_FEE_DISTRIBUTOR_ADDRESS), - address.CS_ACCOUNTING_ADDRESS, - address.CS_FEE_DISTRIBUTOR_ADDRESS, - address.LIDO_STETH_ADDRESS, - address.HASH_CONSENSUS_ADDRESS, - ) + const csFeeDistributorSrv = new CSFeeDistributorSrv( + logger, + csFeeDistributorClient, + getCSFeeDistributorEvents(address.CS_FEE_DISTRIBUTOR_ADDRESS), + address.CS_ACCOUNTING_ADDRESS, + address.CS_FEE_DISTRIBUTOR_ADDRESS, + address.LIDO_STETH_ADDRESS, + address.HASH_CONSENSUS_ADDRESS, + ) - test( - '🔵 INFO: DistributionDataUpdated', - async () => { - const txHash = '0x33a9fb726d09d543c417cb0985a41a7bee39e81e8536b5969784a520f4d2e0c1' + test( + '🔵 INFO: DistributionDataUpdated', + async () => { + const txHash = '0x33a9fb726d09d543c417cb0985a41a7bee39e81e8536b5969784a520f4d2e0c1' - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, - }, - hash: trx.hash, - } + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } - const results = await csFeeDistributorSrv.handleTransaction(transactionDto) + const results = await csFeeDistributorSrv.handleTransaction(transactionDto) - expect(results).toMatchSnapshot() - expect(results.length).toBe(1) - }, - TEST_TIMEOUT, - ) + expect(results).toMatchSnapshot() + expect(results.length).toBe(1) + }, + TEST_TIMEOUT, + ) }) diff --git a/csm-alerts/src/services/CSFeeDistributor/CSFeeDistributor.srv.ts b/csm-alerts/src/services/CSFeeDistributor/CSFeeDistributor.srv.ts index 1e2faeec8..f0444c89b 100644 --- a/csm-alerts/src/services/CSFeeDistributor/CSFeeDistributor.srv.ts +++ b/csm-alerts/src/services/CSFeeDistributor/CSFeeDistributor.srv.ts @@ -1,16 +1,20 @@ import { - BlockEvent, - Finding, - FindingSeverity, - FindingType, - TransactionEvent, - ethers, - filterLog, + BlockEvent, + Finding, + FindingSeverity, + FindingType, + TransactionEvent, + ethers, + filterLog, } from '@fortanetwork/forta-bot' import { Provider } from 'ethers' import { Logger } from 'winston' -import { CSFeeDistributor__factory, HashConsensus__factory, Lido__factory } from '../../generated/typechain' +import { + CSFeeDistributor__factory, + HashConsensus__factory, + Lido__factory, +} from '../../generated/typechain' import { getLogger } from '../../logger' import { SECONDS_PER_DAY, SECONDS_PER_SLOT, SLOTS_PER_EPOCH } from '../../shared/constants' import { getEpoch } from '../../utils/epochs' @@ -20,218 +24,247 @@ import { RedefineMode, requireWithTier } from '../../utils/require' import { formatDelay } from '../../utils/time' import * as Constants from '../constants' -const { DEPLOYED_ADDRESSES } = requireWithTier(module, '../constants', RedefineMode.Merge) +const { DEPLOYED_ADDRESSES } = requireWithTier( + module, + '../constants', + RedefineMode.Merge, +) const ICSFeeDistributor = CSFeeDistributor__factory.createInterface() export class CSFeeDistributorSrv { - private readonly logger: Logger - - private lastFiredAt = { - distributionUpdateOverdue: 0, - } - - private state = { - lastDistributionUpdatedAt: 0, - frameInitialEpoch: 0, - frameInSlots: 0, - } - - constructor() { - this.logger = getLogger(CSFeeDistributorSrv.name) - } - - async initialize(blockIdentifier: ethers.BlockTag, provider: ethers.Provider): Promise { - const hc = HashConsensus__factory.connect(DEPLOYED_ADDRESSES.HASH_CONSENSUS, provider) - const frameConfig = await hc.getFrameConfig({ blockTag: blockIdentifier }) - this.state.frameInitialEpoch = Number(frameConfig.initialEpoch) - - const frameInSlots = Number(frameConfig.epochsPerFrame) * SLOTS_PER_EPOCH - this.state.frameInSlots = frameInSlots - - const distributor = CSFeeDistributor__factory.connect(DEPLOYED_ADDRESSES.CS_FEE_DISTRIBUTOR, provider) - if ((await distributor.treeRoot({ blockTag: blockIdentifier })) === ethers.ZeroHash) { - this.logger.debug('No distribution happened so far') - return + private readonly logger: Logger + + private lastFiredAt = { + distributionUpdateOverdue: 0, } - const toBlock = await provider.getBlock(blockIdentifier) - if (!toBlock) { - throw Error('Unable to get the latest block') + private state = { + lastDistributionUpdatedAt: 0, + frameInitialEpoch: 0, + frameInSlots: 0, } - const distributedEvents = await getLogsByChunks( - distributor as any, - distributor.filters.DistributionDataUpdated, - toBlock.number - frameInSlots * 2, - toBlock.number, - ) - - const lastDistributionEvent = distributedEvents.sort((a, b) => a.blockNumber - b.blockNumber).pop() - if (!lastDistributionEvent) { - this.logger.debug('No distribution event found') - return + constructor() { + this.logger = getLogger(CSFeeDistributorSrv.name) } - this.state.lastDistributionUpdatedAt = (await lastDistributionEvent.getBlock())?.timestamp ?? 0 - this.logger.debug(`Last distribution observed at timestamp ${this.state.lastDistributionUpdatedAt}`) - } + async initialize(blockIdentifier: ethers.BlockTag, provider: ethers.Provider): Promise { + const hc = HashConsensus__factory.connect(DEPLOYED_ADDRESSES.HASH_CONSENSUS, provider) + const frameConfig = await hc.getFrameConfig({ blockTag: blockIdentifier }) + this.state.frameInitialEpoch = Number(frameConfig.initialEpoch) + + const frameInSlots = Number(frameConfig.epochsPerFrame) * SLOTS_PER_EPOCH + this.state.frameInSlots = frameInSlots + + const distributor = CSFeeDistributor__factory.connect( + DEPLOYED_ADDRESSES.CS_FEE_DISTRIBUTOR, + provider, + ) + if ((await distributor.treeRoot({ blockTag: blockIdentifier })) === ethers.ZeroHash) { + this.logger.debug('No distribution happened so far') + return + } + + const toBlock = await provider.getBlock(blockIdentifier) + if (!toBlock) { + throw Error('Unable to get the latest block') + } + + const distributedEvents = await getLogsByChunks( + distributor as any, + distributor.filters.DistributionDataUpdated, + toBlock.number - frameInSlots * 2, + toBlock.number, + ) + + const lastDistributionEvent = distributedEvents + .sort((a, b) => a.blockNumber - b.blockNumber) + .pop() + if (!lastDistributionEvent) { + this.logger.debug('No distribution event found') + return + } + + this.state.lastDistributionUpdatedAt = + (await lastDistributionEvent.getBlock())?.timestamp ?? 0 + this.logger.debug( + `Last distribution observed at timestamp ${this.state.lastDistributionUpdatedAt}`, + ) + } - async handleBlock(blockEvent: BlockEvent, provider: Provider): Promise { - return [...this.handleDistributionOverdue(blockEvent), ...(await this.checkInvariants(blockEvent, provider))] - } + async handleBlock(blockEvent: BlockEvent, provider: Provider): Promise { + return [ + ...this.handleDistributionOverdue(blockEvent), + ...(await this.checkInvariants(blockEvent, provider)), + ] + } - public async handleTransaction(txEvent: TransactionEvent, provider: ethers.Provider): Promise { - const distributionDataUpdatedEvents = filterLog( - txEvent.logs, - ICSFeeDistributor.getEvent('DistributionDataUpdated').format('full'), - DEPLOYED_ADDRESSES.CS_FEE_DISTRIBUTOR, - ) + public async handleTransaction( + txEvent: TransactionEvent, + provider: ethers.Provider, + ): Promise { + const distributionDataUpdatedEvents = filterLog( + txEvent.logs, + ICSFeeDistributor.getEvent('DistributionDataUpdated').format('full'), + DEPLOYED_ADDRESSES.CS_FEE_DISTRIBUTOR, + ) + + if (distributionDataUpdatedEvents.length > 0) { + this.state.lastDistributionUpdatedAt = txEvent.block.timestamp + } + + return ( + await Promise.all([ + this.handleTransferSharesInvalidReceiver(txEvent), + this.handleRevertedTx(txEvent, provider), + ]) + ).flat() + } - if (distributionDataUpdatedEvents.length > 0) { - this.state.lastDistributionUpdatedAt = txEvent.block.timestamp + private handleDistributionOverdue(blockEvent: BlockEvent): Finding[] { + if (this.state.lastDistributionUpdatedAt === 0) { + this.logger.debug('No previous distribution observed so far') + return [] + } + + const now = blockEvent.block.timestamp + + if (getEpoch(blockEvent.chainId, now) < this.state.frameInitialEpoch) { + this.logger.debug('Initial epoch has not been reached yet') + return [] + } + + if (now - this.lastFiredAt.distributionUpdateOverdue < SECONDS_PER_DAY) { + return [] + } + + // Just add 1 day to the frame length because it seems as a good approximation of more complex approach. + // TODO: Fetch the current frame every time? + const distributionIntervalSecondsMax = + this.state.frameInSlots * SECONDS_PER_SLOT + SECONDS_PER_DAY + const distributionDelaySeconds = now - this.state.lastDistributionUpdatedAt + if (distributionDelaySeconds < distributionIntervalSecondsMax) { + return [] + } + + this.lastFiredAt.distributionUpdateOverdue = now + + return [ + Finding.fromObject({ + name: `🔴 CSFeeDistributor: Distribution overdue`, + description: `There has been no DistributionDataUpdated event for more than ${formatDelay(distributionIntervalSecondsMax)}.`, + alertId: 'CSFEE-DISTRIBUTION-OVERDUE', + // NOTE: Do not include the source to reach quorum. + // source: sourceFromEvent(blockEvent), + severity: FindingSeverity.High, + type: FindingType.Info, + }), + ] } - return ( - await Promise.all([this.handleTransferSharesInvalidReceiver(txEvent), this.handleRevertedTx(txEvent, provider)]) - ).flat() - } + public handleTransferSharesInvalidReceiver(txEvent: TransactionEvent): Finding[] { + const transferSharesEvents = filterLog( + txEvent.logs, + Lido__factory.createInterface().getEvent('TransferShares').format('full'), + DEPLOYED_ADDRESSES.LIDO_STETH, + ) + + const out: Finding[] = [] + for (const event of transferSharesEvents) { + if ( + event.args.from.toLowerCase() === + DEPLOYED_ADDRESSES.CS_FEE_DISTRIBUTOR.toLowerCase() && + event.args.to.toLowerCase() !== DEPLOYED_ADDRESSES.CS_ACCOUNTING.toLowerCase() + ) { + const f = Finding.fromObject({ + name: `🚨 CSFeeDistributor: Invalid TransferShares receiver`, + description: `TransferShares from CSFeeDistributor to an invalid address ${event.args.to} (expected CSAccounting)`, + alertId: 'CSFEE-DISTRIBUTOR-INVALID-TRANSFER', + source: sourceFromEvent(txEvent), + severity: FindingSeverity.Critical, + type: FindingType.Info, + }) + + out.push(f) + } + } + return out + } - private handleDistributionOverdue(blockEvent: BlockEvent): Finding[] { - if (this.state.lastDistributionUpdatedAt === 0) { - this.logger.debug('No previous distribution observed so far') - return [] + private async handleRevertedTx( + txEvent: TransactionEvent, + provider: ethers.Provider, + ): Promise { + // if (!(DEPLOYED_ADDRESSES.CS_FEE_DISTRIBUTOR.toLowerCase() in txEvent.addresses)) { + // return [] + // } + // + // const txReceipt = await provider.getTransactionReceipt(txEvent.hash) + // // Nothing to do if the transaction succeeded. + // if (txReceipt?.status === 0) { + // this.logger.debug(`Skipping successful transaction ${txEvent.hash}`) + // return [] + // } + // + // // FIXME: I can't find a node with getTransactionResult call working. + // let decodedLog: ethers.ErrorDescription | null = null + // try { + // const data = await txReceipt?.getResult() + // decodedLog = CSFeeDistributorInterface.parseError(data ?? '') + // } catch (error: any) { + // if (error.code === 'UNSUPPORTED_OPERATION') { + // this.logger.debug('Ethereum RPC does not support `getTransactionResult`') + // return [] + // } + // + // throw error + // } + // + // const reason = decodedLog?.name + // if (!reason) { + // return [] + // } + // + // switch (reason) { + // case 'InvalidShares': + // return [failedTxAlert(txEvent, 'CSFeeOracle reports incorrect amount of shares to distribute', 'InvalidShares')] + // case 'NotEnoughShares': + // return [failedTxAlert(txEvent, 'CSFeeDistributor internal accounting error', 'NotEnoughShares')] + // case 'InvalidTreeRoot': + // return [failedTxAlert(txEvent, 'CSFeeOracle built incorrect report', 'InvalidTreeRoot')] + // case 'InvalidTreeCID': + // return [failedTxAlert(txEvent, 'CSFeeOracle built incorrect report', 'InvalidTreeCID')] + // default: + // this.logger.warn(`Unrecognized revert reason: ${reason}`) + // } + + return [] } - const now = blockEvent.block.timestamp + private async checkInvariants(blockEvent: BlockEvent, provider: ethers.Provider) { + const distributor = CSFeeDistributor__factory.connect( + DEPLOYED_ADDRESSES.CS_FEE_DISTRIBUTOR, + provider, + ) + const steth = Lido__factory.connect(DEPLOYED_ADDRESSES.LIDO_STETH, provider) - if (getEpoch(blockEvent.chainId, now) < this.state.frameInitialEpoch) { - this.logger.debug('Initial epoch has not been reached yet') - return [] - } + const blockTag = blockEvent.blockHash - if (now - this.lastFiredAt.distributionUpdateOverdue < SECONDS_PER_DAY) { - return [] - } + const out: Finding[] = [] - // Just add 1 day to the frame length because it seems as a good approximation of more complex approach. - // TODO: Fetch the current frame every time? - const distributionIntervalSecondsMax = this.state.frameInSlots * SECONDS_PER_SLOT + SECONDS_PER_DAY - const distributionDelaySeconds = now - this.state.lastDistributionUpdatedAt - if (distributionDelaySeconds < distributionIntervalSecondsMax) { - return [] - } + const treeRoot = await distributor.treeRoot({ blockTag }) + const treeCid = await distributor.treeCid({ blockTag }) + if (treeRoot !== ethers.ZeroHash && treeCid === '') { + out.push(invariantAlert(blockEvent, 'Tree exists, but no CID.')) + } - this.lastFiredAt.distributionUpdateOverdue = now - - return [ - Finding.fromObject({ - name: `🔴 CSFeeDistributor: Distribution overdue`, - description: `There has been no DistributionDataUpdated event for more than ${formatDelay(distributionIntervalSecondsMax)}.`, - alertId: 'CSFEE-DISTRIBUTION-OVERDUE', - // NOTE: Do not include the source to reach quorum. - // source: sourceFromEvent(blockEvent), - severity: FindingSeverity.High, - type: FindingType.Info, - }), - ] - } - - public handleTransferSharesInvalidReceiver(txEvent: TransactionEvent): Finding[] { - const transferSharesEvents = filterLog( - txEvent.logs, - Lido__factory.createInterface().getEvent('TransferShares').format('full'), - DEPLOYED_ADDRESSES.LIDO_STETH, - ) - - const out: Finding[] = [] - for (const event of transferSharesEvents) { - if ( - event.args.from.toLowerCase() === DEPLOYED_ADDRESSES.CS_FEE_DISTRIBUTOR.toLowerCase() && - event.args.to.toLowerCase() !== DEPLOYED_ADDRESSES.CS_ACCOUNTING.toLowerCase() - ) { - const f = Finding.fromObject({ - name: `🚨 CSFeeDistributor: Invalid TransferShares receiver`, - description: `TransferShares from CSFeeDistributor to an invalid address ${event.args.to} (expected CSAccounting)`, - alertId: 'CSFEE-DISTRIBUTOR-INVALID-TRANSFER', - source: sourceFromEvent(txEvent), - severity: FindingSeverity.Critical, - type: FindingType.Info, - }) - - out.push(f) - } - } - return out - } - - private async handleRevertedTx(txEvent: TransactionEvent, provider: ethers.Provider): Promise { - // if (!(DEPLOYED_ADDRESSES.CS_FEE_DISTRIBUTOR.toLowerCase() in txEvent.addresses)) { - // return [] - // } - // - // const txReceipt = await provider.getTransactionReceipt(txEvent.hash) - // // Nothing to do if the transaction succeeded. - // if (txReceipt?.status === 0) { - // this.logger.debug(`Skipping successful transaction ${txEvent.hash}`) - // return [] - // } - // - // // FIXME: I can't find a node with getTransactionResult call working. - // let decodedLog: ethers.ErrorDescription | null = null - // try { - // const data = await txReceipt?.getResult() - // decodedLog = CSFeeDistributorInterface.parseError(data ?? '') - // } catch (error: any) { - // if (error.code === 'UNSUPPORTED_OPERATION') { - // this.logger.debug('Ethereum RPC does not support `getTransactionResult`') - // return [] - // } - // - // throw error - // } - // - // const reason = decodedLog?.name - // if (!reason) { - // return [] - // } - // - // switch (reason) { - // case 'InvalidShares': - // return [failedTxAlert(txEvent, 'CSFeeOracle reports incorrect amount of shares to distribute', 'InvalidShares')] - // case 'NotEnoughShares': - // return [failedTxAlert(txEvent, 'CSFeeDistributor internal accounting error', 'NotEnoughShares')] - // case 'InvalidTreeRoot': - // return [failedTxAlert(txEvent, 'CSFeeOracle built incorrect report', 'InvalidTreeRoot')] - // case 'InvalidTreeCID': - // return [failedTxAlert(txEvent, 'CSFeeOracle built incorrect report', 'InvalidTreeCID')] - // default: - // this.logger.warn(`Unrecognized revert reason: ${reason}`) - // } - - return [] - } - - private async checkInvariants(blockEvent: BlockEvent, provider: ethers.Provider) { - const distributor = CSFeeDistributor__factory.connect(DEPLOYED_ADDRESSES.CS_FEE_DISTRIBUTOR, provider) - const steth = Lido__factory.connect(DEPLOYED_ADDRESSES.LIDO_STETH, provider) - - const blockTag = blockEvent.blockHash - - const out: Finding[] = [] - - const treeRoot = await distributor.treeRoot({ blockTag }) - const treeCid = await distributor.treeCid({ blockTag }) - if (treeRoot !== ethers.ZeroHash && treeCid === '') { - out.push(invariantAlert(blockEvent, 'Tree exists, but no CID.')) - } + if ( + (await steth.sharesOf(DEPLOYED_ADDRESSES.CS_FEE_DISTRIBUTOR, { blockTag })) < + (await distributor.totalClaimableShares({ blockTag })) + ) { + out.push(invariantAlert(blockEvent, "distributed more than the contract's balance")) + } - if ( - (await steth.sharesOf(DEPLOYED_ADDRESSES.CS_FEE_DISTRIBUTOR, { blockTag })) < - (await distributor.totalClaimableShares({ blockTag })) - ) { - out.push(invariantAlert(blockEvent, "distributed more than the contract's balance")) + return out } - - return out - } } diff --git a/csm-alerts/src/services/CSFeeOracle/CSFeeOracle.srv.spec.ts b/csm-alerts/src/services/CSFeeOracle/CSFeeOracle.srv.spec.ts index 8a30b8e90..9940e4127 100644 --- a/csm-alerts/src/services/CSFeeOracle/CSFeeOracle.srv.spec.ts +++ b/csm-alerts/src/services/CSFeeOracle/CSFeeOracle.srv.spec.ts @@ -2,10 +2,10 @@ import { DeploymentAddress, DeploymentAddresses } from '../../utils/constants.ho import { expect } from '@jest/globals' import { TransactionDto } from '../../entity/events' import { - CSModule__factory, - CSAccounting__factory, - CSFeeDistributor__factory, - CSFeeOracle__factory, + CSModule__factory, + CSAccounting__factory, + CSFeeDistributor__factory, + CSFeeOracle__factory, } from '../../generated/typechain' import { CSFeeOracleSrv, ICSFeeOracleClient } from './CSFeeOracle.srv' import * as Winston from 'winston' @@ -14,198 +14,210 @@ import { ethers } from '@fortanetwork/forta-bot' import { getFortaConfig } from 'forta-agent/dist/sdk/utils' import promClient from 'prom-client' import { Metrics } from '../../utils/metrics/metrics' -import { getCSFeeOracleEvents, getHashConsensusEvents } from '../../utils/events/cs_fee_oracle_events' +import { + getCSFeeOracleEvents, + getHashConsensusEvents, +} from '../../utils/events/cs_fee_oracle_events' const TEST_TIMEOUT = 120_000 // ms describe('CSFeeOracle and HashConsensus events tests', () => { - const chainId = 17000 - - const logger: Winston.Logger = Winston.createLogger({ - format: Winston.format.simple(), - transports: [new Winston.transports.Console()], - }) - - const address: DeploymentAddress = DeploymentAddresses - - const fortaEthersProvider = new ethers.providers.JsonRpcProvider(getFortaConfig().jsonRpcUrl, chainId) - const csModuleRunner = CSModule__factory.connect(address.CS_MODULE_ADDRESS, fortaEthersProvider) - const csAccountingRunner = CSAccounting__factory.connect(address.CS_ACCOUNTING_ADDRESS, fortaEthersProvider) - const csFeeDistributorRunner = CSFeeDistributor__factory.connect( - address.CS_FEE_DISTRIBUTOR_ADDRESS, - fortaEthersProvider, - ) - const csFeeOracleRunner = CSFeeOracle__factory.connect(address.CS_FEE_ORACLE_ADDRESS, fortaEthersProvider) - - const registry = new promClient.Registry() - const m = new Metrics(registry, 'test_') - - const csFeeOracleClient: ICSFeeOracleClient = new ETHProvider( - logger, - m, - fortaEthersProvider, - csModuleRunner, - csAccountingRunner, - csFeeDistributorRunner, - csFeeOracleRunner, - ) - - const csFeeOracleSrv = new CSFeeOracleSrv( - logger, - csFeeOracleClient, - getHashConsensusEvents(address.HASH_CONSENSUS_ADDRESS), - getCSFeeOracleEvents(address.CS_FEE_ORACLE_ADDRESS), - address.HASH_CONSENSUS_ADDRESS, - address.CS_FEE_ORACLE_ADDRESS, - ) - - test( - '🔵 CSFeeOracle: Processing Started, Report Settled', - async () => { - const txHash = '0xf53cfcc9e576393b481a1c8ff4d28235703b6b5b62f9edb623d913b5d059f9c5' - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + const chainId = 17000 + + const logger: Winston.Logger = Winston.createLogger({ + format: Winston.format.simple(), + transports: [new Winston.transports.Console()], + }) + + const address: DeploymentAddress = DeploymentAddresses + + const fortaEthersProvider = new ethers.providers.JsonRpcProvider( + getFortaConfig().jsonRpcUrl, + chainId, + ) + const csModuleRunner = CSModule__factory.connect(address.CS_MODULE_ADDRESS, fortaEthersProvider) + const csAccountingRunner = CSAccounting__factory.connect( + address.CS_ACCOUNTING_ADDRESS, + fortaEthersProvider, + ) + const csFeeDistributorRunner = CSFeeDistributor__factory.connect( + address.CS_FEE_DISTRIBUTOR_ADDRESS, + fortaEthersProvider, + ) + const csFeeOracleRunner = CSFeeOracle__factory.connect( + address.CS_FEE_ORACLE_ADDRESS, + fortaEthersProvider, + ) + + const registry = new promClient.Registry() + const m = new Metrics(registry, 'test_') + + const csFeeOracleClient: ICSFeeOracleClient = new ETHProvider( + logger, + m, + fortaEthersProvider, + csModuleRunner, + csAccountingRunner, + csFeeDistributorRunner, + csFeeOracleRunner, + ) + + const csFeeOracleSrv = new CSFeeOracleSrv( + logger, + csFeeOracleClient, + getHashConsensusEvents(address.HASH_CONSENSUS_ADDRESS), + getCSFeeOracleEvents(address.CS_FEE_ORACLE_ADDRESS), + address.HASH_CONSENSUS_ADDRESS, + address.CS_FEE_ORACLE_ADDRESS, + ) + + test( + '🔵 CSFeeOracle: Processing Started, Report Settled', + async () => { + const txHash = '0xf53cfcc9e576393b481a1c8ff4d28235703b6b5b62f9edb623d913b5d059f9c5' + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const results = csFeeOracleSrv.handleTransaction(transactionDto) + + expect(results).toMatchSnapshot() + expect(results.length).toBe(2) }, - hash: trx.hash, - } - - const results = csFeeOracleSrv.handleTransaction(transactionDto) - - expect(results).toMatchSnapshot() - expect(results.length).toBe(2) - }, - TEST_TIMEOUT, - ) - - test( - '🔴 HashConsensus: FrameConfig Set', - async () => { - const txHash = '0xa6f4206ce30d66b378ab6e4ddef442ac4f29c95c5175fbb4a8944e6bec663724' - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + TEST_TIMEOUT, + ) + + test( + '🔴 HashConsensus: FrameConfig Set', + async () => { + const txHash = '0xa6f4206ce30d66b378ab6e4ddef442ac4f29c95c5175fbb4a8944e6bec663724' + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const result = csFeeOracleSrv.handleTransaction(transactionDto) + + expect(result).toMatchSnapshot() + expect(result.length).toBe(1) }, - hash: trx.hash, - } - - const result = csFeeOracleSrv.handleTransaction(transactionDto) - - expect(result).toMatchSnapshot() - expect(result.length).toBe(1) - }, - TEST_TIMEOUT, - ) - - test( - '🔴 HashConsensus: Member added, Quorum Set', - async () => { - const txHash = '0xdfcdbe0b9e795b2b83ad405c17b0c7326b00748cb3b11282a460c50b1f4588b0' - - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + TEST_TIMEOUT, + ) + + test( + '🔴 HashConsensus: Member added, Quorum Set', + async () => { + const txHash = '0xdfcdbe0b9e795b2b83ad405c17b0c7326b00748cb3b11282a460c50b1f4588b0' + + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const results = csFeeOracleSrv.handleTransaction(transactionDto) + + expect(results).toMatchSnapshot() + expect(results.length).toBe(2) }, - hash: trx.hash, - } - - const results = csFeeOracleSrv.handleTransaction(transactionDto) - - expect(results).toMatchSnapshot() - expect(results.length).toBe(2) - }, - TEST_TIMEOUT, - ) - - test( - '🔴 HashConsensus: Member Removed, Quorum Set, Consensus Reached, Report Submitted', - async () => { - const txHash = '0xd22f8208b4bb2013a1113d68f9f19e3be13147c1f77ce811baa32ef082deed42' - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + TEST_TIMEOUT, + ) + + test( + '🔴 HashConsensus: Member Removed, Quorum Set, Consensus Reached, Report Submitted', + async () => { + const txHash = '0xd22f8208b4bb2013a1113d68f9f19e3be13147c1f77ce811baa32ef082deed42' + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const result = csFeeOracleSrv.handleTransaction(transactionDto) + + expect(result).toMatchSnapshot() + expect(result.length).toBe(4) }, - hash: trx.hash, - } - - const result = csFeeOracleSrv.handleTransaction(transactionDto) - - expect(result).toMatchSnapshot() - expect(result.length).toBe(4) - }, - TEST_TIMEOUT, - ) - - test( - 'Empty findings', - async () => { - const txHash = '0x74ff368ba6ea748e19a7f0fefd9d0f708078176e56799dbe97d46ae59782ff9d' - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + TEST_TIMEOUT, + ) + + test( + 'Empty findings', + async () => { + const txHash = '0x74ff368ba6ea748e19a7f0fefd9d0f708078176e56799dbe97d46ae59782ff9d' + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const result = csFeeOracleSrv.handleTransaction(transactionDto) + + expect(result.length).toBe(0) }, - hash: trx.hash, - } - - const result = csFeeOracleSrv.handleTransaction(transactionDto) - - expect(result.length).toBe(0) - }, - TEST_TIMEOUT, - ) - - test( - '🔴 CSFeeOracle: Consensus Hash Contract Set, Consensus Version Set, FeeDistributor Contract Set, Perf Leeway Set', - async () => { - const txHash = '0xdc5ed949e5b30a5ff6f325cd718ba5a52a32dc7719d3fe7aaf9661cc3da7e9a6' - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + TEST_TIMEOUT, + ) + + test( + '🔴 CSFeeOracle: Consensus Hash Contract Set, Consensus Version Set, FeeDistributor Contract Set, Perf Leeway Set', + async () => { + const txHash = '0xdc5ed949e5b30a5ff6f325cd718ba5a52a32dc7719d3fe7aaf9661cc3da7e9a6' + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const result = csFeeOracleSrv.handleTransaction(transactionDto) + + expect(result).toMatchSnapshot() + expect(result.length).toBe(4) }, - hash: trx.hash, - } - - const result = csFeeOracleSrv.handleTransaction(transactionDto) - - expect(result).toMatchSnapshot() - expect(result.length).toBe(4) - }, - TEST_TIMEOUT, - ) + TEST_TIMEOUT, + ) }) diff --git a/csm-alerts/src/services/CSFeeOracle/CSFeeOracle.srv.ts b/csm-alerts/src/services/CSFeeOracle/CSFeeOracle.srv.ts index 4e1656c12..b9399a86c 100644 --- a/csm-alerts/src/services/CSFeeOracle/CSFeeOracle.srv.ts +++ b/csm-alerts/src/services/CSFeeOracle/CSFeeOracle.srv.ts @@ -1,4 +1,11 @@ -import { Finding, FindingSeverity, FindingType, TransactionEvent, ethers, filterLog } from '@fortanetwork/forta-bot' +import { + Finding, + FindingSeverity, + FindingType, + TransactionEvent, + ethers, + filterLog, +} from '@fortanetwork/forta-bot' import { CSFeeOracle__factory, HashConsensus__factory } from '../../generated/typechain' import { sourceFromEvent } from '../../utils/findings' @@ -7,9 +14,9 @@ import { etherscanAddress } from '../../utils/string' import * as Constants from '../constants' const { DEPLOYED_ADDRESSES, ORACLE_MEMBERS } = requireWithTier( - module, - '../constants', - RedefineMode.Merge, + module, + '../constants', + RedefineMode.Merge, ) const IHashConsensus = HashConsensus__factory.createInterface() const ICSFeeOracle = CSFeeOracle__factory.createInterface() @@ -17,96 +24,113 @@ const ICSFeeOracle = CSFeeOracle__factory.createInterface() // TODO: Extract HashConsensus part maybe? export class CSFeeOracleSrv { - private membersLastReport: Map< - string, - { - refSlot: bigint - report: string + private membersLastReport: Map< + string, + { + refSlot: bigint + report: string + } + > = new Map() + + public async initialize( + blockIdentifier: ethers.BlockTag, + provider: ethers.Provider, + ): Promise { + const hc = HashConsensus__factory.connect(DEPLOYED_ADDRESSES.HASH_CONSENSUS, provider) + const [members] = await hc.getMembers({ blockTag: blockIdentifier }) + for (const m of members) { + const lastReport = await hc.getConsensusStateForMember(m, { blockTag: blockIdentifier }) + this.membersLastReport.set(m, { + refSlot: lastReport.lastMemberReportRefSlot, + report: lastReport.currentFrameMemberReport, + }) + } } - > = new Map() - - public async initialize(blockIdentifier: ethers.BlockTag, provider: ethers.Provider): Promise { - const hc = HashConsensus__factory.connect(DEPLOYED_ADDRESSES.HASH_CONSENSUS, provider) - const [members] = await hc.getMembers({ blockTag: blockIdentifier }) - for (const m of members) { - const lastReport = await hc.getConsensusStateForMember(m, { blockTag: blockIdentifier }) - this.membersLastReport.set(m, { - refSlot: lastReport.lastMemberReportRefSlot, - report: lastReport.currentFrameMemberReport, - }) - } - } - - public async handleTransaction(txEvent: TransactionEvent, provider: ethers.Provider): Promise { - return [...this.handleAlternativeHashReceived(txEvent), ...(await this.handleReportSubmitted(txEvent, provider))] - } - - private handleAlternativeHashReceived(txEvent: TransactionEvent): Finding[] { - const out: Finding[] = [] - - const [event] = filterLog( - txEvent.logs, - IHashConsensus.getEvent('ReportReceived').format('full'), - DEPLOYED_ADDRESSES.HASH_CONSENSUS, - ) - - if (event) { - const currentReportHashes = [...this.membersLastReport.values()] - .filter((r) => r.refSlot === event.args.refSlot) - .map((r) => r.report) - - if (currentReportHashes.length > 0 && !currentReportHashes.includes(event.args.report)) { - const f = Finding.fromObject({ - name: '🔴 HashConsensus: Another report variant appeared (alternative hash)', - description: `More than one distinct report hash received for slot ${event.args.refSlot}. Member: ${event.args.member}. Report: ${event.args.report}`, - alertId: 'HASH-CONSENSUS-REPORT-RECEIVED', - source: sourceFromEvent(txEvent), - severity: FindingSeverity.High, - type: FindingType.Info, - }) - - out.push(f) - } - this.membersLastReport.set(event.args.member, { - refSlot: event.args.refSlot, - report: event.args.report, - }) + public async handleTransaction( + txEvent: TransactionEvent, + provider: ethers.Provider, + ): Promise { + return [ + ...this.handleAlternativeHashReceived(txEvent), + ...(await this.handleReportSubmitted(txEvent, provider)), + ] } - return out - } + private handleAlternativeHashReceived(txEvent: TransactionEvent): Finding[] { + const out: Finding[] = [] - private async handleReportSubmitted(txEvent: TransactionEvent, provider: ethers.Provider): Promise { - const [event] = filterLog( - txEvent.logs, - ICSFeeOracle.getEvent('ReportSubmitted').format('full'), - DEPLOYED_ADDRESSES.CS_FEE_ORACLE, - ) + const [event] = filterLog( + txEvent.logs, + IHashConsensus.getEvent('ReportReceived').format('full'), + DEPLOYED_ADDRESSES.HASH_CONSENSUS, + ) - if (!event) { - return [] + if (event) { + const currentReportHashes = [...this.membersLastReport.values()] + .filter((r) => r.refSlot === event.args.refSlot) + .map((r) => r.report) + + if ( + currentReportHashes.length > 0 && + !currentReportHashes.includes(event.args.report) + ) { + const f = Finding.fromObject({ + name: '🔴 HashConsensus: Another report variant appeared (alternative hash)', + description: `More than one distinct report hash received for slot ${event.args.refSlot}. Member: ${event.args.member}. Report: ${event.args.report}`, + alertId: 'HASH-CONSENSUS-REPORT-RECEIVED', + source: sourceFromEvent(txEvent), + severity: FindingSeverity.High, + type: FindingType.Info, + }) + + out.push(f) + } + + this.membersLastReport.set(event.args.member, { + refSlot: event.args.refSlot, + report: event.args.report, + }) + } + + return out } - const out: Finding[] = [] - - const hc = HashConsensus__factory.connect(DEPLOYED_ADDRESSES.HASH_CONSENSUS, provider) - const [addresses, lastReportedRefSlots] = await hc.getFastLaneMembers({ blockTag: txEvent.block.hash }) - addresses.forEach(function (addr, index) { - if (event.args.refSlot !== lastReportedRefSlots[index]) { - out.push( - Finding.fromObject({ - name: '🔴 CSM: Sloppy oracle fast lane member', - description: `Member ${etherscanAddress(addr)} (${ORACLE_MEMBERS[addr] || 'unknown'}) was in the fast lane but did not report`, - alertId: 'HASH-CONSENSUS-SLOPPY-MEMBER', - source: sourceFromEvent(txEvent), - severity: FindingSeverity.Medium, - type: FindingType.Info, - }), + private async handleReportSubmitted( + txEvent: TransactionEvent, + provider: ethers.Provider, + ): Promise { + const [event] = filterLog( + txEvent.logs, + ICSFeeOracle.getEvent('ReportSubmitted').format('full'), + DEPLOYED_ADDRESSES.CS_FEE_ORACLE, ) - } - }) - return out - } + if (!event) { + return [] + } + + const out: Finding[] = [] + + const hc = HashConsensus__factory.connect(DEPLOYED_ADDRESSES.HASH_CONSENSUS, provider) + const [addresses, lastReportedRefSlots] = await hc.getFastLaneMembers({ + blockTag: txEvent.block.hash, + }) + addresses.forEach(function (addr, index) { + if (event.args.refSlot !== lastReportedRefSlots[index]) { + out.push( + Finding.fromObject({ + name: '🔴 CSM: Sloppy oracle fast lane member', + description: `Member ${etherscanAddress(addr)} (${ORACLE_MEMBERS[addr] || 'unknown'}) was in the fast lane but did not report`, + alertId: 'HASH-CONSENSUS-SLOPPY-MEMBER', + source: sourceFromEvent(txEvent), + severity: FindingSeverity.Medium, + type: FindingType.Info, + }), + ) + } + }) + + return out + } } diff --git a/csm-alerts/src/services/CSModule/CSModule.srv.spec.ts b/csm-alerts/src/services/CSModule/CSModule.srv.spec.ts index cfd467963..1922a8dd4 100644 --- a/csm-alerts/src/services/CSModule/CSModule.srv.spec.ts +++ b/csm-alerts/src/services/CSModule/CSModule.srv.spec.ts @@ -2,10 +2,10 @@ import { DeploymentAddress, DeploymentAddresses } from '../../utils/constants.ho import { expect } from '@jest/globals' import { TransactionDto } from '../../entity/events' import { - CSModule__factory, - CSAccounting__factory, - CSFeeDistributor__factory, - CSFeeOracle__factory, + CSModule__factory, + CSAccounting__factory, + CSFeeDistributor__factory, + CSFeeOracle__factory, } from '../../generated/typechain' import { CSModuleSrv, ICSModuleClient } from './CSModule.srv' import { getCSModuleEvents } from '../../utils/events/cs_module_events' @@ -19,120 +19,129 @@ import { Metrics } from '../../utils/metrics/metrics' const TEST_TIMEOUT = 120_000 // ms describe('CSModule event tests', () => { - const chainId = 17000 - - const logger: Winston.Logger = Winston.createLogger({ - format: Winston.format.simple(), - transports: [new Winston.transports.Console()], - }) - - const address: DeploymentAddress = DeploymentAddresses - - const fortaEthersProvider = new ethers.providers.JsonRpcProvider(getFortaConfig().jsonRpcUrl, chainId) - const csModuleRunner = CSModule__factory.connect(address.CS_MODULE_ADDRESS, fortaEthersProvider) - const csAccountingRunner = CSAccounting__factory.connect(address.CS_ACCOUNTING_ADDRESS, fortaEthersProvider) - const csFeeDistributorRunner = CSFeeDistributor__factory.connect( - address.CS_FEE_DISTRIBUTOR_ADDRESS, - fortaEthersProvider, - ) - const csFeeOracleRunner = CSFeeOracle__factory.connect(address.CS_FEE_ORACLE_ADDRESS, fortaEthersProvider) - - const registry = new promClient.Registry() - const m = new Metrics(registry, 'test_') - - const csModuleClient: ICSModuleClient = new ETHProvider( - logger, - m, - fortaEthersProvider, - csModuleRunner, - csAccountingRunner, - csFeeDistributorRunner, - csFeeOracleRunner, - ) - - const csModuleSrv = new CSModuleSrv( - logger, - csModuleClient, - address.CS_MODULE_ADDRESS, - address.STAKING_ROUTER_ADDRESS, - getCSModuleEvents(address.CS_MODULE_ADDRESS), - ) - - test( - '🟠 CSModule: Target limit mode changed', - async () => { - const txHash = '0xd8bb4389a056be70fe20e3b6b903c3e7cdbf053610bd8d647c4e2fe49c94f8b6' - - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + const chainId = 17000 + + const logger: Winston.Logger = Winston.createLogger({ + format: Winston.format.simple(), + transports: [new Winston.transports.Console()], + }) + + const address: DeploymentAddress = DeploymentAddresses + + const fortaEthersProvider = new ethers.providers.JsonRpcProvider( + getFortaConfig().jsonRpcUrl, + chainId, + ) + const csModuleRunner = CSModule__factory.connect(address.CS_MODULE_ADDRESS, fortaEthersProvider) + const csAccountingRunner = CSAccounting__factory.connect( + address.CS_ACCOUNTING_ADDRESS, + fortaEthersProvider, + ) + const csFeeDistributorRunner = CSFeeDistributor__factory.connect( + address.CS_FEE_DISTRIBUTOR_ADDRESS, + fortaEthersProvider, + ) + const csFeeOracleRunner = CSFeeOracle__factory.connect( + address.CS_FEE_ORACLE_ADDRESS, + fortaEthersProvider, + ) + + const registry = new promClient.Registry() + const m = new Metrics(registry, 'test_') + + const csModuleClient: ICSModuleClient = new ETHProvider( + logger, + m, + fortaEthersProvider, + csModuleRunner, + csAccountingRunner, + csFeeDistributorRunner, + csFeeOracleRunner, + ) + + const csModuleSrv = new CSModuleSrv( + logger, + csModuleClient, + address.CS_MODULE_ADDRESS, + address.STAKING_ROUTER_ADDRESS, + getCSModuleEvents(address.CS_MODULE_ADDRESS), + ) + + test( + '🟠 CSModule: Target limit mode changed', + async () => { + const txHash = '0xd8bb4389a056be70fe20e3b6b903c3e7cdbf053610bd8d647c4e2fe49c94f8b6' + + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const results = await csModuleSrv.handleTransaction(transactionDto) + + expect(results).toMatchSnapshot() + expect(results.length).toBe(1) }, - hash: trx.hash, - } - - const results = await csModuleSrv.handleTransaction(transactionDto) - - expect(results).toMatchSnapshot() - expect(results.length).toBe(1) - }, - TEST_TIMEOUT, - ) - - test( - '🔴 CSModule: EL Rewards stealing penalty reported', - async () => { - const txHash = '0x45d08822a9e025d374ab182612a119d837a0869b0c343379025303f53c4c63be' - - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + TEST_TIMEOUT, + ) + + test( + '🔴 CSModule: EL Rewards stealing penalty reported', + async () => { + const txHash = '0x45d08822a9e025d374ab182612a119d837a0869b0c343379025303f53c4c63be' + + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const results = await csModuleSrv.handleTransaction(transactionDto) + + expect(results).toMatchSnapshot() + expect(results.length).toBe(1) }, - hash: trx.hash, - } - - const results = await csModuleSrv.handleTransaction(transactionDto) - - expect(results).toMatchSnapshot() - expect(results.length).toBe(1) - }, - TEST_TIMEOUT, - ) - - test( - '🔵 CSModule: Notable Node Operator creation', - async () => { - const txHash = '0x87ece6668905293fd00c7eeff15ff685ed1d10810bc5cbba204f0881ab877be6' - - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + TEST_TIMEOUT, + ) + + test( + '🔵 CSModule: Notable Node Operator creation', + async () => { + const txHash = '0x87ece6668905293fd00c7eeff15ff685ed1d10810bc5cbba204f0881ab877be6' + + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const results = await csModuleSrv.handleTransaction(transactionDto) + + expect(results).toMatchSnapshot() + expect(results.length).toBe(1) }, - hash: trx.hash, - } - - const results = await csModuleSrv.handleTransaction(transactionDto) - - expect(results).toMatchSnapshot() - expect(results.length).toBe(1) - }, - TEST_TIMEOUT, - ) + TEST_TIMEOUT, + ) }) diff --git a/csm-alerts/src/services/CSModule/CSModule.srv.ts b/csm-alerts/src/services/CSModule/CSModule.srv.ts index 6e8ffc6ab..8cf8a87aa 100644 --- a/csm-alerts/src/services/CSModule/CSModule.srv.ts +++ b/csm-alerts/src/services/CSModule/CSModule.srv.ts @@ -1,11 +1,11 @@ import { - BlockEvent, - Finding, - FindingSeverity, - FindingType, - TransactionEvent, - ethers, - filterLog, + BlockEvent, + Finding, + FindingSeverity, + FindingType, + TransactionEvent, + ethers, + filterLog, } from '@fortanetwork/forta-bot' import { Logger } from 'winston' @@ -17,7 +17,11 @@ import { sourceFromEvent } from '../../utils/findings' import { RedefineMode, requireWithTier } from '../../utils/require' import * as Constants from '../constants' -const { DEPLOYED_ADDRESSES } = requireWithTier(module, '../constants', RedefineMode.Merge) +const { DEPLOYED_ADDRESSES } = requireWithTier( + module, + '../constants', + RedefineMode.Merge, +) const ICSModule = CSModule__factory.createInterface() const CHECK_QUEUE_INTERVAL_BLOCKS = 1801 // ~ 4 times a day @@ -26,214 +30,229 @@ const QUEUE_EMPTY_BATCHES_MAX = 30 const QUEUE_VALIDATORS_MAX = 200 class Batch { - value: bigint + value: bigint - constructor(v: bigint) { - this.value = v - } + constructor(v: bigint) { + this.value = v + } - noId(): bigint { - return this.value >> 192n - } + noId(): bigint { + return this.value >> 192n + } - keys(): bigint { - return (this.value >> 128n) & BigInt('0xFFFFFFFFFFFFFFFF') - } + keys(): bigint { + return (this.value >> 128n) & BigInt('0xFFFFFFFFFFFFFFFF') + } - next(): bigint { - return this.value & BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF') - } + next(): bigint { + return this.value & BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF') + } } export class CSModuleSrv { - private readonly logger: Logger - - private lastFiredAt = { - moduleShareIsCloseToTargetShare: 0, - tooManyEmptyBatches: 0, - tooManyValidators: 0, - } - - constructor() { - this.logger = getLogger(CSModuleSrv.name) - } - - async handleBlock(blockEvent: BlockEvent, provider: ethers.Provider): Promise { - return [ - ...(await this.checkDepositQueue(blockEvent, provider)), - ...(await this.checkModuleShare(blockEvent, provider)), - ] - } - - async handleTransaction(txEvent: TransactionEvent, provider: ethers.Provider): Promise { - return this.handleNotableOperatorsCreated(txEvent) - } - - private handleNotableOperatorsCreated(txEvent: TransactionEvent) { - const out: Finding[] = [] - - const nodeOperatorAddedEvents = filterLog( - txEvent.logs, - ICSModule.getEvent('NodeOperatorAdded').format('full'), - DEPLOYED_ADDRESSES.CS_MODULE, - ) - - for (const event of nodeOperatorAddedEvents) { - if (event.args.nodeOperatorId % 100n === 0n || event.args.nodeOperatorId === 69n) { - const f = Finding.fromObject({ - name: `🔵 CSModule: Notable Node Operator creation`, - description: `Operator #${event.args.nodeOperatorId} was created.`, - alertId: 'CS-MODULE-NOTABLE-NODE-OPERATOR-CREATION', - source: sourceFromEvent(txEvent), - severity: FindingSeverity.Info, - type: FindingType.Info, - }) - out.push(f) - } - } - - return out - } - - private async checkModuleShare(blockEvent: BlockEvent, provider: ethers.Provider): Promise { - const stakingRouter = StakingRouter__factory.connect(DEPLOYED_ADDRESSES.STAKING_ROUTER, provider) - - let totalActiveValidators = 0n - let csmActiveValidators = 0n - let csmTargetShareBP = 0n + private readonly logger: Logger - const digests = await stakingRouter.getAllStakingModuleDigests({ blockTag: blockEvent.blockHash }) - - for (const d of digests) { - const moduleActiveValidators = d.summary.totalDepositedValidators - d.summary.totalExitedValidators - totalActiveValidators += moduleActiveValidators - - if (d.state.stakingModuleAddress === DEPLOYED_ADDRESSES.CS_MODULE) { - csmActiveValidators = moduleActiveValidators - csmTargetShareBP = d.state.targetShare - } + private lastFiredAt = { + moduleShareIsCloseToTargetShare: 0, + tooManyEmptyBatches: 0, + tooManyValidators: 0, } - if (totalActiveValidators === 0n || csmActiveValidators === 0n) { - this.logger.debug('No validators in modules or in the CSM') - return [] + constructor() { + this.logger = getLogger(CSModuleSrv.name) } - if (csmTargetShareBP === 0n) { - this.logger.debug('CSM has no target share') - return [] + async handleBlock(blockEvent: BlockEvent, provider: ethers.Provider): Promise { + return [ + ...(await this.checkDepositQueue(blockEvent, provider)), + ...(await this.checkModuleShare(blockEvent, provider)), + ] } - const csmCurrentShareBP = (csmActiveValidators * BASIS_POINT_MUL) / totalActiveValidators - const percentUsed = (csmCurrentShareBP * 100n) / csmTargetShareBP - - const now = blockEvent.block.timestamp - const out: Finding[] = [] - - if (now - this.lastFiredAt.moduleShareIsCloseToTargetShare > SECONDS_PER_DAY) { - if (percentUsed > TARGET_SHARE_USED_PERCENT_MAX) { - const f = Finding.fromObject({ - name: `🟢 CSModule: Module's share is close to the target share.`, - description: `The module's share is close to the target share (${percentUsed}% utilization). Current share is ${(Number(csmCurrentShareBP * 100n) / Number(BASIS_POINT_MUL)).toFixed(2)}%. Target share is ${(Number(csmTargetShareBP * 100n) / Number(BASIS_POINT_MUL)).toFixed(2)}%`, - alertId: 'CS-MODULE-CLOSE-TO-TARGET-SHARE', - // NOTE: Do not include the source to reach quorum. - // source: sourceFromEvent(blockEvent), - severity: FindingSeverity.Low, - type: FindingType.Info, - }) - out.push(f) - this.lastFiredAt.moduleShareIsCloseToTargetShare = now - } + async handleTransaction( + txEvent: TransactionEvent, + provider: ethers.Provider, + ): Promise { + return this.handleNotableOperatorsCreated(txEvent) } - return out - } - - private async checkDepositQueue(blockEvent: BlockEvent, provider: ethers.Provider): Promise { - if (blockEvent.blockNumber % CHECK_QUEUE_INTERVAL_BLOCKS !== 0 && !IS_CLI) { - return [] + private handleNotableOperatorsCreated(txEvent: TransactionEvent) { + const out: Finding[] = [] + + const nodeOperatorAddedEvents = filterLog( + txEvent.logs, + ICSModule.getEvent('NodeOperatorAdded').format('full'), + DEPLOYED_ADDRESSES.CS_MODULE, + ) + + for (const event of nodeOperatorAddedEvents) { + if (event.args.nodeOperatorId % 100n === 0n || event.args.nodeOperatorId === 69n) { + const f = Finding.fromObject({ + name: `🔵 CSModule: Notable Node Operator creation`, + description: `Operator #${event.args.nodeOperatorId} was created.`, + alertId: 'CS-MODULE-NOTABLE-NODE-OPERATOR-CREATION', + source: sourceFromEvent(txEvent), + severity: FindingSeverity.Info, + type: FindingType.Info, + }) + out.push(f) + } + } + + return out } - const csm = CSModule__factory.connect(DEPLOYED_ADDRESSES.CS_MODULE, provider) - const ethCallOpts = { blockTag: blockEvent.blockHash } - - const queueLookup: Map = new Map() - let validatorsInQueue = 0n - let emptyBatchCount = 0 - - const queue = await csm.depositQueue(ethCallOpts) - let index = queue.head - - this.logger.debug(`Queue head: ${queue.head}`) - this.logger.debug(`Queue tail: ${queue.tail}`) - - while (index >= queue.head && index < queue.tail) { - const batchValue = await csm.depositQueueItem(index, ethCallOpts) - const batch = new Batch(batchValue) - - // Covers zero-batch case as well. - if (batch.next() === 0n) { - break - } - - const nodeOperatorId = batch.noId() - const keysInBatch = batch.keys() - const no = await csm.getNodeOperator(nodeOperatorId, ethCallOpts) - - const keysSeenForOperator = queueLookup.get(nodeOperatorId) ?? 0n - if (keysSeenForOperator >= no.depositableValidatorsCount) { - emptyBatchCount++ - } else { - const depositableFromBatch = - keysSeenForOperator + keysInBatch > no.depositableValidatorsCount - ? no.depositableValidatorsCount - keysSeenForOperator - : keysInBatch - validatorsInQueue += depositableFromBatch - queueLookup.set(nodeOperatorId, depositableFromBatch) - } - - // TODO: Think about how it works in on-chain code. - index = batch.next() - } - - this.logger.debug(`Empty batches: ${emptyBatchCount}`) - this.logger.debug(`Validators in the queue: ${validatorsInQueue}`) - - const now = blockEvent.block.timestamp - const out: Finding[] = [] - - if (now - this.lastFiredAt.tooManyEmptyBatches > SECONDS_PER_DAY) { - if (emptyBatchCount > QUEUE_EMPTY_BATCHES_MAX) { - const f = Finding.fromObject({ - name: `🟢 CSModule: Too many empty batches in the deposit queue.`, - description: `More than ${QUEUE_EMPTY_BATCHES_MAX} empty batches in the deposit queue.`, - alertId: 'CS-MODULE-TOO-MANY-EMPTY-BATCHES-IN-THE-QUEUE', - // NOTE: Do not include the source to reach quorum. - // source: sourceFromEvent(blockEvent), - severity: FindingSeverity.Low, - type: FindingType.Info, + private async checkModuleShare( + blockEvent: BlockEvent, + provider: ethers.Provider, + ): Promise { + const stakingRouter = StakingRouter__factory.connect( + DEPLOYED_ADDRESSES.STAKING_ROUTER, + provider, + ) + + let totalActiveValidators = 0n + let csmActiveValidators = 0n + let csmTargetShareBP = 0n + + const digests = await stakingRouter.getAllStakingModuleDigests({ + blockTag: blockEvent.blockHash, }) - out.push(f) - this.lastFiredAt.tooManyEmptyBatches = now - } - } - if (now - this.lastFiredAt.tooManyValidators > SECONDS_PER_DAY) { - if (validatorsInQueue > QUEUE_VALIDATORS_MAX) { - const f = Finding.fromObject({ - name: '🟢 CSModule: Too many validators in the queue.', - description: `There's ${validatorsInQueue} keys waiting for deposit in CSM.`, - alertId: 'CS-MODULE-TOO-MANY-VALIDATORS-IN-THE-QUEUE', - // NOTE: Do not include the source to reach quorum. - // source: sourceFromEvent(blockEvent), - severity: FindingSeverity.Low, - type: FindingType.Info, - }) - out.push(f) - this.lastFiredAt.tooManyValidators = now - } + for (const d of digests) { + const moduleActiveValidators = + d.summary.totalDepositedValidators - d.summary.totalExitedValidators + totalActiveValidators += moduleActiveValidators + + if (d.state.stakingModuleAddress === DEPLOYED_ADDRESSES.CS_MODULE) { + csmActiveValidators = moduleActiveValidators + csmTargetShareBP = d.state.targetShare + } + } + + if (totalActiveValidators === 0n || csmActiveValidators === 0n) { + this.logger.debug('No validators in modules or in the CSM') + return [] + } + + if (csmTargetShareBP === 0n) { + this.logger.debug('CSM has no target share') + return [] + } + + const csmCurrentShareBP = (csmActiveValidators * BASIS_POINT_MUL) / totalActiveValidators + const percentUsed = (csmCurrentShareBP * 100n) / csmTargetShareBP + + const now = blockEvent.block.timestamp + const out: Finding[] = [] + + if (now - this.lastFiredAt.moduleShareIsCloseToTargetShare > SECONDS_PER_DAY) { + if (percentUsed > TARGET_SHARE_USED_PERCENT_MAX) { + const f = Finding.fromObject({ + name: `🟢 CSModule: Module's share is close to the target share.`, + description: `The module's share is close to the target share (${percentUsed}% utilization). Current share is ${(Number(csmCurrentShareBP * 100n) / Number(BASIS_POINT_MUL)).toFixed(2)}%. Target share is ${(Number(csmTargetShareBP * 100n) / Number(BASIS_POINT_MUL)).toFixed(2)}%`, + alertId: 'CS-MODULE-CLOSE-TO-TARGET-SHARE', + // NOTE: Do not include the source to reach quorum. + // source: sourceFromEvent(blockEvent), + severity: FindingSeverity.Low, + type: FindingType.Info, + }) + out.push(f) + this.lastFiredAt.moduleShareIsCloseToTargetShare = now + } + } + + return out } - return out - } + private async checkDepositQueue( + blockEvent: BlockEvent, + provider: ethers.Provider, + ): Promise { + if (blockEvent.blockNumber % CHECK_QUEUE_INTERVAL_BLOCKS !== 0 && !IS_CLI) { + return [] + } + + const csm = CSModule__factory.connect(DEPLOYED_ADDRESSES.CS_MODULE, provider) + const ethCallOpts = { blockTag: blockEvent.blockHash } + + const queueLookup: Map = new Map() + let validatorsInQueue = 0n + let emptyBatchCount = 0 + + const queue = await csm.depositQueue(ethCallOpts) + let index = queue.head + + this.logger.debug(`Queue head: ${queue.head}`) + this.logger.debug(`Queue tail: ${queue.tail}`) + + while (index >= queue.head && index < queue.tail) { + const batchValue = await csm.depositQueueItem(index, ethCallOpts) + const batch = new Batch(batchValue) + + // Covers zero-batch case as well. + if (batch.next() === 0n) { + break + } + + const nodeOperatorId = batch.noId() + const keysInBatch = batch.keys() + const no = await csm.getNodeOperator(nodeOperatorId, ethCallOpts) + + const keysSeenForOperator = queueLookup.get(nodeOperatorId) ?? 0n + if (keysSeenForOperator >= no.depositableValidatorsCount) { + emptyBatchCount++ + } else { + const depositableFromBatch = + keysSeenForOperator + keysInBatch > no.depositableValidatorsCount + ? no.depositableValidatorsCount - keysSeenForOperator + : keysInBatch + validatorsInQueue += depositableFromBatch + queueLookup.set(nodeOperatorId, depositableFromBatch) + } + + // TODO: Think about how it works in on-chain code. + index = batch.next() + } + + this.logger.debug(`Empty batches: ${emptyBatchCount}`) + this.logger.debug(`Validators in the queue: ${validatorsInQueue}`) + + const now = blockEvent.block.timestamp + const out: Finding[] = [] + + if (now - this.lastFiredAt.tooManyEmptyBatches > SECONDS_PER_DAY) { + if (emptyBatchCount > QUEUE_EMPTY_BATCHES_MAX) { + const f = Finding.fromObject({ + name: `🟢 CSModule: Too many empty batches in the deposit queue.`, + description: `More than ${QUEUE_EMPTY_BATCHES_MAX} empty batches in the deposit queue.`, + alertId: 'CS-MODULE-TOO-MANY-EMPTY-BATCHES-IN-THE-QUEUE', + // NOTE: Do not include the source to reach quorum. + // source: sourceFromEvent(blockEvent), + severity: FindingSeverity.Low, + type: FindingType.Info, + }) + out.push(f) + this.lastFiredAt.tooManyEmptyBatches = now + } + } + + if (now - this.lastFiredAt.tooManyValidators > SECONDS_PER_DAY) { + if (validatorsInQueue > QUEUE_VALIDATORS_MAX) { + const f = Finding.fromObject({ + name: '🟢 CSModule: Too many validators in the queue.', + description: `There's ${validatorsInQueue} keys waiting for deposit in CSM.`, + alertId: 'CS-MODULE-TOO-MANY-VALIDATORS-IN-THE-QUEUE', + // NOTE: Do not include the source to reach quorum. + // source: sourceFromEvent(blockEvent), + severity: FindingSeverity.Low, + type: FindingType.Info, + }) + out.push(f) + this.lastFiredAt.tooManyValidators = now + } + } + + return out + } } diff --git a/csm-alerts/src/services/EventsWatcher/EventsWatcher.srv.spec.ts b/csm-alerts/src/services/EventsWatcher/EventsWatcher.srv.spec.ts index 8092f4241..7abae91ea 100644 --- a/csm-alerts/src/services/EventsWatcher/EventsWatcher.srv.spec.ts +++ b/csm-alerts/src/services/EventsWatcher/EventsWatcher.srv.spec.ts @@ -1,18 +1,18 @@ import { - CONTRACTS_WITH_ASSET_RECOVERER, - CSM_PROXY_CONTRACTS, - DeploymentAddress, - DeploymentAddresses, - PAUSABLE_CONTRACTS, - ROLES_MONITORING_CONTRACTS, + CONTRACTS_WITH_ASSET_RECOVERER, + CSM_PROXY_CONTRACTS, + DeploymentAddress, + DeploymentAddresses, + PAUSABLE_CONTRACTS, + ROLES_MONITORING_CONTRACTS, } from '../../utils/constants.holesky' import { expect } from '@jest/globals' import { TransactionDto } from '../../entity/events' import { - CSModule__factory, - CSAccounting__factory, - CSFeeDistributor__factory, - CSFeeOracle__factory, + CSModule__factory, + CSAccounting__factory, + CSFeeDistributor__factory, + CSFeeOracle__factory, } from '../../generated/typechain' import { getOssifiedProxyEvents } from '../../utils/events/ossified_proxy_events' import { getPausableEvents } from '../../utils/events/pausable_events' @@ -29,147 +29,156 @@ import { Metrics } from '../../utils/metrics/metrics' const TEST_TIMEOUT = 120_000 // ms describe('ProxyWatcher event tests', () => { - const chainId = 17000 - - const logger: Winston.Logger = Winston.createLogger({ - format: Winston.format.simple(), - transports: [new Winston.transports.Console()], - }) - - const address: DeploymentAddress = DeploymentAddresses - - const fortaEthersProvider = new ethers.providers.JsonRpcProvider(getFortaConfig().jsonRpcUrl, chainId) - const csModuleRunner = CSModule__factory.connect(address.CS_MODULE_ADDRESS, fortaEthersProvider) - const csAccountingRunner = CSAccounting__factory.connect(address.CS_ACCOUNTING_ADDRESS, fortaEthersProvider) - const csFeeDistributorRunner = CSFeeDistributor__factory.connect( - address.CS_FEE_DISTRIBUTOR_ADDRESS, - fortaEthersProvider, - ) - const csFeeOracleRunner = CSFeeOracle__factory.connect(address.CS_FEE_ORACLE_ADDRESS, fortaEthersProvider) - - const registry = new promClient.Registry() - const m = new Metrics(registry, 'test_') - - const proxyWatcherClient: IProxyWatcherClient = new ETHProvider( - logger, - m, - fortaEthersProvider, - csModuleRunner, - csAccountingRunner, - csFeeDistributorRunner, - csFeeOracleRunner, - ) - - const proxyWatcherSrv = new ProxyWatcherSrv( - logger, - proxyWatcherClient, - getOssifiedProxyEvents(CSM_PROXY_CONTRACTS), - getPausableEvents(PAUSABLE_CONTRACTS), - getAssetRecovererEvents(CONTRACTS_WITH_ASSET_RECOVERER), - getRolesMonitoringEvents(ROLES_MONITORING_CONTRACTS), - ) - - test( - '🚨 Proxy Watcher: Admin Changed', - async () => { - const txHash = '0x92410350f567757d8f73b2f4b3670454af3899d095103ea0e745c92714673277' - - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + const chainId = 17000 + + const logger: Winston.Logger = Winston.createLogger({ + format: Winston.format.simple(), + transports: [new Winston.transports.Console()], + }) + + const address: DeploymentAddress = DeploymentAddresses + + const fortaEthersProvider = new ethers.providers.JsonRpcProvider( + getFortaConfig().jsonRpcUrl, + chainId, + ) + const csModuleRunner = CSModule__factory.connect(address.CS_MODULE_ADDRESS, fortaEthersProvider) + const csAccountingRunner = CSAccounting__factory.connect( + address.CS_ACCOUNTING_ADDRESS, + fortaEthersProvider, + ) + const csFeeDistributorRunner = CSFeeDistributor__factory.connect( + address.CS_FEE_DISTRIBUTOR_ADDRESS, + fortaEthersProvider, + ) + const csFeeOracleRunner = CSFeeOracle__factory.connect( + address.CS_FEE_ORACLE_ADDRESS, + fortaEthersProvider, + ) + + const registry = new promClient.Registry() + const m = new Metrics(registry, 'test_') + + const proxyWatcherClient: IProxyWatcherClient = new ETHProvider( + logger, + m, + fortaEthersProvider, + csModuleRunner, + csAccountingRunner, + csFeeDistributorRunner, + csFeeOracleRunner, + ) + + const proxyWatcherSrv = new ProxyWatcherSrv( + logger, + proxyWatcherClient, + getOssifiedProxyEvents(CSM_PROXY_CONTRACTS), + getPausableEvents(PAUSABLE_CONTRACTS), + getAssetRecovererEvents(CONTRACTS_WITH_ASSET_RECOVERER), + getRolesMonitoringEvents(ROLES_MONITORING_CONTRACTS), + ) + + test( + '🚨 Proxy Watcher: Admin Changed', + async () => { + const txHash = '0x92410350f567757d8f73b2f4b3670454af3899d095103ea0e745c92714673277' + + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const results = proxyWatcherSrv.handleTransaction(transactionDto) + + expect(results).toMatchSnapshot() + expect(results.length).toBe(4) }, - hash: trx.hash, - } - - const results = proxyWatcherSrv.handleTransaction(transactionDto) - - expect(results).toMatchSnapshot() - expect(results.length).toBe(4) - }, - TEST_TIMEOUT, - ) - - test( - '🚨 Proxy Watcher: Implementation Upgraded', - async () => { - const txHash = '0x262faac95560f7fc0c831580d17e48daa69b17831b798e0b00bc43168a310c52' - - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + TEST_TIMEOUT, + ) + + test( + '🚨 Proxy Watcher: Implementation Upgraded', + async () => { + const txHash = '0x262faac95560f7fc0c831580d17e48daa69b17831b798e0b00bc43168a310c52' + + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const results = proxyWatcherSrv.handleTransaction(transactionDto) + + expect(results).toMatchSnapshot() + expect(results.length).toBe(4) }, - hash: trx.hash, - } - - const results = proxyWatcherSrv.handleTransaction(transactionDto) - - expect(results).toMatchSnapshot() - expect(results.length).toBe(4) - }, - TEST_TIMEOUT, - ) - - test( - '🚨 Proxy Watcher: Paused', - async () => { - const txHash = '0x56a49219b4e40d146c0dc11d795b6d43696947f0ceb63fad7e9b820eab2b3e14' - - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + TEST_TIMEOUT, + ) + + test( + '🚨 Proxy Watcher: Paused', + async () => { + const txHash = '0x56a49219b4e40d146c0dc11d795b6d43696947f0ceb63fad7e9b820eab2b3e14' + + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const results = proxyWatcherSrv.handleTransaction(transactionDto) + + expect(results).toMatchSnapshot() + expect(results.length).toBe(3) }, - hash: trx.hash, - } - - const results = proxyWatcherSrv.handleTransaction(transactionDto) - - expect(results).toMatchSnapshot() - expect(results.length).toBe(3) - }, - TEST_TIMEOUT, - ) - - test( - '🚨 Proxy Watcher: Resumed', - async () => { - const txHash = '0xa44ac96956f254fe1b9d6ff0a60ad2b5b4a7eaff951eb98150c0f7bbe90dc5df' - - const trx = await fortaEthersProvider.getTransaction(txHash) - const receipt = await trx.wait() - - const transactionDto: TransactionDto = { - logs: receipt.logs, - to: trx.to ? trx.to : null, - block: { - timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), - number: trx.blockNumber ? trx.blockNumber : 1, + TEST_TIMEOUT, + ) + + test( + '🚨 Proxy Watcher: Resumed', + async () => { + const txHash = '0xa44ac96956f254fe1b9d6ff0a60ad2b5b4a7eaff951eb98150c0f7bbe90dc5df' + + const trx = await fortaEthersProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + hash: trx.hash, + } + + const results = proxyWatcherSrv.handleTransaction(transactionDto) + + expect(results).toMatchSnapshot() + expect(results.length).toBe(3) }, - hash: trx.hash, - } - - const results = proxyWatcherSrv.handleTransaction(transactionDto) - - expect(results).toMatchSnapshot() - expect(results.length).toBe(3) - }, - TEST_TIMEOUT, - ) + TEST_TIMEOUT, + ) }) diff --git a/csm-alerts/src/services/EventsWatcher/EventsWatcher.srv.ts b/csm-alerts/src/services/EventsWatcher/EventsWatcher.srv.ts index 46d891539..0407680e2 100644 --- a/csm-alerts/src/services/EventsWatcher/EventsWatcher.srv.ts +++ b/csm-alerts/src/services/EventsWatcher/EventsWatcher.srv.ts @@ -7,62 +7,61 @@ import * as Constants from '../constants' import * as Events from './events' const { ALIASES, DEPLOYED_ADDRESSES, ORACLE_MEMBERS } = requireWithTier( - module, - '../constants', - RedefineMode.Merge, + module, + '../constants', + RedefineMode.Merge, ) function asContract(key: keyof DeployedAddresses): { name: string; address: string } { - return { name: ALIASES[key], address: DEPLOYED_ADDRESSES[key] } + return { name: ALIASES[key], address: DEPLOYED_ADDRESSES[key] } } const CSM_PROXY_CONTRACTS = [ - asContract('CS_MODULE'), - asContract('CS_ACCOUNTING'), - asContract('CS_FEE_DISTRIBUTOR'), - asContract('CS_FEE_ORACLE'), + asContract('CS_MODULE'), + asContract('CS_ACCOUNTING'), + asContract('CS_FEE_DISTRIBUTOR'), + asContract('CS_FEE_ORACLE'), ] const CSM_ACL_CONTRACTS = [ - asContract('CS_MODULE'), - asContract('CS_ACCOUNTING'), - asContract('CS_FEE_DISTRIBUTOR'), - asContract('CS_FEE_ORACLE'), - asContract('HASH_CONSENSUS'), + asContract('CS_MODULE'), + asContract('CS_ACCOUNTING'), + asContract('CS_FEE_DISTRIBUTOR'), + asContract('CS_FEE_ORACLE'), + asContract('HASH_CONSENSUS'), ] const CSM_ASSET_RECOVERER_CONTRACTS = [ - asContract('CS_MODULE'), - asContract('CS_ACCOUNTING'), - asContract('CS_FEE_DISTRIBUTOR'), - asContract('CS_FEE_ORACLE'), + asContract('CS_MODULE'), + asContract('CS_ACCOUNTING'), + asContract('CS_FEE_DISTRIBUTOR'), + asContract('CS_FEE_ORACLE'), ] -// prettier-ignore const CSM_PAUSABLE_CONTRACTS = [ - asContract('CS_MODULE'), - asContract('CS_ACCOUNTING'), - asContract('CS_FEE_ORACLE'), + asContract('CS_MODULE'), + asContract('CS_ACCOUNTING'), + asContract('CS_FEE_ORACLE'), ] export class EventsWatcherSrv { - private readonly eventsOfNotice: EventOfNotice[] + private readonly eventsOfNotice: EventOfNotice[] - constructor() { - this.eventsOfNotice = [ - ...Events.getHashConsensusEvents(DEPLOYED_ADDRESSES.HASH_CONSENSUS, ORACLE_MEMBERS), - ...Events.getCSFeeDistributorEvents(DEPLOYED_ADDRESSES.CS_FEE_DISTRIBUTOR), - ...Events.getCSAccountingEvents(DEPLOYED_ADDRESSES.CS_ACCOUNTING), - ...Events.getCSFeeOracleEvents(DEPLOYED_ADDRESSES.CS_FEE_ORACLE), - ...Events.getCSModuleEvents(DEPLOYED_ADDRESSES.CS_MODULE), - ...Events.getAssetRecovererEvents(CSM_ASSET_RECOVERER_CONTRACTS), - ...Events.getOssifiedProxyEvents(CSM_PROXY_CONTRACTS), - ...Events.getRolesMonitoringEvents(CSM_ACL_CONTRACTS), - ...Events.getPausableEvents(CSM_PAUSABLE_CONTRACTS), - ] - } + constructor() { + this.eventsOfNotice = [ + ...Events.getHashConsensusEvents(DEPLOYED_ADDRESSES.HASH_CONSENSUS, ORACLE_MEMBERS), + ...Events.getCSFeeDistributorEvents(DEPLOYED_ADDRESSES.CS_FEE_DISTRIBUTOR), + ...Events.getCSAccountingEvents(DEPLOYED_ADDRESSES.CS_ACCOUNTING), + ...Events.getCSFeeOracleEvents(DEPLOYED_ADDRESSES.CS_FEE_ORACLE), + ...Events.getCSModuleEvents(DEPLOYED_ADDRESSES.CS_MODULE), + ...Events.getAssetRecovererEvents(CSM_ASSET_RECOVERER_CONTRACTS), + ...Events.getOssifiedProxyEvents(CSM_PROXY_CONTRACTS), + ...Events.getRolesMonitoringEvents(CSM_ACL_CONTRACTS), + ...Events.getPausableEvents(CSM_PAUSABLE_CONTRACTS), + ] + } - public handleTransaction(txEvent: TransactionEvent, provider: ethers.Provider): Finding[] { - return handleEventsOfNotice(txEvent, this.eventsOfNotice) - } + public handleTransaction(txEvent: TransactionEvent, provider: ethers.Provider): Finding[] { + return handleEventsOfNotice(txEvent, this.eventsOfNotice) + } } diff --git a/csm-alerts/src/services/EventsWatcher/events/accounting.ts b/csm-alerts/src/services/EventsWatcher/events/accounting.ts index c8896643c..f83018bbf 100644 --- a/csm-alerts/src/services/EventsWatcher/events/accounting.ts +++ b/csm-alerts/src/services/EventsWatcher/events/accounting.ts @@ -7,40 +7,44 @@ import { etherscanAddress, formatEther } from '../../../utils/string' const ICSAccounting = CSAccounting__factory.createInterface() export function getCSAccountingEvents(accounting: string): EventOfNotice[] { - return [ - { - address: accounting, - abi: ICSAccounting.getEvent('ChargePenaltyRecipientSet').format('full'), - alertId: 'CS-ACCOUNTING-CHARGE-PENALTY-RECIPIENT-SET', - name: '🚨 CSAccounting: Charge penalty recipient set', - description: (args: ethers.Result) => - `Charge penalty recipient set to ${etherscanAddress(args.chargeRecipient)} (expecting the treasury)`, - severity: FindingSeverity.Critical, - type: FindingType.Info, - }, - { - address: accounting, - abi: ICSAccounting.getEvent('BondCurveUpdated').format('full'), - alertId: 'CS-ACCOUNTING-BOND-CURVE-UPDATED', - name: '🚨 CSAccounting: Bond curve updated', - description: (args: ethers.Result) => { - const bondCurveString = args.bondCurve.map((value: bigint) => formatEther(value)).join(', ') - return `Bond curve #${args.curveId} updated. New values: [${bondCurveString}]` - }, - severity: FindingSeverity.Critical, - type: FindingType.Info, - }, - { - address: accounting, - abi: ICSAccounting.getEvent('BondCurveAdded').format('full'), - alertId: 'CS-ACCOUNTING-BOND-CURVE-ADDED', - name: '🔴 CSAccounting: Bond curve added', - description: (args: ethers.Result) => { - const bondCurveString = args.bondCurve.map((value: bigint) => formatEther(value)).join(', ') - return `Bond curve added: [${bondCurveString}]` - }, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - ] + return [ + { + address: accounting, + abi: ICSAccounting.getEvent('ChargePenaltyRecipientSet').format('full'), + alertId: 'CS-ACCOUNTING-CHARGE-PENALTY-RECIPIENT-SET', + name: '🚨 CSAccounting: Charge penalty recipient set', + description: (args: ethers.Result) => + `Charge penalty recipient set to ${etherscanAddress(args.chargeRecipient)} (expecting the treasury)`, + severity: FindingSeverity.Critical, + type: FindingType.Info, + }, + { + address: accounting, + abi: ICSAccounting.getEvent('BondCurveUpdated').format('full'), + alertId: 'CS-ACCOUNTING-BOND-CURVE-UPDATED', + name: '🚨 CSAccounting: Bond curve updated', + description: (args: ethers.Result) => { + const bondCurveString = args.bondCurve + .map((value: bigint) => formatEther(value)) + .join(', ') + return `Bond curve #${args.curveId} updated. New values: [${bondCurveString}]` + }, + severity: FindingSeverity.Critical, + type: FindingType.Info, + }, + { + address: accounting, + abi: ICSAccounting.getEvent('BondCurveAdded').format('full'), + alertId: 'CS-ACCOUNTING-BOND-CURVE-ADDED', + name: '🔴 CSAccounting: Bond curve added', + description: (args: ethers.Result) => { + const bondCurveString = args.bondCurve + .map((value: bigint) => formatEther(value)) + .join(', ') + return `Bond curve added: [${bondCurveString}]` + }, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + ] } diff --git a/csm-alerts/src/services/EventsWatcher/events/assetRecoverer.ts b/csm-alerts/src/services/EventsWatcher/events/assetRecoverer.ts index 07a4196c9..08634cfee 100644 --- a/csm-alerts/src/services/EventsWatcher/events/assetRecoverer.ts +++ b/csm-alerts/src/services/EventsWatcher/events/assetRecoverer.ts @@ -6,73 +6,75 @@ import { etherscanAddress, formatEther, formatShares } from '../../../utils/stri const IAssetRecoverer = AssetRecoverer__factory.createInterface() -export function getAssetRecovererEvents(contracts: { name: string; address: string }[]): EventOfNotice[] { - return contracts.flatMap((contract) => { - return [ - { - address: contract.address, - abi: IAssetRecoverer.getEvent('ERC20Recovered').format('full'), - alertId: 'ASSET-RECOVERER-ERC20-RECOVERED', - name: '🔴 AssetRecoverer: ERC20 recovered', - description: (args: ethers.Result) => - `ERC20 recovered on ${contract.name}:\n` + - `Recipient: ${etherscanAddress(args.recipient)}\n` + - `Token: ${etherscanAddress(args.token)}\n` + - `Amount: ${args.amount}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address: contract.address, - abi: IAssetRecoverer.getEvent('ERC721Recovered').format('full'), - alertId: 'ASSET-RECOVERER-ERC721-RECOVERED', - name: '🔴 AssetRecoverer: ERC721 recovered', - description: (args: ethers.Result) => - `ERC721 recovered on ${contract.name}:\n` + - `Recipient: ${etherscanAddress(args.recipient)}\n` + - `Token: ${etherscanAddress(args.token)}\n` + - `Token ID: ${args.tokenId}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address: contract.address, - abi: IAssetRecoverer.getEvent('ERC1155Recovered').format('full'), - alertId: 'ASSET-RECOVERER-ERC1155-RECOVERED', - name: '🔴 AssetRecoverer: ERC1155 recovered', - description: (args: ethers.Result) => - `ERC1155 recovered on ${contract.name}:\n` + - `Recipient: ${etherscanAddress(args.recipient)}\n` + - `Token: ${etherscanAddress(args.token)}\n` + - `Token ID: ${args.tokenId}\n` + - `Amount: ${args.amount}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address: contract.address, - abi: IAssetRecoverer.getEvent('EtherRecovered').format('full'), - alertId: 'ASSET-RECOVERER-ETHER-RECOVERED', - name: '🔴 AssetRecoverer: Ether recovered', - description: (args: ethers.Result) => - `Ether recovered on ${contract.name}:\n` + - `Recipient: ${etherscanAddress(args.recipient)}\n` + - `Amount: ${formatEther(args.amount)}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address: contract.address, - abi: IAssetRecoverer.getEvent('StETHSharesRecovered').format('full'), - alertId: 'ASSET-RECOVERER-STETH-SHARES-RECOVERED', - name: '🔴 AssetRecoverer: StETH Shares recovered', - description: (args: ethers.Result) => - `StETH Shares recovered on ${contract.name}:\n` + - `Recipient: ${etherscanAddress(args.recipient)}\n` + - `Amount: ${formatShares(args.shares)}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - ] - }) +export function getAssetRecovererEvents( + contracts: { name: string; address: string }[], +): EventOfNotice[] { + return contracts.flatMap((contract) => { + return [ + { + address: contract.address, + abi: IAssetRecoverer.getEvent('ERC20Recovered').format('full'), + alertId: 'ASSET-RECOVERER-ERC20-RECOVERED', + name: '🔴 AssetRecoverer: ERC20 recovered', + description: (args: ethers.Result) => + `ERC20 recovered on ${contract.name}:\n` + + `Recipient: ${etherscanAddress(args.recipient)}\n` + + `Token: ${etherscanAddress(args.token)}\n` + + `Amount: ${args.amount}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address: contract.address, + abi: IAssetRecoverer.getEvent('ERC721Recovered').format('full'), + alertId: 'ASSET-RECOVERER-ERC721-RECOVERED', + name: '🔴 AssetRecoverer: ERC721 recovered', + description: (args: ethers.Result) => + `ERC721 recovered on ${contract.name}:\n` + + `Recipient: ${etherscanAddress(args.recipient)}\n` + + `Token: ${etherscanAddress(args.token)}\n` + + `Token ID: ${args.tokenId}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address: contract.address, + abi: IAssetRecoverer.getEvent('ERC1155Recovered').format('full'), + alertId: 'ASSET-RECOVERER-ERC1155-RECOVERED', + name: '🔴 AssetRecoverer: ERC1155 recovered', + description: (args: ethers.Result) => + `ERC1155 recovered on ${contract.name}:\n` + + `Recipient: ${etherscanAddress(args.recipient)}\n` + + `Token: ${etherscanAddress(args.token)}\n` + + `Token ID: ${args.tokenId}\n` + + `Amount: ${args.amount}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address: contract.address, + abi: IAssetRecoverer.getEvent('EtherRecovered').format('full'), + alertId: 'ASSET-RECOVERER-ETHER-RECOVERED', + name: '🔴 AssetRecoverer: Ether recovered', + description: (args: ethers.Result) => + `Ether recovered on ${contract.name}:\n` + + `Recipient: ${etherscanAddress(args.recipient)}\n` + + `Amount: ${formatEther(args.amount)}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address: contract.address, + abi: IAssetRecoverer.getEvent('StETHSharesRecovered').format('full'), + alertId: 'ASSET-RECOVERER-STETH-SHARES-RECOVERED', + name: '🔴 AssetRecoverer: StETH Shares recovered', + description: (args: ethers.Result) => + `StETH Shares recovered on ${contract.name}:\n` + + `Recipient: ${etherscanAddress(args.recipient)}\n` + + `Amount: ${formatShares(args.shares)}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + ] + }) } diff --git a/csm-alerts/src/services/EventsWatcher/events/distributor.ts b/csm-alerts/src/services/EventsWatcher/events/distributor.ts index 0c57acf43..0162cc30a 100644 --- a/csm-alerts/src/services/EventsWatcher/events/distributor.ts +++ b/csm-alerts/src/services/EventsWatcher/events/distributor.ts @@ -6,27 +6,27 @@ import { EventOfNotice } from '../../../shared/types' const ICSFeeDistributor = CSFeeDistributor__factory.createInterface() export function getCSFeeDistributorEvents(distributorAddress: string): EventOfNotice[] { - return [ - { - address: distributorAddress, - abi: ICSFeeDistributor.getEvent('DistributionDataUpdated').format('full'), - alertId: 'CSFEE-DISTRIBUTOR-DISTRIBUTION-DATA-UPDATED', - name: '🔵 CSFeeDistributor: Distribution data updated', - description: (args: ethers.Result) => - `Total Claimable Shares: ${args.totalClaimableShares}\n` + - `Tree Root: ${args.treeRoot}\n` + - `Tree CID: ${args.treeCid}`, - severity: FindingSeverity.Info, - type: FindingType.Info, - }, - { - address: distributorAddress, - abi: ICSFeeDistributor.getEvent('DistributionLogUpdated').format('full'), - alertId: 'CSFEE-DISTRIBUTOR-DISTRIBUTION-LOG-UPDATED', - name: '🔵 CSFeeDistributor: Distribution log updated', - description: (args: ethers.Result) => `Log CID: ${args.logCid}`, - severity: FindingSeverity.Info, - type: FindingType.Info, - }, - ] + return [ + { + address: distributorAddress, + abi: ICSFeeDistributor.getEvent('DistributionDataUpdated').format('full'), + alertId: 'CSFEE-DISTRIBUTOR-DISTRIBUTION-DATA-UPDATED', + name: '🔵 CSFeeDistributor: Distribution data updated', + description: (args: ethers.Result) => + `Total Claimable Shares: ${args.totalClaimableShares}\n` + + `Tree Root: ${args.treeRoot}\n` + + `Tree CID: ${args.treeCid}`, + severity: FindingSeverity.Info, + type: FindingType.Info, + }, + { + address: distributorAddress, + abi: ICSFeeDistributor.getEvent('DistributionLogUpdated').format('full'), + alertId: 'CSFEE-DISTRIBUTOR-DISTRIBUTION-LOG-UPDATED', + name: '🔵 CSFeeDistributor: Distribution log updated', + description: (args: ethers.Result) => `Log CID: ${args.logCid}`, + severity: FindingSeverity.Info, + type: FindingType.Info, + }, + ] } diff --git a/csm-alerts/src/services/EventsWatcher/events/module.ts b/csm-alerts/src/services/EventsWatcher/events/module.ts index 16d8d6987..8729db654 100644 --- a/csm-alerts/src/services/EventsWatcher/events/module.ts +++ b/csm-alerts/src/services/EventsWatcher/events/module.ts @@ -7,76 +7,81 @@ import { formatEther } from '../../../utils/string' const ICSModule = CSModule__factory.createInterface() export function getCSModuleEvents(csmAddress: string): EventOfNotice[] { - return [ - { - address: csmAddress, - abi: ICSModule.getEvent('PublicRelease').format('full'), - alertId: 'CS-MODULE-PUBLIC-RELEASE', - name: '🔵 CSModule: Public release', - description: () => 'CSM public release is activated! 🥳', - severity: FindingSeverity.Info, - type: FindingType.Info, - }, - { - address: csmAddress, - abi: ICSModule.getEvent('VettedSigningKeysCountDecreased').format('full'), - alertId: 'CS-MODULE-VETTED-SIGNING-KEYS-DECREASED', - name: '🔵 CSModule: Node Operator vetted signing keys decreased', - description: (args: ethers.Result) => `Vetted signing keys decreased for Node Operator #${args.nodeOperatorId}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address: csmAddress, - abi: ICSModule.getEvent('TargetValidatorsCountChanged').format('full'), - alertId: 'CS-MODULE-TARGET-LIMIT-MODE-CHANGED', - name: '🟠 CSModule: Target limit mode changed', - description: (args: ethers.Result) => - `Target limit mode: ${args.targetLimitMode} (${ - args.targetLimitMode === 0n ? 'disabled' : args.targetLimitMode === 1n ? 'soft' : 'forced' - }) set for Node Operator ${args.nodeOperatorId}`, - severity: FindingSeverity.Medium, - type: FindingType.Info, - }, - { - address: csmAddress, - abi: ICSModule.getEvent('ELRewardsStealingPenaltyReported').format('full'), - alertId: 'CS-MODULE-EL-REWARDS-STEALING-PENALTY-REPORTED', - name: '🔴 CSModule: EL Rewards stealing penalty reported', - description: (args: ethers.Result) => - `EL Rewards stealing penalty reported for Node Operator #${args.nodeOperatorId} with ${formatEther(args.stolenAmount)} potentially stolen`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address: csmAddress, - abi: ICSModule.getEvent('ELRewardsStealingPenaltyCancelled').format('full'), - alertId: 'CS-MODULE-EL-REWARDS-STEALING-PENALTY-CANCELLED', - name: '🔴 CSModule: EL Rewards stealing penalty cancelled', - description: (args: ethers.Result) => - `EL Rewards stealing penalty (${formatEther(args.amount)}) cancelled for Node Operator #${args.nodeOperatorId}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address: csmAddress, - abi: ICSModule.getEvent('ELRewardsStealingPenaltySettled').format('full'), - alertId: 'CS-MODULE-EL-REWARDS-STEALING-PENALTY-SETTLED', - name: '🔴 CSModule: EL Rewards stealing penalty settled', - description: (args: ethers.Result) => - `EL Rewards stealing penalty settled for Node Operator #${args.nodeOperatorId}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address: csmAddress, - abi: ICSModule.getEvent('ELRewardsStealingPenaltyCompensated').format('full'), - alertId: 'CS-MODULE-EL-REWARDS-STEALING-PENALTY-COMPENSATED', - name: '🔴 CSModule: EL Rewards stealing penalty compensated', - description: (args: ethers.Result) => - `${formatEther(args.stolenAmount)} of EL Rewards stealing penalty was compensated for Node Operator #${args.nodeOperatorId}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - ] + return [ + { + address: csmAddress, + abi: ICSModule.getEvent('PublicRelease').format('full'), + alertId: 'CS-MODULE-PUBLIC-RELEASE', + name: '🔵 CSModule: Public release', + description: () => 'CSM public release is activated! 🥳', + severity: FindingSeverity.Info, + type: FindingType.Info, + }, + { + address: csmAddress, + abi: ICSModule.getEvent('VettedSigningKeysCountDecreased').format('full'), + alertId: 'CS-MODULE-VETTED-SIGNING-KEYS-DECREASED', + name: '🔵 CSModule: Node Operator vetted signing keys decreased', + description: (args: ethers.Result) => + `Vetted signing keys decreased for Node Operator #${args.nodeOperatorId}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address: csmAddress, + abi: ICSModule.getEvent('TargetValidatorsCountChanged').format('full'), + alertId: 'CS-MODULE-TARGET-LIMIT-MODE-CHANGED', + name: '🟠 CSModule: Target limit mode changed', + description: (args: ethers.Result) => + `Target limit mode: ${args.targetLimitMode} (${ + args.targetLimitMode === 0n + ? 'disabled' + : args.targetLimitMode === 1n + ? 'soft' + : 'forced' + }) set for Node Operator ${args.nodeOperatorId}`, + severity: FindingSeverity.Medium, + type: FindingType.Info, + }, + { + address: csmAddress, + abi: ICSModule.getEvent('ELRewardsStealingPenaltyReported').format('full'), + alertId: 'CS-MODULE-EL-REWARDS-STEALING-PENALTY-REPORTED', + name: '🔴 CSModule: EL Rewards stealing penalty reported', + description: (args: ethers.Result) => + `EL Rewards stealing penalty reported for Node Operator #${args.nodeOperatorId} with ${formatEther(args.stolenAmount)} potentially stolen`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address: csmAddress, + abi: ICSModule.getEvent('ELRewardsStealingPenaltyCancelled').format('full'), + alertId: 'CS-MODULE-EL-REWARDS-STEALING-PENALTY-CANCELLED', + name: '🔴 CSModule: EL Rewards stealing penalty cancelled', + description: (args: ethers.Result) => + `EL Rewards stealing penalty (${formatEther(args.amount)}) cancelled for Node Operator #${args.nodeOperatorId}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address: csmAddress, + abi: ICSModule.getEvent('ELRewardsStealingPenaltySettled').format('full'), + alertId: 'CS-MODULE-EL-REWARDS-STEALING-PENALTY-SETTLED', + name: '🔴 CSModule: EL Rewards stealing penalty settled', + description: (args: ethers.Result) => + `EL Rewards stealing penalty settled for Node Operator #${args.nodeOperatorId}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address: csmAddress, + abi: ICSModule.getEvent('ELRewardsStealingPenaltyCompensated').format('full'), + alertId: 'CS-MODULE-EL-REWARDS-STEALING-PENALTY-COMPENSATED', + name: '🔴 CSModule: EL Rewards stealing penalty compensated', + description: (args: ethers.Result) => + `${formatEther(args.stolenAmount)} of EL Rewards stealing penalty was compensated for Node Operator #${args.nodeOperatorId}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + ] } diff --git a/csm-alerts/src/services/EventsWatcher/events/oracle.ts b/csm-alerts/src/services/EventsWatcher/events/oracle.ts index be01d606f..9d19fe42a 100644 --- a/csm-alerts/src/services/EventsWatcher/events/oracle.ts +++ b/csm-alerts/src/services/EventsWatcher/events/oracle.ts @@ -7,170 +7,175 @@ import { etherscanAddress } from '../../../utils/string' const IHashConsensus = HashConsensus__factory.createInterface() const ICSFeeOracle = CSFeeOracle__factory.createInterface() -export function getHashConsensusEvents(address: string, knownMembers: { [key: string]: string }): EventOfNotice[] { - return [ - { - address, - abi: IHashConsensus.getEvent('MemberAdded').format('full'), - alertId: 'HASH-CONSENSUS-MEMBER-ADDED', - name: '🔴 HashConsensus: Member added', - description: (args: ethers.Result) => - `New member ${etherscanAddress(args.addr)} (${knownMembers[args.addr] ?? 'unknown'}) added\n` + - `Total members: ${args.newTotalMembers}\n` + - `New quorum: ${args.newQuorum}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address, - abi: IHashConsensus.getEvent('MemberRemoved').format('full'), - alertId: 'HASH-CONSENSUS-MEMBER-REMOVED', - name: '🔴 HashConsensus: Member removed', - description: (args: ethers.Result) => - `Member ${etherscanAddress(args.addr)} (${knownMembers[args.addr] ?? 'unknown'}) removed\n` + - `Total members: ${args.newTotalMembers}\n` + - `New quorum: ${args.newQuorum}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address, - abi: IHashConsensus.getEvent('QuorumSet').format('full'), - alertId: 'HASH-CONSENSUS-QUORUM-SET', - name: '🔴 HashConsensus: Quorum set', - description: (args: ethers.Result) => - `Quorum set to ${args.newQuorum}.\n` + - `Total members: ${args.totalMembers}\n` + - `Previous quorum: ${args.prevQuorum}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address, - abi: IHashConsensus.getEvent('FastLaneConfigSet').format('full'), - alertId: 'HASH-CONSENSUS-FASTLANE-CONFIG-SET', - name: '🔴 HashConsensus: Fastlane config set', - description: (args: ethers.Result) => `Fastlane configuration set with length slots: ${args.fastLaneLengthSlots}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address, - abi: IHashConsensus.getEvent('FrameConfigSet').format('full'), - alertId: 'HASH-CONSENSUS-FRAME-CONFIG-SET', - name: '🔴 HashConsensus: Frame config set', - description: (args: ethers.Result) => - `Frame configuration set.\n` + - `New initial epoch: ${args.newInitialEpoch}\n` + - `Epochs per frame: ${args.newEpochsPerFrame}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address, - abi: IHashConsensus.getEvent('ReportProcessorSet').format('full'), - alertId: 'HASH-CONSENSUS-REPORT-PROCESSOR-SET', - name: '🔴 HashConsensus: Report processor set', - description: (args: ethers.Result) => - `Current processor: ${etherscanAddress(args.processor)}\nPrevious processor: ${etherscanAddress(args.prevProcessor)}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address, - abi: IHashConsensus.getEvent('ConsensusLost').format('full'), - alertId: 'HASH-CONSENSUS-LOST', - name: '🔴 HashConsensus: Consensus lost', - description: (args: ethers.Result) => `Consensus lost for slot ${args.refSlot}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address, - abi: IHashConsensus.getEvent('ConsensusReached').format('full'), - alertId: 'HASH-CONSENSUS-REACHED', - name: '🔵 HashConsensus: Consensus reached, report received', - // prettier-ignore - description: (args: ethers.Result) => - `Consensus reached for slot ${args.refSlot}\n` + - `Report hash: ${args.report}\n` + - `Support: ${args.support}`, - severity: FindingSeverity.Info, - type: FindingType.Info, - }, - ] +export function getHashConsensusEvents( + address: string, + knownMembers: { [key: string]: string }, +): EventOfNotice[] { + return [ + { + address, + abi: IHashConsensus.getEvent('MemberAdded').format('full'), + alertId: 'HASH-CONSENSUS-MEMBER-ADDED', + name: '🔴 HashConsensus: Member added', + description: (args: ethers.Result) => + `New member ${etherscanAddress(args.addr)} (${knownMembers[args.addr] ?? 'unknown'}) added\n` + + `Total members: ${args.newTotalMembers}\n` + + `New quorum: ${args.newQuorum}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address, + abi: IHashConsensus.getEvent('MemberRemoved').format('full'), + alertId: 'HASH-CONSENSUS-MEMBER-REMOVED', + name: '🔴 HashConsensus: Member removed', + description: (args: ethers.Result) => + `Member ${etherscanAddress(args.addr)} (${knownMembers[args.addr] ?? 'unknown'}) removed\n` + + `Total members: ${args.newTotalMembers}\n` + + `New quorum: ${args.newQuorum}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address, + abi: IHashConsensus.getEvent('QuorumSet').format('full'), + alertId: 'HASH-CONSENSUS-QUORUM-SET', + name: '🔴 HashConsensus: Quorum set', + description: (args: ethers.Result) => + `Quorum set to ${args.newQuorum}.\n` + + `Total members: ${args.totalMembers}\n` + + `Previous quorum: ${args.prevQuorum}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address, + abi: IHashConsensus.getEvent('FastLaneConfigSet').format('full'), + alertId: 'HASH-CONSENSUS-FASTLANE-CONFIG-SET', + name: '🔴 HashConsensus: Fastlane config set', + description: (args: ethers.Result) => + `Fastlane configuration set with length slots: ${args.fastLaneLengthSlots}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address, + abi: IHashConsensus.getEvent('FrameConfigSet').format('full'), + alertId: 'HASH-CONSENSUS-FRAME-CONFIG-SET', + name: '🔴 HashConsensus: Frame config set', + description: (args: ethers.Result) => + `Frame configuration set.\n` + + `New initial epoch: ${args.newInitialEpoch}\n` + + `Epochs per frame: ${args.newEpochsPerFrame}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address, + abi: IHashConsensus.getEvent('ReportProcessorSet').format('full'), + alertId: 'HASH-CONSENSUS-REPORT-PROCESSOR-SET', + name: '🔴 HashConsensus: Report processor set', + description: (args: ethers.Result) => + `Current processor: ${etherscanAddress(args.processor)}\nPrevious processor: ${etherscanAddress(args.prevProcessor)}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address, + abi: IHashConsensus.getEvent('ConsensusLost').format('full'), + alertId: 'HASH-CONSENSUS-LOST', + name: '🔴 HashConsensus: Consensus lost', + description: (args: ethers.Result) => `Consensus lost for slot ${args.refSlot}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address, + abi: IHashConsensus.getEvent('ConsensusReached').format('full'), + alertId: 'HASH-CONSENSUS-REACHED', + name: '🔵 HashConsensus: Consensus reached, report received', + description: (args: ethers.Result) => + `Consensus reached for slot ${args.refSlot}\n` + + `Report hash: ${args.report}\n` + + `Support: ${args.support}`, + severity: FindingSeverity.Info, + type: FindingType.Info, + }, + ] } export function getCSFeeOracleEvents(address: string): EventOfNotice[] { - return [ - { - address, - abi: ICSFeeOracle.getEvent('ConsensusHashContractSet').format('full'), - alertId: 'CSFEE-ORACLE-CONSENSUS-HASH-CONTRACT-SET', - name: '🚨 CSFeeOracle: Consensus hash contract set', - description: (args: ethers.Result) => - `Consensus hash contract set to ${etherscanAddress(args.addr)}, previous contract was ${etherscanAddress(args.prevAddr)}`, - severity: FindingSeverity.Critical, - type: FindingType.Info, - }, - { - address, - abi: ICSFeeOracle.getEvent('PerfLeewaySet').format('full'), - alertId: 'CSFEE-ORACLE-PERF-LEEWAY-SET', - name: '🔴 CSFeeOracle: Performance leeway updated', - description: (args: ethers.Result) => `Performance leeway set to ${args.valueBP} basis points`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address, - abi: ICSFeeOracle.getEvent('FeeDistributorContractSet').format('full'), - alertId: 'CSFEE-ORACLE-FEE-DISTRIBUTOR-CONTRACT-SET', - name: '🔴 CSFeeOracle: New CSFeeDistributor set', - description: (args: ethers.Result) => - `New CSFeeDistributor contract set to ${etherscanAddress(args.feeDistributorContract)}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address, - abi: ICSFeeOracle.getEvent('ConsensusVersionSet').format('full'), - alertId: 'CSFEE-ORACLE-CONSENSUS-VERSION-SET', - name: '🔴 CSFeeOracle: Consensus version set', - description: (args: ethers.Result) => - `Consensus version set to ${args.version}, previous version was ${args.prevVersion}`, - severity: FindingSeverity.High, - type: FindingType.Info, - }, - { - address, - abi: ICSFeeOracle.getEvent('WarnProcessingMissed').format('full'), - alertId: 'CSFEE-ORACLE-PROCESSING-MISSED', - name: '🔵 CSFeeOracle: Processing missed', - description: (args: ethers.Result) => `Processing missed for slot ${args.refSlot}`, - severity: FindingSeverity.Info, - type: FindingType.Info, - }, - { - address, - abi: ICSFeeOracle.getEvent('ReportSubmitted').format('full'), - alertId: 'CSFEE-ORACLE-REPORT-SUBMITTED', - name: '🔵 CSFeeOracle: Report submitted', - description: (args: ethers.Result) => - `Report submitted for slot ${args.refSlot}\n` + - `Report hash: ${args.hash}\n` + - `Processing deadline time: ${args.processingDeadlineTime}`, - severity: FindingSeverity.Info, - type: FindingType.Info, - }, - { - address, - abi: ICSFeeOracle.getEvent('ProcessingStarted').format('full'), - alertId: 'CSFEE-ORACLE-PROCESSING-STARTED', - name: '🔵 CSFeeOracle: Processing started', - description: (args: ethers.Result) => `Processing started for slot ${args.refSlot}\nReport hash: ${args.hash}`, - severity: FindingSeverity.Info, - type: FindingType.Info, - }, - ] + return [ + { + address, + abi: ICSFeeOracle.getEvent('ConsensusHashContractSet').format('full'), + alertId: 'CSFEE-ORACLE-CONSENSUS-HASH-CONTRACT-SET', + name: '🚨 CSFeeOracle: Consensus hash contract set', + description: (args: ethers.Result) => + `Consensus hash contract set to ${etherscanAddress(args.addr)}, previous contract was ${etherscanAddress(args.prevAddr)}`, + severity: FindingSeverity.Critical, + type: FindingType.Info, + }, + { + address, + abi: ICSFeeOracle.getEvent('PerfLeewaySet').format('full'), + alertId: 'CSFEE-ORACLE-PERF-LEEWAY-SET', + name: '🔴 CSFeeOracle: Performance leeway updated', + description: (args: ethers.Result) => + `Performance leeway set to ${args.valueBP} basis points`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address, + abi: ICSFeeOracle.getEvent('FeeDistributorContractSet').format('full'), + alertId: 'CSFEE-ORACLE-FEE-DISTRIBUTOR-CONTRACT-SET', + name: '🔴 CSFeeOracle: New CSFeeDistributor set', + description: (args: ethers.Result) => + `New CSFeeDistributor contract set to ${etherscanAddress(args.feeDistributorContract)}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address, + abi: ICSFeeOracle.getEvent('ConsensusVersionSet').format('full'), + alertId: 'CSFEE-ORACLE-CONSENSUS-VERSION-SET', + name: '🔴 CSFeeOracle: Consensus version set', + description: (args: ethers.Result) => + `Consensus version set to ${args.version}, previous version was ${args.prevVersion}`, + severity: FindingSeverity.High, + type: FindingType.Info, + }, + { + address, + abi: ICSFeeOracle.getEvent('WarnProcessingMissed').format('full'), + alertId: 'CSFEE-ORACLE-PROCESSING-MISSED', + name: '🔵 CSFeeOracle: Processing missed', + description: (args: ethers.Result) => `Processing missed for slot ${args.refSlot}`, + severity: FindingSeverity.Info, + type: FindingType.Info, + }, + { + address, + abi: ICSFeeOracle.getEvent('ReportSubmitted').format('full'), + alertId: 'CSFEE-ORACLE-REPORT-SUBMITTED', + name: '🔵 CSFeeOracle: Report submitted', + description: (args: ethers.Result) => + `Report submitted for slot ${args.refSlot}\n` + + `Report hash: ${args.hash}\n` + + `Processing deadline time: ${args.processingDeadlineTime}`, + severity: FindingSeverity.Info, + type: FindingType.Info, + }, + { + address, + abi: ICSFeeOracle.getEvent('ProcessingStarted').format('full'), + alertId: 'CSFEE-ORACLE-PROCESSING-STARTED', + name: '🔵 CSFeeOracle: Processing started', + description: (args: ethers.Result) => + `Processing started for slot ${args.refSlot}\nReport hash: ${args.hash}`, + severity: FindingSeverity.Info, + type: FindingType.Info, + }, + ] } diff --git a/csm-alerts/src/services/EventsWatcher/events/pausable.ts b/csm-alerts/src/services/EventsWatcher/events/pausable.ts index 1503a88e0..44cbe419b 100644 --- a/csm-alerts/src/services/EventsWatcher/events/pausable.ts +++ b/csm-alerts/src/services/EventsWatcher/events/pausable.ts @@ -9,26 +9,26 @@ import { formatDelay } from '../../../utils/time' const IPausableUntil = PausableUntil__factory.createInterface() export function getPausableEvents(contracts: { name: string; address: string }[]): EventOfNotice[] { - return contracts.flatMap((contract) => { - return [ - { - address: contract.address, - abi: IPausableUntil.getEvent('Paused').format('full'), - alertId: `${toKebabCase(contract.name)}-PAUSED`, - name: `🚨 ${contract.name}: contract was paused`, - description: (args: Result) => `Contract paused for ${formatDelay(args.duration)}`, - severity: FindingSeverity.Critical, - type: FindingType.Info, - }, - { - address: contract.address, - abi: IPausableUntil.getEvent('Resumed').format('full'), - alertId: `${toKebabCase(contract.name)}-RESUMED`, - name: `🚨 ${contract.name}: contract was resumed`, - description: () => `Contract ${contract.address} was resumed`, - severity: FindingSeverity.Critical, - type: FindingType.Info, - }, - ] - }) + return contracts.flatMap((contract) => { + return [ + { + address: contract.address, + abi: IPausableUntil.getEvent('Paused').format('full'), + alertId: `${toKebabCase(contract.name)}-PAUSED`, + name: `🚨 ${contract.name}: contract was paused`, + description: (args: Result) => `Contract paused for ${formatDelay(args.duration)}`, + severity: FindingSeverity.Critical, + type: FindingType.Info, + }, + { + address: contract.address, + abi: IPausableUntil.getEvent('Resumed').format('full'), + alertId: `${toKebabCase(contract.name)}-RESUMED`, + name: `🚨 ${contract.name}: contract was resumed`, + description: () => `Contract ${contract.address} was resumed`, + severity: FindingSeverity.Critical, + type: FindingType.Info, + }, + ] + }) } diff --git a/csm-alerts/src/services/EventsWatcher/events/proxies.ts b/csm-alerts/src/services/EventsWatcher/events/proxies.ts index c2391abb7..10a4a9365 100644 --- a/csm-alerts/src/services/EventsWatcher/events/proxies.ts +++ b/csm-alerts/src/services/EventsWatcher/events/proxies.ts @@ -6,37 +6,41 @@ import { etherscanAddress } from '../../../utils/string' const IOssifiableProxy = OssifiableProxy__factory.createInterface() -export function getOssifiedProxyEvents(contracts: { address: string; name: string }[]): EventOfNotice[] { - return contracts.flatMap((contract) => { - return [ - { - address: contract.address, - abi: IOssifiableProxy.getEvent('ProxyOssified').format('full'), - alertId: 'PROXY-OSSIFIED', - name: `🚨 ${contract.name}: Proxy Ossified`, - description: () => `Proxy for ${contract.name}(${etherscanAddress(contract.address)}) was ossified`, - severity: FindingSeverity.Critical, - type: FindingType.Info, - }, - { - address: contract.address, - abi: IOssifiableProxy.getEvent('Upgraded').format('full'), - alertId: 'PROXY-UPGRADED', - name: `🚨 ${contract.name}: Implementation Upgraded`, - description: (args: ethers.Result) => `The proxy implementation has been upgraded to ${args.implementation}`, - severity: FindingSeverity.Critical, - type: FindingType.Info, - }, - { - address: contract.address, - abi: IOssifiableProxy.getEvent('AdminChanged').format('full'), - alertId: 'PROXY-ADMIN-CHANGED', - name: `🚨 ${contract.name}: Admin Changed`, - description: (args: ethers.Result) => - `The proxy admin for ${contract.name}(${contract.address}) has been changed from ${etherscanAddress(args.previousAdmin)} to ${etherscanAddress(args.newAdmin)}`, - severity: FindingSeverity.Critical, - type: FindingType.Info, - }, - ] - }) +export function getOssifiedProxyEvents( + contracts: { address: string; name: string }[], +): EventOfNotice[] { + return contracts.flatMap((contract) => { + return [ + { + address: contract.address, + abi: IOssifiableProxy.getEvent('ProxyOssified').format('full'), + alertId: 'PROXY-OSSIFIED', + name: `🚨 ${contract.name}: Proxy Ossified`, + description: () => + `Proxy for ${contract.name}(${etherscanAddress(contract.address)}) was ossified`, + severity: FindingSeverity.Critical, + type: FindingType.Info, + }, + { + address: contract.address, + abi: IOssifiableProxy.getEvent('Upgraded').format('full'), + alertId: 'PROXY-UPGRADED', + name: `🚨 ${contract.name}: Implementation Upgraded`, + description: (args: ethers.Result) => + `The proxy implementation has been upgraded to ${args.implementation}`, + severity: FindingSeverity.Critical, + type: FindingType.Info, + }, + { + address: contract.address, + abi: IOssifiableProxy.getEvent('AdminChanged').format('full'), + alertId: 'PROXY-ADMIN-CHANGED', + name: `🚨 ${contract.name}: Admin Changed`, + description: (args: ethers.Result) => + `The proxy admin for ${contract.name}(${contract.address}) has been changed from ${etherscanAddress(args.previousAdmin)} to ${etherscanAddress(args.newAdmin)}`, + severity: FindingSeverity.Critical, + type: FindingType.Info, + }, + ] + }) } diff --git a/csm-alerts/src/services/EventsWatcher/events/roles.ts b/csm-alerts/src/services/EventsWatcher/events/roles.ts index 6e7149cd1..ed8028f7e 100644 --- a/csm-alerts/src/services/EventsWatcher/events/roles.ts +++ b/csm-alerts/src/services/EventsWatcher/events/roles.ts @@ -4,29 +4,31 @@ import { RolesMapping } from '../../../shared/roles' import { EventOfNotice } from '../../../shared/types' import { etherscanAddress } from '../../../utils/string' -export function getRolesMonitoringEvents(contracts: { name: string; address: string }[]): EventOfNotice[] { - return contracts.flatMap((contract) => { - return [ - { - address: contract.address, - abi: 'event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender)', - alertId: `ROLE-GRANTED`, - name: `🚨 ${contract.name}: Role granted`, - description: (args: ethers.Result) => - `Role ${args.role} (${RolesMapping[args.role] ?? 'unknown'}) was granted to ${etherscanAddress(args.account)} on ${etherscanAddress(contract.address)}`, - severity: FindingSeverity.Critical, - type: FindingType.Info, - }, - { - address: contract.address, - abi: 'event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender)', - alertId: `ROLE-REVOKED`, - name: `🚨 ${contract.name}: Role revoked`, - description: (args: ethers.Result) => - `Role ${args.role} (${RolesMapping[args.role] ?? 'unknown'}) was revoked from ${etherscanAddress(args.account)} on ${etherscanAddress(contract.address)}`, - severity: FindingSeverity.Critical, - type: FindingType.Info, - }, - ] - }) +export function getRolesMonitoringEvents( + contracts: { name: string; address: string }[], +): EventOfNotice[] { + return contracts.flatMap((contract) => { + return [ + { + address: contract.address, + abi: 'event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender)', + alertId: `ROLE-GRANTED`, + name: `🚨 ${contract.name}: Role granted`, + description: (args: ethers.Result) => + `Role ${args.role} (${RolesMapping[args.role] ?? 'unknown'}) was granted to ${etherscanAddress(args.account)} on ${etherscanAddress(contract.address)}`, + severity: FindingSeverity.Critical, + type: FindingType.Info, + }, + { + address: contract.address, + abi: 'event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender)', + alertId: `ROLE-REVOKED`, + name: `🚨 ${contract.name}: Role revoked`, + description: (args: ethers.Result) => + `Role ${args.role} (${RolesMapping[args.role] ?? 'unknown'}) was revoked from ${etherscanAddress(args.account)} on ${etherscanAddress(contract.address)}`, + severity: FindingSeverity.Critical, + type: FindingType.Info, + }, + ] + }) } diff --git a/csm-alerts/src/services/constants.holesky.ts b/csm-alerts/src/services/constants.holesky.ts index 2b1e1b1b5..e545bfcc6 100644 --- a/csm-alerts/src/services/constants.holesky.ts +++ b/csm-alerts/src/services/constants.holesky.ts @@ -1,28 +1,28 @@ import { DeployedAddresses } from '../shared/types' export const DEPLOYED_ADDRESSES: DeployedAddresses = { - CS_MODULE: '0x4562c3e63c2e586cD1651B958C22F88135aCAd4f', - CS_ACCOUNTING: '0xc093e53e8F4b55A223c18A2Da6fA00e60DD5EFE1', - CS_FEE_DISTRIBUTOR: '0xD7ba648C8F72669C6aE649648B516ec03D07c8ED', - CS_FEE_ORACLE: '0xaF57326C7d513085051b50912D51809ECC5d98Ee', - CS_VERIFIER: '0x6DcA479178E6Ae41CCEB72a88FfDaa3e10E83CB7', - CS_EARLY_ADOPTION: '0x71E92eA77C198a770d9f33A03277DbeB99989660', - CS_GATE_SEAL: '0x41F2677fae0222cF1f08Cd1c0AAa607B469654Ce', - LIDO_STETH: '0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034', - BURNER: '0x4E46BD7147ccf666E1d73A3A456fC7a68de82eCA', - HASH_CONSENSUS: '0xbF38618Ea09B503c1dED867156A0ea276Ca1AE37', - STAKING_ROUTER: '0xd6EbF043D30A7fe46D1Db32BA90a0A51207FE229', + CS_MODULE: '0x4562c3e63c2e586cD1651B958C22F88135aCAd4f', + CS_ACCOUNTING: '0xc093e53e8F4b55A223c18A2Da6fA00e60DD5EFE1', + CS_FEE_DISTRIBUTOR: '0xD7ba648C8F72669C6aE649648B516ec03D07c8ED', + CS_FEE_ORACLE: '0xaF57326C7d513085051b50912D51809ECC5d98Ee', + CS_VERIFIER: '0x6DcA479178E6Ae41CCEB72a88FfDaa3e10E83CB7', + CS_EARLY_ADOPTION: '0x71E92eA77C198a770d9f33A03277DbeB99989660', + CS_GATE_SEAL: '0x41F2677fae0222cF1f08Cd1c0AAa607B469654Ce', + LIDO_STETH: '0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034', + BURNER: '0x4E46BD7147ccf666E1d73A3A456fC7a68de82eCA', + HASH_CONSENSUS: '0xbF38618Ea09B503c1dED867156A0ea276Ca1AE37', + STAKING_ROUTER: '0xd6EbF043D30A7fe46D1Db32BA90a0A51207FE229', } export const ORACLE_MEMBERS: { [key: string]: string } = { - '0xF0F23944EfC5A63c53632C571E7377b85d5E6B6f': 'Chorus One', - '0xb29dD2f6672C0DFF2d2f173087739A42877A5172': 'Staking Facilities', - '0xD3b1e36A372Ca250eefF61f90E833Ca070559970': 'Stakefish', - '0x31fa51343297FFce0CC1E67a50B2D3428057D1b1': 'P2P', - '0xf7aE520e99ed3C41180B5E12681d31Aa7302E4e5': 'Chainlayer', - '0x4c75FA734a39f3a21C57e583c1c29942F021C6B7': 'bloXroute', - '0x81E411f1BFDa43493D7994F82fb61A415F6b8Fd4': 'Instadapp', - '0xfe43A8B0b481Ae9fB1862d31826532047d2d538c': 'MatrixedLink', - '0x12A1D74F8697b9f4F1eEBb0a9d0FB6a751366399': 'Lido', - '0xD892c09b556b547c80B7d8c8cB8d75bf541B2284': 'Lido', + '0xF0F23944EfC5A63c53632C571E7377b85d5E6B6f': 'Chorus One', + '0xb29dD2f6672C0DFF2d2f173087739A42877A5172': 'Staking Facilities', + '0xD3b1e36A372Ca250eefF61f90E833Ca070559970': 'Stakefish', + '0x31fa51343297FFce0CC1E67a50B2D3428057D1b1': 'P2P', + '0xf7aE520e99ed3C41180B5E12681d31Aa7302E4e5': 'Chainlayer', + '0x4c75FA734a39f3a21C57e583c1c29942F021C6B7': 'bloXroute', + '0x81E411f1BFDa43493D7994F82fb61A415F6b8Fd4': 'Instadapp', + '0xfe43A8B0b481Ae9fB1862d31826532047d2d538c': 'MatrixedLink', + '0x12A1D74F8697b9f4F1eEBb0a9d0FB6a751366399': 'Lido', + '0xD892c09b556b547c80B7d8c8cB8d75bf541B2284': 'Lido', } diff --git a/csm-alerts/src/services/constants.ts b/csm-alerts/src/services/constants.ts index c76ab86b7..5ae576c08 100644 --- a/csm-alerts/src/services/constants.ts +++ b/csm-alerts/src/services/constants.ts @@ -1,41 +1,41 @@ import { DeployedAddresses } from '../shared/types' export const DEPLOYED_ADDRESSES: DeployedAddresses = { - CS_MODULE: '0x0000000000000000000000000000000000000000', - CS_ACCOUNTING: '0x0000000000000000000000000000000000000000', - CS_FEE_DISTRIBUTOR: '0x0000000000000000000000000000000000000000', - CS_FEE_ORACLE: '0x0000000000000000000000000000000000000000', - CS_VERIFIER: '0x0000000000000000000000000000000000000000', - CS_EARLY_ADOPTION: '0x0000000000000000000000000000000000000000', - CS_GATE_SEAL: '0x0000000000000000000000000000000000000000', - LIDO_STETH: '0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034', - BURNER: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84 ', - HASH_CONSENSUS: '0x0000000000000000000000000000000000000000', - STAKING_ROUTER: '0x0000000000000000000000000000000000000000', + CS_MODULE: '0x0000000000000000000000000000000000000000', + CS_ACCOUNTING: '0x0000000000000000000000000000000000000000', + CS_FEE_DISTRIBUTOR: '0x0000000000000000000000000000000000000000', + CS_FEE_ORACLE: '0x0000000000000000000000000000000000000000', + CS_VERIFIER: '0x0000000000000000000000000000000000000000', + CS_EARLY_ADOPTION: '0x0000000000000000000000000000000000000000', + CS_GATE_SEAL: '0x0000000000000000000000000000000000000000', + LIDO_STETH: '0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034', + BURNER: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84 ', + HASH_CONSENSUS: '0x0000000000000000000000000000000000000000', + STAKING_ROUTER: '0x0000000000000000000000000000000000000000', } export const ALIASES: Record = { - CS_MODULE: 'CSM', - CS_ACCOUNTING: 'CSAccounting', - CS_FEE_DISTRIBUTOR: 'CSFeeDistributor', - CS_FEE_ORACLE: 'CSFeeOracle', - CS_VERIFIER: 'CSAccounting', - CS_EARLY_ADOPTION: 'CSEarlyAdoption', - CS_GATE_SEAL: 'CSM Gate Seal', - LIDO_STETH: 'Lido', - BURNER: 'Burner', - HASH_CONSENSUS: 'CSM HashConsensus', - STAKING_ROUTER: 'StakingRouter', + CS_MODULE: 'CSM', + CS_ACCOUNTING: 'CSAccounting', + CS_FEE_DISTRIBUTOR: 'CSFeeDistributor', + CS_FEE_ORACLE: 'CSFeeOracle', + CS_VERIFIER: 'CSAccounting', + CS_EARLY_ADOPTION: 'CSEarlyAdoption', + CS_GATE_SEAL: 'CSM Gate Seal', + LIDO_STETH: 'Lido', + BURNER: 'Burner', + HASH_CONSENSUS: 'CSM HashConsensus', + STAKING_ROUTER: 'StakingRouter', } export const ORACLE_MEMBERS: { [key: string]: string } = { - '0x140Bd8FbDc884f48dA7cb1c09bE8A2fAdfea776E': 'Chorus One', - '0x404335BcE530400a5814375E7Ec1FB55fAff3eA2': 'Staking Facilities', - '0x946D3b081ed19173dC83Cd974fC69e1e760B7d78': 'Stakefish', - '0x007DE4a5F7bc37E2F26c0cb2E8A95006EE9B89b5': 'P2P', - '0xc79F702202E3A6B0B6310B537E786B9ACAA19BAf': 'Chainlayer', - '0x61c91ECd902EB56e314bB2D5c5C07785444Ea1c8': 'bloXroute', - '0x1Ca0fEC59b86F549e1F1184d97cb47794C8Af58d': 'Instadapp', - '0xe57B3792aDCc5da47EF4fF588883F0ee0c9835C9': 'MatrixedLink', - '0xA7410857ABbf75043d61ea54e07D57A6EB6EF186': 'Kyber Network', + '0x140Bd8FbDc884f48dA7cb1c09bE8A2fAdfea776E': 'Chorus One', + '0x404335BcE530400a5814375E7Ec1FB55fAff3eA2': 'Staking Facilities', + '0x946D3b081ed19173dC83Cd974fC69e1e760B7d78': 'Stakefish', + '0x007DE4a5F7bc37E2F26c0cb2E8A95006EE9B89b5': 'P2P', + '0xc79F702202E3A6B0B6310B537E786B9ACAA19BAf': 'Chainlayer', + '0x61c91ECd902EB56e314bB2D5c5C07785444Ea1c8': 'bloXroute', + '0x1Ca0fEC59b86F549e1F1184d97cb47794C8Af58d': 'Instadapp', + '0xe57B3792aDCc5da47EF4fF588883F0ee0c9835C9': 'MatrixedLink', + '0xA7410857ABbf75043d61ea54e07D57A6EB6EF186': 'Kyber Network', } diff --git a/csm-alerts/src/shared/roles.ts b/csm-alerts/src/shared/roles.ts index daad2963b..8bd70b0c4 100644 --- a/csm-alerts/src/shared/roles.ts +++ b/csm-alerts/src/shared/roles.ts @@ -1,26 +1,26 @@ import { ethers, keccak256 } from '@fortanetwork/forta-bot' export const RolesMapping = { - [ethers.ZeroHash]: 'DEFAULT ADMIN', - [keccak256('PAUSE_ROLE')]: 'PAUSE', - [keccak256('RESUME_ROLE')]: 'RESUME', - [keccak256('MODULE_MANAGER_ROLE')]: 'MODULE MANAGER', - [keccak256('STAKING_ROUTER_ROLE')]: 'STAKING ROUTER', - [keccak256('REPORT_EL_REWARDS_STEALING_PENALTY_ROLE')]: 'REPORT EL REWARDS STEALING PENALTY', - [keccak256('SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE')]: 'SETTLE EL REWARDS STEALING PENALTY', - [keccak256('VERIFIER_ROLE')]: 'VERIFIER', - [keccak256('RECOVERER_ROLE')]: 'RECOVERER', - [keccak256('ACCOUNTING_MANAGER_ROLE')]: 'ACCOUNTING MANAGER', - [keccak256('MANAGE_BOND_CURVES_ROLE')]: 'MANAGE BOND CURVES', - [keccak256('SET_BOND_CURVE_ROLE')]: 'SET BOND CURVE', - [keccak256('RESET_BOND_CURVE_ROLE')]: 'RESET BOND CURVE', - [keccak256('CONTRACT_MANAGER_ROLE')]: 'CONTRACT MANAGER', - [keccak256('SUBMIT_DATA_ROLE')]: 'SUBMIT DATA', - [keccak256('DISABLE_CONSENSUS_ROLE')]: 'DISABLE CONSENSUS', - [keccak256('MANAGE_MEMBERS_AND_QUORUM_ROLE')]: 'MANAGE MEMBERS AND QUORUM', - [keccak256('MANAGE_FRAME_CONFIG_ROLE')]: 'MANAGE FRAME CONFIG', - [keccak256('MANAGE_FAST_LANE_CONFIG_ROLE')]: 'MANAGE FAST LANE CONFIG', - [keccak256('MANAGE_REPORT_PROCESSOR_ROLE')]: 'MANAGE REPORT PROCESSOR', - [keccak256('MANAGE_CONSENSUS_CONTRACT_ROLE')]: 'MANAGE CONSENSUS CONTRACT', - [keccak256('MANAGE_CONSENSUS_VERSION_ROLE')]: 'MANAGE CONSENSUS VERSION', + [ethers.ZeroHash]: 'DEFAULT ADMIN', + [keccak256('PAUSE_ROLE')]: 'PAUSE', + [keccak256('RESUME_ROLE')]: 'RESUME', + [keccak256('MODULE_MANAGER_ROLE')]: 'MODULE MANAGER', + [keccak256('STAKING_ROUTER_ROLE')]: 'STAKING ROUTER', + [keccak256('REPORT_EL_REWARDS_STEALING_PENALTY_ROLE')]: 'REPORT EL REWARDS STEALING PENALTY', + [keccak256('SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE')]: 'SETTLE EL REWARDS STEALING PENALTY', + [keccak256('VERIFIER_ROLE')]: 'VERIFIER', + [keccak256('RECOVERER_ROLE')]: 'RECOVERER', + [keccak256('ACCOUNTING_MANAGER_ROLE')]: 'ACCOUNTING MANAGER', + [keccak256('MANAGE_BOND_CURVES_ROLE')]: 'MANAGE BOND CURVES', + [keccak256('SET_BOND_CURVE_ROLE')]: 'SET BOND CURVE', + [keccak256('RESET_BOND_CURVE_ROLE')]: 'RESET BOND CURVE', + [keccak256('CONTRACT_MANAGER_ROLE')]: 'CONTRACT MANAGER', + [keccak256('SUBMIT_DATA_ROLE')]: 'SUBMIT DATA', + [keccak256('DISABLE_CONSENSUS_ROLE')]: 'DISABLE CONSENSUS', + [keccak256('MANAGE_MEMBERS_AND_QUORUM_ROLE')]: 'MANAGE MEMBERS AND QUORUM', + [keccak256('MANAGE_FRAME_CONFIG_ROLE')]: 'MANAGE FRAME CONFIG', + [keccak256('MANAGE_FAST_LANE_CONFIG_ROLE')]: 'MANAGE FAST LANE CONFIG', + [keccak256('MANAGE_REPORT_PROCESSOR_ROLE')]: 'MANAGE REPORT PROCESSOR', + [keccak256('MANAGE_CONSENSUS_CONTRACT_ROLE')]: 'MANAGE CONSENSUS CONTRACT', + [keccak256('MANAGE_CONSENSUS_VERSION_ROLE')]: 'MANAGE CONSENSUS VERSION', } diff --git a/csm-alerts/src/shared/types.ts b/csm-alerts/src/shared/types.ts index 705d0b176..48a669eab 100644 --- a/csm-alerts/src/shared/types.ts +++ b/csm-alerts/src/shared/types.ts @@ -1,25 +1,25 @@ import { FindingSeverity, FindingType } from '@fortanetwork/forta-bot' export type EventOfNotice = { - name: string - address: string - abi: string | string[] - alertId: string - description: CallableFunction - severity: FindingSeverity - type: FindingType + name: string + address: string + abi: string | string[] + alertId: string + description: CallableFunction + severity: FindingSeverity + type: FindingType } export type DeployedAddresses = { - CS_MODULE: string - CS_ACCOUNTING: string - CS_FEE_DISTRIBUTOR: string - CS_FEE_ORACLE: string - CS_VERIFIER: string - CS_EARLY_ADOPTION: string - CS_GATE_SEAL: string - LIDO_STETH: string - BURNER: string - HASH_CONSENSUS: string - STAKING_ROUTER: string + CS_MODULE: string + CS_ACCOUNTING: string + CS_FEE_DISTRIBUTOR: string + CS_FEE_ORACLE: string + CS_VERIFIER: string + CS_EARLY_ADOPTION: string + CS_GATE_SEAL: string + LIDO_STETH: string + BURNER: string + HASH_CONSENSUS: string + STAKING_ROUTER: string } diff --git a/csm-alerts/src/utils/epochs.ts b/csm-alerts/src/utils/epochs.ts index 93973828f..484d7173c 100644 --- a/csm-alerts/src/utils/epochs.ts +++ b/csm-alerts/src/utils/epochs.ts @@ -1,18 +1,18 @@ import { SECONDS_PER_SLOT, SLOTS_PER_EPOCH } from '../shared/constants' export function getEpoch(chainId: number, timestamp: number) { - let genesisTimestamp: number | undefined + let genesisTimestamp: number | undefined - switch (chainId) { - case 1: - genesisTimestamp = 1606824023 - break - case 17_000: - genesisTimestamp = 1695902400 - break - default: - throw Error(`Unsupported chain ${chainId} to get genesis timestamp`) - } + switch (chainId) { + case 1: + genesisTimestamp = 1606824023 + break + case 17_000: + genesisTimestamp = 1695902400 + break + default: + throw Error(`Unsupported chain ${chainId} to get genesis timestamp`) + } - return Math.floor((timestamp - genesisTimestamp) / SECONDS_PER_SLOT / SLOTS_PER_EPOCH) + return Math.floor((timestamp - genesisTimestamp) / SECONDS_PER_SLOT / SLOTS_PER_EPOCH) } diff --git a/csm-alerts/src/utils/findings.ts b/csm-alerts/src/utils/findings.ts index 186c282ce..f2972e89a 100644 --- a/csm-alerts/src/utils/findings.ts +++ b/csm-alerts/src/utils/findings.ts @@ -1,4 +1,10 @@ -import { BlockEvent, Finding, FindingSeverity, FindingType, TransactionEvent } from '@fortanetwork/forta-bot' +import { + BlockEvent, + Finding, + FindingSeverity, + FindingType, + TransactionEvent, +} from '@fortanetwork/forta-bot' import { FindingSource } from '@fortanetwork/forta-bot/dist/findings/finding.source' import { toKebabCase } from './string' @@ -6,110 +12,115 @@ import { APP_NAME } from '../config' import Version from './version' export function sourceFromEvent(event: TransactionEvent | BlockEvent): FindingSource { - const source: FindingSource = {} + const source: FindingSource = {} - if ('transaction' in event) source.transactions = [{ chainId: event.chainId, hash: event.transaction.hash }] - source.blocks = [{ chainId: event.chainId, hash: event.block.hash, number: event.block.number }] + if ('transaction' in event) + source.transactions = [{ chainId: event.chainId, hash: event.transaction.hash }] + source.blocks = [{ chainId: event.chainId, hash: event.block.hash, number: event.block.number }] - return source + return source } export function launchAlert(): Finding { - return Finding.fromObject({ - name: `🚀🚀🚀 Bot ${APP_NAME} launched`, - description: `Commit ${Version.commitHashShort}`, - alertId: 'BOT-LAUNCH', - severity: FindingSeverity.Info, - type: FindingType.Info, - }) + return Finding.fromObject({ + name: `🚀🚀🚀 Bot ${APP_NAME} launched`, + description: `Commit ${Version.commitHashShort}`, + alertId: 'BOT-LAUNCH', + severity: FindingSeverity.Info, + type: FindingType.Info, + }) } export function invariantAlert(event: BlockEvent | TransactionEvent, message: string): Finding { - return Finding.fromObject({ - name: '🚨 Assert invariant failed', - description: message, - alertId: 'ASSERT-FAILED', - source: sourceFromEvent(event), - severity: FindingSeverity.Critical, - type: FindingType.Info, - }) + return Finding.fromObject({ + name: '🚨 Assert invariant failed', + description: message, + alertId: 'ASSERT-FAILED', + source: sourceFromEvent(event), + severity: FindingSeverity.Critical, + type: FindingType.Info, + }) } export function errorAlert(name: string, err: string | Error | undefined): Finding { - return Finding.fromObject({ - name: name, - description: String(err), - alertId: 'CODE-ERROR', - severity: FindingSeverity.Unknown, - type: FindingType.Degraded, - metadata: { - stack: `${err instanceof Error ? err.stack : null}`, - message: `${err instanceof Error ? err.message : null}`, - name: `${err instanceof Error ? err.name : null}`, - }, - }) + return Finding.fromObject({ + name: name, + description: String(err), + alertId: 'CODE-ERROR', + severity: FindingSeverity.Unknown, + type: FindingType.Degraded, + metadata: { + stack: `${err instanceof Error ? err.stack : null}`, + message: `${err instanceof Error ? err.message : null}`, + name: `${err instanceof Error ? err.name : null}`, + }, + }) } -export function failedTxAlert(txEvent: TransactionEvent, description: string, reason: string): Finding { - return Finding.fromObject({ - name: `🟣 CRITICAL: ${reason}`, - description: `Transaction reverted. ${description}`, - alertId: `CSFEE-${toKebabCase(reason)}`, - source: sourceFromEvent(txEvent), - severity: FindingSeverity.Critical, - type: FindingType.Info, - }) +export function failedTxAlert( + txEvent: TransactionEvent, + description: string, + reason: string, +): Finding { + return Finding.fromObject({ + name: `🟣 CRITICAL: ${reason}`, + description: `Transaction reverted. ${description}`, + alertId: `CSFEE-${toKebabCase(reason)}`, + source: sourceFromEvent(txEvent), + severity: FindingSeverity.Critical, + type: FindingType.Info, + }) } export const NetworkErrorFinding = 'NETWORK-ERROR' export function networkAlert(err: Error, name: string, desc: string): Finding { - const f = Finding.fromObject({ - name: name, - description: desc, - alertId: NetworkErrorFinding, - severity: FindingSeverity.Unknown, - type: FindingType.Degraded, - metadata: { - stack: `${err.stack}`, - message: `${err.message}`, - name: `${err.name}`, - }, - }) - - return f + const f = Finding.fromObject({ + name: name, + description: desc, + alertId: NetworkErrorFinding, + severity: FindingSeverity.Unknown, + type: FindingType.Degraded, + metadata: { + stack: `${err.stack}`, + message: `${err.message}`, + name: `${err.name}`, + }, + }) + + return f } export function dbAlert(err: Error, name: string, desc: string): Finding { - const f = Finding.fromObject({ - name: name, - description: desc, - alertId: 'DB-ERROR', - severity: FindingSeverity.Unknown, - type: FindingType.Degraded, - metadata: { - stack: `${err.stack}`, - message: `${err.message}`, - name: `${err.name}`, - }, - }) - - return f + const f = Finding.fromObject({ + name: name, + description: desc, + alertId: 'DB-ERROR', + severity: FindingSeverity.Unknown, + type: FindingType.Degraded, + metadata: { + stack: `${err.stack}`, + message: `${err.message}`, + name: `${err.name}`, + }, + }) + + return f } export class NetworkError extends Error { - constructor(e: unknown, name?: string) { - super() - - if (name !== undefined) { - this.name = name - } - - if (e instanceof Error) { - this.stack = e.stack - this.message = e.message - } else { - this.message = `${e}` + constructor(e: unknown, name?: string) { + super() + + if (name !== undefined) { + this.name = name + } + + if (e instanceof Error) { + this.stack = e.stack + this.message = e.message + } else { + this.message = `${e}` + } } - } } diff --git a/csm-alerts/src/utils/logs.ts b/csm-alerts/src/utils/logs.ts index 9ebdd0015..60e0cdfe9 100644 --- a/csm-alerts/src/utils/logs.ts +++ b/csm-alerts/src/utils/logs.ts @@ -3,20 +3,22 @@ import { ethers } from '@fortanetwork/forta-bot' const LOG_FILTER_CHUNK = 2000 export async function getLogsByChunks( - contract: ethers.Contract, - filter: ethers.ContractEventName, - startblock: number, - endBlock: number, + contract: ethers.Contract, + filter: ethers.ContractEventName, + startblock: number, + endBlock: number, ) { - const events: ethers.Log[] = [] - let endBlockChunk - let startBlockChunk = startblock - do { - endBlockChunk = - endBlock > startBlockChunk + LOG_FILTER_CHUNK - 1 ? startBlockChunk + LOG_FILTER_CHUNK - 1 : endBlock - const eventsChunk = await contract.queryFilter(filter, startBlockChunk, endBlockChunk) - events.push(...eventsChunk) - startBlockChunk = endBlockChunk + 1 - } while (endBlockChunk < endBlock) - return events + const events: ethers.Log[] = [] + let endBlockChunk + let startBlockChunk = startblock + do { + endBlockChunk = + endBlock > startBlockChunk + LOG_FILTER_CHUNK - 1 + ? startBlockChunk + LOG_FILTER_CHUNK - 1 + : endBlock + const eventsChunk = await contract.queryFilter(filter, startBlockChunk, endBlockChunk) + events.push(...eventsChunk) + startBlockChunk = endBlockChunk + 1 + } while (endBlockChunk < endBlock) + return events } diff --git a/csm-alerts/src/utils/metrics/metrics.ts b/csm-alerts/src/utils/metrics/metrics.ts index 743e4e790..e10942706 100644 --- a/csm-alerts/src/utils/metrics/metrics.ts +++ b/csm-alerts/src/utils/metrics/metrics.ts @@ -6,83 +6,83 @@ export const HandleBlockLabel = 'handleBlock' export const HandleTxLabel = 'handleTx' export class Metrics { - private readonly registry: Registry - private readonly prefix: string + private readonly registry: Registry + private readonly prefix: string - public readonly healthStatus: Gauge - public readonly buildInfo: Gauge + public readonly healthStatus: Gauge + public readonly buildInfo: Gauge - public readonly etherJsRequest: Counter - public readonly networkErrors: Counter - public readonly lastBlockNumber: Gauge - public readonly etherJsDurationHistogram: Histogram - public readonly lastAgentTouch: Gauge - public readonly processedIterations: Counter - public readonly summaryHandlers: Summary + public readonly etherJsRequest: Counter + public readonly networkErrors: Counter + public readonly lastBlockNumber: Gauge + public readonly etherJsDurationHistogram: Histogram + public readonly lastAgentTouch: Gauge + public readonly processedIterations: Counter + public readonly summaryHandlers: Summary - constructor(registry: Registry, prefix: string) { - this.registry = registry - this.prefix = prefix + constructor(registry: Registry, prefix: string) { + this.registry = registry + this.prefix = prefix - this.buildInfo = new Gauge({ - name: this.prefix + 'build_info', - help: 'Build information', - labelNames: ['commitHash' as const], - registers: [this.registry], - }) + this.buildInfo = new Gauge({ + name: this.prefix + 'build_info', + help: 'Build information', + labelNames: ['commitHash' as const], + registers: [this.registry], + }) - this.healthStatus = new Gauge({ - name: this.prefix + 'health_status', - help: 'Bot health status', - labelNames: ['instance'] as const, - registers: [this.registry], - }) + this.healthStatus = new Gauge({ + name: this.prefix + 'health_status', + help: 'Bot health status', + labelNames: ['instance'] as const, + registers: [this.registry], + }) - this.etherJsRequest = new Counter({ - name: this.prefix + 'etherjs_request_total', - help: 'Total number of requests via ether.js library', - labelNames: ['method' as const, 'status' as const] as const, - registers: [this.registry], - }) + this.etherJsRequest = new Counter({ + name: this.prefix + 'etherjs_request_total', + help: 'Total number of requests via ether.js library', + labelNames: ['method' as const, 'status' as const] as const, + registers: [this.registry], + }) - this.etherJsDurationHistogram = new Histogram({ - name: this.prefix + 'ether_requests_duration_seconds', - help: 'Histogram of the duration of requests in seconds', - labelNames: ['method', 'status'], - buckets: [0.001, 0.01, 0.1, 0.5, 1, 2.5, 5, 10], - }) + this.etherJsDurationHistogram = new Histogram({ + name: this.prefix + 'ether_requests_duration_seconds', + help: 'Histogram of the duration of requests in seconds', + labelNames: ['method', 'status'], + buckets: [0.001, 0.01, 0.1, 0.5, 1, 2.5, 5, 10], + }) - this.lastAgentTouch = new Gauge({ - name: this.prefix + 'block_timestamp', - help: 'The last agent iteration', - labelNames: ['method' as const] as const, - registers: [this.registry], - }) + this.lastAgentTouch = new Gauge({ + name: this.prefix + 'block_timestamp', + help: 'The last agent iteration', + labelNames: ['method' as const] as const, + registers: [this.registry], + }) - this.lastBlockNumber = new Gauge({ - name: this.prefix + 'last_block_number', - help: 'The last agent block number', - registers: [this.registry], - }) + this.lastBlockNumber = new Gauge({ + name: this.prefix + 'last_block_number', + help: 'The last agent block number', + registers: [this.registry], + }) - this.networkErrors = new Counter({ - name: this.prefix + 'network_errors_total', - help: 'Total number of network errors', - registers: [this.registry], - }) + this.networkErrors = new Counter({ + name: this.prefix + 'network_errors_total', + help: 'Total number of network errors', + registers: [this.registry], + }) - this.processedIterations = new Counter({ - name: this.prefix + 'processed_iterations_total', - help: 'Total number of finding iterations', - labelNames: ['method', 'status'], - registers: [this.registry], - }) + this.processedIterations = new Counter({ + name: this.prefix + 'processed_iterations_total', + help: 'Total number of finding iterations', + labelNames: ['method', 'status'], + registers: [this.registry], + }) - this.summaryHandlers = new Summary({ - name: this.prefix + 'request_processing_seconds', - help: 'Time spent processing request (block or transaction)', - labelNames: ['method'], - registers: [this.registry], - }) - } + this.summaryHandlers = new Summary({ + name: this.prefix + 'request_processing_seconds', + help: 'Time spent processing request (block or transaction)', + labelNames: ['method'], + registers: [this.registry], + }) + } } diff --git a/csm-alerts/src/utils/require.ts b/csm-alerts/src/utils/require.ts index 4d1ff8591..53bced876 100644 --- a/csm-alerts/src/utils/require.ts +++ b/csm-alerts/src/utils/require.ts @@ -1,8 +1,8 @@ import { RUN_TIER } from '../config' export enum RedefineMode { - Strict = 'strict', - Merge = 'merge', + Strict = 'strict', + Merge = 'merge', } /** @@ -13,39 +13,45 @@ export enum RedefineMode { * @param path relative to module path to the main file to import. * @param mode `strict` or `merge`. Default: `strict`. */ -export function requireWithTier(module: NodeModule, path: string, mode: RedefineMode = RedefineMode.Strict): T { - const defaultContent = require(`${module.path}/${path}`) - if (!RUN_TIER) return defaultContent - let tieredContent: any - // NOTE: It fails if it can't find the requested tier. - tieredContent = require(`${module.path}/${path}.${RUN_TIER}`) - module.exports.__tier__ = RUN_TIER - if (mode == RedefineMode.Strict) { - const valid = (key: string) => { - return key in tieredContent && typeof defaultContent[key] == typeof tieredContent[key] - } - if (Object.keys(defaultContent).every((key) => valid(key))) { - return tieredContent - } else { - throw new Error( - `Failed to import module: '${module.path}/${path}.${RUN_TIER}' doesn't contain all keys or unmatched types +export function requireWithTier( + module: NodeModule, + path: string, + mode: RedefineMode = RedefineMode.Strict, +): T { + const defaultContent = require(`${module.path}/${path}`) + if (!RUN_TIER) return defaultContent + let tieredContent: any + // NOTE: It fails if it can't find the requested tier. + tieredContent = require(`${module.path}/${path}.${RUN_TIER}`) + module.exports.__tier__ = RUN_TIER + if (mode == RedefineMode.Strict) { + const valid = (key: string) => { + return key in tieredContent && typeof defaultContent[key] == typeof tieredContent[key] + } + if (Object.keys(defaultContent).every((key) => valid(key))) { + return tieredContent + } else { + throw new Error( + `Failed to import module: '${module.path}/${path}.${RUN_TIER}' doesn't contain all keys or unmatched types with '${module.path}/${path}'`, - ) - } - } - if (mode == RedefineMode.Merge) { - const valid = (key: string) => { - if (key in defaultContent) { - return typeof defaultContent[key] == typeof tieredContent[key] - } else { - return true - } + ) + } } - if (Object.keys(tieredContent).every((key) => valid(key))) { - return { ...defaultContent, ...tieredContent } - } else { - throw new Error(`Failed to import module: '${path}.${RUN_TIER}' unmatched types with '${path}'`) + if (mode == RedefineMode.Merge) { + const valid = (key: string) => { + if (key in defaultContent) { + return typeof defaultContent[key] == typeof tieredContent[key] + } else { + return true + } + } + if (Object.keys(tieredContent).every((key) => valid(key))) { + return { ...defaultContent, ...tieredContent } + } else { + throw new Error( + `Failed to import module: '${path}.${RUN_TIER}' unmatched types with '${path}'`, + ) + } } - } - throw new Error(`Unknown require mode: ${mode}`) + throw new Error(`Unknown require mode: ${mode}`) } diff --git a/csm-alerts/src/utils/string.ts b/csm-alerts/src/utils/string.ts index fe1333ecd..02b637495 100644 --- a/csm-alerts/src/utils/string.ts +++ b/csm-alerts/src/utils/string.ts @@ -4,20 +4,20 @@ import { RUN_TIER } from '../config' import { SHARES_PRECISION, WEI_PER_ETH } from '../shared/constants' export function etherscanAddress(address: string): string { - const subpath = RUN_TIER == 'holesky' ? 'holesky.' : '' - return `[${address}](https://${subpath}etherscan.io/address/${address})` + const subpath = RUN_TIER == 'holesky' ? 'holesky.' : '' + return `[${address}](https://${subpath}etherscan.io/address/${address})` } export function toKebabCase(str: string): string { - return str.replace(/_/g, '-') + return str.replace(/_/g, '-') } export function formatShares(amount: bigint): string { - amount = amount - (amount % (SHARES_PRECISION / 100n)) - return `${ethers.formatEther(amount)} × 1e18 shares` + amount = amount - (amount % (SHARES_PRECISION / 100n)) + return `${ethers.formatEther(amount)} × 1e18 shares` } export function formatEther(amount: bigint): string { - amount = amount - (amount % (WEI_PER_ETH / 100n)) - return `${ethers.formatEther(amount)} ether` + amount = amount - (amount % (WEI_PER_ETH / 100n)) + return `${ethers.formatEther(amount)} ether` } diff --git a/csm-alerts/src/utils/time.ts b/csm-alerts/src/utils/time.ts index aa02ad33b..9dccd23d2 100644 --- a/csm-alerts/src/utils/time.ts +++ b/csm-alerts/src/utils/time.ts @@ -1,50 +1,50 @@ export function formatTime(timeInMillis: number): string { - const seconds = (timeInMillis / 1000).toFixed(3) - return `${seconds} seconds` + const seconds = (timeInMillis / 1000).toFixed(3) + return `${seconds} seconds` } export function elapsedTime(methodName: string, startTime: number): string { - const elapsedTime = new Date().getTime() - startTime - return `${methodName} started at ${formatTimeToHumanReadable(new Date(startTime))}. Elapsed: ${formatTime( - elapsedTime, - )}` + const elapsedTime = new Date().getTime() - startTime + return `${methodName} started at ${formatTimeToHumanReadable(new Date(startTime))}. Elapsed: ${formatTime( + elapsedTime, + )}` } function formatTimeToHumanReadable(date: Date): string { - return date.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false, - }) + return date.toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }) } export function formatDelay(fullDelaySec: bigint | number): string { - fullDelaySec = BigInt(fullDelaySec) - - const sign = fullDelaySec >= 0 ? 1n : -1n - fullDelaySec = sign * fullDelaySec - - let delayDays = 0n - let delayHours = 0n - - let delayMin = fullDelaySec / 60n - const delaySec = fullDelaySec - delayMin * 60n - - if (delayMin >= 60) { - delayHours = delayMin / 60n - delayMin -= delayHours * 60n - } - if (delayHours >= 24) { - delayDays = delayHours / 24n - delayHours -= delayDays * 24n - } - - return ( - (sign == 1n ? '' : '-') + - (delayDays > 0 ? `${delayDays} day` : '') + - (delayHours > 0 ? ` ${delayHours} hr` : '') + - (delayMin > 0 ? ` ${delayMin} min` : '') + - (delaySec > 0 ? ` ${delaySec} sec` : '') - ) + fullDelaySec = BigInt(fullDelaySec) + + const sign = fullDelaySec >= 0 ? 1n : -1n + fullDelaySec = sign * fullDelaySec + + let delayDays = 0n + let delayHours = 0n + + let delayMin = fullDelaySec / 60n + const delaySec = fullDelaySec - delayMin * 60n + + if (delayMin >= 60) { + delayHours = delayMin / 60n + delayMin -= delayHours * 60n + } + if (delayHours >= 24) { + delayDays = delayHours / 24n + delayHours -= delayDays * 24n + } + + return ( + (sign == 1n ? '' : '-') + + (delayDays > 0 ? `${delayDays} day` : '') + + (delayHours > 0 ? ` ${delayHours} hr` : '') + + (delayMin > 0 ? ` ${delayMin} min` : '') + + (delaySec > 0 ? ` ${delaySec} sec` : '') + ) } diff --git a/csm-alerts/src/utils/version.ts b/csm-alerts/src/utils/version.ts index 3fd42b5c3..6913e5525 100644 --- a/csm-alerts/src/utils/version.ts +++ b/csm-alerts/src/utils/version.ts @@ -1,27 +1,27 @@ import path from 'path' export interface Version { - desc: string - commitHash: string - commitHashShort: string - commitMsg: string - commitMsgShort: string - isWdClean: boolean + desc: string + commitHash: string + commitHashShort: string + commitMsg: string + commitMsgShort: string + isWdClean: boolean } export default readVersion(path.join(__dirname, '..', '..', 'version.json')) function readVersion(versionFilePath: string): Version { - try { - return require(versionFilePath) - } catch (e) { - return { - desc: 'unknown', - commitHash: 'unknown', - commitHashShort: 'unknown', - commitMsg: 'unknown', - commitMsgShort: 'unknown', - isWdClean: false, + try { + return require(versionFilePath) + } catch (e) { + return { + desc: 'unknown', + commitHash: 'unknown', + commitHashShort: 'unknown', + commitMsg: 'unknown', + commitMsgShort: 'unknown', + isWdClean: false, + } } - } }