diff --git a/.github/workflows/_tests.yml b/.github/workflows/_tests.yml index faacdcd7..e290412b 100644 --- a/.github/workflows/_tests.yml +++ b/.github/workflows/_tests.yml @@ -29,7 +29,7 @@ jobs: - name: Install node uses: actions/setup-node@v3 with: - node-version-file: "${{ inputs.path }}/.nvmrc" + node-version: 20.10.0 cache: yarn cache-dependency-path: "**/yarn.lock" @@ -42,7 +42,6 @@ jobs: $HOME/.forta/forta.config.json env: URL: ${{ secrets.ETHEREUM_RPC_URL }} - if: ${{ steps.check_scripts.outputs.has_e2e == 'true' }} - name: Run unit tests run: yarn test diff --git a/.github/workflows/test-l2-bridge-linea.yml b/.github/workflows/test-l2-bridge-linea.yml new file mode 100644 index 00000000..e08375d1 --- /dev/null +++ b/.github/workflows/test-l2-bridge-linea.yml @@ -0,0 +1,15 @@ +--- +name: Tests @ l2-bridge-linea + +on: + workflow_dispatch: + pull_request: + paths: + - "l2-bridge-linea/**" + +jobs: + tests: + uses: ./.github/workflows/_tests.yml + with: + path: ./l2-bridge-linea + secrets: inherit diff --git a/l2-bridge-balance/src/agent-balance.ts b/l2-bridge-balance/src/agent-balance.ts index 46f19ea0..f7987d15 100644 --- a/l2-bridge-balance/src/agent-balance.ts +++ b/l2-bridge-balance/src/agent-balance.ts @@ -52,7 +52,7 @@ export async function handleBlock(blockEvent: BlockEvent) { findings, BRIDGE_PARAMS_WSTETH.Mantle, ), - handleBridgeBalanceWstETH(blockEvent, findings, BRIDGE_PARAMS_WSTETH.Linea), + handleBridgeBalanceLDO(blockEvent, findings, BRIDGE_PARAMS_LDO.Arbitrum), handleBridgeBalanceLDO(blockEvent, findings, BRIDGE_PARAMS_LDO.Optimism), ]); diff --git a/l2-bridge-balance/src/config/bot-config.json b/l2-bridge-balance/src/config/bot-config.json index 90766940..d1f67470 100644 --- a/l2-bridge-balance/src/config/bot-config.json +++ b/l2-bridge-balance/src/config/bot-config.json @@ -13,8 +13,5 @@ }, "Mantle": { "RpcUrl": "https://mantle.publicnode.com" - }, - "Linea": { - "RpcUrl": "https://linea.blockpi.network/v1/rpc/public" } } diff --git a/l2-bridge-balance/src/constants.ts b/l2-bridge-balance/src/constants.ts index 84cd6169..0abafcd4 100644 --- a/l2-bridge-balance/src/constants.ts +++ b/l2-bridge-balance/src/constants.ts @@ -23,7 +23,6 @@ export interface BridgeParamsWstETH { Base: BridgeParamWstETH; ZkSync: BridgeParamWstETH; Mantle: BridgeParamWstETH; - Linea: BridgeParamWstETH; } export const BRIDGE_PARAMS_WSTETH: BridgeParamsWstETH = { @@ -57,12 +56,6 @@ export const BRIDGE_PARAMS_WSTETH: BridgeParamsWstETH = { wstEthBridged: "0x458ed78EB972a369799fb278c0243b25e5242A83", rpcUrl: config.Mantle.RpcUrl, }, - Linea: { - name: "Linea", - l1Gateway: "0x051f1d88f0af5763fb888ec4378b4d8b29ea3319", - wstEthBridged: "0xB5beDd42000b71FddE22D3eE8a79Bd49A568fC8F", - rpcUrl: config.Linea.RpcUrl, - }, }; export const LDO_ADDRESS = "0x5a98fcbea516cf06857215779fd812ca3bef1b32"; diff --git a/l2-bridge-linea/README.md b/l2-bridge-linea/README.md index 20e2376b..fa96efe5 100644 --- a/l2-bridge-linea/README.md +++ b/l2-bridge-linea/README.md @@ -16,9 +16,10 @@ the cache. ## Alerts 1. Bridge events - 1. 🚨 Linea L2 Bridge: Paused - 2. 🚨 Linea L2 Bridge: Implementation initialized - 3. ⚠️ Linea L2 Bridge: Unpaused + 1. 🚨🚨🚨 Linea bridge balance mismatch 🚨🚨🚨 + 2. 🚨 Linea L2 Bridge: Paused + 3. 🚨 Linea L2 Bridge: Implementation initialized + 4. ⚠️ Linea L2 Bridge: Unpaused 2. Gov Events 1. 🚨 Linea Gov Bridge: Ethereum Governance Executor Updated 2. 🚨 Linea Gov Bridge: Guardian Updated diff --git a/l2-bridge-linea/package.json b/l2-bridge-linea/package.json index 301701d9..94fd4d1d 100644 --- a/l2-bridge-linea/package.json +++ b/l2-bridge-linea/package.json @@ -47,8 +47,8 @@ "generate-types": "typechain --target=ethers-v5 --out-dir=./src/generated ./src/abi/*", "eslint:lint": "eslint ./src", "eslint:format": "eslint ./src --fix", - "prettier:check": "prettier --check .", - "prettier:format": "prettier --write .", + "prettier:check": "prettier --check ./src", + "prettier:format": "prettier --write ./src README.md", "lint": "yarn run prettier:check && yarn run eslint:lint", "format": "yarn run eslint:format && yarn run prettier:format", "postinstall": "yarn generate-types" diff --git a/l2-bridge-linea/src/abi/ERC20Short.json b/l2-bridge-linea/src/abi/ERC20Short.json new file mode 100644 index 00000000..81b8dd00 --- /dev/null +++ b/l2-bridge-linea/src/abi/ERC20Short.json @@ -0,0 +1,34 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/l2-bridge-linea/tests/agent.spec.ts b/l2-bridge-linea/src/agent.spec.ts similarity index 58% rename from l2-bridge-linea/tests/agent.spec.ts rename to l2-bridge-linea/src/agent.spec.ts index cc84871e..38130332 100644 --- a/l2-bridge-linea/tests/agent.spec.ts +++ b/l2-bridge-linea/src/agent.spec.ts @@ -1,8 +1,6 @@ -import { App } from '../src/app' +import { App } from './app' import * as E from 'fp-ts/Either' import { Finding } from 'forta-agent' -import { handleBlock, initialize } from '../src/agent' -import { etherBlockToFortaBlockEvent } from './utils' describe('agent-linea e2e tests', () => { test('should process handleBlocks', async () => { @@ -20,7 +18,7 @@ describe('agent-linea e2e tests', () => { throw monitorWithdrawalsInitResp } - const logs = await app.blockSrv.getLogs(blocksDto) + const logs = await app.blockSrv.getL2Logs(blocksDto) if (E.isLeft(logs)) { throw logs } @@ -30,9 +28,9 @@ describe('agent-linea e2e tests', () => { blockNumbers.push(block.number) } - const bridgeEventFindings = app.bridgeWatcher.handleLogs(logs.right) - const govEventFindings = app.govWatcher.handleLogs(logs.right) - const proxyAdminEventFindings = app.proxyEventWatcher.handleLogs(logs.right) + const bridgeEventFindings = app.bridgeWatcher.handleL2Logs(logs.right) + const govEventFindings = app.govWatcher.handleL2Logs(logs.right) + const proxyAdminEventFindings = app.proxyEventWatcher.handleL2Logs(logs.right) const monitorWithdrawalsFindings = app.monitorWithdrawals.handleBlocks(logs.right, blocksDto) const proxyWatcherFindings = await app.proxyWatcher.handleBlocks(blockNumbers) @@ -47,24 +45,4 @@ describe('agent-linea e2e tests', () => { expect(findings.length).toEqual(0) }, 120_000) - - test('should process app', async () => { - const init = initialize() - await init() - - const handleBlocks = handleBlock() - const app = await App.getInstance() - - const blocksDto = await app.LineaClient.fetchBlocks(2476530, 2476540) - const out: Finding[] = [] - for (const b of blocksDto) { - const blockEvent = etherBlockToFortaBlockEvent(b) - const findings = await handleBlocks(blockEvent) - - out.push(...findings) - } - - expect(out.length).toEqual(1) - expect(out[0].name).toEqual(`Agent launched`) - }, 120_000) }) diff --git a/l2-bridge-linea/src/agent.ts b/l2-bridge-linea/src/agent.ts index 68277c95..5490398a 100644 --- a/l2-bridge-linea/src/agent.ts +++ b/l2-bridge-linea/src/agent.ts @@ -83,34 +83,39 @@ export const handleBlock = (): HandleBlock => { findings.push(...findingsAsync) } - const blocksDto = await app.blockSrv.getBlocks() - if (E.isLeft(blocksDto)) { + const l2blocksDto = await app.blockSrv.getL2Blocks() + if (E.isLeft(l2blocksDto)) { isHandleBLockRunning = false - return [blocksDto.left] + return [l2blocksDto.left] } app.logger.info( - `ETH block ${blockEvent.blockNumber.toString()}. Fetched linea blocks from ${blocksDto.right[0].number} to ${ - blocksDto.right[blocksDto.right.length - 1].number - }. Total: ${blocksDto.right.length}`, + `ETH block ${blockEvent.block.number}. Fetched linea blocks from ${l2blocksDto.right[0].number} to ${ + l2blocksDto.right[l2blocksDto.right.length - 1].number + }. Total: ${l2blocksDto.right.length}`, ) - const logs = await app.blockSrv.getLogs(blocksDto.right) - if (E.isLeft(logs)) { + const l2logs = await app.blockSrv.getL2Logs(l2blocksDto.right) + if (E.isLeft(l2logs)) { isHandleBLockRunning = false - return [logs.left] + return [l2logs.left] } - const bridgeEventFindings = app.bridgeWatcher.handleLogs(logs.right) - const govEventFindings = app.govWatcher.handleLogs(logs.right) - const proxyAdminEventFindings = app.proxyEventWatcher.handleLogs(logs.right) - const monitorWithdrawalsFindings = app.monitorWithdrawals.handleBlocks(logs.right, blocksDto.right) + const bridgeEventFindings = app.bridgeWatcher.handleL2Logs(l2logs.right) + const govEventFindings = app.govWatcher.handleL2Logs(l2logs.right) + const proxyAdminEventFindings = app.proxyEventWatcher.handleL2Logs(l2logs.right) + const monitorWithdrawalsFindings = app.monitorWithdrawals.handleBlocks(l2logs.right, l2blocksDto.right) - const blockNumbers: Set = new Set() - for (const log of logs.right) { - blockNumbers.add(new BigNumber(log.blockNumber, 10).toNumber()) + const l2blockNumbersSet: Set = new Set() + for (const l2log of l2logs.right) { + l2blockNumbersSet.add(new BigNumber(l2log.blockNumber, 10).toNumber()) } - const proxyWatcherFindings = await app.proxyWatcher.handleBlocks(Array.from(blockNumbers)) + const l2blockNumbers = Array.from(l2blockNumbersSet) + + const [proxyWatcherFindings, bridgeBalanceFindings] = await Promise.all([ + app.proxyWatcher.handleBlocks(l2blockNumbers), + app.BridgeBalanceSrv.handleBlock(blockEvent.block.number, l2blockNumbers), + ]) findings.push( ...bridgeEventFindings, @@ -118,6 +123,7 @@ export const handleBlock = (): HandleBlock => { ...proxyAdminEventFindings, ...monitorWithdrawalsFindings, ...proxyWatcherFindings, + ...bridgeBalanceFindings, ) app.logger.info(elapsedTime('handleBlock', startTime) + '\n') diff --git a/l2-bridge-linea/src/app.ts b/l2-bridge-linea/src/app.ts index e7d8a193..3df2f134 100644 --- a/l2-bridge-linea/src/app.ts +++ b/l2-bridge-linea/src/app.ts @@ -1,26 +1,30 @@ -import { FortaGuardClient } from './clients/forta_guard_client' -import { ethers } from 'forta-agent' -import { ILineaProvider, LineaProvider } from './clients/linea_provider' +import { LineaProvider } from './clients/linea_provider' import { EventWatcher } from './services/event_watcher' import { getL2BridgeEvents } from './utils/events/bridge_events' import { getGovEvents } from './utils/events/gov_events' import { getProxyAdminEvents } from './utils/events/proxy_admin_events' import { ProxyContract } from './clients/proxy_contract_client' import { Address } from './utils/constants' -import { ProxyAdmin__factory, TokenBridge__factory } from './generated' +import { ERC20Short__factory, ProxyAdmin__factory, TokenBridge__factory } from './generated' import { BlockClient } from './clients/linea_block_client' import { ProxyWatcher } from './services/proxy_watcher' import { MonitorWithdrawals } from './services/monitor_withdrawals' import { FindingsRW } from './utils/mutex' import * as Winston from 'winston' import { Logger } from 'winston' +import { ETHProvider } from './clients/eth_provider_client' +import { BridgeBalanceSrv } from './services/bridge_balance' +import { getJsonRpcUrl } from 'forta-agent/dist/sdk/utils' +import { ethers } from 'ethers' export type Container = { - LineaClient: ILineaProvider + ethClient: ETHProvider + LineaClient: LineaProvider proxyWatcher: ProxyWatcher monitorWithdrawals: MonitorWithdrawals blockSrv: BlockClient bridgeWatcher: EventWatcher + BridgeBalanceSrv: BridgeBalanceSrv govWatcher: EventWatcher proxyEventWatcher: EventWatcher findingsRW: FindingsRW @@ -39,14 +43,17 @@ export class App { transports: [new Winston.transports.Console()], }) - const LineaRpcURL = FortaGuardClient.getSecret() + const LineaRpcURL = 'https://linea.drpc.org' const lineaNetworkID = 59144 - const nodeClient = new ethers.providers.JsonRpcProvider(LineaRpcURL, lineaNetworkID) + const drpcLineaProvider = new ethers.providers.JsonRpcProvider(LineaRpcURL, lineaNetworkID) const adr: Address = Address - const l2Bridge = TokenBridge__factory.connect(adr.LINEA_TOKEN_BRIDGE, nodeClient) - const lineaClient = new LineaProvider(nodeClient, l2Bridge, logger) + const l2Bridge = TokenBridge__factory.connect(adr.LINEA_TOKEN_BRIDGE, drpcLineaProvider) + + const bridgedWSthEthRunner = ERC20Short__factory.connect(adr.LINEA_WST_CUSTOM_BRIDGED_TOKEN, drpcLineaProvider) + + const lineaClient = new LineaProvider(drpcLineaProvider, l2Bridge, logger, bridgedWSthEthRunner) const bridgeEventWatcher = new EventWatcher( 'BridgeEventWatcher', @@ -65,13 +72,13 @@ export class App { adr.LINEA_L2_ERC20_TOKEN_BRIDGE.name, adr.LINEA_L2_ERC20_TOKEN_BRIDGE.hash, adr.ADMIN_OF_LINEA_L2_TOKEN_BRIDGE, - ProxyAdmin__factory.connect(adr.ADMIN_OF_LINEA_L2_TOKEN_BRIDGE, nodeClient), + ProxyAdmin__factory.connect(adr.ADMIN_OF_LINEA_L2_TOKEN_BRIDGE, drpcLineaProvider), ), new ProxyContract( adr.LINEA_WST_CUSTOM_BRIDGED.name, adr.LINEA_WST_CUSTOM_BRIDGED.hash, adr.LINEA_PROXY_ADMIN_FOR_WSTETH, - ProxyAdmin__factory.connect(adr.LINEA_PROXY_ADMIN_FOR_WSTETH, nodeClient), + ProxyAdmin__factory.connect(adr.LINEA_PROXY_ADMIN_FOR_WSTETH, drpcLineaProvider), ), ] @@ -80,8 +87,21 @@ export class App { const monitorWithdrawals = new MonitorWithdrawals(lineaClient, adr.LINEA_TOKEN_BRIDGE, logger) + const mainnet = 1 + const drpcUrl = 'https://eth.drpc.org/' + const ethProvider = new ethers.providers.FallbackProvider([ + new ethers.providers.JsonRpcProvider(getJsonRpcUrl(), mainnet), + new ethers.providers.JsonRpcProvider(drpcUrl, mainnet), + ]) + + const wSthEthRunner = ERC20Short__factory.connect(adr.WSTETH_ADDRESS, ethProvider) + const ethClient = new ETHProvider(logger, wSthEthRunner) + const bridgeBalanceSrv = new BridgeBalanceSrv(logger, ethClient, adr.LINEA_L1_TOKEN_BRIDGE, lineaClient) + App.instance = { + ethClient: ethClient, LineaClient: lineaClient, + BridgeBalanceSrv: bridgeBalanceSrv, proxyWatcher: proxyWorker, monitorWithdrawals: monitorWithdrawals, blockSrv: blockSrv, diff --git a/l2-bridge-linea/src/clients/eth_provider.spec.ts b/l2-bridge-linea/src/clients/eth_provider.spec.ts new file mode 100644 index 00000000..ee5a1327 --- /dev/null +++ b/l2-bridge-linea/src/clients/eth_provider.spec.ts @@ -0,0 +1,19 @@ +import { App } from '../app' +import * as E from 'fp-ts/Either' +import { Address, ETH_DECIMALS } from '../utils/constants' +import BigNumber from 'bignumber.js' + +describe('eth provider tests', () => { + test('getBalanceByBlockHash is 1774.48511061073977627 wsETH', async () => { + const app = await App.getInstance() + const adr = Address + + const blockNumber = 19_619_102 + const balance = await app.ethClient.getWstEthBalance(blockNumber, adr.LINEA_L1_TOKEN_BRIDGE) + if (E.isLeft(balance)) { + throw balance.left + } + + expect(balance.right.dividedBy(ETH_DECIMALS)).toEqual(new BigNumber('1774.48511061073977627')) + }, 120_000) +}) diff --git a/l2-bridge-linea/src/clients/eth_provider_client.ts b/l2-bridge-linea/src/clients/eth_provider_client.ts new file mode 100644 index 00000000..b0a214cf --- /dev/null +++ b/l2-bridge-linea/src/clients/eth_provider_client.ts @@ -0,0 +1,39 @@ +import { Logger } from 'winston' +import { IL1BridgeBalanceClient } from '../services/bridge_balance' +import * as E from 'fp-ts/Either' +import BigNumber from 'bignumber.js' +import { NetworkError } from '../utils/error' +import { retryAsync } from 'ts-retry' +import { ERC20Short as wStEthRunner } from '../generated' + +const DELAY_IN_500MS = 500 +const ATTEMPTS_5 = 5 + +export class ETHProvider implements IL1BridgeBalanceClient { + private readonly wStEthRunner: wStEthRunner + private readonly logger: Logger + + constructor(logger: Logger, wStEthRunner: wStEthRunner) { + this.wStEthRunner = wStEthRunner + this.logger = logger + } + + public async getWstEthBalance(l1blockNumber: number, address: string): Promise> { + try { + const out = await retryAsync( + async (): Promise => { + const [balance] = await this.wStEthRunner.functions.balanceOf(address, { + blockTag: l1blockNumber, + }) + + return balance.toString() + }, + { delay: DELAY_IN_500MS, maxTry: ATTEMPTS_5 }, + ) + + return E.right(new BigNumber(out)) + } catch (e) { + return E.left(new NetworkError(e, `Could not call wStEthRunner.functions.balanceOf`)) + } + } +} diff --git a/l2-bridge-linea/src/clients/forta_guard_client.ts b/l2-bridge-linea/src/clients/forta_guard_client.ts deleted file mode 100644 index 87ad9710..00000000 --- a/l2-bridge-linea/src/clients/forta_guard_client.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { fetchJwt } from 'forta-agent' -import { verifyJwt } from 'forta-agent/dist/sdk/jwt' -import * as E from 'fp-ts/Either' - -export class FortaGuardClient { - private fortaGuardURL: string = 'http://127.0.0.1/secret' - private readonly verifyJwt: boolean - - constructor(verifyJwt: boolean) { - this.verifyJwt = verifyJwt - } - - public static getSecret(): string { - return 'https://linea.drpc.org' - } - - public async getSecret(key: string): Promise> { - let token: string - try { - token = await fetchJwt({}) - } catch (e) { - return E.left(new Error(`Could not fetch jwt. cause ${e}`)) - } - - if (this.verifyJwt) { - try { - const isTokenOk = await verifyJwt(token) - if (!isTokenOk) { - return E.left(new Error(`Token verification failed`)) - } - } catch (e) { - return E.left(new Error(`Token verification failed`)) - } - } - - try { - const response = await fetch(this.fortaGuardURL + '/' + key, { - method: 'GET', - headers: { - Authorization: 'Bearer ' + token, - }, - }) - - if (!response.ok) { - return E.left(new Error(`Could not fetch secret, status: ${response.status}`)) - } - const out = await response.text() - - return E.right(out) - } catch (e) { - return E.left(new Error(`Could not fetch secret, cause: ${e}`)) - } - } -} diff --git a/l2-bridge-linea/src/clients/linea_block_client.ts b/l2-bridge-linea/src/clients/linea_block_client.ts index e07d4a86..7be1c460 100644 --- a/l2-bridge-linea/src/clients/linea_block_client.ts +++ b/l2-bridge-linea/src/clients/linea_block_client.ts @@ -1,11 +1,23 @@ import { BlockDto } from 'src/entity/blockDto' -import { ILineaProvider } from './linea_provider' -import { Log } from '@ethersproject/abstract-provider' +import { Block, Log, TransactionResponse } from '@ethersproject/abstract-provider' import { Finding } from 'forta-agent' import * as E from 'fp-ts/Either' import { Logger } from 'winston' import { elapsedTime } from '../utils/time' import { networkAlert } from '../utils/finding.helpers' +import { NetworkError } from '../utils/error' + +export abstract class ILineaProvider { + public abstract fetchBlocks(startBlock: number, endBlock: number): Promise + + public abstract getLogs(startBlock: number, endBlock: number): Promise> + + public abstract getLatestBlock(): Promise> + + public abstract getTransaction(txHash: string): Promise> + + public abstract getBlockNumber(): Promise> +} export class BlockClient { private provider: ILineaProvider @@ -17,18 +29,18 @@ export class BlockClient { this.logger = logger } - public async getBlocks(): Promise> { + public async getL2Blocks(): Promise> { const start = new Date().getTime() const blocks = await this.fetchBlocks() - this.logger.info(elapsedTime(BlockClient.name + '.' + this.getBlocks.name, start)) + this.logger.info(elapsedTime(BlockClient.name + '.' + this.getL2Blocks.name, start)) return blocks } - public async getLogs(workingBlocks: BlockDto[]): Promise> { + public async getL2Logs(workingBlocks: BlockDto[]): Promise> { const start = new Date().getTime() const logs = await this.fetchLogs(workingBlocks) - this.logger.info(elapsedTime(BlockClient.name + '.' + this.getLogs.name, start)) + this.logger.info(elapsedTime(BlockClient.name + '.' + this.getL2Logs.name, start)) return logs } @@ -42,7 +54,7 @@ export class BlockClient { return E.left( networkAlert( block.left, - `Error in ${BlockClient.name}.${this.getBlocks.name}:21`, + `Error in ${BlockClient.name}.${this.getL2Blocks.name}:21`, `Could not call provider.getLatestBlock`, 0, ), @@ -62,7 +74,7 @@ export class BlockClient { return E.left( networkAlert( latestBlock.left, - `Error in ${BlockClient.name}.${this.getBlocks.name}:39`, + `Error in ${BlockClient.name}.${this.getL2Blocks.name}:39`, `Could not call provider.getLatestBlock`, 0, ), @@ -99,7 +111,7 @@ export class BlockClient { return E.left( networkAlert( logs.left, - `Error in ${BlockClient.name}.${this.getLogs.name}:76`, + `Error in ${BlockClient.name}.${this.getL2Logs.name}:76`, `Could not call provider.getLogs`, workingBlocks[workingBlocks.length - 1].number, ), diff --git a/l2-bridge-linea/src/clients/linea_provider.spec.ts b/l2-bridge-linea/src/clients/linea_provider.spec.ts new file mode 100644 index 00000000..05dec1d2 --- /dev/null +++ b/l2-bridge-linea/src/clients/linea_provider.spec.ts @@ -0,0 +1,34 @@ +import { App } from '../app' +import * as E from 'fp-ts/Either' +import { ETH_DECIMALS } from '../utils/constants' +import BigNumber from 'bignumber.js' + +describe('linea provider tests', () => { + test('should fetch block logs', async () => { + const app = await App.getInstance() + + const latestBlock = await app.LineaClient.getLatestBlock() + if (E.isLeft(latestBlock)) { + throw latestBlock + } + + const blocksDto = await app.LineaClient.getLogs(latestBlock.right.number, latestBlock.right.number) + if (E.isLeft(blocksDto)) { + throw blocksDto + } + + expect(blocksDto.right.length).toBeGreaterThan(1) + }, 120_000) + + test('getWstEth is 1696.070092078019991932 wsETH', async () => { + const app = await App.getInstance() + + const lineaBlockNumber = 3_567_282 + const balance = await app.LineaClient.getWstEthTotalSupply(lineaBlockNumber) + if (E.isLeft(balance)) { + throw balance.left + } + + expect(balance.right.dividedBy(ETH_DECIMALS)).toEqual(new BigNumber('1696.070092078019991932')) + }, 120_000) +}) diff --git a/l2-bridge-linea/src/clients/linea_provider.ts b/l2-bridge-linea/src/clients/linea_provider.ts index 884bfa69..11c0f4e3 100644 --- a/l2-bridge-linea/src/clients/linea_provider.ts +++ b/l2-bridge-linea/src/clients/linea_provider.ts @@ -7,39 +7,30 @@ import { BridgingInitiatedEvent, TokenBridge } from '../generated/TokenBridge' import { WithdrawalRecord } from '../entity/blockDto' import BigNumber from 'bignumber.js' import { Logger } from 'winston' +import { IL2BridgeBalanceClient } from '../services/bridge_balance' +import { ERC20Short as BridgedWstEthRunner } from '../generated' +import { ILineaProvider } from './linea_block_client' +import { IMonitorWithdrawalsClient } from '../services/monitor_withdrawals' -export abstract class IMonitorWithdrawalsClient { - public abstract getWithdrawalEvents( - fromBlockNumber: number, - toBlockNumber: number, - ): Promise> - - public abstract getWithdrawalRecords( - withdrawalEvents: BridgingInitiatedEvent[], - ): Promise> -} - -export abstract class ILineaProvider { - public abstract fetchBlocks(startBlock: number, endBlock: number): Promise - - public abstract getLogs(startBlock: number, endBlock: number): Promise> - - public abstract getLatestBlock(): Promise> - - public abstract getTransaction(txHash: string): Promise> - - public abstract getBlockNumber(): Promise> -} +const DELAY_IN_500MS = 500 +const ATTEMPTS_5 = 5 -export class LineaProvider implements ILineaProvider, IMonitorWithdrawalsClient { +export class LineaProvider implements ILineaProvider, IMonitorWithdrawalsClient, IL2BridgeBalanceClient { private readonly jsonRpcProvider: ethers.providers.JsonRpcProvider private readonly lineaTokenBridge: TokenBridge private readonly logger: Logger - - constructor(jsonRpcProvider: ethers.providers.JsonRpcProvider, lineaTokenBridge: TokenBridge, logger: Logger) { + private readonly bridgedWstEthRunner: BridgedWstEthRunner + + constructor( + jsonRpcProvider: ethers.providers.JsonRpcProvider, + lineaTokenBridge: TokenBridge, + logger: Logger, + bridgedWstEthRunner: BridgedWstEthRunner, + ) { this.jsonRpcProvider = jsonRpcProvider this.lineaTokenBridge = lineaTokenBridge this.logger = logger + this.bridgedWstEthRunner = bridgedWstEthRunner } public async fetchBlocks(startBlock: number, endBlock: number): Promise { @@ -254,4 +245,23 @@ export class LineaProvider implements ILineaProvider, IMonitorWithdrawalsClient return E.right(out) } + + public async getWstEthTotalSupply(l2blockNumber: number): Promise> { + try { + const out = await retryAsync( + async (): Promise => { + const [balance] = await this.bridgedWstEthRunner.functions.totalSupply({ + blockTag: l2blockNumber, + }) + + return balance.toString() + }, + { delay: DELAY_IN_500MS, maxTry: ATTEMPTS_5 }, + ) + + return E.right(new BigNumber(out)) + } catch (e) { + return E.left(new NetworkError(e, `Could not call bridgedWstEthRunner.functions.totalSupply`)) + } + } } diff --git a/l2-bridge-linea/src/services/bridge_balance.ts b/l2-bridge-linea/src/services/bridge_balance.ts new file mode 100644 index 00000000..bb9bd2c5 --- /dev/null +++ b/l2-bridge-linea/src/services/bridge_balance.ts @@ -0,0 +1,98 @@ +import { Finding, FindingSeverity, FindingType } from 'forta-agent' +import { elapsedTime } from '../utils/time' +import { Logger } from 'winston' +import BigNumber from 'bignumber.js' +import * as E from 'fp-ts/Either' +import { getUniqueKey, networkAlert } from '../utils/finding.helpers' +import { ETH_DECIMALS } from '../utils/constants' + +export abstract class IL1BridgeBalanceClient { + abstract getWstEthBalance(l1blockNumber: number, address: string): Promise> +} + +export abstract class IL2BridgeBalanceClient { + abstract getWstEthTotalSupply(l1blockNumber: number): Promise> +} + +export class BridgeBalanceSrv { + private name = `BridgeBalanceSrv` + private readonly logger: Logger + private readonly clientL1: IL1BridgeBalanceClient + private readonly clientL2: IL2BridgeBalanceClient + + private readonly lineaL1TokenBridge: string + + constructor( + logger: Logger, + clientL1: IL1BridgeBalanceClient, + lineaL1TokenBridge: string, + clientL2: IL2BridgeBalanceClient, + ) { + this.logger = logger + this.clientL1 = clientL1 + this.clientL2 = clientL2 + this.lineaL1TokenBridge = lineaL1TokenBridge + } + + async handleBlock(l1BlockNumber: number, l2BlockNumbers: number[]): Promise { + const start = new Date().getTime() + + const findings = await this.handleBridgeBalanceWstETH(l1BlockNumber, l2BlockNumbers) + + this.logger.info(elapsedTime(this.name + '.' + this.handleBlock.name, start)) + return findings + } + + private async handleBridgeBalanceWstETH(l1BlockNumber: number, l2BlockNumbers: number[]): Promise { + const wstETHBalance_onL1LineaBridge = await this.clientL1.getWstEthBalance(l1BlockNumber, this.lineaL1TokenBridge) + + const out: Finding[] = [] + if (E.isLeft(wstETHBalance_onL1LineaBridge)) { + return [ + networkAlert( + wstETHBalance_onL1LineaBridge.left, + `Error in ${BridgeBalanceSrv.name}.${this.handleBridgeBalanceWstETH.name}:36`, + `Could not call clientL1.getWstEth`, + l1BlockNumber, + ), + ] + } + + for (const l2blockNumber of l2BlockNumbers) { + const wstETHTotalSupply_onLinea = await this.clientL2.getWstEthTotalSupply(l2blockNumber) + + if (E.isLeft(wstETHTotalSupply_onLinea)) { + out.push( + networkAlert( + wstETHTotalSupply_onLinea.left, + `Error in ${BridgeBalanceSrv.name}.${this.handleBridgeBalanceWstETH.name}:36`, + `Could not call clientL2.getWstEth`, + l2blockNumber, + ), + ) + + continue + } + + if (wstETHTotalSupply_onLinea.right.isGreaterThan(wstETHBalance_onL1LineaBridge.right)) { + out.push( + Finding.fromObject({ + name: `🚨🚨🚨 Linea bridge balance mismatch 🚨🚨🚨`, + description: + `Total supply of bridged wstETH is greater than balanceOf L1 bridge side!\n` + + `L2 total supply: ${wstETHTotalSupply_onLinea.right.dividedBy(ETH_DECIMALS).toFixed(2)}\n` + + `L1 balanceOf: ${wstETHBalance_onL1LineaBridge.right.dividedBy(ETH_DECIMALS).toFixed(2)}\n\n` + + `ETH: ${l1BlockNumber}\n` + + `Linea: ${l2blockNumber}\n`, + alertId: 'BRIDGE-BALANCE-MISMATCH', + severity: FindingSeverity.Critical, + type: FindingType.Suspicious, + uniqueKey: getUniqueKey('032138b3-e581-4179-be4a-d92ca5763032', l1BlockNumber + l2blockNumber), + }), + ) + } + } + + return out + } +} diff --git a/l2-bridge-linea/src/services/event_watcher.ts b/l2-bridge-linea/src/services/event_watcher.ts index 4f3269f4..19731090 100644 --- a/l2-bridge-linea/src/services/event_watcher.ts +++ b/l2-bridge-linea/src/services/event_watcher.ts @@ -20,7 +20,7 @@ export class EventWatcher { return this.name } - handleLogs(logs: Log[]): Finding[] { + handleL2Logs(logs: Log[]): Finding[] { const start = new Date().getTime() const addresses: string[] = [] @@ -50,7 +50,7 @@ export class EventWatcher { } } - this.logger.info(elapsedTime(this.getName() + '.' + this.handleLogs.name, start)) + this.logger.info(elapsedTime(this.getName() + '.' + this.handleL2Logs.name, start)) return findings } } diff --git a/l2-bridge-linea/src/services/monitor_withdrawals.ts b/l2-bridge-linea/src/services/monitor_withdrawals.ts index 12fea2ac..3427624c 100644 --- a/l2-bridge-linea/src/services/monitor_withdrawals.ts +++ b/l2-bridge-linea/src/services/monitor_withdrawals.ts @@ -3,10 +3,11 @@ import { filterLog, Finding, FindingSeverity, FindingType } from 'forta-agent' import { BlockDto, WithdrawalRecord } from 'src/entity/blockDto' import { Log } from '@ethersproject/abstract-provider' import * as E from 'fp-ts/Either' -import { IMonitorWithdrawalsClient } from '../clients/linea_provider' import { Logger } from 'winston' import { elapsedTime } from '../utils/time' import { getUniqueKey } from '../utils/finding.helpers' +import { NetworkError } from '../utils/error' +import { BridgingInitiatedEvent } from '../generated/TokenBridge' // 48 hours const MAX_WITHDRAWALS_WINDOW = 60 * 60 * 24 * 2 @@ -23,6 +24,17 @@ export type MonitorWithdrawalsInitResp = { currentWithdrawals: string } +export abstract class IMonitorWithdrawalsClient { + public abstract getWithdrawalEvents( + fromBlockNumber: number, + toBlockNumber: number, + ): Promise> + + public abstract getWithdrawalRecords( + withdrawalEvents: BridgingInitiatedEvent[], + ): Promise> +} + export class MonitorWithdrawals { private name: string = 'WithdrawalsMonitor' diff --git a/l2-bridge-linea/src/services/proxy_watcher.ts b/l2-bridge-linea/src/services/proxy_watcher.ts index 4b6bca57..dd007fcd 100644 --- a/l2-bridge-linea/src/services/proxy_watcher.ts +++ b/l2-bridge-linea/src/services/proxy_watcher.ts @@ -60,16 +60,16 @@ export class ProxyWatcher { }) } - public async handleBlocks(blockNumbers: number[]): Promise { + public async handleBlocks(l2blockNumbers: number[]): Promise { const start = new Date().getTime() const findings: Finding[] = [] const BLOCK_INTERVAL = 10 - for (const blockNumber of blockNumbers) { - if (blockNumber % BLOCK_INTERVAL === 0) { + for (const l2blockNumber of l2blockNumbers) { + if (l2blockNumber % BLOCK_INTERVAL === 0) { const [implFindings, adminFindings] = await Promise.all([ - this.handleProxyImplementationChanges(blockNumber), - this.handleProxyAdminChanges(blockNumber), + this.handleProxyImplementationChanges(l2blockNumber), + this.handleProxyAdminChanges(l2blockNumber), ]) findings.push(...implFindings, ...adminFindings) @@ -80,20 +80,20 @@ export class ProxyWatcher { return findings } - private async handleProxyImplementationChanges(blockNumber: number): Promise { + private async handleProxyImplementationChanges(l2blockNumber: number): Promise { const out: Finding[] = [] for (const contract of this.contractCallers) { const lastImpl = this.lastImpls.get(contract.getAddress()) || '' - const newImpl = await contract.getProxyImplementation(blockNumber) + const newImpl = await contract.getProxyImplementation(l2blockNumber) if (E.isLeft(newImpl)) { return [ networkAlert( newImpl.left, `Error in ${ProxyWatcher.name}.${this.handleProxyAdminChanges.name}:90`, newImpl.left.message, - blockNumber, + l2blockNumber, ), ] } @@ -111,7 +111,7 @@ export class ProxyWatcher { severity: FindingSeverity.High, type: FindingType.Info, metadata: { newImpl: newImpl.right, lastImpl: lastImpl }, - uniqueKey: getUniqueKey(uniqueKey + '-' + contract.getAddress(), blockNumber), + uniqueKey: getUniqueKey(uniqueKey + '-' + contract.getAddress(), l2blockNumber), }), ) } @@ -122,20 +122,20 @@ export class ProxyWatcher { return out } - private async handleProxyAdminChanges(blockNumber: number): Promise { + private async handleProxyAdminChanges(l2blockNumber: number): Promise { const out: Finding[] = [] for (const contract of this.contractCallers) { const lastAdmin: string = this.lastAdmins.get(contract.getAddress()) || '' - const newAdmin = await contract.getProxyAdmin(blockNumber) + const newAdmin = await contract.getProxyAdmin(l2blockNumber) if (E.isLeft(newAdmin)) { return [ networkAlert( newAdmin.left, `Error in ${ProxyWatcher.name}.${this.handleProxyAdminChanges.name}:125`, newAdmin.left.message, - blockNumber, + l2blockNumber, ), ] } @@ -153,7 +153,7 @@ export class ProxyWatcher { severity: FindingSeverity.High, type: FindingType.Info, metadata: { newAdmin: newAdmin.right, lastAdmin: lastAdmin }, - uniqueKey: getUniqueKey(uniqueKey + '-' + contract.getAddress(), blockNumber), + uniqueKey: getUniqueKey(uniqueKey + '-' + contract.getAddress(), l2blockNumber), }), ) } diff --git a/l2-bridge-linea/src/utils/constants.ts b/l2-bridge-linea/src/utils/constants.ts index 689b5d03..49634b18 100644 --- a/l2-bridge-linea/src/utils/constants.ts +++ b/l2-bridge-linea/src/utils/constants.ts @@ -1,9 +1,15 @@ +import BigNumber from 'bignumber.js' + const LINEA_BRIDGE_EXECUTOR = '0x74Be82F00CC867614803ffd7f36A2a4aF0405670' const LINEA_L2_TOKEN_BRIDGE = '0x353012dc4a9a6cf55c941badc267f82004a8ceb9' const ADMIN_OF_LINEA_L2_TOKEN_BRIDGE = '0xa11ba93afbd6d18e26fefdb2c6311da6c9b370d6' const LINEA_WST_CUSTOM_BRIDGED_TOKEN = '0xB5beDd42000b71FddE22D3eE8a79Bd49A568fC8F' const LINEA_PROXY_ADMIN_FOR_WSTETH = '0xF951d7592e03eDB0Bab3D533935e678Ce64Eb927' const LINEA_TOKEN_BRIDGE = '0x2bfdf4a0d54c93a4baf74f8dcea8a275d8ee97a9' +const LINEA_L1_TOKEN_BRIDGE = '0x051f1d88f0af5763fb888ec4378b4d8b29ea3319' +const WSTETH_ADDRESS = '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0' + +export const ETH_DECIMALS = new BigNumber(10).pow(18) export type ProxyContract = { name: string @@ -11,7 +17,9 @@ export type ProxyContract = { } export type Address = { + WSTETH_ADDRESS: string LINEA_BRIDGE_EXECUTOR: string + LINEA_L1_TOKEN_BRIDGE: string LINEA_L2_TOKEN_BRIDGE: string ADMIN_OF_LINEA_L2_TOKEN_BRIDGE: string LINEA_WST_CUSTOM_BRIDGED_TOKEN: string @@ -22,7 +30,9 @@ export type Address = { } export const Address: Address = { + WSTETH_ADDRESS: WSTETH_ADDRESS, LINEA_BRIDGE_EXECUTOR: LINEA_BRIDGE_EXECUTOR, + LINEA_L1_TOKEN_BRIDGE: LINEA_L1_TOKEN_BRIDGE, LINEA_L2_TOKEN_BRIDGE: LINEA_L2_TOKEN_BRIDGE, ADMIN_OF_LINEA_L2_TOKEN_BRIDGE: ADMIN_OF_LINEA_L2_TOKEN_BRIDGE, LINEA_WST_CUSTOM_BRIDGED_TOKEN: LINEA_WST_CUSTOM_BRIDGED_TOKEN, diff --git a/l2-bridge-linea/tests/linea_client.spec.ts b/l2-bridge-linea/tests/linea_client.spec.ts deleted file mode 100644 index c4c70422..00000000 --- a/l2-bridge-linea/tests/linea_client.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { App } from '../src/app' -import * as E from 'fp-ts/Either' - -describe('agent-linea e2e tests', () => { - test('should fetch block logs', async () => { - const app = await App.getInstance() - - const latestBlock = await app.LineaClient.getLatestBlock() - if (E.isLeft(latestBlock)) { - throw latestBlock - } - - const blocksDto = await app.LineaClient.getLogs(latestBlock.right.number, latestBlock.right.number) - if (E.isLeft(blocksDto)) { - throw blocksDto - } - - expect(blocksDto.right.length).toBeGreaterThan(1) - }, 120_000) -}) diff --git a/l2-bridge-linea/tests/utils.ts b/l2-bridge-linea/tests/utils.ts deleted file mode 100644 index 7b46bc2c..00000000 --- a/l2-bridge-linea/tests/utils.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Block as EtherBlock } from '@ethersproject/abstract-provider' -import { Block, BlockEvent, EventType, Network, 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 etherBlockToFortaBlockEvent(block: EtherBlock): BlockEvent { - const blok: Block = { - difficulty: block.difficulty.toString(), - extraData: block.extraData, - gasLimit: block.gasLimit.toString(), - gasUsed: block.gasUsed.toString(), - hash: block.hash, - logsBloom: '', - miner: formatAddress(block.miner), - mixHash: '', - nonce: block.nonce, - number: block.number, - parentHash: block.parentHash, - receiptsRoot: '', - sha3Uncles: '', - size: '', - stateRoot: '', - timestamp: block.timestamp, - totalDifficulty: block.difficulty.toString(), - transactions: block.transactions, - transactionsRoot: '', - uncles: [], - } - - return new BlockEvent(EventType.BLOCK, Network.MAINNET, blok) -}