UNPKG

ethers-opt

Version:

Collection of heavily optimized functions for ethers.js V6

222 lines (191 loc) 7.5 kB
import { verifyMerkleProof } from '@ethereumjs/mpt'; import { RLP } from '@ethereumjs/rlp'; import { Provider, keccak256, getBytes, toBigInt, AbiCoder, toNumber, hexlify } from 'ethers'; import { DataFeed, ERC20 } from '../typechain/index.js'; import { EIP1186Proof, getProof } from '../proof.js'; import { SignerWithAddress } from '../signer.js'; import { toEvenHex } from '../utils.js'; import { getAggregatorRoundId, RoundData } from '../price.js'; /** * Verifies the storage proof for a given contract's storage at a state root. * @param contractAddress Address of the contract to verify. * @param storageKey Storage slot to verify (hex string). * @param stateRoot The block state root. * @param proof The EIP-1186 proof object. * @returns Promise resolving to the storage root as a hex string on success, or throws on failure. */ export async function verifyStorageProof( contractAddress: string, storageKey: string, stateRoot: string, proof: EIP1186Proof, ) { const storageProof = proof.storageProof[0]; const storageProofValue = toEvenHex(storageProof.value); // 2. Verify account proof const accountRLP = await verifyMerkleProof( getBytes(contractAddress), proof.accountProof.map((n) => getBytes(n)), { useKeyHashing: true, root: getBytes(stateRoot), }, ); if (!accountRLP) { throw new Error('Account does not exist in state trie'); } // accountRLP = RLP([nonce, balance, storageRoot, codeHash]) const storageRoot = RLP.decode(accountRLP)[2] as Uint8Array; // 3. Verify storage proof const storageRLP = await verifyMerkleProof( getBytes(storageKey), storageProof.proof.map((n) => getBytes(n)), { useKeyHashing: true, root: storageRoot, }, ); if (!storageRLP) { throw new Error('Storage does not exist in state trie'); } const decodedValue = hexlify(RLP.decode(storageRLP as Uint8Array) as Uint8Array); if (decodedValue === storageProofValue) { return hexlify(storageRoot); } } const abiEncoder = AbiCoder.defaultAbiCoder(); /** * Included: Info about an individual storage/account proof. */ export interface ProofData { number: number; hash: string; stateRoot: string; storageKey: string; storageRoot: string; proof: EIP1186Proof; } /** * Verifies an ERC20 token balance proof at a specific block number. * @param erc20 ERC20 contract instance. * @param tokenBalanceSlot Storage slot for the token balance mapping. * @param owner Owner address or signer (whose balance is to be verified). * @param balance Optional expected balance (will auto-fetch if not given). * @param blockNumber Block number to verify at (current block if not specified). * @returns Resolves to proof data including tokenBalance, or throws if invalid. */ export async function verifyERC20Proof( erc20: unknown, tokenBalanceSlot: number | string, owner?: SignerWithAddress | string, balance?: bigint, blockNumber?: number, ): Promise<(ProofData & { tokenBalance: bigint }) | undefined> { const token = erc20 as ERC20; const provider = (token.runner?.provider || token.runner) as Provider; const ownerAddress = (owner as SignerWithAddress)?.address || (owner as string) || (token.runner as SignerWithAddress)?.address; if (!blockNumber) { blockNumber = await provider.getBlockNumber(); } const storageKey = typeof tokenBalanceSlot === 'number' ? keccak256(abiEncoder.encode(['address', 'uint256'], [ownerAddress, tokenBalanceSlot])) : tokenBalanceSlot; const [tokenBalance, block, proof] = await Promise.all([ balance ?? token.balanceOf(ownerAddress), provider.getBlock(blockNumber), getProof(provider, token.target as string, storageKey, blockNumber), ]); const { number, stateRoot, hash } = block || {}; const storageProof = proof.storageProof[0]; const storageProofValue = BigInt(storageProof.value); if (tokenBalance !== storageProofValue) { throw new Error(`Invalid storage value, wants ${tokenBalance} have ${storageProofValue}`); } const storageRoot = await verifyStorageProof( token.target as string, storageKey, stateRoot as string, proof, ); if (storageRoot) { return { number: number as number, hash: hash as string, stateRoot: stateRoot as string, storageKey, storageRoot, tokenBalance, proof, }; } } /** * Verifies proof for a Chainlink price feed (round data) at a given block. * @param _oracle DataFeed oracle contract instance. * @param oracleSlot Proof slot index for Chainlink transmission mapping. * @param aggregator (Optional) Oracle aggregator address. * @param expectedAnswers (Optional) Expected round data to check. * @param blockNumber (Optional) Block number to verify. * @returns Resolves to proof data and round info, or throws if invalid. */ export async function verifyChainlinkProof( _oracle: unknown, oracleSlot: number | string, aggregator?: string, expectedAnswers?: RoundData, blockNumber?: number, ): Promise<(ProofData & { aggregator: string; roundData: RoundData }) | undefined> { const oracle = _oracle as DataFeed; const provider = (oracle.runner?.provider || oracle.runner) as Provider; const [_blockNumber, _aggregator, roundData] = await Promise.all([ blockNumber || provider.getBlockNumber(), aggregator || oracle.aggregator(), expectedAnswers || oracle.latestRoundData(), ]); const aggregatorRoundId = getAggregatorRoundId(roundData.roundId); const storageKey = typeof oracleSlot === 'number' ? keccak256(abiEncoder.encode(['uint32', 'uint256'], [aggregatorRoundId, oracleSlot])) : oracleSlot; const [block, proof] = await Promise.all([ provider.getBlock(_blockNumber), getProof(provider, _aggregator, storageKey, _blockNumber), ]); const { number, stateRoot, hash } = block || {}; const transmissionsBytes = getBytes(proof.storageProof[0].value); const answer = toBigInt(transmissionsBytes.slice(8, 32)); const startedAt = toNumber(transmissionsBytes.slice(4, 8)); const updatedAt = toNumber(transmissionsBytes.slice(0, 4)); if ( answer !== roundData.answer || startedAt !== Number(roundData.startedAt) || updatedAt !== Number(roundData.updatedAt) ) { throw new Error( `Unexpected answer, wants ${JSON.stringify(roundData)} have ${JSON.stringify([answer, startedAt, updatedAt])}`, ); } const storageRoot = await verifyStorageProof(_aggregator, storageKey, stateRoot as string, proof); if (storageRoot) { return { number: number as number, hash: hash as string, aggregator: _aggregator, stateRoot: stateRoot as string, storageKey, storageRoot, roundData: { roundId: roundData.roundId, aggregatorRoundId, answer: roundData.answer, startedAt: Number(roundData.startedAt), updatedAt: Number(roundData.updatedAt), } as RoundData, proof, }; } }