diff --git a/ethereum-steth/.nvmrc b/ethereum-steth/.nvmrc deleted file mode 100644 index 790e1105..00000000 --- a/ethereum-steth/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v20.10.0 diff --git a/ethereum-steth/README.md b/ethereum-steth/README.md index 3733ff2f..d7b82ac6 100644 --- a/ethereum-steth/README.md +++ b/ethereum-steth/README.md @@ -79,8 +79,8 @@ 2. 🚨🚨🚨 Withdrawal Vault balance mismatch. [within oracle report] 3. 🚨🚨🚨 EL Vault balance mismatch. [without oracle report] 4. 🚨🚨🚨 EL Vault balance mismatch. [within oracle report] - 5. đŸ’ĩ Withdrawal Vault Balance significant change (checks every on 100-th block) - 6. đŸ’ĩ EL Vault Balance significant change + 5. ℹī¸ Withdrawal Vault Balance significant change (checks every on 100-th block) + 6. ℹī¸ EL Vault Balance significant change 2. HandleTransaction 1. 🚨 Burner shares transfer @@ -97,11 +97,3 @@ Running in a live mode: ``` yarn start:dev ``` - -Testing on a specific block/range/transaction: - -``` -yarn block 13626668 -yarn range '13626667..13626668' -yarn tx 0x2d2774c04e3faf9f17cd26e0978bb812081b9d0b5cc6fd8bf04cc441f92c0a8c -``` diff --git a/ethereum-steth/src/agent.ts b/ethereum-steth/src/agent.ts index a4b5881b..b97c4b19 100644 --- a/ethereum-steth/src/agent.ts +++ b/ethereum-steth/src/agent.ts @@ -19,7 +19,8 @@ import { TransactionEvent } from 'forta-agent/dist/sdk/transaction.event' import { Metadata } from './entity/metadata' import Version from './utils/version' import { ETH_DECIMALS } from './utils/constants' -import { BlockDto } from './entity/events' +import { BlockDto, TransactionDto } from './entity/events' +import BigNumber from 'bignumber.js' export function initialize(): Initialize { const metadata: Metadata = { @@ -172,10 +173,20 @@ export const handleTransaction = (): HandleTransaction => { const app = await App.getInstance() const out: Finding[] = [] - const stethOperationFindings = await app.StethOperationSrv.handleTransaction(txEvent, txEvent.block.number) - const withdrawalsFindings = await app.WithdrawalsSrv.handleTransaction(txEvent) - const gateSealFindings = app.GateSealSrv.handleTransaction(txEvent) - const vaultFindings = app.VaultSrv.handleTransaction(txEvent) + const txDto: TransactionDto = { + logs: txEvent.logs, + to: txEvent.to, + timestamp: txEvent.timestamp, + block: { + timestamp: txEvent.block.timestamp, + number: new BigNumber(txEvent.block.number, 10).toNumber(), + }, + } + + const withdrawalsFindings = await app.WithdrawalsSrv.handleTransaction(txDto) + const stethOperationFindings = await app.StethOperationSrv.handleTransaction(txDto) + const gateSealFindings = app.GateSealSrv.handleTransaction(txDto) + const vaultFindings = app.VaultSrv.handleTransaction(txDto) out.push(...stethOperationFindings, ...withdrawalsFindings, ...gateSealFindings, ...vaultFindings) diff --git a/ethereum-steth/src/app.ts b/ethereum-steth/src/app.ts index d96f0a4a..86ec772f 100644 --- a/ethereum-steth/src/app.ts +++ b/ethereum-steth/src/app.ts @@ -101,15 +101,12 @@ export class App { const address: Address = Address - const lidoContact = Lido__factory.connect(address.LIDO_STETH_ADDRESS, ethersProvider) + const lidoRunner = Lido__factory.connect(address.LIDO_STETH_ADDRESS, ethersProvider) - const wdQueueContact = WithdrawalQueueERC721__factory.connect(address.WITHDRAWALS_QUEUE_ADDRESS, drpcProvider) + const wdQueueRunner = WithdrawalQueueERC721__factory.connect(address.WITHDRAWALS_QUEUE_ADDRESS, drpcProvider) const gateSealContact = GateSeal__factory.connect(address.GATE_SEAL_DEFAULT_ADDRESS, ethersProvider) - const exitBusOracleContract = ValidatorsExitBusOracle__factory.connect( - address.EXIT_BUS_ORACLE_ADDRESS, - ethersProvider, - ) + const veboRunner = ValidatorsExitBusOracle__factory.connect(address.EXIT_BUS_ORACLE_ADDRESS, ethersProvider) const logger: Winston.Logger = Winston.createLogger({ format: Winston.format.simple(), @@ -120,10 +117,10 @@ export class App { logger, ethersProvider, etherscanProvider, - lidoContact, - wdQueueContact, + lidoRunner, + wdQueueRunner, gateSealContact, - exitBusOracleContract, + veboRunner, ) const stethOperationCache = new StethOperationCache() @@ -162,10 +159,10 @@ export class App { logger, drpcProvider, etherscanProvider, - lidoContact, - wdQueueContact, + lidoRunner, + wdQueueRunner, gateSealContact, - exitBusOracleContract, + veboRunner, ) const vaultSrv = new VaultSrv( diff --git a/ethereum-steth/src/clients/contracts.ts b/ethereum-steth/src/clients/contracts.ts deleted file mode 100644 index e5a1f200..00000000 --- a/ethereum-steth/src/clients/contracts.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { BlockTag } from '@ethersproject/providers' -import { TransactionResponse } from '@ethersproject/abstract-provider' -import { BigNumber as EtherBigNumber } from '@ethersproject/bignumber/lib/bignumber' - -export abstract class IEtherscanProvider { - abstract getHistory( - addressOrName: string | Promise, - startBlock?: BlockTag, - endBlock?: BlockTag, - ): Promise> - - abstract getBalance( - addressOrName: string | Promise, - blockTag?: BlockTag | Promise, - ): Promise -} diff --git a/ethereum-steth/src/clients/eth_provider.spec.ts b/ethereum-steth/src/clients/eth_provider.spec.ts index 128e7cc7..6fefc2dd 100644 --- a/ethereum-steth/src/clients/eth_provider.spec.ts +++ b/ethereum-steth/src/clients/eth_provider.spec.ts @@ -1,6 +1,6 @@ import { App } from '../app' import * as E from 'fp-ts/Either' -import { Address, ETH_DECIMALS } from '../utils/constants' +import { Address, ETH_DECIMALS, GATE_SEAL_DEFAULT_ADDRESS_BEFORE_26_APR_2024 } from '../utils/constants' import { GateSeal } from '../entity/gate_seal' import { JsonRpcProvider } from '@ethersproject/providers' import { ethers } from 'forta-agent' @@ -37,7 +37,7 @@ describe('eth provider tests', () => { const blockNumber = 19140476 - const resp = await app.ethClient.checkGateSeal(blockNumber, Address.GATE_SEAL_DEFAULT_ADDRESS) + const resp = await app.ethClient.checkGateSeal(blockNumber, GATE_SEAL_DEFAULT_ADDRESS_BEFORE_26_APR_2024) if (E.isLeft(resp)) { throw resp.left.message } diff --git a/ethereum-steth/src/clients/eth_provider.ts b/ethereum-steth/src/clients/eth_provider.ts index eb7deb4c..e2bbe4a0 100644 --- a/ethereum-steth/src/clients/eth_provider.ts +++ b/ethereum-steth/src/clients/eth_provider.ts @@ -7,52 +7,65 @@ import BigNumber from 'bignumber.js' import { ETH_DECIMALS } from '../utils/constants' import { StakingLimitInfo } from '../entity/staking_limit_info' import { - GateSeal as GateSealContract, - Lido as LidoContract, - ValidatorsExitBusOracle as ExitBusContract, - WithdrawalQueueERC721 as WithdrawalQueueContract, + GateSeal as GateSealRunner, + Lido as LidoRunner, + ValidatorsExitBusOracle as VeboRunner, + WithdrawalQueueERC721 as WithdrawalQueueRunner, } from '../generated' import { GateSeal, GateSealExpiredErr } from '../entity/gate_seal' import { ETHDistributedEvent } from '../generated/Lido' import { DataRW } from '../utils/mutex' -import { IEtherscanProvider } from './contracts' import { WithdrawalRequest } from '../entity/withdrawal_request' import { TypedEvent } from '../generated/common' import { NetworkError } from '../utils/errors' -import { IGateSealClient } from '../services/gate-seal/contract' import { IStethClient } from '../services/steth_operation/contracts' import { IVaultClient } from '../services/vault/contract' import { IWithdrawalsClient } from '../services/withdrawals/contract' import { Logger } from 'winston' +import { IGateSealClient } from '../services/gate-seal/GateSeal.srv' +import { BlockTag } from '@ethersproject/providers' const DELAY_IN_500MS = 500 const ATTEMPTS_5 = 5 +export abstract class IEtherscanProvider { + abstract getHistory( + addressOrName: string | Promise, + startBlock?: BlockTag, + endBlock?: BlockTag, + ): Promise> + + abstract getBalance( + addressOrName: string | Promise, + blockTag?: BlockTag | Promise, + ): Promise +} + export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, IWithdrawalsClient { private jsonRpcProvider: ethers.providers.JsonRpcProvider private etherscanProvider: IEtherscanProvider - private readonly lidoContract: LidoContract - private readonly wdQueueContract: WithdrawalQueueContract - private readonly exitBusContract: ExitBusContract - private gateSeal: GateSealContract + private readonly lidoRunner: LidoRunner + private readonly wdQueueRunner: WithdrawalQueueRunner + private readonly veboRunner: VeboRunner + private gateSealRunner: GateSealRunner private readonly logger: Logger constructor( logger: Logger, jsonRpcProvider: ethers.providers.JsonRpcProvider, etherscanProvider: IEtherscanProvider, - lidoContract: LidoContract, - wdQueueContract: WithdrawalQueueContract, - gateSeal: GateSealContract, - exitBusContract: ExitBusContract, + lidoRunner: LidoRunner, + wdQueueRunner: WithdrawalQueueRunner, + gateSealRunner: GateSealRunner, + veboRunner: VeboRunner, ) { this.jsonRpcProvider = jsonRpcProvider this.etherscanProvider = etherscanProvider - this.lidoContract = lidoContract - this.wdQueueContract = wdQueueContract - this.gateSeal = gateSeal - this.exitBusContract = exitBusContract + this.lidoRunner = lidoRunner + this.wdQueueRunner = wdQueueRunner + this.gateSealRunner = gateSealRunner + this.veboRunner = veboRunner this.logger = logger } @@ -230,7 +243,7 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { const out = await retryAsync( async (): Promise => { - const resp = await this.lidoContract.functions.getStakeLimitFullInfo({ + const resp = await this.lidoRunner.functions.getStakeLimitFullInfo({ blockTag: blockNumber, }) @@ -253,7 +266,7 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { const out = await retryAsync( async (): Promise => { - const out = await this.wdQueueContract.unfinalizedStETH({ + const out = await this.wdQueueRunner.unfinalizedStETH({ blockTag: blockNumber, }) @@ -276,7 +289,7 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { return await retryAsync( async (): Promise => { - const resp = await this.wdQueueContract.functions.getWithdrawalStatus(requestIds, { + const resp = await this.wdQueueRunner.functions.getWithdrawalStatus(requestIds, { blockTag: blockNumber, }) @@ -329,7 +342,7 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { const resp = await retryAsync( async (): Promise => { - return await this.lidoContract.getBufferedEther({ + return await this.lidoRunner.getBufferedEther({ blockTag: blockNumber, }) }, @@ -368,8 +381,8 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, const out: GateSeal = { roleForWithdrawalQueue: isGateSealHasPauseRole.right, roleForExitBus: isGateSealHasExitBusPauseRoleMember.right, - exitBusOracleAddress: this.exitBusContract.address, - withdrawalQueueAddress: this.wdQueueContract.address, + exitBusOracleAddress: this.veboRunner.address, + withdrawalQueueAddress: this.wdQueueRunner.address, } return E.right(out) @@ -379,7 +392,7 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { const expiryTimestamp = await retryAsync( async (): Promise => { - const [resp] = await this.gateSeal.functions.get_expiry_timestamp({ + const [resp] = await this.gateSealRunner.functions.get_expiry_timestamp({ blockTag: blockNumber, }) @@ -401,8 +414,8 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { const report = await retryAsync( async (): Promise => { - const [resp] = await this.lidoContract.queryFilter( - this.lidoContract.filters.ETHDistributed(), + const [resp] = await this.lidoRunner.queryFilter( + this.lidoRunner.filters.ETHDistributed(), fromBlockNumber, toBlockNumber, ) @@ -423,12 +436,12 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, } private async isGateSealExpired(blockNumber: number, gateSealAddress: string): Promise> { - this.gateSeal = this.gateSeal.attach(gateSealAddress) + this.gateSealRunner = this.gateSealRunner.attach(gateSealAddress) try { const isExpired = await retryAsync( async (): Promise => { - const [resp] = await this.gateSeal.functions.is_expired({ + const [resp] = await this.gateSealRunner.functions.is_expired({ blockTag: blockNumber, }) @@ -452,7 +465,7 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { const queuePauseRoleMember = await retryAsync( async (): Promise => { - const [resp] = await this.wdQueueContract.functions.hasRole(keccakPauseRole, gateSealAddress, { + const [resp] = await this.wdQueueRunner.functions.hasRole(keccakPauseRole, gateSealAddress, { blockTag: blockNumber, }) @@ -476,7 +489,7 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { const exitBusPauseRoleMember = await retryAsync( async (): Promise => { - const [resp] = await this.exitBusContract.functions.hasRole(keccakPauseRole, gateSealAddress, { + const [resp] = await this.veboRunner.functions.hasRole(keccakPauseRole, gateSealAddress, { blockTag: blockNumber, }) @@ -495,7 +508,7 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { const out = await retryAsync( async (): Promise => { - return await this.lidoContract.getTotalPooledEther({ + return await this.lidoRunner.getTotalPooledEther({ blockTag: blockNumber, }) }, @@ -512,7 +525,7 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { const out = await retryAsync( async (): Promise => { - return await this.lidoContract.getTotalShares({ + return await this.lidoRunner.getTotalShares({ blockTag: blockNumber, }) }, @@ -546,7 +559,7 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { const isBunkerMode = await retryAsync( async (): Promise => { - const [isBunkerMode] = await this.wdQueueContract.functions.isBunkerModeActive({ + const [isBunkerMode] = await this.wdQueueRunner.functions.isBunkerModeActive({ blockTag: blockNumber, }) @@ -565,7 +578,7 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { const bunkerModeSinceTimestamp = await retryAsync( async (): Promise => { - const [resp] = await this.wdQueueContract.functions.bunkerModeSinceTimestamp({ + const [resp] = await this.wdQueueRunner.functions.bunkerModeSinceTimestamp({ blockTag: blockNumber, }) @@ -584,7 +597,7 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { const lastRequestId = await retryAsync( async (): Promise => { - const [resp] = await this.wdQueueContract.functions.getLastRequestId({ + const [resp] = await this.wdQueueRunner.functions.getLastRequestId({ blockTag: blockNumber, }) @@ -606,7 +619,7 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { const out = await retryAsync( async (): Promise => { - const resp = await this.wdQueueContract.functions.getWithdrawalStatus([requestId], { + const resp = await this.wdQueueRunner.functions.getWithdrawalStatus([requestId], { blockTag: blockNumber, }) @@ -628,9 +641,9 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { const out = await retryAsync( async (): Promise => { - const filter = this.lidoContract.filters.Unbuffered() + const filter = this.lidoRunner.filters.Unbuffered() - return await this.lidoContract.queryFilter(filter, fromBlockNumber, toBlockNumber) + return await this.lidoRunner.queryFilter(filter, fromBlockNumber, toBlockNumber) }, { delay: DELAY_IN_500MS, maxTry: ATTEMPTS_5 }, ) @@ -648,9 +661,9 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { const out = await retryAsync( async (): Promise => { - const filter = this.wdQueueContract.filters.WithdrawalsFinalized() + const filter = this.wdQueueRunner.filters.WithdrawalsFinalized() - return await this.wdQueueContract.queryFilter(filter, fromBlockNumber, toBlockNumber) + return await this.wdQueueRunner.queryFilter(filter, fromBlockNumber, toBlockNumber) }, { delay: DELAY_IN_500MS, maxTry: ATTEMPTS_5 }, ) @@ -665,7 +678,7 @@ export class ETHProvider implements IGateSealClient, IStethClient, IVaultClient, try { const out = await retryAsync( async (): Promise => { - return await this.lidoContract.getDepositableEther({ + return await this.lidoRunner.getDepositableEther({ blockTag: blockNumber, }) }, diff --git a/ethereum-steth/src/clients/mocks/mock.ts b/ethereum-steth/src/clients/mocks/mock.ts new file mode 100644 index 00000000..a2026394 --- /dev/null +++ b/ethereum-steth/src/clients/mocks/mock.ts @@ -0,0 +1,6 @@ +import { IEtherscanProvider } from '../eth_provider' + +export const EtherscanProviderMock = (): jest.Mocked => ({ + getHistory: jest.fn(), + getBalance: jest.fn(), +}) diff --git a/ethereum-steth/src/entity/events.ts b/ethereum-steth/src/entity/events.ts index 142d8041..d39e185d 100644 --- a/ethereum-steth/src/entity/events.ts +++ b/ethereum-steth/src/entity/events.ts @@ -1,9 +1,10 @@ -import { FindingSeverity, FindingType } from 'forta-agent' +import { ethers, Finding, FindingSeverity, FindingType } from 'forta-agent' +import { Log } from '@ethersproject/abstract-provider' export type EventOfNotice = { name: string address: string - event: string + abi: string alertId: string description: CallableFunction severity: FindingSeverity @@ -15,3 +16,50 @@ export type BlockDto = { timestamp: number parentHash: string } + +export type TransactionDto = { + logs: Log[] + to: string | null + timestamp: number + block: { + timestamp: number + number: number + } +} + +export function handleEventsOfNotice(txEvent: TransactionDto, eventsOfNotice: EventOfNotice[]): Finding[] { + const out: Finding[] = [] + + const addresses = new Set() + for (const eventOfNotice of eventsOfNotice) { + addresses.add(eventOfNotice.address.toLowerCase()) + } + + for (const log of txEvent.logs) { + if (addresses.has(log.address.toLowerCase())) { + for (const eventInfo of eventsOfNotice) { + const parser = new ethers.utils.Interface([eventInfo.abi]) + + try { + const logDesc = parser.parseLog(log) + + out.push( + Finding.fromObject({ + name: eventInfo.name, + description: eventInfo.description(logDesc.args), + alertId: eventInfo.alertId, + severity: eventInfo.severity, + type: eventInfo.type, + metadata: { args: String(logDesc.args) }, + }), + ) + } catch (e) { + // Only one from eventsOfNotice could be correct + // Others - skipping + } + } + } + } + + return out +} diff --git a/ethereum-steth/src/services/gate-seal/GateSeal.srv.ts b/ethereum-steth/src/services/gate-seal/GateSeal.srv.ts index 66aad1f1..de987390 100644 --- a/ethereum-steth/src/services/gate-seal/GateSeal.srv.ts +++ b/ethereum-steth/src/services/gate-seal/GateSeal.srv.ts @@ -1,32 +1,33 @@ import { elapsedTime, formatDelay } from '../../utils/time' import * as E from 'fp-ts/Either' -import { GateSealExpiredErr } from '../../entity/gate_seal' +import { GateSeal, GateSealExpiredErr } from '../../entity/gate_seal' import { GateSealCache } from './GateSeal.cache' -import { TransactionEvent } from 'forta-agent/dist/sdk/transaction.event' import { GATE_SEAL_FACTORY_GATE_SEAL_CREATED_EVENT, GATE_SEAL_SEALED_EVENT } from '../../utils/events/gate_seal_events' import { etherscanAddress } from '../../utils/string' import { Logger } from 'winston' import { networkAlert } from '../../utils/errors' -import { IGateSealClient } from './contract' -import { filterLog, Finding, FindingSeverity, FindingType } from 'forta-agent' -import { BlockDto } from '../../entity/events' +import { ethers, Finding, FindingSeverity, FindingType } from 'forta-agent' +import { BlockDto, TransactionDto } from '../../entity/events' +import BigNumber from 'bignumber.js' const ONE_HOUR = 60 * 60 const ONE_DAY = 24 * ONE_HOUR const ONE_WEEK = 7 * ONE_DAY +const TWO_WEEKS = 2 * ONE_WEEK const ONE_MONTH = ONE_WEEK * 4 const THREE_MONTHS = ONE_MONTH * 3 -const GATE_SEAL_WITHOUT_PAUSE_ROLE_TRIGGER_EVERY = ONE_DAY +export abstract class IGateSealClient { + public abstract checkGateSeal(blockNumber: number, gateSealAddress: string): Promise> -const GATE_SEAL_EXPIRY_TRIGGER_EVERY = ONE_WEEK -const GATE_SEAL_EXPIRY_THRESHOLD = THREE_MONTHS + public abstract getExpiryTimestamp(blockNumber: number): Promise> +} export class GateSealSrv { private readonly name = 'GateSealSrv' private readonly logger: Logger - private readonly ethProvider: IGateSealClient + private readonly gateSealClient: IGateSealClient private readonly cache: GateSealCache private readonly gateSealFactoryAddress: string @@ -40,7 +41,7 @@ export class GateSealSrv { gateSealFactoryAddress: string, ) { this.logger = logger - this.ethProvider = ethProvider + this.gateSealClient = ethProvider this.cache = cache this.gateSealAddress = gateSealAddress this.gateSealFactoryAddress = gateSealFactoryAddress @@ -53,7 +54,7 @@ export class GateSealSrv { return new Error(`Gate seal address is not provided`) } - const status = await this.ethProvider.checkGateSeal(currentBlock, this.gateSealAddress) + const status = await this.gateSealClient.checkGateSeal(currentBlock, this.gateSealAddress) if (E.isLeft(status)) { if (status.left === GateSealExpiredErr) { const f = Finding.fromObject({ @@ -125,7 +126,7 @@ export class GateSealSrv { } const currentBlockTimestamp = blockDto.timestamp - const status = await this.ethProvider.checkGateSeal(blockDto.number, this.gateSealAddress) + const status = await this.gateSealClient.checkGateSeal(blockDto.number, this.gateSealAddress) if (E.isLeft(status)) { if (status.left === GateSealExpiredErr) { const f = Finding.fromObject({ @@ -162,10 +163,7 @@ export class GateSealSrv { status.right.withdrawalQueueAddress, )}` } - if ( - currentBlockTimestamp - this.cache.getLastNoPauseRoleAlertTimestamp() > - GATE_SEAL_WITHOUT_PAUSE_ROLE_TRIGGER_EVERY - ) { + if (currentBlockTimestamp - this.cache.getLastNoPauseRoleAlertTimestamp() > ONE_DAY) { out.push( Finding.fromObject({ name: "🚨 GateSeal: actual address doesn't have PAUSE_ROLE for contracts", @@ -189,7 +187,7 @@ export class GateSealSrv { } const currentBlockTimestamp = blockDto.timestamp - const expiryTimestamp = await this.ethProvider.getExpiryTimestamp(blockDto.number) + const expiryTimestamp = await this.gateSealClient.getExpiryTimestamp(blockDto.number) if (E.isLeft(expiryTimestamp)) { return [ @@ -213,11 +211,8 @@ export class GateSealSrv { }), ) this.gateSealAddress = undefined - } else if ( - currentBlockTimestamp - this.cache.getLastExpiryGateSealAlertTimestamp() > - GATE_SEAL_EXPIRY_TRIGGER_EVERY - ) { - if (expiryTimestamp.right.toNumber() - currentBlockTimestamp <= GATE_SEAL_EXPIRY_THRESHOLD) { + } else if (currentBlockTimestamp - this.cache.getLastExpiryGateSealAlertTimestamp() > TWO_WEEKS) { + if (expiryTimestamp.right.toNumber() - currentBlockTimestamp <= THREE_MONTHS) { out.push( Finding.fromObject({ name: '⚠ī¸ GateSeal: is about to be expired', @@ -234,7 +229,7 @@ export class GateSealSrv { return out } - public handleTransaction(txEvent: TransactionEvent): Finding[] { + public handleTransaction(txEvent: TransactionDto): Finding[] { const findings: Finding[] = [] const sealedGateSealFindings = this.handleSealedGateSeal(txEvent) @@ -245,60 +240,72 @@ export class GateSealSrv { return findings } - public handleSealedGateSeal(txEvent: TransactionEvent): Finding[] { + public handleSealedGateSeal(txEvent: TransactionDto): Finding[] { if (this.gateSealAddress === undefined) { return [] } - const sealedEvents = filterLog(txEvent.logs, GATE_SEAL_SEALED_EVENT, this.gateSealAddress) - if (sealedEvents.length === 0) { - return [] - } + const iface = new ethers.utils.Interface([GATE_SEAL_SEALED_EVENT]) const out: Finding[] = [] - for (const sealedEvent of sealedEvents) { - const { sealed_by, sealed_for, sealable } = sealedEvent.args - const duration = formatDelay(Number(sealed_for)) - out.push( - Finding.fromObject({ - name: '🚨🚨🚨 GateSeal: is sealed 🚨🚨🚨', - description: `GateSeal address: ${etherscanAddress(this.gateSealAddress)}\nSealed by: ${etherscanAddress( - sealed_by, - )}\nSealed for: ${duration}\nSealable: ${etherscanAddress(sealable)}`, - alertId: 'GATE-SEAL-IS-SEALED', - severity: FindingSeverity.Critical, - type: FindingType.Info, - }), - ) + for (const log of txEvent.logs) { + if (log.address.toLowerCase() !== this.gateSealAddress.toLowerCase()) { + continue + } + + try { + const sealedEvent = iface.parseLog(log) + const { sealed_by, sealed_for, sealable } = sealedEvent.args + const duration = formatDelay(Number(sealed_for)) + + out.push( + Finding.fromObject({ + name: '🚨🚨🚨 GateSeal: is sealed 🚨🚨🚨', + description: `GateSeal address: ${etherscanAddress(this.gateSealAddress)}\nSealed by: ${etherscanAddress( + sealed_by, + )}\nSealed for: ${duration}\nSealable: ${etherscanAddress(sealable)}`, + alertId: 'GATE-SEAL-IS-SEALED', + severity: FindingSeverity.Critical, + type: FindingType.Info, + }), + ) + } catch (e) { + // Only one from eventsOfNotice could be correct + // Others - skipping + } } return out } - public handleNewGateSeal(txEvent: TransactionEvent): Finding[] { - const newGateSealEvents = filterLog( - txEvent.logs, - GATE_SEAL_FACTORY_GATE_SEAL_CREATED_EVENT, - this.gateSealFactoryAddress, - ) - if (newGateSealEvents.length === 0) { - return [] - } - + public handleNewGateSeal(txEvent: TransactionDto): Finding[] { + const iface = new ethers.utils.Interface([GATE_SEAL_FACTORY_GATE_SEAL_CREATED_EVENT]) const out: Finding[] = [] - for (const newGateSealEvent of newGateSealEvents) { - const { gate_seal } = newGateSealEvent.args - out.push( - Finding.fromObject({ - name: '⚠ī¸ GateSeal: a new instance deployed from factory', - description: `New instance address: ${etherscanAddress( - gate_seal, - )}\ndev: Please, check if \`GATE_SEAL_DEFAULT_ADDRESS\` should be updated in the nearest future`, - alertId: 'GATE-SEAL-NEW-ONE-CREATED', - severity: FindingSeverity.Medium, - type: FindingType.Info, - }), - ) - this.gateSealAddress = gate_seal + + for (const log of txEvent.logs) { + if (log.address.toLowerCase() !== this.gateSealFactoryAddress.toLowerCase()) { + continue + } + + try { + const newGateSealEvent = iface.parseLog(log) + const { gate_seal } = newGateSealEvent.args + + out.push( + Finding.fromObject({ + name: '⚠ī¸ GateSeal: a new instance deployed from factory', + description: `New instance address: ${etherscanAddress( + gate_seal, + )}\ndev: Please, check if \`GATE_SEAL_DEFAULT_ADDRESS\` should be updated in the nearest future`, + alertId: 'GATE-SEAL-NEW-ONE-CREATED', + severity: FindingSeverity.Medium, + type: FindingType.Info, + }), + ) + this.gateSealAddress = gate_seal + } catch (e) { + // Only one from eventsOfNotice could be correct + // Others - skipping + } } return out diff --git a/ethereum-steth/src/services/gate-seal/contract.ts b/ethereum-steth/src/services/gate-seal/contract.ts deleted file mode 100644 index 7b7b3362..00000000 --- a/ethereum-steth/src/services/gate-seal/contract.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as E from 'fp-ts/Either' -import { GateSeal } from '../../entity/gate_seal' -import BigNumber from 'bignumber.js' - -export abstract class IGateSealClient { - public abstract checkGateSeal(blockNumber: number, gateSealAddress: string): Promise> - - public abstract getExpiryTimestamp(blockNumber: number): Promise> -} diff --git a/ethereum-steth/src/services/gate-seal/gate-seal.spec.ts b/ethereum-steth/src/services/gate-seal/gate-seal.spec.ts index 253ec1ba..860637ea 100644 --- a/ethereum-steth/src/services/gate-seal/gate-seal.spec.ts +++ b/ethereum-steth/src/services/gate-seal/gate-seal.spec.ts @@ -1,23 +1,62 @@ -import { ethers, Finding, FindingSeverity, FindingType, getEthersProvider, Network, Transaction } from 'forta-agent' -import { App } from '../../app' -import { Address } from '../../utils/constants' +import { ethers, Finding, FindingSeverity, FindingType } from 'forta-agent' +import { Address, GATE_SEAL_DEFAULT_ADDRESS_BEFORE_26_APR_2024 } from '../../utils/constants' import * as E from 'fp-ts/Either' import { GateSeal } from '../../entity/gate_seal' import { expect } from '@jest/globals' -import { BlockDto } from '../../entity/events' -import { createTransactionEvent } from '../../utils/forta' +import { BlockDto, TransactionDto } from '../../entity/events' +import { + GateSeal__factory, + Lido__factory, + ValidatorsExitBusOracle__factory, + WithdrawalQueueERC721__factory, +} from '../../generated' +import { GateSealSrv } from './GateSeal.srv' +import { GateSealCache } from './GateSeal.cache' +import * as Winston from 'winston' +import { ETHProvider } from '../../clients/eth_provider' +import { EtherscanProviderMock } from '../../clients/mocks/mock' const TEST_TIMEOUT = 120_000 // ms describe('GateSeal srv functional tests', () => { - const ethProvider = getEthersProvider() + const drpcURL = `https://eth.drpc.org` + const mainnet = 1 + const ethProvider = new ethers.providers.JsonRpcProvider(drpcURL, mainnet) + + const logger: Winston.Logger = Winston.createLogger({ + format: Winston.format.simple(), + transports: [new Winston.transports.Console()], + }) + + const adr: Address = Address + + const lidoRunner = Lido__factory.connect(adr.LIDO_STETH_ADDRESS, ethProvider) + const gateSealRunner = GateSeal__factory.connect(GATE_SEAL_DEFAULT_ADDRESS_BEFORE_26_APR_2024, ethProvider) + const veboRunner = ValidatorsExitBusOracle__factory.connect(adr.EXIT_BUS_ORACLE_ADDRESS, ethProvider) + const wdQueueRunner = WithdrawalQueueERC721__factory.connect(adr.WITHDRAWALS_QUEUE_ADDRESS, ethProvider) + + const gateSealClient = new ETHProvider( + logger, + ethProvider, + EtherscanProviderMock(), + lidoRunner, + wdQueueRunner, + gateSealRunner, + veboRunner, + ) + + const gateSealSrv = new GateSealSrv( + logger, + gateSealClient, + new GateSealCache(), + GATE_SEAL_DEFAULT_ADDRESS_BEFORE_26_APR_2024, + adr.GATE_SEAL_FACTORY_ADDRESS, + ) test( 'handle pause role true', async () => { - const app = await App.getInstance() - - const blockNumber = 19113580 + const blockNumber = 19_113_580 const block = await ethProvider.getBlock(blockNumber) const blockDto: BlockDto = { number: block.number, @@ -25,12 +64,12 @@ describe('GateSeal srv functional tests', () => { parentHash: block.parentHash, } - const initErr = await app.GateSealSrv.initialize(blockNumber) + const initErr = await gateSealSrv.initialize(blockNumber) if (initErr instanceof Error) { throw initErr } - const status = await app.ethClient.checkGateSeal(blockNumber, Address.GATE_SEAL_DEFAULT_ADDRESS) + const status = await gateSealClient.checkGateSeal(blockNumber, GATE_SEAL_DEFAULT_ADDRESS_BEFORE_26_APR_2024) if (E.isLeft(status)) { throw status } @@ -43,7 +82,7 @@ describe('GateSeal srv functional tests', () => { } expect(status.right).toEqual(expected) - const result = await app.GateSealSrv.handlePauseRole(blockDto) + const result = await gateSealSrv.handlePauseRole(blockDto) expect(result.length).toEqual(0) }, @@ -53,11 +92,9 @@ describe('GateSeal srv functional tests', () => { test( '⚠ī¸ GateSeal: is about to be expired', async () => { - const app = await App.getInstance() - const initBlock = 19_172_614 - const initErr = await app.GateSealSrv.initialize(initBlock) + const initErr = await gateSealSrv.initialize(initBlock) if (initErr instanceof Error) { throw initErr } @@ -69,12 +106,12 @@ describe('GateSeal srv functional tests', () => { timestamp: block.timestamp, parentHash: block.parentHash, } - const result = await app.GateSealSrv.handleExpiryGateSeal(blockDto) + const result = await gateSealSrv.handleExpiryGateSeal(blockDto) const expected = Finding.fromObject({ alertId: 'GATE-SEAL-IS-ABOUT-TO-BE-EXPIRED', description: - 'GateSeal address: [0x1ad5cb2955940f998081c1ef5f5f00875431aa90](https://etherscan.io/address/0x1ad5cb2955940f998081c1ef5f5f00875431aa90)\n' + + `GateSeal address: [${GATE_SEAL_DEFAULT_ADDRESS_BEFORE_26_APR_2024}](https://etherscan.io/address/${GATE_SEAL_DEFAULT_ADDRESS_BEFORE_26_APR_2024})\n` + 'Expiry date Wed, 01 May 2024 00:00:00 GMT', name: '⚠ī¸ GateSeal: is about to be expired', severity: FindingSeverity.Medium, @@ -94,19 +131,21 @@ describe('GateSeal srv functional tests', () => { test( '⚠ī¸ GateSeal: a new instance deployed from factory', async () => { - const app = await App.getInstance() const txHash = '0x1547f17108830a92673b967aff13971fae18b4d35681b93a38a97a22083deb93' + const trx = await ethProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + } - const receipt = await ethProvider.send('eth_getTransactionReceipt', [txHash]) - const block = await ethProvider.send('eth_getBlockByNumber', [ - ethers.utils.hexValue(parseInt(receipt.blockNumber)), - true, - ]) - const transaction = block.transactions.find((tx: Transaction) => tx.hash.toLowerCase() === txHash)! - - const txEvent = createTransactionEvent(transaction, block, Network.MAINNET, [], receipt.logs) - - const results = await app.GateSealSrv.handleTransaction(txEvent) + const results = gateSealSrv.handleTransaction(transactionDto) const expected = Finding.fromObject({ alertId: 'GATE-SEAL-NEW-ONE-CREATED', @@ -127,24 +166,20 @@ describe('GateSeal srv functional tests', () => { const txHashEmptyFindings = '0xb00783f3eb79bd60f63e5744bbf0cb4fdc2f98bbca54cd2d3f611f032faa6a57' - const receiptEmptyFindings = await ethProvider.send('eth_getTransactionReceipt', [txHashEmptyFindings]) - const blockEmptyFindings = await ethProvider.send('eth_getBlockByNumber', [ - ethers.utils.hexValue(parseInt(receipt.blockNumber)), - true, - ]) - const transactionEmptyFindings = blockEmptyFindings.transactions.find( - (tx: Transaction) => tx.hash.toLowerCase() === txHash, - )! - - const txEventEmptyFindings = createTransactionEvent( - transactionEmptyFindings, - blockEmptyFindings, - Network.MAINNET, - [], - receiptEmptyFindings.logs, - ) - - const resultsEmptyFindings = await app.GateSealSrv.handleTransaction(txEventEmptyFindings) + const trxWithEmptyFindings = await ethProvider.getTransaction(txHashEmptyFindings) + const receiptWithEmptyFindings = await trxWithEmptyFindings.wait() + + const trxDTOWithEmptyFindings: TransactionDto = { + logs: receiptWithEmptyFindings.logs, + to: trxWithEmptyFindings.to ? trxWithEmptyFindings.to : null, + timestamp: trxWithEmptyFindings.timestamp ? trxWithEmptyFindings.timestamp : new Date().getTime(), + block: { + timestamp: trxWithEmptyFindings.timestamp ? trxWithEmptyFindings.timestamp : new Date().getTime(), + number: trxWithEmptyFindings.blockNumber ? trxWithEmptyFindings.blockNumber : 1, + }, + } + + const resultsEmptyFindings = gateSealSrv.handleTransaction(trxDTOWithEmptyFindings) expect(resultsEmptyFindings.length).toEqual(0) }, diff --git a/ethereum-steth/src/services/gate-seal/mocks/mock.ts b/ethereum-steth/src/services/gate-seal/mocks/mock.ts index a95d139a..bddcb930 100644 --- a/ethereum-steth/src/services/gate-seal/mocks/mock.ts +++ b/ethereum-steth/src/services/gate-seal/mocks/mock.ts @@ -1,4 +1,4 @@ -import { IGateSealClient } from '../contract' +import { IGateSealClient } from '../GateSeal.srv' export const GateSealClientMock = (): jest.Mocked => ({ checkGateSeal: jest.fn(), diff --git a/ethereum-steth/src/services/steth_operation/StethOperation.functional.spec.ts b/ethereum-steth/src/services/steth_operation/StethOperation.functional.spec.ts index 89cd2b5d..a22cd87e 100644 --- a/ethereum-steth/src/services/steth_operation/StethOperation.functional.spec.ts +++ b/ethereum-steth/src/services/steth_operation/StethOperation.functional.spec.ts @@ -1,9 +1,8 @@ -import { ethers, Finding, FindingSeverity, FindingType, getEthersProvider, Network, Transaction } from 'forta-agent' +import { Finding, FindingSeverity, FindingType, getEthersProvider } from 'forta-agent' import { App } from '../../app' import { JsonRpcProvider } from '@ethersproject/providers' -import { createTransactionEvent } from '../../utils/forta' import BigNumber from 'bignumber.js' -import { BlockDto } from '../../entity/events' +import { BlockDto, TransactionDto } from '../../entity/events' const TEST_TIMEOUT = 60_000 // ms @@ -86,16 +85,20 @@ describe('Steth.srv functional tests', () => { const app = await App.getInstance() const txHash = '0x11a48020ae69cf08bd063f1fbc8ecf65bd057015aaa991bf507dbc598aadb68e' - const receipt = await ethProvider.send('eth_getTransactionReceipt', [txHash]) - const block = await ethProvider.send('eth_getBlockByNumber', [ - ethers.utils.hexValue(parseInt(receipt.blockNumber)), - true, - ]) - const transaction = block.transactions.find((tx: Transaction) => tx.hash.toLowerCase() === txHash)! + const trx = await ethProvider.getTransaction(txHash) + const receipt = await trx.wait() - const txEvent = createTransactionEvent(transaction, block, Network.MAINNET, [], receipt.logs) + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + } - const results = await app.StethOperationSrv.handleTransaction(txEvent, txEvent.blockNumber) + const results = await app.StethOperationSrv.handleTransaction(transactionDto) const expected = [ { @@ -141,16 +144,19 @@ describe('Steth.srv functional tests', () => { const app = await App.getInstance() const txHash = '0x91c7c2f33faf3b5fb097138c1d49c1d4e83f99e1c3b346b3cad35a5928c03b3a' - const receipt = await ethProvider.send('eth_getTransactionReceipt', [txHash]) - const block = await ethProvider.send('eth_getBlockByNumber', [ - ethers.utils.hexValue(parseInt(receipt.blockNumber)), - true, - ]) - const transaction = block.transactions.find((tx: Transaction) => tx.hash.toLowerCase() === txHash)! - - const txEvent = createTransactionEvent(transaction, block, Network.MAINNET, [], receipt.logs) + const trx = await ethProvider.getTransaction(txHash) + const receipt = await trx.wait() - const results = await app.StethOperationSrv.handleTransaction(txEvent, txEvent.blockNumber) + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + } + const results = await app.StethOperationSrv.handleTransaction(transactionDto) const expected = [ { @@ -195,14 +201,20 @@ describe('Steth.srv functional tests', () => { const app = await App.getInstance() const txHash = '0xe71ac8b9f8f7b360f5defd3f6738f8482f8c15f1dd5f6827544bef8b7b4fbd37' - const receipt = await ethProvider.send('eth_getTransactionReceipt', [txHash]) - const block = await ethProvider.send('eth_getBlockByNumber', [ - ethers.utils.hexValue(parseInt(receipt.blockNumber)), - true, - ]) - const transaction = block.transactions.find((tx: Transaction) => tx.hash.toLowerCase() === txHash)! - const txEvent = createTransactionEvent(transaction, block, Network.MAINNET, [], receipt.logs) - const results = await app.StethOperationSrv.handleTransaction(txEvent, parseInt(receipt.blockNumber)) + const trx = await ethProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + } + + const results = await app.StethOperationSrv.handleTransaction(transactionDto) const expected: Finding = Finding.fromObject({ name: 'ℹī¸ Lido: Token rebased', diff --git a/ethereum-steth/src/services/steth_operation/StethOperation.srv.ts b/ethereum-steth/src/services/steth_operation/StethOperation.srv.ts index 651d3a2b..65d99033 100644 --- a/ethereum-steth/src/services/steth_operation/StethOperation.srv.ts +++ b/ethereum-steth/src/services/steth_operation/StethOperation.srv.ts @@ -2,9 +2,9 @@ import { StethOperationCache } from './StethOperation.cache' import { ETH_DECIMALS } from '../../utils/constants' import * as E from 'fp-ts/Either' import { Finding, FindingSeverity, FindingType } from 'forta-agent' -import { EventOfNotice } from '../../entity/events' +import { EventOfNotice, handleEventsOfNotice, TransactionDto } from '../../entity/events' import { elapsedTime } from '../../utils/time' -import { IStethClient, TransactionEventContract } from './contracts' +import { IStethClient } from './contracts' import { Logger } from 'winston' import { alertId_token_rebased } from '../../utils/events/lido_events' import { networkAlert } from '../../utils/errors' @@ -12,18 +12,16 @@ import { BlockDto } from '../../entity/events' // Formula: (60 * 60 * 72) / 13 = 19_938 const HISTORY_BLOCK_OFFSET: number = Math.floor((60 * 60 * 72) / 13) -const BLOCK_CHECK_INTERVAL: number = 100 -const BLOCK_CHECK_INTERVAL_SMAll: number = 25 -export const MAX_DEPOSITABLE_ETH_AMOUNT_MEDIUM = 10_000 // 10000 ETH -export const MAX_DEPOSITABLE_ETH_AMOUNT_CRITICAL: number = 20_000 // 20000 ETH -const REPORT_WINDOW = 60 * 60 * 24 // 24 hours -export const MAX_DEPOSITABLE_ETH_AMOUNT_CRITICAL_TIME = 60 * 60 // 1 hour - -export const MAX_DEPOSITOR_TX_DELAY = 60 * 60 * 72 // 72 Hours -const REPORT_WINDOW_EXECUTOR_BALANCE = 60 * 60 * 4 // 4 Hours -export const MIN_DEPOSIT_EXECUTOR_BALANCE = 2 // 2 ETH -const REPORT_WINDOW_STAKING_LIMIT_10 = 60 * 60 * 12 // 12 hours -const REPORT_WINDOW_STAKING_LIMIT_30 = 60 * 60 * 12 // 12 hours +const ONCE_PER_100_BLOCKS: number = 100 +const ONCE_PER_25_BLOCKS: number = 25 +export const ETH_10K = 10_000 // 10000 ETH +export const ETH_20K: number = 20_000 // 20000 ETH +const HOURS_24 = 60 * 60 * 24 // 24 hours +export const HOUR_1 = 60 * 60 // 1 hour +const HOURS_4 = 60 * 60 * 4 // 4 Hours +const HOURS_12 = 60 * 60 * 12 // 12 Hours +export const DAYS_3 = 60 * 60 * 72 // 72 Hours +export const ETH_2 = 2 // 2 ETH export class StethOperationSrv { private readonly name = 'StethOperationSrv' @@ -163,22 +161,22 @@ export class StethOperationSrv { return findings } - public async handleTransaction(txEvent: TransactionEventContract, blockNumber: number): Promise { + public async handleTransaction(txEvent: TransactionDto): Promise { const out: Finding[] = [] - if (txEvent.to == this.depositSecurityAddress) { + if (txEvent.to !== null && txEvent.to.toLowerCase() == this.depositSecurityAddress.toLowerCase()) { this.cache.setLastDepositorTxTime(txEvent.timestamp) } - const depositSecFindings = this.handleEventsOfNotice(txEvent, this.depositSecurityEvents) - const lidoFindings = this.handleEventsOfNotice(txEvent, this.lidoEvents) - const insuranceFundFindings = this.handleEventsOfNotice(txEvent, this.insuranceFundEvents) - const burnerFindings = this.handleEventsOfNotice(txEvent, this.burnerEvents) - out.push(...depositSecFindings, ...lidoFindings, ...insuranceFundFindings, ...burnerFindings) + const lidoFindings = handleEventsOfNotice(txEvent, this.lidoEvents) + const depositSecFindings = handleEventsOfNotice(txEvent, this.depositSecurityEvents) + const insuranceFundFindings = handleEventsOfNotice(txEvent, this.insuranceFundEvents) + const burnerFindings = handleEventsOfNotice(txEvent, this.burnerEvents) + out.push(...lidoFindings, ...depositSecFindings, ...insuranceFundFindings, ...burnerFindings) for (const f of lidoFindings) { if (f.alertId === alertId_token_rebased) { - const shareRate = await this.ethProvider.getShareRate(blockNumber) + const shareRate = await this.ethProvider.getShareRate(txEvent.block.number) if (E.isLeft(shareRate)) { const f: Finding = networkAlert( shareRate.left, @@ -190,7 +188,7 @@ export class StethOperationSrv { } else { this.cache.setShareRate({ amount: shareRate.right, - blockNumber: blockNumber, + blockNumber: txEvent.block.number, }) } } @@ -199,30 +197,6 @@ export class StethOperationSrv { return out } - public handleEventsOfNotice(txEvent: TransactionEventContract, eventsOfNotice: EventOfNotice[]) { - const out: Finding[] = [] - for (const eventInfo of eventsOfNotice) { - if (eventInfo.address in txEvent.addresses) { - const filteredEvents = txEvent.filterLog(eventInfo.event, eventInfo.address) - - for (const filteredEvent of filteredEvents) { - out.push( - Finding.fromObject({ - name: eventInfo.name, - description: eventInfo.description(filteredEvent.args), - alertId: eventInfo.alertId, - severity: eventInfo.severity, - type: eventInfo.type, - metadata: { args: String(filteredEvent.args) }, - }), - ) - } - } - } - - return out - } - public async handleBufferedEth(blockNumber: number, blockTimestamp: number): Promise { const shiftedBlockNumber = blockNumber - 3 const [bufferedEthRaw, shifte3dBufferedEthRaw, shifte4dBufferedEthRaw] = await Promise.all([ @@ -306,7 +280,7 @@ export class StethOperationSrv { } } - if (blockNumber % BLOCK_CHECK_INTERVAL === 0) { + if (blockNumber % ONCE_PER_100_BLOCKS === 0) { const depositableEtherRaw = await this.ethProvider.getDepositableEther(blockNumber) if (E.isLeft(depositableEtherRaw)) { return [ @@ -320,7 +294,7 @@ export class StethOperationSrv { const depositableEther = depositableEtherRaw.right.div(ETH_DECIMALS).toNumber() // Keep track of buffer size above MAX_BUFFERED_ETH_AMOUNT_CRITICAL - if (depositableEther > MAX_DEPOSITABLE_ETH_AMOUNT_CRITICAL) { + if (depositableEther > ETH_20K) { if (this.cache.getCriticalDepositableAmountTimestamp() === 0) { this.cache.setCriticalDepositableAmountTimestamp(blockTimestamp) } @@ -329,10 +303,10 @@ export class StethOperationSrv { this.cache.setCriticalDepositableAmountTimestamp(0) } - if (this.cache.getLastReportedDepositableEthTimestamp() + REPORT_WINDOW < blockTimestamp) { + if (this.cache.getLastReportedDepositableEthTimestamp() + HOURS_24 < blockTimestamp) { if ( - depositableEther > MAX_DEPOSITABLE_ETH_AMOUNT_CRITICAL && - this.cache.getCriticalDepositableAmountTimestamp() + MAX_DEPOSITABLE_ETH_AMOUNT_CRITICAL_TIME < blockTimestamp + depositableEther > ETH_20K && + this.cache.getCriticalDepositableAmountTimestamp() + HOUR_1 < blockTimestamp ) { out.push( Finding.fromObject({ @@ -340,7 +314,7 @@ export class StethOperationSrv { description: `There are ${depositableEther.toFixed(2)} ` + `depositable ETH in DAO for more than ` + - `${Math.floor(MAX_DEPOSITABLE_ETH_AMOUNT_CRITICAL_TIME / (60 * 60))} hour(s)`, + `${Math.floor(HOUR_1 / (60 * 60))} hour(s)`, alertId: 'HUGE-DEPOSITABLE-ETH', severity: FindingSeverity.High, type: FindingType.Suspicious, @@ -348,9 +322,9 @@ export class StethOperationSrv { ) this.cache.setLastReportedDepositableEthTimestamp(blockTimestamp) } else if ( - depositableEther > MAX_DEPOSITABLE_ETH_AMOUNT_MEDIUM && + depositableEther > ETH_10K && this.cache.getLastDepositorTxTime() !== 0 && - this.cache.getLastDepositorTxTime() + MAX_DEPOSITOR_TX_DELAY < blockTimestamp + this.cache.getLastDepositorTxTime() + DAYS_3 < blockTimestamp ) { const bufferedEth = bufferedEthRaw.right.div(ETH_DECIMALS).toNumber() @@ -360,7 +334,7 @@ export class StethOperationSrv { description: `There are ${bufferedEth.toFixed(2)} ` + `depositable ETH in DAO and there are more than ` + - `${Math.floor(MAX_DEPOSITOR_TX_DELAY / (60 * 60))} ` + + `${Math.floor(DAYS_3 / (60 * 60))} ` + `hours since last Depositor TX`, alertId: 'HIGH-DEPOSITABLE-ETH', severity: FindingSeverity.Medium, @@ -377,11 +351,8 @@ export class StethOperationSrv { public async handleDepositExecutorBalance(blockNumber: number, currentBlockTimestamp: number): Promise { const out: Finding[] = [] - if (blockNumber % BLOCK_CHECK_INTERVAL === 0) { - if ( - this.cache.getLastReportedExecutorBalanceTimestamp() + REPORT_WINDOW_EXECUTOR_BALANCE < - currentBlockTimestamp - ) { + if (blockNumber % ONCE_PER_100_BLOCKS === 0) { + if (this.cache.getLastReportedExecutorBalanceTimestamp() + HOURS_4 < currentBlockTimestamp) { const executorBalanceRaw = await this.ethProvider.getBalance(this.lidoDepositExecutorAddress, blockNumber) if (E.isLeft(executorBalanceRaw)) { return [ @@ -394,7 +365,7 @@ export class StethOperationSrv { } const executorBalance = executorBalanceRaw.right.div(ETH_DECIMALS).toNumber() - if (executorBalance < MIN_DEPOSIT_EXECUTOR_BALANCE) { + if (executorBalance < ETH_2) { this.cache.setLastReportedExecutorBalanceTimestamp(currentBlockTimestamp) out.push( Finding.fromObject({ @@ -415,7 +386,7 @@ export class StethOperationSrv { public async handleStakingLimit(blockNumber: number, currentBlockTimestamp: number): Promise { const out: Finding[] = [] - if (blockNumber % BLOCK_CHECK_INTERVAL_SMAll === 0) { + if (blockNumber % ONCE_PER_25_BLOCKS === 0) { const stakingLimitInfo = await this.ethProvider.getStakingLimitInfo(blockNumber) if (E.isLeft(stakingLimitInfo)) { return [ @@ -431,7 +402,7 @@ export class StethOperationSrv { const maxStakingLimit = stakingLimitInfo.right.maxStakeLimit if ( - this.cache.getLastReportedStakingLimit10Timestamp() + REPORT_WINDOW_STAKING_LIMIT_10 < currentBlockTimestamp && + this.cache.getLastReportedStakingLimit10Timestamp() + HOURS_12 < currentBlockTimestamp && currentStakingLimit.isLessThan(maxStakingLimit.times(0.1)) ) { out.push( @@ -445,7 +416,7 @@ export class StethOperationSrv { ) this.cache.setLastReportedStakingLimit10Timestamp(currentBlockTimestamp) } else if ( - this.cache.getLastReportedStakingLimit30Timestamp() + REPORT_WINDOW_STAKING_LIMIT_30 < currentBlockTimestamp && + this.cache.getLastReportedStakingLimit30Timestamp() + HOURS_12 < currentBlockTimestamp && currentStakingLimit.isLessThan(maxStakingLimit.times(0.3)) ) { out.push( diff --git a/ethereum-steth/src/services/steth_operation/StethOperation.unit.spec.ts b/ethereum-steth/src/services/steth_operation/StethOperation.unit.spec.ts index 6b370221..39aebff7 100644 --- a/ethereum-steth/src/services/steth_operation/StethOperation.unit.spec.ts +++ b/ethereum-steth/src/services/steth_operation/StethOperation.unit.spec.ts @@ -1,11 +1,4 @@ -import { - MAX_DEPOSITABLE_ETH_AMOUNT_CRITICAL, - MAX_DEPOSITABLE_ETH_AMOUNT_CRITICAL_TIME, - MAX_DEPOSITABLE_ETH_AMOUNT_MEDIUM, - MAX_DEPOSITOR_TX_DELAY, - MIN_DEPOSIT_EXECUTOR_BALANCE, - StethOperationSrv, -} from './StethOperation.srv' +import { ETH_20K, HOUR_1, ETH_10K, DAYS_3, ETH_2, StethOperationSrv } from './StethOperation.srv' import { StethOperationCache } from './StethOperation.cache' import * as E from 'fp-ts/Either' import { Address, ETH_DECIMALS } from '../../utils/constants' @@ -18,19 +11,13 @@ import { TransactionResponse } from '@ethersproject/abstract-provider' import { faker } from '@faker-js/faker' import { BigNumber as EtherBigNumber } from 'ethers' import BigNumber from 'bignumber.js' -import { Finding, FindingSeverity, FindingType, LogDescription } from 'forta-agent' +import { Finding, FindingSeverity, FindingType } from 'forta-agent' import * as Winston from 'winston' import { TypedEvent } from '../../generated/common' import { StakingLimitInfo } from '../../entity/staking_limit_info' -import { - getFilteredBurnerEventsMock, - getFilteredDepositSecurityEventsMock, - getFilteredInsuranceFundEventsMock, - getFilteredLidoEventsMock, -} from '../../utils/events/mocks/events.mock' import { IStethClient } from './contracts' import { StethClientMock } from './mocks/mock' -import { TransactionEventContractMock, TypedEventMock } from './mocks/eth_evnt.mock' +import { TypedEventMock } from './mocks/eth_evnt.mock' describe('StethOperationSrv', () => { let ethProviderMock: jest.Mocked @@ -491,9 +478,7 @@ describe('StethOperationSrv', () => { const bufferedEther = new BigNumber(180).multipliedBy(ETH_DECIMALS) ethProviderMock.getBufferedEther.mockResolvedValueOnce(E.right(bufferedEther)) - const mockDepositableEther = EtherBigNumber.from(MAX_DEPOSITABLE_ETH_AMOUNT_MEDIUM + 1).mul( - EtherBigNumber.from(ETH_DECIMALS.toString()), - ) + const mockDepositableEther = EtherBigNumber.from(ETH_10K + 1).mul(EtherBigNumber.from(ETH_DECIMALS.toString())) ethProviderMock.getDepositableEther.mockResolvedValue(E.right(new BigNumber(mockDepositableEther.toString()))) // shifte3dBufferedEthRaw @@ -514,7 +499,7 @@ describe('StethOperationSrv', () => { const cache = new StethOperationCache() - cache.setLastDepositorTxTime(date.setHours(-(MAX_DEPOSITOR_TX_DELAY + 1))) + cache.setLastDepositorTxTime(date.setHours(-(DAYS_3 + 1))) const srv = new StethOperationSrv( logger, cache, @@ -537,7 +522,7 @@ describe('StethOperationSrv', () => { description: `There are ${bufferedEth.toFixed(2)} ` + `depositable ETH in DAO and there are more than ` + - `${Math.floor(MAX_DEPOSITOR_TX_DELAY / (60 * 60))} ` + + `${Math.floor(DAYS_3 / (60 * 60))} ` + `hours since last Depositor TX`, name: '⚠ī¸ High depositable ETH amount', severity: FindingSeverity.Medium, @@ -558,9 +543,7 @@ describe('StethOperationSrv', () => { const bufferedEther = new BigNumber(180).multipliedBy(ETH_DECIMALS) ethProviderMock.getBufferedEther.mockResolvedValueOnce(E.right(bufferedEther)) - const mockDepositableEther = EtherBigNumber.from(MAX_DEPOSITABLE_ETH_AMOUNT_CRITICAL + 1).mul( - EtherBigNumber.from(ETH_DECIMALS.toString()), - ) + const mockDepositableEther = EtherBigNumber.from(ETH_20K + 1).mul(EtherBigNumber.from(ETH_DECIMALS.toString())) ethProviderMock.getDepositableEther.mockResolvedValueOnce(E.right(new BigNumber(mockDepositableEther.toString()))) // shifte3dBufferedEthRaw @@ -602,8 +585,7 @@ describe('StethOperationSrv', () => { const expected = Finding.fromObject({ alertId: 'HUGE-DEPOSITABLE-ETH', description: - `There are 20001.00 depositable ETH in DAO for more than ` + - `${Math.floor(MAX_DEPOSITABLE_ETH_AMOUNT_CRITICAL_TIME / (60 * 60))} hour(s)`, + `There are 20001.00 depositable ETH in DAO for more than ` + `${Math.floor(HOUR_1 / (60 * 60))} hour(s)`, name: '🚨 Huge depositable ETH amount', severity: FindingSeverity.High, type: FindingType.Suspicious, @@ -661,7 +643,7 @@ describe('StethOperationSrv', () => { }) test('⚠ī¸ Low deposit executor balance', async () => { - const executorBalanceRaw = new BigNumber(MIN_DEPOSIT_EXECUTOR_BALANCE - 1).multipliedBy(ETH_DECIMALS) + const executorBalanceRaw = new BigNumber(ETH_2 - 1).multipliedBy(ETH_DECIMALS) ethProviderMock.getBalance.mockResolvedValueOnce(E.right(executorBalanceRaw)) const cache = new StethOperationCache() @@ -833,154 +815,6 @@ describe('StethOperationSrv', () => { }) }) - describe('handleTransaction', () => { - test('success', async () => { - const cache = new StethOperationCache() - - const depositEvents = getDepositSecurityEvents(address.DEPOSIT_SECURITY_ADDRESS) - const lidoEvents = getLidoEvents(address.LIDO_STETH_ADDRESS) - const insuranceFundEvents = getInsuranceFundEvents(address.INSURANCE_FUND_ADDRESS, address.KNOWN_ERC20) - const burnerEvents = getBurnerEvents(address.BURNER_ADDRESS) - - const events = [...depositEvents, ...lidoEvents, ...insuranceFundEvents, ...burnerEvents] - - const shareRateMock = new BigNumber('1.15490045560519776042410219381324898101464198621e+27') - ethProviderMock.getShareRate.mockResolvedValue(E.right(shareRateMock)) - - const srv = new StethOperationSrv( - logger, - cache, - ethProviderMock, - address.DEPOSIT_SECURITY_ADDRESS, - address.LIDO_STETH_ADDRESS, - address.DEPOSIT_EXECUTOR_ADDRESS, - - depositEvents, - lidoEvents, - insuranceFundEvents, - burnerEvents, - ) - - const txEventMock = TransactionEventContractMock() - txEventMock.addresses = { - [address.DEPOSIT_SECURITY_ADDRESS]: true, - [address.LIDO_STETH_ADDRESS]: true, - [address.INSURANCE_FUND_ADDRESS]: true, - [address.BURNER_ADDRESS]: true, - } - - txEventMock.to = address.DEPOSIT_SECURITY_ADDRESS - - const filteredEvents: LogDescription[] = [] - for (const logDescription of [ - ...getFilteredDepositSecurityEventsMock(), - ...getFilteredLidoEventsMock(), - ...getFilteredInsuranceFundEventsMock(), - ...getFilteredBurnerEventsMock(), - ]) { - filteredEvents.push(logDescription) - txEventMock.filterLog.mockReturnValueOnce([logDescription]) - } - - const blockNumber = 19061521 - const result = await srv.handleTransaction(txEventMock, blockNumber) - - for (let i = 0; i < result.length; i++) { - const expected: Finding = Finding.fromObject({ - name: events[i].name, - description: events[i].description(filteredEvents[i].args), - alertId: events[i].alertId, - severity: events[i].severity, - type: events[i].type, - metadata: { args: String(filteredEvents[i].args) }, - }) - - expect(result[i]).toEqual(expected) - } - - expect(srv.getStorage().getShareRate().amount).toEqual(shareRateMock) - expect(srv.getStorage().getShareRate().blockNumber).toEqual(19061521) - }) - - test('get shareRate error', async () => { - const cache = new StethOperationCache() - - const depositEvents = getDepositSecurityEvents(address.DEPOSIT_SECURITY_ADDRESS) - const lidoEvents = getLidoEvents(address.LIDO_STETH_ADDRESS) - const insuranceFundEvents = getInsuranceFundEvents(address.INSURANCE_FUND_ADDRESS, address.KNOWN_ERC20) - const burnerEvents = getBurnerEvents(address.BURNER_ADDRESS) - - const events = [...depositEvents, ...lidoEvents, ...insuranceFundEvents, ...burnerEvents] - - ethProviderMock.getShareRate.mockResolvedValue(E.left(new Error('shareRateErr'))) - - const srv = new StethOperationSrv( - logger, - cache, - ethProviderMock, - address.DEPOSIT_SECURITY_ADDRESS, - address.LIDO_STETH_ADDRESS, - address.DEPOSIT_EXECUTOR_ADDRESS, - - depositEvents, - lidoEvents, - insuranceFundEvents, - burnerEvents, - ) - - const txEventMock = TransactionEventContractMock() - txEventMock.addresses = { - [address.DEPOSIT_SECURITY_ADDRESS]: true, - [address.LIDO_STETH_ADDRESS]: true, - [address.INSURANCE_FUND_ADDRESS]: true, - [address.BURNER_ADDRESS]: true, - } - - txEventMock.to = address.DEPOSIT_SECURITY_ADDRESS - - const filteredEvents: LogDescription[] = [] - for (const logDescription of [ - ...getFilteredDepositSecurityEventsMock(), - ...getFilteredLidoEventsMock(), - ...getFilteredInsuranceFundEventsMock(), - ...getFilteredBurnerEventsMock(), - ]) { - filteredEvents.push(logDescription) - txEventMock.filterLog.mockReturnValueOnce([logDescription]) - } - - const blockNumber = 19061521 - const result = await srv.handleTransaction(txEventMock, blockNumber) - - for (let i = 0; i < result.length - 1; i++) { - const expected: Finding = Finding.fromObject({ - name: events[i].name, - description: events[i].description(filteredEvents[i].args), - alertId: events[i].alertId, - severity: events[i].severity, - type: events[i].type, - metadata: { args: String(filteredEvents[i].args) }, - }) - - expect(result[i]).toEqual(expected) - } - - const expectedShareRateErrFinding = Finding.fromObject({ - alertId: 'NETWORK-ERROR', - description: `Could not call ethProvider.getShareRate`, - name: 'Error in StethOperationSrv.handleTransaction:192', - severity: FindingSeverity.Unknown, - type: FindingType.Degraded, - }) - - expect(result[result.length - 1].name).toEqual(expectedShareRateErrFinding.name) - expect(result[result.length - 1].description).toEqual(expectedShareRateErrFinding.description) - expect(result[result.length - 1].alertId).toEqual(expectedShareRateErrFinding.alertId) - expect(result[result.length - 1].severity).toEqual(expectedShareRateErrFinding.severity) - expect(result[result.length - 1].type).toEqual(expectedShareRateErrFinding.type) - }) - }) - describe('handleShareRateChange', () => { test(`ethProviderErr`, async () => { const want = new Error(`getShareRateErr`) diff --git a/ethereum-steth/src/services/steth_operation/mocks/eth_evnt.mock.ts b/ethereum-steth/src/services/steth_operation/mocks/eth_evnt.mock.ts index d0d59777..ec12f0b9 100644 --- a/ethereum-steth/src/services/steth_operation/mocks/eth_evnt.mock.ts +++ b/ethereum-steth/src/services/steth_operation/mocks/eth_evnt.mock.ts @@ -1,6 +1,4 @@ -import { TransactionEventContract } from '../contracts' import { TypedEvent } from '../../../generated/common' -import { faker } from '@faker-js/faker' export const TypedEventMock = (): jest.Mocked => ({ address: '', @@ -18,11 +16,3 @@ export const TypedEventMock = (): jest.Mocked => ({ transactionHash: '', transactionIndex: 0, }) - -export const TransactionEventContractMock = (): jest.Mocked => ({ - addresses: {}, - logs: [], - filterLog: jest.fn(), - to: faker.finance.ethereumAddress(), - timestamp: faker.date.past().getTime(), -}) diff --git a/ethereum-steth/src/services/vault/Vault.srv.ts b/ethereum-steth/src/services/vault/Vault.srv.ts index 5ba6454c..7dd5acfb 100644 --- a/ethereum-steth/src/services/vault/Vault.srv.ts +++ b/ethereum-steth/src/services/vault/Vault.srv.ts @@ -1,20 +1,19 @@ import BigNumber from 'bignumber.js' import { ETH_DECIMALS } from '../../utils/constants' import * as E from 'fp-ts/Either' -import { Finding, FindingSeverity, FindingType } from 'forta-agent' +import { ethers, Finding, FindingSeverity, FindingType } from 'forta-agent' import { elapsedTime } from '../../utils/time' import { toEthString } from '../../utils/string' import { ETHDistributedEvent } from '../../generated/Lido' -import { TransactionEvent } from 'forta-agent/dist/sdk/transaction.event' import { TRANSFER_SHARES_EVENT } from '../../utils/events/vault_events' import { Logger } from 'winston' import { networkAlert } from '../../utils/errors' import { IVaultClient } from './contract' -import { BlockDto } from '../../entity/events' +import { BlockDto, TransactionDto } from '../../entity/events' -const WITHDRAWAL_VAULT_BALANCE_BLOCK_INTERVAL = 100 -const WITHDRAWAL_VAULT_BALANCE_DIFF_INFO = ETH_DECIMALS.times(1000) -const EL_VAULT_BALANCE_DIFF_INFO = ETH_DECIMALS.times(50) +const ONCE_PER_100_BLOCKS = 100 +const ETH_1K = ETH_DECIMALS.times(1000) +const ETH_50 = ETH_DECIMALS.times(50) export class VaultSrv { private readonly logger: Logger @@ -122,11 +121,8 @@ export class VaultSrv { public async handleWithdrawalVaultBalance(blockNumber: number): Promise { const out: Finding[] = [] - if (blockNumber % WITHDRAWAL_VAULT_BALANCE_BLOCK_INTERVAL === 0) { - const report = await this.ethProvider.getETHDistributedEvent( - blockNumber - WITHDRAWAL_VAULT_BALANCE_BLOCK_INTERVAL, - blockNumber, - ) + if (blockNumber % ONCE_PER_100_BLOCKS === 0) { + const report = await this.ethProvider.getETHDistributedEvent(blockNumber - ONCE_PER_100_BLOCKS, blockNumber) if (E.isLeft(report)) { return [ networkAlert( @@ -139,7 +135,7 @@ export class VaultSrv { const prevWithdrawalVaultBalance = await this.ethProvider.getBalance( this.withdrawalsVaultAddress, - blockNumber - WITHDRAWAL_VAULT_BALANCE_BLOCK_INTERVAL, + blockNumber - ONCE_PER_100_BLOCKS, ) if (E.isLeft(prevWithdrawalVaultBalance)) { return [ @@ -171,13 +167,13 @@ export class VaultSrv { .minus(prevWithdrawalVaultBalance.right) .plus(withdrawalsWithdrawn) - if (withdrawalVaultBalanceDiff.gte(WITHDRAWAL_VAULT_BALANCE_DIFF_INFO)) { + if (withdrawalVaultBalanceDiff.gte(ETH_1K)) { out.push( Finding.fromObject({ - name: 'đŸ’ĩ Withdrawal Vault Balance significant change', + name: 'ℹī¸ Withdrawal Vault Balance significant change', description: `Withdrawal Vault Balance has increased by ${toEthString( withdrawalVaultBalanceDiff, - )} during the last ${WITHDRAWAL_VAULT_BALANCE_BLOCK_INTERVAL} blocks`, + )} during the last ${ONCE_PER_100_BLOCKS} blocks`, alertId: 'WITHDRAWAL-VAULT-BALANCE-CHANGE', type: FindingType.Info, severity: FindingSeverity.Info, @@ -204,10 +200,10 @@ export class VaultSrv { const elVaultBalanceDiff = elVaultBalance.right.minus(prevBalance) const out: Finding[] = [] - if (elVaultBalanceDiff.gte(EL_VAULT_BALANCE_DIFF_INFO)) { + if (elVaultBalanceDiff.gte(ETH_50)) { out.push( Finding.fromObject({ - name: 'đŸ’ĩ EL Vault Balance significant change', + name: 'ℹī¸ EL Vault Balance significant change', description: `EL Vault Balance has increased by ${toEthString(elVaultBalanceDiff)}`, alertId: 'EL-VAULT-BALANCE-CHANGE', type: FindingType.Info, @@ -329,26 +325,35 @@ export class VaultSrv { return out } - public handleTransaction(txEvent: TransactionEvent): Finding[] { + public handleTransaction(txEvent: TransactionDto): Finding[] { return this.handleBurnerSharesTx(txEvent) } - public handleBurnerSharesTx(txEvent: TransactionEvent): Finding[] { - const events = txEvent - .filterLog(TRANSFER_SHARES_EVENT, this.lidoStethAddress) - .filter((e) => e.args.from.toLowerCase() === this.burnerAddress.toLowerCase()) - + public handleBurnerSharesTx(txEvent: TransactionDto): Finding[] { + const iface = new ethers.utils.Interface([TRANSFER_SHARES_EVENT]) const out: Finding[] = [] - for (const event of events) { - out.push( - Finding.fromObject({ - name: '🚨 Burner shares transfer', - description: `Burner shares transfer to ${event.args.to} has occurred`, - alertId: 'BURNER-SHARES-TRANSFER', - severity: FindingSeverity.High, - type: FindingType.Suspicious, - }), - ) + for (const log of txEvent.logs) { + if (log.address.toLowerCase() !== this.lidoStethAddress.toLowerCase()) { + continue + } + + try { + const event = iface.parseLog(log) + if (event.args.from.toLowerCase() === this.burnerAddress.toLowerCase()) { + out.push( + Finding.fromObject({ + name: '🚨 Burner shares transfer', + description: `Burner shares transfer to ${event.args.to} has occurred`, + alertId: 'BURNER-SHARES-TRANSFER', + severity: FindingSeverity.High, + type: FindingType.Suspicious, + }), + ) + } + } catch (e) { + // Only one from eventsOfNotice could be correct + // Others - skipping + } } return out diff --git a/ethereum-steth/src/services/vault/Vaults.functional.spec.ts b/ethereum-steth/src/services/vault/Vaults.functional.spec.ts index 8dcac617..af92ceec 100644 --- a/ethereum-steth/src/services/vault/Vaults.functional.spec.ts +++ b/ethereum-steth/src/services/vault/Vaults.functional.spec.ts @@ -35,7 +35,7 @@ describe('Vaults.srv functional tests', () => { const expected = Finding.fromObject({ alertId: 'EL-VAULT-BALANCE-CHANGE', description: `EL Vault Balance has increased by 689.017 ETH`, - name: 'đŸ’ĩ EL Vault Balance significant change', + name: 'ℹī¸ EL Vault Balance significant change', severity: FindingSeverity.Info, type: FindingType.Info, }) diff --git a/ethereum-steth/src/services/withdrawals/Withdrawals.functional.spec.ts b/ethereum-steth/src/services/withdrawals/Withdrawals.functional.spec.ts index f02c3c66..bc218d13 100644 --- a/ethereum-steth/src/services/withdrawals/Withdrawals.functional.spec.ts +++ b/ethereum-steth/src/services/withdrawals/Withdrawals.functional.spec.ts @@ -1,21 +1,11 @@ -import { - Finding, - FindingSeverity, - FindingType, - ethers, - filterLog, - getEthersProvider, - Network, - Transaction, -} from 'forta-agent' +import { Finding, FindingSeverity, FindingType, filterLog, getEthersProvider } from 'forta-agent' import { App, Container } from '../../app' -import { createTransactionEvent } from '../../utils/forta' import { WITHDRAWAL_QUEUE_WITHDRAWAL_CLAIMED_EVENT } from '../../utils/events/withdrawals_events' import { Address } from '../../utils/constants' import BigNumber from 'bignumber.js' import { WithdrawalsRepo } from './Withdrawals.repo' import * as E from 'fp-ts/Either' -import { BlockDto } from '../../entity/events' +import { BlockDto, TransactionDto } from '../../entity/events' const TEST_TIMEOUT = 120_000 // ms @@ -105,19 +95,24 @@ describe('Withdrawals.srv functional tests', () => { async () => { const txHash = '0xdf4c31a9886fc4269bfef601c6d0a287633d516d16d61d5b62b9341e704eb52c' - const receipt = await ethProvider.send('eth_getTransactionReceipt', [txHash]) - const block = await ethProvider.send('eth_getBlockByNumber', [ - ethers.utils.hexValue(parseInt(receipt.blockNumber)), - true, - ]) - const transaction = block.transactions.find((tx: Transaction) => tx.hash.toLowerCase() === txHash)! - const txEvent = createTransactionEvent(transaction, block, Network.MAINNET, [], receipt.logs) + const trx = await ethProvider.getTransaction(txHash) + const receipt = await trx.wait() + + const transactionDto: TransactionDto = { + logs: receipt.logs, + to: trx.to ? trx.to : null, + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + block: { + timestamp: trx.timestamp ? trx.timestamp : new Date().getTime(), + number: trx.blockNumber ? trx.blockNumber : 1, + }, + } const initErr = await app.WithdrawalsSrv.initialize(19113262) if (initErr !== null) { fail(initErr.message) } - const result = await app.WithdrawalsSrv.handleWithdrawalClaimed(txEvent) + const result = await app.WithdrawalsSrv.handleWithdrawalClaimed(transactionDto) expect(result.length).toEqual(0) const requestID = 24651 @@ -127,7 +122,7 @@ describe('Withdrawals.srv functional tests', () => { } const claimedEvents = filterLog( - txEvent.logs, + transactionDto.logs, WITHDRAWAL_QUEUE_WITHDRAWAL_CLAIMED_EVENT, Address.WITHDRAWALS_QUEUE_ADDRESS, ) diff --git a/ethereum-steth/src/services/withdrawals/Withdrawals.srv.ts b/ethereum-steth/src/services/withdrawals/Withdrawals.srv.ts index 5527734c..60bf5bd8 100644 --- a/ethereum-steth/src/services/withdrawals/Withdrawals.srv.ts +++ b/ethereum-steth/src/services/withdrawals/Withdrawals.srv.ts @@ -4,7 +4,6 @@ import { filterLog, Finding, FindingSeverity, FindingType } from 'forta-agent' import * as E from 'fp-ts/Either' import { ETH_DECIMALS } from '../../utils/constants' import { elapsedTime, formatDelay } from '../../utils/time' -import { TransactionEvent } from 'forta-agent/dist/sdk/transaction.event' import { LIDO_TOKEN_REBASED_EVENT, WITHDRAWAL_QUEUE_WITHDRAWAL_CLAIMED_EVENT, @@ -14,7 +13,7 @@ import { WITHDRAWALS_BUNKER_MODE_ENABLED_EVENT, } from '../../utils/events/withdrawals_events' import { etherscanAddress, etherscanNft } from '../../utils/string' -import { EventOfNotice } from '../../entity/events' +import { EventOfNotice, handleEventsOfNotice, TransactionDto } from '../../entity/events' import { Logger } from 'winston' import { WithdrawalRequest } from '../../entity/withdrawal_request' import { WithdrawalsRepo } from './Withdrawals.repo' @@ -31,10 +30,10 @@ const THRESHOLD_OF_100K_STETH = new BigNumber(100_000) const THRESHOLD_OF_5K_STETH = new BigNumber(5_000) const THRESHOLD_OF_150K_STETH = new BigNumber(150_000) -const BLOCK_CHECK_INTERVAL = 100 // 20 minutes (100 blocks x 12 sec = 12000 seconds = 20 minutes) -const QUEUE_ON_PAR_STAKE_LIMIT_RATE_THRESHOLD = 0.95 -const UNCLAIMED_REQUESTS_SIZE_RATE_THRESHOLD = 0.2 -const CLAIMED_AMOUNT_MORE_THAN_REQUESTED_MAX_ALERTS_PER_HOUR = 5 +const ONCE_PER_100_BLOCKS = 100 // 20 minutes (100 blocks x 12 sec = 12000 seconds = 20 minutes) +const THRESHOLD_095 = 0.95 +const THRESHOLD_02 = 0.2 +const CLAIMED_MORE_THEN_REQUESTED_5_ATTEMPT_PER_HOUR = 5 export class WithdrawalsSrv { private name = `WithdrawalsSrv` @@ -182,7 +181,7 @@ export class WithdrawalsSrv { } } - if (blockDto.number % BLOCK_CHECK_INTERVAL === 0) { + if (blockDto.number % ONCE_PER_100_BLOCKS === 0) { const [queueOnParWithStakeLimitFindings, unfinalizedRequestNumberFindings, unclaimedRequestsFindings] = await Promise.all([ this.handleQueueOnParWithStakeLimit(blockDto), @@ -202,16 +201,18 @@ export class WithdrawalsSrv { return findings } - public async handleTransaction(txEvent: TransactionEvent): Promise { + public async handleTransaction(txEvent: TransactionDto): Promise { const out: Finding[] = [] + const withdrawalsEventsFindings = handleEventsOfNotice(txEvent, this.withdrawalsEvents) const bunkerStatusFindings = this.handleBunkerStatus(txEvent) this.handleLastTokenRebase(txEvent) - const finalizedFindings = await this.handleWithdrawalFinalized(txEvent) - const withdrawalRequestFindings = await this.handleWithdrawalRequest(txEvent) - const withdrawalClaimedFindings = await this.handleWithdrawalClaimed(txEvent) - const withdrawalsEventsFindings = this.handleEventsOfNotice(txEvent, this.withdrawalsEvents) + const [finalizedFindings, withdrawalRequestFindings, withdrawalClaimedFindings] = await Promise.all([ + this.handleWithdrawalFinalized(txEvent), + this.handleWithdrawalRequest(txEvent), + this.handleWithdrawalClaimed(txEvent), + ]) out.push( ...bunkerStatusFindings, @@ -258,7 +259,7 @@ export class WithdrawalsSrv { } const spentStakeLimit = stakeLimitFullInfo.right.maxStakeLimit.minus(stakeLimitFullInfo.right.currentStakeLimit) const spentStakeLimitRate = spentStakeLimit.div(stakeLimitFullInfo.right.maxStakeLimit) - const thresholdStakeLimit = stakeLimitFullInfo.right.maxStakeLimit.times(QUEUE_ON_PAR_STAKE_LIMIT_RATE_THRESHOLD) + const thresholdStakeLimit = stakeLimitFullInfo.right.maxStakeLimit.times(THRESHOLD_095) const findings: Finding[] = [] if (spentStakeLimit.gte(thresholdStakeLimit) && unfinalizedStETH.right.gte(thresholdStakeLimit)) { @@ -449,7 +450,7 @@ export class WithdrawalsSrv { const totalFinalizedSize = claimedStETH.plus(unclaimedStETH) const unclaimedSizeRate = unclaimedStETH.div(totalFinalizedSize) if (currentBlockTimestamp - this.cache.getLastUnclaimedRequestsAlertTimestamp() > ONE_DAY) { - if (unclaimedSizeRate.gte(UNCLAIMED_REQUESTS_SIZE_RATE_THRESHOLD)) { + if (unclaimedSizeRate.gte(THRESHOLD_02)) { out.push( Finding.fromObject({ name: `ℹī¸ Withdrawals: ${unclaimedSizeRate.times(100).toFixed(2)}% of finalized requests are unclaimed`, @@ -497,7 +498,7 @@ export class WithdrawalsSrv { return out } - public handleBunkerStatus(txEvent: TransactionEvent): Finding[] { + public handleBunkerStatus(txEvent: TransactionDto): Finding[] { const [bunkerEnabled] = filterLog(txEvent.logs, WITHDRAWALS_BUNKER_MODE_ENABLED_EVENT, this.withdrawalsQueueAddress) const out: Finding[] = [] @@ -544,7 +545,7 @@ export class WithdrawalsSrv { return out } - public async handleWithdrawalRequest(txEvent: TransactionEvent): Promise { + public async handleWithdrawalRequest(txEvent: TransactionDto): Promise { const requestEvents = filterLog( txEvent.logs, WITHDRAWAL_QUEUE_WITHDRAWAL_REQUESTED_EVENT, @@ -627,7 +628,7 @@ export class WithdrawalsSrv { return out } - public handleLastTokenRebase(txEvent: TransactionEvent): void { + public handleLastTokenRebase(txEvent: TransactionDto): void { const [rebaseEvent] = filterLog(txEvent.logs, LIDO_TOKEN_REBASED_EVENT, this.lidoStethAddress) if (!rebaseEvent) { return @@ -637,7 +638,7 @@ export class WithdrawalsSrv { this.cache.setAmountOfRequestedStETHSinceLastTokenRebase(new BigNumber(0)) } - public async handleWithdrawalFinalized(txEvent: TransactionEvent): Promise { + public async handleWithdrawalFinalized(txEvent: TransactionDto): Promise { const [withdrawalEvent] = filterLog( txEvent.logs, WITHDRAWAL_QUEUE_WITHDRAWALS_FINALIZED_EVENT, @@ -677,7 +678,7 @@ export class WithdrawalsSrv { return [] } - public async handleWithdrawalClaimed(txEvent: TransactionEvent): Promise { + public async handleWithdrawalClaimed(txEvent: TransactionDto): Promise { const claimedEvents = filterLog( txEvent.logs, WITHDRAWAL_QUEUE_WITHDRAWAL_CLAIMED_EVENT, @@ -692,10 +693,7 @@ export class WithdrawalsSrv { this.cache.setClaimedAmountMoreThanRequestedAlertsCount(0) } - if ( - this.cache.getClaimedAmountMoreThanRequestedAlertsCount() >= - CLAIMED_AMOUNT_MORE_THAN_REQUESTED_MAX_ALERTS_PER_HOUR - ) { + if (this.cache.getClaimedAmountMoreThanRequestedAlertsCount() >= CLAIMED_MORE_THEN_REQUESTED_5_ATTEMPT_PER_HOUR) { return [] } @@ -753,28 +751,4 @@ export class WithdrawalsSrv { return out } - - public handleEventsOfNotice(txEvent: TransactionEvent, eventsOfNotice: EventOfNotice[]): Finding[] { - const out: Finding[] = [] - for (const eventInfo of eventsOfNotice) { - if (eventInfo.address in txEvent.addresses) { - const filteredEvents = filterLog(txEvent.logs, eventInfo.event, eventInfo.address) - - for (const filteredEvent of filteredEvents) { - out.push( - Finding.fromObject({ - name: eventInfo.name, - description: eventInfo.description(filteredEvent.args), - alertId: eventInfo.alertId, - severity: eventInfo.severity, - type: eventInfo.type, - metadata: { args: String(filteredEvent.args) }, - }), - ) - } - } - } - - return out - } } diff --git a/ethereum-steth/src/utils/constants.ts b/ethereum-steth/src/utils/constants.ts index f17d6009..e9333a8f 100644 --- a/ethereum-steth/src/utils/constants.ts +++ b/ethereum-steth/src/utils/constants.ts @@ -38,12 +38,16 @@ const INSURANCE_FUND_ADDRESS: string = '0x8b3f33234abd88493c0cd28de33d583b70bede const DAI_ADDRESS: string = '0x6b175474e89094c44da98b954eedeac495271d0f' const USDT_ADDRESS: string = '0xdac17f958d2ee523a2206206994597c13d831ec7' const USDC_ADDRESS: string = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' -const GATE_SEAL_DEFAULT_ADDRESS: string = '0x1ad5cb2955940f998081c1ef5f5f00875431aa90' +const GATE_SEAL_DEFAULT_ADDRESS: string = '0x79243345eDbe01A7E42EDfF5900156700d22611c' const EXITBUS_ORACLE_ADDRESS: string = '0x0de4ea0184c2ad0baca7183356aea5b8d5bf5c6e' const GATE_SEAL_FACTORY_ADDRESS: string = '0x6c82877cac5a7a739f16ca0a89c0a328b8764a24' const WITHDRAWALS_VAULT_ADDRESS: string = '0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f' const EL_REWARDS_VAULT_ADDRESS: string = '0x388c818ca8b9251b393131c08a736a67ccb19297' +// const for backward functional test capability +// https://github.com/lidofinance/alerting-forta/issues/533 +export const GATE_SEAL_DEFAULT_ADDRESS_BEFORE_26_APR_2024: string = '0x1ad5cb2955940f998081c1ef5f5f00875431aa90' + const KNOWN_ERC20 = new Map([ [LIDO_STETH_ADDRESS, { decimals: 18, name: 'stETH' }], [WSTETH_ADDRESS, { decimals: 18, name: 'wstETH' }], diff --git a/ethereum-steth/src/utils/events/burner_events.ts b/ethereum-steth/src/utils/events/burner_events.ts index 7a508224..51d18afb 100644 --- a/ethereum-steth/src/utils/events/burner_events.ts +++ b/ethereum-steth/src/utils/events/burner_events.ts @@ -7,7 +7,7 @@ export function getBurnerEvents(BURNER_ADDRESS: string): EventOfNotice[] { return [ { address: BURNER_ADDRESS, - event: 'event ERC20Recovered(address indexed requestedBy, address indexed token,uint256 amount)', + abi: 'event ERC20Recovered(address indexed requestedBy, address indexed token,uint256 amount)', alertId: 'LIDO-BURNER-ERC20-RECOVERED', name: 'ℹī¸ Lido Burner: ERC20 recovered', description: (args: Result) => @@ -20,7 +20,7 @@ export function getBurnerEvents(BURNER_ADDRESS: string): EventOfNotice[] { }, { address: BURNER_ADDRESS, - event: 'event ERC721Recovered(address indexed requestedBy, address indexed token, uint256 tokenId)', + abi: 'event ERC721Recovered(address indexed requestedBy, address indexed token, uint256 tokenId)', alertId: 'LIDO-BURNER-ERC721-RECOVERED', name: 'ℹī¸ Lido Burner: ERC721 recovered', description: (args: Result) => diff --git a/ethereum-steth/src/utils/events/deposit_security_events.ts b/ethereum-steth/src/utils/events/deposit_security_events.ts index c2bf6d54..89f3767e 100644 --- a/ethereum-steth/src/utils/events/deposit_security_events.ts +++ b/ethereum-steth/src/utils/events/deposit_security_events.ts @@ -7,7 +7,7 @@ export function getDepositSecurityEvents(DEPOSIT_SECURITY_ADDRESS: string): Even return [ { address: DEPOSIT_SECURITY_ADDRESS, - event: 'event DepositsPaused(address indexed guardian, uint24 indexed stakingModuleId)', + abi: 'event DepositsPaused(address indexed guardian, uint24 indexed stakingModuleId)', alertId: 'LIDO-DEPOSITS-PAUSED', name: '🚨 Deposit Security: Deposits paused', description: (args: Result) => @@ -17,7 +17,7 @@ export function getDepositSecurityEvents(DEPOSIT_SECURITY_ADDRESS: string): Even }, { address: DEPOSIT_SECURITY_ADDRESS, - event: 'event GuardianQuorumChanged(uint256 newValue)', + abi: 'event GuardianQuorumChanged(uint256 newValue)', alertId: 'LIDO-DEPOSITOR-GUARDIAN-QUORUM-CHANGED', name: '🚨 Deposit Security: Guardian quorum changed', description: (args: Result) => `New quorum size ${args.newValue}`, @@ -26,7 +26,7 @@ export function getDepositSecurityEvents(DEPOSIT_SECURITY_ADDRESS: string): Even }, { address: DEPOSIT_SECURITY_ADDRESS, - event: 'event OwnerChanged(address newValue)', + abi: 'event OwnerChanged(address newValue)', alertId: 'LIDO-DEPOSITOR-OWNER-CHANGED', name: '🚨 Deposit Security: Owner changed', description: (args: Result) => `New owner ${etherscanAddress(args.newValue)}`, @@ -35,7 +35,7 @@ export function getDepositSecurityEvents(DEPOSIT_SECURITY_ADDRESS: string): Even }, { address: DEPOSIT_SECURITY_ADDRESS, - event: 'event DepositsUnpaused(uint24 indexed stakingModuleId)', + abi: 'event DepositsUnpaused(uint24 indexed stakingModuleId)', alertId: 'LIDO-DEPOSITS-UNPAUSED', name: '⚠ī¸ Deposit Security: Deposits resumed', description: (args: Result) => `Deposits were resumed for ${args.stakingModuleId} staking module`, @@ -44,7 +44,7 @@ export function getDepositSecurityEvents(DEPOSIT_SECURITY_ADDRESS: string): Even }, { address: DEPOSIT_SECURITY_ADDRESS, - event: 'event GuardianAdded(address guardian)', + abi: 'event GuardianAdded(address guardian)', alertId: 'LIDO-DEPOSITOR-GUARDIAN-ADDED', name: '⚠ī¸ Deposit Security: Guardian added', description: (args: Result) => `New guardian added ${etherscanAddress(args.guardian)}`, @@ -53,7 +53,7 @@ export function getDepositSecurityEvents(DEPOSIT_SECURITY_ADDRESS: string): Even }, { address: DEPOSIT_SECURITY_ADDRESS, - event: 'event GuardianRemoved(address guardian)', + abi: 'event GuardianRemoved(address guardian)', alertId: 'LIDO-DEPOSITOR-GUARDIAN-REMOVED', name: '⚠ī¸ Deposit Security: Guardian removed', description: (args: Result) => `Guardian ${etherscanAddress(args.guardian)} was removed`, @@ -62,7 +62,7 @@ export function getDepositSecurityEvents(DEPOSIT_SECURITY_ADDRESS: string): Even }, { address: DEPOSIT_SECURITY_ADDRESS, - event: 'event MaxDepositsChanged(uint256 newValue)', + abi: 'event MaxDepositsChanged(uint256 newValue)', alertId: 'LIDO-DEPOSITOR-MAX-DEPOSITS-CHANGED', name: '⚠ī¸ Deposit Security: Max deposits changed', description: (args: Result) => `New value ${args.newValue}`, @@ -71,7 +71,7 @@ export function getDepositSecurityEvents(DEPOSIT_SECURITY_ADDRESS: string): Even }, { address: DEPOSIT_SECURITY_ADDRESS, - event: 'event MinDepositBlockDistanceChanged(uint256 newValue)', + abi: 'event MinDepositBlockDistanceChanged(uint256 newValue)', alertId: 'LIDO-DEPOSITOR-MIN-DEPOSITS-BLOCK-DISTANCE-CHANGED', name: '⚠ī¸ Deposit Security: Min deposit block distance changed', description: (args: Result) => `New value ${args.newValue}`, diff --git a/ethereum-steth/src/utils/events/insurance_fund_events.ts b/ethereum-steth/src/utils/events/insurance_fund_events.ts index a42c3a20..1a575b32 100644 --- a/ethereum-steth/src/utils/events/insurance_fund_events.ts +++ b/ethereum-steth/src/utils/events/insurance_fund_events.ts @@ -12,7 +12,7 @@ export function getInsuranceFundEvents( return [ { address: INSURANCE_FUND_ADDRESS, - event: 'event ERC20Transferred(address indexed _token, address indexed _recipient, uint256 _amount)', + abi: 'event ERC20Transferred(address indexed _token, address indexed _recipient, uint256 _amount)', alertId: 'INS-FUND-ERC20-TRANSFERRED', name: '🚨 Insurance fund: ERC20 transferred', description: (args: Result) => { @@ -29,7 +29,7 @@ export function getInsuranceFundEvents( }, { address: INSURANCE_FUND_ADDRESS, - event: 'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)', + abi: 'event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)', alertId: 'INS-FUND-OWNERSHIP-TRANSFERRED', name: '🚨 Insurance fund: Ownership transferred', description: (args: Result) => @@ -41,7 +41,7 @@ export function getInsuranceFundEvents( }, { address: INSURANCE_FUND_ADDRESS, - event: 'event EtherTransferred(address indexed _recipient, uint256 _amount)', + abi: 'event EtherTransferred(address indexed _recipient, uint256 _amount)', alertId: 'INS-FUND-ETH-TRANSFERRED', name: '⚠ī¸ Insurance fund: ETH transferred', description: (args: Result) => @@ -53,8 +53,7 @@ export function getInsuranceFundEvents( }, { address: INSURANCE_FUND_ADDRESS, - event: - 'event ERC721Transferred(address indexed _token, address indexed _recipient, uint256 _tokenId, bytes _data)', + abi: 'event ERC721Transferred(address indexed _token, address indexed _recipient, uint256 _tokenId, bytes _data)', alertId: 'INS-FUND-ERC721-TRANSFERRED', name: '⚠ī¸ Insurance fund: ERC721 transferred', description: (args: Result) => @@ -66,8 +65,7 @@ export function getInsuranceFundEvents( }, { address: INSURANCE_FUND_ADDRESS, - event: - 'event ERC1155Transferred(address indexed _token, address indexed _recipient, uint256 _tokenId, uint256 _amount, bytes _data)', + abi: 'event ERC1155Transferred(address indexed _token, address indexed _recipient, uint256 _tokenId, uint256 _amount, bytes _data)', alertId: 'INS-FUND-ERC1155-TRANSFERRED', name: '⚠ī¸ Insurance fund: ERC1155 transferred', description: (args: Result) => diff --git a/ethereum-steth/src/utils/events/lido_events.ts b/ethereum-steth/src/utils/events/lido_events.ts index 433f80a5..f0f6d5b5 100644 --- a/ethereum-steth/src/utils/events/lido_events.ts +++ b/ethereum-steth/src/utils/events/lido_events.ts @@ -9,7 +9,7 @@ export function getLidoEvents(LIDO_STETH_ADDRESS: string): EventOfNotice[] { return [ { address: LIDO_STETH_ADDRESS, - event: 'event Stopped()', + abi: 'event Stopped()', alertId: 'LIDO-STOPPED', name: '🚨🚨🚨 Lido: Stopped 🚨🚨🚨', description: () => `Lido DAO contract was stopped`, @@ -18,7 +18,7 @@ export function getLidoEvents(LIDO_STETH_ADDRESS: string): EventOfNotice[] { }, { address: LIDO_STETH_ADDRESS, - event: 'event StakingLimitRemoved()', + abi: 'event StakingLimitRemoved()', alertId: 'LIDO-STAKING-LIMIT-REMOVED', name: '🚨 Lido: Staking limit removed', description: () => `Staking limit was removed`, @@ -27,7 +27,7 @@ export function getLidoEvents(LIDO_STETH_ADDRESS: string): EventOfNotice[] { }, { address: LIDO_STETH_ADDRESS, - event: 'event LidoLocatorSet(address lidoLocator)', + abi: 'event LidoLocatorSet(address lidoLocator)', alertId: 'LIDO-LOCATOR-SET', name: '🚨 Lido: Locator set', description: (args: Result) => `Lido locator was set to: ${etherscanAddress(args.lidoLocator)}`, @@ -36,7 +36,7 @@ export function getLidoEvents(LIDO_STETH_ADDRESS: string): EventOfNotice[] { }, { address: LIDO_STETH_ADDRESS, - event: 'event StakingPaused()', + abi: 'event StakingPaused()', alertId: 'LIDO-STAKING-PAUSED', name: '🚨 Lido: Staking paused', description: () => `Staking was paused!`, @@ -45,7 +45,7 @@ export function getLidoEvents(LIDO_STETH_ADDRESS: string): EventOfNotice[] { }, { address: LIDO_STETH_ADDRESS, - event: 'event Resumed()', + abi: 'event Resumed()', alertId: 'LIDO-RESUMED', name: '⚠ī¸ Lido: Resumed', description: () => `Lido DAO contract was resumed`, @@ -54,7 +54,7 @@ export function getLidoEvents(LIDO_STETH_ADDRESS: string): EventOfNotice[] { }, { address: LIDO_STETH_ADDRESS, - event: 'event StakingResumed()', + abi: 'event StakingResumed()', alertId: 'LIDO-STAKING-RESUMED', name: '⚠ī¸ Lido: Staking resumed', description: () => `Staking was resumed!`, @@ -63,7 +63,7 @@ export function getLidoEvents(LIDO_STETH_ADDRESS: string): EventOfNotice[] { }, { address: LIDO_STETH_ADDRESS, - event: 'event StakingLimitSet(uint256 maxStakeLimit, uint256 stakeLimitIncreasePerBlock)', + abi: 'event StakingLimitSet(uint256 maxStakeLimit, uint256 stakeLimitIncreasePerBlock)', alertId: 'LIDO-STAKING-LIMIT-SET', name: '⚠ī¸ Lido: Staking limit set', description: (args: Result) => @@ -75,7 +75,7 @@ export function getLidoEvents(LIDO_STETH_ADDRESS: string): EventOfNotice[] { }, { address: LIDO_STETH_ADDRESS, - event: 'event RecoverToVault(address vault, address token, uint256 amount)', + abi: 'event RecoverToVault(address vault, address token, uint256 amount)', alertId: 'LIDO-RECOVER-TO-VAULT', name: '⚠ī¸ Lido: Funds recovered to vault', description: (args: Result) => @@ -88,7 +88,7 @@ export function getLidoEvents(LIDO_STETH_ADDRESS: string): EventOfNotice[] { }, { address: LIDO_STETH_ADDRESS, - event: 'event ContractVersionSet(uint256 version)', + abi: 'event ContractVersionSet(uint256 version)', alertId: 'LIDO-CONTRACT-VERSION-SET', name: '⚠ī¸ Lido: Contract version set', description: (args: Result) => `Contract version set:\n` + `Version: ${args.version}`, @@ -97,8 +97,7 @@ export function getLidoEvents(LIDO_STETH_ADDRESS: string): EventOfNotice[] { }, { address: LIDO_STETH_ADDRESS, - event: - 'event TokenRebased(uint256 indexed reportTimestamp, uint256 timeElapsed, uint256 preTotalShares, uint256 preTotalEther, uint256 postTotalShares, uint256 postTotalEther, uint256 sharesMintedAsFees)', + abi: 'event TokenRebased(uint256 indexed reportTimestamp, uint256 timeElapsed, uint256 preTotalShares, uint256 preTotalEther, uint256 postTotalShares, uint256 postTotalEther, uint256 sharesMintedAsFees)', alertId: alertId_token_rebased, name: 'ℹī¸ Lido: Token rebased', description: (args: Result) => `reportTimestamp: ${args.reportTimestamp}`, diff --git a/ethereum-steth/src/utils/events/withdrawals_events.ts b/ethereum-steth/src/utils/events/withdrawals_events.ts index 7df57a06..eb71f9f7 100644 --- a/ethereum-steth/src/utils/events/withdrawals_events.ts +++ b/ethereum-steth/src/utils/events/withdrawals_events.ts @@ -23,7 +23,7 @@ export function getWithdrawalsEvents(WITHDRAWAL_QUEUE_ADDRESS: string): EventOfN return [ { address: WITHDRAWAL_QUEUE_ADDRESS, - event: 'event Paused(uint256 duration)', + abi: 'event Paused(uint256 duration)', alertId: 'WITHDRAWALS-PAUSED', name: '🚨 Withdrawals: contract was paused', description: (args: Result) => `For ${new BigNumber(args.duration).div(60 * 60)} hours`, @@ -32,7 +32,7 @@ export function getWithdrawalsEvents(WITHDRAWAL_QUEUE_ADDRESS: string): EventOfN }, { address: WITHDRAWAL_QUEUE_ADDRESS, - event: 'event Resumed()', + abi: 'event Resumed()', alertId: 'WITHDRAWALS-UNPAUSED', name: '⚠ī¸ Withdrawals: contract was unpaused', description: () => 'Contract was resumed', diff --git a/ethereum-steth/src/utils/forta.ts b/ethereum-steth/src/utils/forta.ts deleted file mode 100644 index 297ba4ac..00000000 --- a/ethereum-steth/src/utils/forta.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { EventType, Trace } from 'forta-agent' -import { formatAddress, isZeroAddress } from 'forta-agent/dist/cli/utils' -import { TransactionEvent } from 'forta-agent/dist/sdk/transaction.event' -import { getContractAddress } from 'ethers/lib/utils' -import { JsonRpcBlock, JsonRpcTransaction } from 'forta-agent/dist/cli/utils/get.block.with.transactions' -import { JsonRpcLog } from 'forta-agent/dist/cli/utils/get.transaction.receipt' - -export function createTransactionEvent( - transaction: JsonRpcTransaction, - block: JsonRpcBlock, - networkId: number, - traces: Trace[] = [], - logs: JsonRpcLog[] = [], -): TransactionEvent { - const tx = { - hash: transaction.hash, - from: formatAddress(transaction.from), - to: transaction.to ? formatAddress(transaction.to) : null, - nonce: parseInt(transaction.nonce), - gas: transaction.gas, - gasPrice: transaction.gasPrice, - value: transaction.value, - data: transaction.input, - r: transaction.r, - s: transaction.s, - v: transaction.v, - } - const addresses = { - [tx.from]: true, - } - if (tx.to) { - addresses[tx.to] = true - } - - const blok = { - hash: block.hash, - number: parseInt(block.number), - timestamp: parseInt(block.timestamp), - } - - const trcs: Trace[] = [] - traces.forEach((trace) => { - if (trace.action.address) { - addresses[formatAddress(trace.action.address)] = true - } - if (trace.action.refundAddress) { - addresses[formatAddress(trace.action.refundAddress)] = true - } - addresses[formatAddress(trace.action.to)] = true - addresses[formatAddress(trace.action.from)] = true - - trcs.push({ - action: { - callType: trace.action.callType, - to: formatAddress(trace.action.to), - input: trace.action.input, - from: formatAddress(trace.action.from), - value: trace.action.value, - init: trace.action.init, - address: formatAddress(trace.action.address), - balance: trace.action.balance, - refundAddress: formatAddress(trace.action.refundAddress), - }, - blockHash: trace.blockHash, - blockNumber: trace.blockNumber, - result: { - gasUsed: trace.result?.gasUsed, - address: trace.result?.address, - code: trace.result?.code, - output: trace.result?.output, - }, - subtraces: trace.subtraces, - traceAddress: trace.traceAddress, - transactionHash: trace.transactionHash, - transactionPosition: trace.transactionPosition, - type: trace.type, - error: trace.error, - }) - }) - - const lgs = logs.map((log) => ({ - address: formatAddress(log.address), - topics: log.topics, - data: log.data, - logIndex: parseInt(log.logIndex), - blockNumber: parseInt(log.blockNumber), - blockHash: log.blockHash, - transactionIndex: parseInt(log.transactionIndex), - transactionHash: log.transactionHash, - removed: log.removed, - })) - lgs.forEach((log) => (addresses[log.address] = true)) - - let contractAddress = null - if (isZeroAddress(transaction.to)) { - contractAddress = formatAddress(getContractAddress({ from: transaction.from, nonce: transaction.nonce })) - } - - return new TransactionEvent(EventType.BLOCK, networkId, tx, trcs, addresses, blok, lgs, contractAddress) -}