From e39da6da806298bfe61f0cd47c294a9651ca01a0 Mon Sep 17 00:00:00 2001 From: Marko Arambasic Date: Wed, 4 Dec 2024 14:21:13 +0100 Subject: [PATCH] feat: update token for upgradable transactions where implementation is token --- packages/data-fetcher/src/app.module.ts | 2 + .../data-fetcher/src/log/log.service.spec.ts | 39 +++++++++ packages/data-fetcher/src/log/log.service.ts | 33 +++++++- .../default.handler.spec.ts | 72 ++++++++++++++++ .../extractProxyHandlers/default.handler.ts | 37 +++++++++ .../upgradable/extractProxyHandlers/index.ts | 1 + .../extractProxyHandler.interface.ts | 7 ++ .../interface/proxyAddress.interface.ts | 5 ++ .../src/upgradable/upgradable.service.spec.ts | 82 +++++++++++++++++++ .../src/upgradable/upgradable.service.ts | 34 ++++++++ 10 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 packages/data-fetcher/src/upgradable/extractProxyHandlers/default.handler.spec.ts create mode 100644 packages/data-fetcher/src/upgradable/extractProxyHandlers/default.handler.ts create mode 100644 packages/data-fetcher/src/upgradable/extractProxyHandlers/index.ts create mode 100644 packages/data-fetcher/src/upgradable/interface/extractProxyHandler.interface.ts create mode 100644 packages/data-fetcher/src/upgradable/interface/proxyAddress.interface.ts create mode 100644 packages/data-fetcher/src/upgradable/upgradable.service.spec.ts create mode 100644 packages/data-fetcher/src/upgradable/upgradable.service.ts diff --git a/packages/data-fetcher/src/app.module.ts b/packages/data-fetcher/src/app.module.ts index 0b4f1f0eff..00d543b700 100644 --- a/packages/data-fetcher/src/app.module.ts +++ b/packages/data-fetcher/src/app.module.ts @@ -13,6 +13,7 @@ import { TransferService } from "./transfer/transfer.service"; import { TokenService } from "./token/token.service"; import { JsonRpcProviderModule } from "./rpcProvider/jsonRpcProvider.module"; import { MetricsModule } from "./metrics"; +import { UpgradableService } from "./upgradable/upgradable.service"; @Module({ imports: [ @@ -26,6 +27,7 @@ import { MetricsModule } from "./metrics"; providers: [ BlockchainService, AddressService, + UpgradableService, BalanceService, TransferService, TokenService, diff --git a/packages/data-fetcher/src/log/log.service.spec.ts b/packages/data-fetcher/src/log/log.service.spec.ts index 150b0c12d5..20ba621dec 100644 --- a/packages/data-fetcher/src/log/log.service.spec.ts +++ b/packages/data-fetcher/src/log/log.service.spec.ts @@ -9,6 +9,8 @@ import { TokenService, Token } from "../token/token.service"; import { AddressService } from "../address/address.service"; import { BalanceService } from "../balance/balance.service"; import { ContractAddress } from "../address/interface/contractAddress.interface"; +import { UpgradableService } from "../upgradable/upgradable.service"; +import { ProxyAddress } from "src/upgradable/interface/proxyAddress.interface"; describe("LogService", () => { let logService: LogService; @@ -16,12 +18,14 @@ describe("LogService", () => { let balanceServiceMock: BalanceService; let transferServiceMock: TransferService; let tokenServiceMock: TokenService; + let upgradableServiceMock: UpgradableService; beforeEach(async () => { addressServiceMock = mock(); balanceServiceMock = mock(); transferServiceMock = mock(); tokenServiceMock = mock(); + upgradableServiceMock = mock(); const app = await Test.createTestingModule({ providers: [ @@ -42,6 +46,10 @@ describe("LogService", () => { provide: TokenService, useValue: tokenServiceMock, }, + { + provide: UpgradableService, + useValue: upgradableServiceMock, + }, ], }).compile(); @@ -61,6 +69,17 @@ describe("LogService", () => { mock({ address: "0xD144ca8Aa2E7DFECD56a3CCcBa1cd873c8e5db58" }), ]; + const upgradableAddresses = [ + mock({ + address: "0xEBf9D3ead9A8c2bb8cEa438B8Dfa9f1AFf44bfa7", + implementationAddress: "0xf43624d811c5DC9eF91cF237ab9B8eE220D438eE", + }), + mock({ + address: "0xdc187378edD8Ed1585fb47549Cc5fe633295d571", + implementationAddress: "0xD144ca8Aa2E7DFECD56a3CCcBa1cd873c8e5db58", + }), + ]; + const transfers = [ { from: "from1", to: "to1", logIndex: 0 } as Transfer, { from: "from2", to: "to2", logIndex: 1 } as Transfer, @@ -92,6 +111,7 @@ describe("LogService", () => { describe("when transaction details and receipt are defined", () => { beforeEach(() => { + jest.spyOn(upgradableServiceMock, "getUpgradableAddresses").mockResolvedValueOnce([]); transactionReceipt = mock({ index: 0, logs: logs, @@ -133,6 +153,9 @@ describe("LogService", () => { }); describe("when transaction details and receipt are not defined", () => { + beforeEach(() => { + jest.spyOn(upgradableServiceMock, "getUpgradableAddresses").mockResolvedValueOnce([]); + }); it("tracks changed balances", async () => { await logService.getData(logs, blockDetails); expect(balanceServiceMock.trackChangedBalances).toHaveBeenCalledTimes(1); @@ -146,5 +169,21 @@ describe("LogService", () => { expect(logsData.transfers).toEqual(transfers); }); }); + + describe("when there are upgradable addresses", () => { + beforeEach(() => { + jest.spyOn(upgradableServiceMock, "getUpgradableAddresses").mockResolvedValueOnce(upgradableAddresses); + }); + it("returns data with upgradable addresses", async () => { + const logsData = await logService.getData(logs, blockDetails, transactionDetails, transactionReceipt); + expect(upgradableServiceMock.getUpgradableAddresses).toHaveBeenCalledTimes(1); + expect(tokenServiceMock.getERC20Token).toHaveBeenCalledTimes(3); + expect(logsData.tokens).toEqual([ + { l1Address: "l1Address1" }, + { l1Address: "l1Address2" }, + { l2Address: "0xEBf9D3ead9A8c2bb8cEa438B8Dfa9f1AFf44bfa7" }, + ]); + }); + }); }); }); diff --git a/packages/data-fetcher/src/log/log.service.ts b/packages/data-fetcher/src/log/log.service.ts index 27220302d9..69fa2e1e45 100644 --- a/packages/data-fetcher/src/log/log.service.ts +++ b/packages/data-fetcher/src/log/log.service.ts @@ -7,6 +7,7 @@ import { TokenService } from "../token/token.service"; import { Transfer } from "../transfer/interfaces/transfer.interface"; import { ContractAddress } from "../address/interface/contractAddress.interface"; import { Token } from "../token/token.service"; +import { UpgradableService } from "../upgradable/upgradable.service"; export interface LogsData { transfers: Transfer[]; @@ -22,7 +23,8 @@ export class LogService { private readonly addressService: AddressService, private readonly balanceService: BalanceService, private readonly transferService: TransferService, - private readonly tokenService: TokenService + private readonly tokenService: TokenService, + private readonly upgradableService: UpgradableService ) { this.logger = new Logger(LogService.name); } @@ -60,6 +62,35 @@ export class LogService { ) ).filter((token) => !!token); + this.logger.debug({ + message: "Extracting upgradable addresses", + blockNumber: blockDetails.number, + transactionHash, + }); + + const upgradableAddresses = await this.upgradableService.getUpgradableAddresses(logs, transactionReceipt); + const upgradableTokens = ( + await Promise.all( + upgradableAddresses + .filter( + (address) => !contractAddresses.some((contractAddress) => contractAddress.address === address.address) + ) + .map(async (upgradableAddress) => { + const proxyAddress = upgradableAddress.address; + const implementationAddress = upgradableAddress.implementationAddress; + const token = await this.tokenService.getERC20Token( + { ...upgradableAddress, address: implementationAddress }, + transactionReceipt + ); + return { + ...token, + l2Address: proxyAddress, + }; + }) + ) + ).filter((token) => !!token); + tokens.push(...upgradableTokens); + logsData.contractAddresses = contractAddresses; logsData.tokens = tokens; } diff --git a/packages/data-fetcher/src/upgradable/extractProxyHandlers/default.handler.spec.ts b/packages/data-fetcher/src/upgradable/extractProxyHandlers/default.handler.spec.ts new file mode 100644 index 0000000000..66e54bee57 --- /dev/null +++ b/packages/data-fetcher/src/upgradable/extractProxyHandlers/default.handler.spec.ts @@ -0,0 +1,72 @@ +import { types } from "zksync-ethers"; +import { mock } from "jest-mock-extended"; +import { defaultContractUpgradableHandler } from "./default.handler"; + +describe("defaultContractUpgradableHandler", () => { + let log: types.Log; + beforeEach(() => { + log = mock({ + transactionIndex: 1, + blockNumber: 3233097, + transactionHash: "0x5e018d2a81dbd1ef80ff45171dd241cb10670dcb091e324401ff8f52293841b0", + address: "0x1BEB2aBb1678D8a25431d9728A425455f29d12B7", + topics: [ + "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", + "0x000000000000000000000000a1810a1f32F4DC6c5112b5b837b6975E56b489cc", + ], + data: "0x", + index: 8, + blockHash: "0xdfd071dcb9c802f7d11551f4769ca67842041ffb81090c49af7f089c5823f39c", + l1BatchNumber: 604161, + }); + }); + + describe("matches", () => { + it("returns true", () => { + const result = defaultContractUpgradableHandler.matches(log); + expect(result).toBe(true); + }); + }); + + describe("extract", () => { + let transactionReceipt; + + beforeEach(() => { + transactionReceipt = mock({ + blockNumber: 10, + hash: "transactionHash", + from: "from", + }); + }); + + it("extracts upgraded contract address", () => { + const result = defaultContractUpgradableHandler.extract(log, transactionReceipt); + expect(result.address).toBe("0x1BEB2aBb1678D8a25431d9728A425455f29d12B7"); + }); + + it("extracts block number for the upgraded contract", () => { + const result = defaultContractUpgradableHandler.extract(log, transactionReceipt); + expect(result.blockNumber).toBe(transactionReceipt.blockNumber); + }); + + it("extracts transaction hash for the upgraded contract", () => { + const result = defaultContractUpgradableHandler.extract(log, transactionReceipt); + expect(result.transactionHash).toBe(transactionReceipt.hash); + }); + + it("extracts creator address for the upgraded contract", () => { + const result = defaultContractUpgradableHandler.extract(log, transactionReceipt); + expect(result.creatorAddress).toBe(transactionReceipt.from); + }); + + it("extracts logIndex for the upgraded contract", () => { + const result = defaultContractUpgradableHandler.extract(log, transactionReceipt); + expect(result.logIndex).toBe(log.index); + }); + + it("extracts implementation address for the upgraded contract", () => { + const result = defaultContractUpgradableHandler.extract(log, transactionReceipt); + expect(result.implementationAddress).toBe("0xa1810a1f32F4DC6c5112b5b837b6975E56b489cc"); + }); + }); +}); diff --git a/packages/data-fetcher/src/upgradable/extractProxyHandlers/default.handler.ts b/packages/data-fetcher/src/upgradable/extractProxyHandlers/default.handler.ts new file mode 100644 index 0000000000..a3ccbf2de8 --- /dev/null +++ b/packages/data-fetcher/src/upgradable/extractProxyHandlers/default.handler.ts @@ -0,0 +1,37 @@ +import { types } from "zksync-ethers"; +import { AbiCoder, keccak256 } from "ethers"; +import { ExtractProxyAddressHandler } from "../interface/extractProxyHandler.interface"; +import { ProxyAddress } from "../interface/proxyAddress.interface"; + +const abiCoder: AbiCoder = AbiCoder.defaultAbiCoder(); + +export const encodedUpgradableEvents = [ + "Upgraded(address)", + "BeaconUpgraded(address)", + "OwnershipTransferred(address,address)", + "AdminChanged(address,address)", + "OwnershipTransferred(address,address)", +]; + +const decodedUpgradableEvents = encodedUpgradableEvents.map((event) => `${keccak256(Buffer.from(event)).toString()}`); + +export const defaultContractUpgradableHandler: ExtractProxyAddressHandler = { + matches: (log: types.Log): boolean => { + return decodedUpgradableEvents.includes(log.topics[0]); + }, + extract: (log: types.Log, txReceipt: types.TransactionReceipt): ProxyAddress => { + if (!log.topics[1]) { + return null; + } + + const [address] = abiCoder.decode(["address"], log.topics[1]); + return { + address: log.address, + blockNumber: txReceipt.blockNumber, + transactionHash: txReceipt.hash, + creatorAddress: txReceipt.from, + logIndex: log.index, + implementationAddress: address, + }; + }, +}; diff --git a/packages/data-fetcher/src/upgradable/extractProxyHandlers/index.ts b/packages/data-fetcher/src/upgradable/extractProxyHandlers/index.ts new file mode 100644 index 0000000000..872dfd9c9b --- /dev/null +++ b/packages/data-fetcher/src/upgradable/extractProxyHandlers/index.ts @@ -0,0 +1 @@ +export * from "./default.handler"; diff --git a/packages/data-fetcher/src/upgradable/interface/extractProxyHandler.interface.ts b/packages/data-fetcher/src/upgradable/interface/extractProxyHandler.interface.ts new file mode 100644 index 0000000000..9b8e04e28b --- /dev/null +++ b/packages/data-fetcher/src/upgradable/interface/extractProxyHandler.interface.ts @@ -0,0 +1,7 @@ +import { types } from "zksync-ethers"; +import { ProxyAddress } from "./proxyAddress.interface"; + +export interface ExtractProxyAddressHandler { + matches: (log: types.Log) => boolean; + extract: (log: types.Log, txReceipt: types.TransactionReceipt) => ProxyAddress | null; +} diff --git a/packages/data-fetcher/src/upgradable/interface/proxyAddress.interface.ts b/packages/data-fetcher/src/upgradable/interface/proxyAddress.interface.ts new file mode 100644 index 0000000000..dec00404ee --- /dev/null +++ b/packages/data-fetcher/src/upgradable/interface/proxyAddress.interface.ts @@ -0,0 +1,5 @@ +import { ContractAddress } from "../../address/interface/contractAddress.interface"; + +export type ProxyAddress = ContractAddress & { + implementationAddress: string; +}; diff --git a/packages/data-fetcher/src/upgradable/upgradable.service.spec.ts b/packages/data-fetcher/src/upgradable/upgradable.service.spec.ts new file mode 100644 index 0000000000..75793e150d --- /dev/null +++ b/packages/data-fetcher/src/upgradable/upgradable.service.spec.ts @@ -0,0 +1,82 @@ +import { Test } from "@nestjs/testing"; +import { Logger } from "@nestjs/common"; +import { mock } from "jest-mock-extended"; +import { types } from "zksync-ethers"; + +import { UpgradableService } from "./upgradable.service"; +describe("UpgradableService", () => { + let upgradableService: UpgradableService; + + beforeEach(async () => { + const app = await Test.createTestingModule({ + providers: [UpgradableService], + }).compile(); + + app.useLogger(mock()); + + upgradableService = app.get(UpgradableService); + }); + + describe("getUpgradableAddresses", () => { + const logs = [ + mock({ + topics: [ + "0x290afdae231a3fc0bbae8b1af63698b0a1d79b21ad17df0342dfb952fe74f8e5", + "0x000000000000000000000000c7e0220d02d549c4846a6ec31d89c3b670ebe35c", + "0x0100014340e955cbf39159da998b3374bee8f3c0b3c75a7a9e3df6b85052379d", + "0x000000000000000000000000dc187378edd8ed1585fb47549cc5fe633295d571", + ], + index: 1, + }), + mock({ + topics: [ + "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", + "0x0000000000000000000000000db321efaa9e380d0b37b55b530cdaa62728b9a3", + ], + address: "0xdc187378edD8Ed1585fb47549Cc5fe633295d571", + index: 2, + }), + mock({ + topics: [ + "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", + "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54", + ], + address: "0xD144ca8Aa2E7DFECD56a3CCcBa1cd873c8e5db58", + index: 3, + }), + ]; + + const transactionReceipt = mock({ + blockNumber: 10, + hash: "transactionHash", + from: "from", + }); + + it("returns upgradable addresses", async () => { + const upgradableAddresses = await upgradableService.getUpgradableAddresses(logs, transactionReceipt); + expect(upgradableAddresses).toStrictEqual([ + { + address: "0xdc187378edD8Ed1585fb47549Cc5fe633295d571", + implementationAddress: "0x0Db321EFaa9E380d0B37B55B530CDaA62728B9a3", + blockNumber: transactionReceipt.blockNumber, + transactionHash: transactionReceipt.hash, + creatorAddress: transactionReceipt.from, + logIndex: logs[1].index, + }, + { + address: "0xD144ca8Aa2E7DFECD56a3CCcBa1cd873c8e5db58", + implementationAddress: "0x481E48Ce19781c3cA573967216deE75FDcF70F54", + blockNumber: transactionReceipt.blockNumber, + transactionHash: transactionReceipt.hash, + creatorAddress: transactionReceipt.from, + logIndex: logs[2].index, + }, + ]); + }); + + it("returns an empty array if no logs specified", async () => { + const result = await upgradableService.getUpgradableAddresses(null, transactionReceipt); + expect(result).toStrictEqual([]); + }); + }); +}); diff --git a/packages/data-fetcher/src/upgradable/upgradable.service.ts b/packages/data-fetcher/src/upgradable/upgradable.service.ts new file mode 100644 index 0000000000..c333598b2c --- /dev/null +++ b/packages/data-fetcher/src/upgradable/upgradable.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from "@nestjs/common"; +import { types } from "zksync-ethers"; +import { ExtractProxyAddressHandler } from "./interface/extractProxyHandler.interface"; +import { defaultContractUpgradableHandler } from "./extractProxyHandlers"; +import { ProxyAddress } from "./interface/proxyAddress.interface"; + +const extractProxyHandlers: ExtractProxyAddressHandler[] = [defaultContractUpgradableHandler]; + +@Injectable() +export class UpgradableService { + public async getUpgradableAddresses( + logs: ReadonlyArray, + transactionReceipt: types.TransactionReceipt + ): Promise { + const proxyAddresses: ProxyAddress[] = []; + if (!logs) { + return proxyAddresses; + } + + logs.forEach((log) => { + const handlerForLog = extractProxyHandlers.find((handler) => handler.matches(log)); + if (!handlerForLog) { + return; + } + + const proxyAddress = handlerForLog.extract(log, transactionReceipt); + if (proxyAddress) { + proxyAddresses.push(proxyAddress); + } + }); + + return proxyAddresses; + } +}