Skip to content

Commit

Permalink
feat: tvl by chain (#39)
Browse files Browse the repository at this point in the history
# 🤖 Linear

Closes ZKS-76

## Description

Retrieve TVL by Chain ID of Shared Bridge from static token list:

`tvl(chainId: number)` method implemented
added `multicall` method to EvmProviderService
uses multicall for fetching token balances
uses Pricing Service for fetching prices
  • Loading branch information
0xnigir1 authored Aug 7, 2024
1 parent 512da31 commit 9238bcc
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 18 deletions.
54 changes: 49 additions & 5 deletions libs/metrics/src/l1/l1MetricsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { AssetTvl, GasInfo } from "@zkchainhub/metrics/types";
import { IPricingService, PRICING_PROVIDER } from "@zkchainhub/pricing";
import { EvmProviderService } from "@zkchainhub/providers";
import { AbiWithAddress, ChainId, L1_CONTRACTS, vitalikAddress } from "@zkchainhub/shared";
import { ETH_TOKEN_ADDRESS } from "@zkchainhub/shared/constants/addresses";
import {
erc20Tokens,
isNativeToken,
Expand Down Expand Up @@ -151,12 +152,55 @@ export class L1MetricsService {
): Promise<{ commited: number; verified: number; proved: number }> {
return { commited: 100, verified: 100, proved: 100 };
}
//TODO: Implement tvl.
async tvl(
_chainId: number,
): Promise<{ [asset: string]: { amount: number; amountUsd: number } }> {
return { ETH: { amount: 1000000, amountUsd: 1000000 } };

/**
* Retrieves the Total Value Locked for {chainId} by L1 token
* @returns A Promise that resolves to an array of AssetTvl objects representing the TVL for each asset.
*/
async tvl(chainId: number): Promise<AssetTvl[]> {
const erc20Addresses = erc20Tokens.map((token) => token.contractAddress);

const balances = await this.fetchTokenBalancesByChain(chainId, erc20Addresses);
const pricesRecord = await this.pricingService.getTokenPrices(
tokens.map((token) => token.coingeckoId),
);

assert(Object.keys(pricesRecord).length === tokens.length, "Invalid prices length");

return this.calculateTvl(balances, erc20Addresses, pricesRecord);
}

/**
* Fetches the token balances for the given addresses and ETH balance on {chainId}
* Note: The last balance in the returned array is the ETH balance, so the fetch length should be addresses.length + 1.
* @param addresses - An array of addresses for which to fetch the token balances.
* @returns A promise that resolves to an object containing the ETH balance and an array of address balances.
*/
private async fetchTokenBalancesByChain(chainId: number, addresses: Address[]) {
const chainIdBn = BigInt(chainId);
const balances = await this.evmProviderService.multicall({
contracts: [
...addresses.map((tokenAddress) => {
return {
address: this.sharedBridge.address,
abi: sharedBridgeAbi,
functionName: "chainBalance",
args: [chainIdBn, tokenAddress],
} as const;
}),
{
address: this.sharedBridge.address,
abi: sharedBridgeAbi,
functionName: "chainBalance",
args: [chainIdBn, ETH_TOKEN_ADDRESS],
} as const,
],
allowFailure: false,
});

return { ethBalance: balances[addresses.length]!, addressesBalance: balances.slice(0, -1) };
}

//TODO: Implement chainType.
async chainType(_chainId: number): Promise<"validium" | "rollup"> {
return "rollup";
Expand Down
99 changes: 95 additions & 4 deletions libs/metrics/test/unit/l1/l1MetricsService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { tokenBalancesAbi } from "@zkchainhub/metrics/l1/abis/tokenBalances.abi"
import { tokenBalancesBytecode } from "@zkchainhub/metrics/l1/bytecode";
import { IPricingService, PRICING_PROVIDER } from "@zkchainhub/pricing";
import { EvmProviderService } from "@zkchainhub/providers";
import { L1_CONTRACTS, vitalikAddress } from "@zkchainhub/shared";
import { ETH_TOKEN_ADDRESS, L1_CONTRACTS, vitalikAddress } from "@zkchainhub/shared";
import { nativeToken, WETH } from "@zkchainhub/shared/tokens/tokens";

// Mock implementations of the dependencies
Expand Down Expand Up @@ -249,9 +249,100 @@ describe("L1MetricsService", () => {
});

describe("tvl", () => {
it("return tvl", async () => {
const result = await l1MetricsService.tvl(1);
expect(result).toEqual({ ETH: { amount: 1000000, amountUsd: 1000000 } });
it("return the TVL for chain id", async () => {
const mockBalances = [60_841_657_140641n, 135_63005559n, 123_803_824374847279970609n]; // Mocked balances
const mockPrices = { "wrapped-bitcoin": 66_129, "usd-coin": 0.999, ethereum: 3_181.09 }; // Mocked prices
const chainId = 324; // this is ZKsyncEra chain id

jest.spyOn(mockEvmProviderService, "multicall").mockResolvedValue(mockBalances);
jest.spyOn(mockPricingService, "getTokenPrices").mockResolvedValue(mockPrices);

const result = await l1MetricsService.tvl(chainId);

expect(result).toHaveLength(3);
expect(result).toEqual([
{
amount: "123803.824374847279970609",
amountUsd: expect.stringContaining("393831107.68"),
price: "3181.09",
name: "Ethereum",
symbol: "ETH",
contractAddress: null,
type: "native",
imageUrl:
"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628",
decimals: 18,
},
{
amount: "60841657.140641",
amountUsd: expect.stringContaining("60780815.48"),
price: "0.999",
name: "USDC",
symbol: "USDC",
contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
imageUrl:
"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694",
type: "erc20",
decimals: 6,
},
{
amount: "135.63005559",
amountUsd: expect.stringContaining("8969079.94"),
price: "66129",
name: "Wrapped BTC",
symbol: "WBTC",
contractAddress: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
imageUrl:
"https://coin-images.coingecko.com/coins/images/7598/large/wrapped_bitcoin_wbtc.png?1696507857",
type: "erc20",
decimals: 8,
},
]);
expect(mockEvmProviderService.multicall).toHaveBeenCalledWith({
contracts: [
{
address: L1_CONTRACTS.SHARED_BRIDGE,
abi: sharedBridgeAbi,
functionName: "chainBalance",
args: [BigInt(chainId), "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
},
{
address: L1_CONTRACTS.SHARED_BRIDGE,
abi: sharedBridgeAbi,
functionName: "chainBalance",
args: [BigInt(chainId), "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"],
},
{
address: L1_CONTRACTS.SHARED_BRIDGE,
abi: sharedBridgeAbi,
functionName: "chainBalance",
args: [BigInt(chainId), ETH_TOKEN_ADDRESS],
},
],
allowFailure: false,
});
expect(mockPricingService.getTokenPrices).toHaveBeenCalledWith([
"ethereum",
"usd-coin",
"wrapped-bitcoin",
]);
});

it("throws an error if the prices length is invalid", async () => {
const chainId = 324;
jest.spyOn(mockEvmProviderService, "multicall").mockResolvedValue([
60_841_657_140641n,
135_63005559n,
123_803_824374847279970609n,
]);
jest.spyOn(mockPricingService, "getTokenPrices").mockResolvedValue({
ethereum: 3_181.09,
"usd-coin": 0.999,
});

await expect(l1MetricsService.tvl(chainId)).rejects.toThrowError(
"Invalid prices length",
);
});
});

Expand Down
1 change: 1 addition & 0 deletions libs/providers/src/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./invalidArgument.exception";
export * from "./dataDecode.exception";
export * from "./multicallNotFound.exception";
5 changes: 5 additions & 0 deletions libs/providers/src/exceptions/multicallNotFound.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class MulticallNotFound extends Error {
constructor() {
super("Multicall contract address not found");
}
}
27 changes: 26 additions & 1 deletion libs/providers/src/providers/evmProvider.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ContractConstructorArgs,
ContractFunctionArgs,
ContractFunctionName,
ContractFunctionParameters,
ContractFunctionReturnType,
createPublicClient,
decodeAbiParameters,
Expand All @@ -18,10 +19,16 @@ import {
Hex,
http,
HttpTransport,
MulticallParameters,
MulticallReturnType,
toHex,
} from "viem";

import { DataDecodeException, InvalidArgumentException } from "@zkchainhub/providers/exceptions";
import {
DataDecodeException,
InvalidArgumentException,
MulticallNotFound,
} from "@zkchainhub/providers/exceptions";
import { AbiWithConstructor } from "@zkchainhub/providers/types";

/**
Expand Down Expand Up @@ -164,4 +171,22 @@ export class EvmProviderService {
throw new DataDecodeException("Error decoding return data with given AbiParameters");
}
}

/**
* Similar to readContract, but batches up multiple functions
* on a contract in a single RPC call via the multicall3 contract.
* @param {MulticallParameters} args - The parameters for the multicall.
* @returns — An array of results. If allowFailure is true, with accompanying status
* @throws {MulticallNotFound} if the Multicall contract is not found.
*/
async multicall<
contracts extends readonly unknown[] = readonly ContractFunctionParameters[],
allowFailure extends boolean = true,
>(
args: MulticallParameters<contracts, allowFailure>,
): Promise<MulticallReturnType<contracts, allowFailure>> {
if (!this.chain.contracts?.multicall3?.address) throw new MulticallNotFound();

return this.client.multicall<contracts, allowFailure>(args);
}
}
69 changes: 61 additions & 8 deletions libs/providers/test/unit/providers/evmProvider.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { localhost } from "viem/chains";
import { Logger } from "winston";

import { EvmProviderService } from "@zkchainhub/providers";
import { DataDecodeException } from "@zkchainhub/providers/exceptions";
import { DataDecodeException, MulticallNotFound } from "@zkchainhub/providers/exceptions";
import {
arrayAbiFixture,
structAbiFixture,
Expand All @@ -27,16 +27,18 @@ export const mockLogger: Partial<Logger> = {
error: jest.fn(),
debug: jest.fn(),
};
const testAbi = parseAbi([
"constructor(uint256 totalSupply)",
"function balanceOf(address owner) view returns (uint256)",
"function tokenURI(uint256 tokenId) pure returns (string)",
]);

describe("EvmProviderService", () => {
let viemProvider: EvmProviderService;
const testAbi = parseAbi([
"constructor(uint256 totalSupply)",
"function balanceOf(address owner) view returns (uint256)",
"function tokenURI(uint256 tokenId) pure returns (string)",
]);
let mockChain: viem.Chain;

beforeEach(async () => {
mockChain = jest.mocked<viem.Chain>({ ...localhost, contracts: { multicall3: undefined } });
const app: TestingModule = await Test.createTestingModule({
providers: [
{
Expand All @@ -47,8 +49,7 @@ describe("EvmProviderService", () => {
provide: EvmProviderService,
useFactory: () => {
const rpcUrl = "http://localhost:8545";
const chain = localhost;
return new EvmProviderService(rpcUrl, chain, mockLogger as Logger);
return new EvmProviderService(rpcUrl, mockChain, mockLogger as Logger);
},
},
],
Expand Down Expand Up @@ -287,4 +288,56 @@ describe("EvmProviderService", () => {
expect(returnValue).toEqual(arrayAbiFixture.args[0]);
});
});
describe("multicall", () => {
it("calls the multicall method of the Viem client with the correct arguments", async () => {
const contracts = [
{
address: "0x123456789",
abi: testAbi,
functionName: "balanceOf",
args: ["0x987654321"],
} as const,
{
address: "0x123456789",
abi: testAbi,
functionName: "tokenURI",
args: [1n],
} as const,
{
address: "0x987654321",
abi: testAbi,
functionName: "totalSupply",
args: [],
} as const,
];

const expectedReturnValue = [
{ result: 100n, status: true },
{ result: "tokenUri", status: true },
{ result: 1000n, status: true },
];
mockChain.contracts = { multicall3: { address: "0x123456789" } };
jest.spyOn(mockClient, "multicall").mockResolvedValue(expectedReturnValue);

const returnValue = await viemProvider.multicall({ contracts });

expect(returnValue).toEqual(expectedReturnValue);
expect(mockClient.multicall).toHaveBeenCalledWith({ contracts });
});

it("throws a MulticallNotFound error if the Multicall contract is not found for the chain", async () => {
const contracts = [
{
address: "0x123456789",
abi: testAbi,
functionName: "balanceOf",
args: ["0x987654321"],
} as const,
];

await expect(viemProvider.multicall({ contracts })).rejects.toThrowError(
MulticallNotFound,
);
});
});
});
3 changes: 3 additions & 0 deletions libs/shared/src/constants/addresses.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Address } from "abitype";

// This is the address given to native ETH on L2 chains.
// See: https://github.com/matter-labs/era-contracts/blob/8a70bbbc48125f5bde6189b4e3c6a3ee79631678/l1-contracts/contracts/common/Config.sol#L105
export const ETH_TOKEN_ADDRESS: Address = "0x0000000000000000000000000000000000000001";
export const vitalikAddress: Address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";

0 comments on commit 9238bcc

Please sign in to comment.