UNPKG

check-ethereum-scanner

Version:

Safety checks for new Ethereum tokens

379 lines (329 loc) 11.4 kB
import { RLP } from '@ethereumjs/rlp' import { keccak256 } from 'ethereum-cryptography/keccak' import { secp256k1 } from 'ethereum-cryptography/secp256k1' import { bytesToHex } from 'ethereum-cryptography/utils' import { arrToBufArr, bigIntToUnpaddedBuffer, bufArrToArr, bufferToBigInt, bufferToHex, toBuffer, zeros, } from './bytes' import { KECCAK256_NULL, KECCAK256_RLP } from './constants' import { assertIsBuffer, assertIsHexString, assertIsString } from './helpers' import { stripHexPrefix } from './internal' import type { BigIntLike, BufferLike } from './types' const _0n = BigInt(0) export interface AccountData { nonce?: BigIntLike balance?: BigIntLike storageRoot?: BufferLike codeHash?: BufferLike } export type AccountBodyBuffer = [Buffer, Buffer, Buffer | Uint8Array, Buffer | Uint8Array] export class Account { nonce: bigint balance: bigint storageRoot: Buffer codeHash: Buffer static fromAccountData(accountData: AccountData) { const { nonce, balance, storageRoot, codeHash } = accountData return new Account( nonce !== undefined ? bufferToBigInt(toBuffer(nonce)) : undefined, balance !== undefined ? bufferToBigInt(toBuffer(balance)) : undefined, storageRoot !== undefined ? toBuffer(storageRoot) : undefined, codeHash !== undefined ? toBuffer(codeHash) : undefined ) } public static fromRlpSerializedAccount(serialized: Buffer) { const values = arrToBufArr(RLP.decode(Uint8Array.from(serialized)) as Uint8Array[]) as Buffer[] if (!Array.isArray(values)) { throw new Error('Invalid serialized account input. Must be array') } return this.fromValuesArray(values) } public static fromValuesArray(values: Buffer[]) { const [nonce, balance, storageRoot, codeHash] = values return new Account(bufferToBigInt(nonce), bufferToBigInt(balance), storageRoot, codeHash) } /** * This constructor assigns and validates the values. * Use the static factory methods to assist in creating an Account from varying data types. */ constructor(nonce = _0n, balance = _0n, storageRoot = KECCAK256_RLP, codeHash = KECCAK256_NULL) { this.nonce = nonce this.balance = balance this.storageRoot = storageRoot this.codeHash = codeHash this._validate() } private _validate() { if (this.nonce < _0n) { throw new Error('nonce must be greater than zero') } if (this.balance < _0n) { throw new Error('balance must be greater than zero') } if (this.storageRoot.length !== 32) { throw new Error('storageRoot must have a length of 32') } if (this.codeHash.length !== 32) { throw new Error('codeHash must have a length of 32') } } /** * Returns a Buffer Array of the raw Buffers for the account, in order. */ raw(): Buffer[] { return [ bigIntToUnpaddedBuffer(this.nonce), bigIntToUnpaddedBuffer(this.balance), this.storageRoot, this.codeHash, ] } /** * Returns the RLP serialization of the account as a `Buffer`. */ serialize(): Buffer { return Buffer.from(RLP.encode(bufArrToArr(this.raw()))) } /** * Returns a `Boolean` determining if the account is a contract. */ isContract(): boolean { return !this.codeHash.equals(KECCAK256_NULL) } /** * Returns a `Boolean` determining if the account is empty complying to the definition of * account emptiness in [EIP-161](https://eips.ethereum.org/EIPS/eip-161): * "An account is considered empty when it has no code and zero nonce and zero balance." */ isEmpty(): boolean { return this.balance === _0n && this.nonce === _0n && this.codeHash.equals(KECCAK256_NULL) } } /** * Checks if the address is a valid. Accepts checksummed addresses too. */ export const isValidAddress = function (hexAddress: string): boolean { try { assertIsString(hexAddress) } catch (e: any) { return false } return /^0x[0-9a-fA-F]{40}$/.test(hexAddress) } /** * Returns a checksummed address. * * If an eip1191ChainId is provided, the chainId will be included in the checksum calculation. This * has the effect of checksummed addresses for one chain having invalid checksums for others. * For more details see [EIP-1191](https://eips.ethereum.org/EIPS/eip-1191). * * WARNING: Checksums with and without the chainId will differ and the EIP-1191 checksum is not * backwards compatible to the original widely adopted checksum format standard introduced in * [EIP-55](https://eips.ethereum.org/EIPS/eip-55), so this will break in existing applications. * Usage of this EIP is therefore discouraged unless you have a very targeted use case. */ export const toChecksumAddress = function ( hexAddress: string, eip1191ChainId?: BigIntLike ): string { assertIsHexString(hexAddress) const address = stripHexPrefix(hexAddress).toLowerCase() let prefix = '' if (eip1191ChainId !== undefined) { const chainId = bufferToBigInt(toBuffer(eip1191ChainId)) prefix = chainId.toString() + '0x' } const buf = Buffer.from(prefix + address, 'utf8') const hash = bytesToHex(keccak256(buf)) let ret = '0x' for (let i = 0; i < address.length; i++) { if (parseInt(hash[i], 16) >= 8) { ret += address[i].toUpperCase() } else { ret += address[i] } } return ret } /** * Checks if the address is a valid checksummed address. * * See toChecksumAddress' documentation for details about the eip1191ChainId parameter. */ export const isValidChecksumAddress = function ( hexAddress: string, eip1191ChainId?: BigIntLike ): boolean { return isValidAddress(hexAddress) && toChecksumAddress(hexAddress, eip1191ChainId) === hexAddress } /** * Generates an address of a newly created contract. * @param from The address which is creating this new address * @param nonce The nonce of the from account */ export const generateAddress = function (from: Buffer, nonce: Buffer): Buffer { assertIsBuffer(from) assertIsBuffer(nonce) if (bufferToBigInt(nonce) === BigInt(0)) { // in RLP we want to encode null in the case of zero nonce // read the RLP documentation for an answer if you dare return Buffer.from(keccak256(RLP.encode(bufArrToArr([from, null] as any)))).slice(-20) } // Only take the lower 160bits of the hash return Buffer.from(keccak256(RLP.encode(bufArrToArr([from, nonce])))).slice(-20) } /** * Generates an address for a contract created using CREATE2. * @param from The address which is creating this new address * @param salt A salt * @param initCode The init code of the contract being created */ export const generateAddress2 = function (from: Buffer, salt: Buffer, initCode: Buffer): Buffer { assertIsBuffer(from) assertIsBuffer(salt) assertIsBuffer(initCode) if (from.length !== 20) { throw new Error('Expected from to be of length 20') } if (salt.length !== 32) { throw new Error('Expected salt to be of length 32') } const address = keccak256( Buffer.concat([Buffer.from('ff', 'hex'), from, salt, keccak256(initCode)]) ) return toBuffer(address).slice(-20) } /** * Checks if the private key satisfies the rules of the curve secp256k1. */ export const isValidPrivate = function (privateKey: Buffer): boolean { return secp256k1.utils.isValidPrivateKey(privateKey) } /** * Checks if the public key satisfies the rules of the curve secp256k1 * and the requirements of Ethereum. * @param publicKey The two points of an uncompressed key, unless sanitize is enabled * @param sanitize Accept public keys in other formats */ export const isValidPublic = function (publicKey: Buffer, sanitize: boolean = false): boolean { assertIsBuffer(publicKey) if (publicKey.length === 64) { // Convert to SEC1 for secp256k1 // Automatically checks whether point is on curve try { secp256k1.ProjectivePoint.fromHex(Buffer.concat([Buffer.from([4]), publicKey])) return true } catch (e) { return false } } if (!sanitize) { return false } try { secp256k1.ProjectivePoint.fromHex(publicKey) return true } catch (e) { return false } } /** * Returns the ethereum address of a given public key. * Accepts "Ethereum public keys" and SEC1 encoded keys. * @param pubKey The two points of an uncompressed key, unless sanitize is enabled * @param sanitize Accept public keys in other formats */ export const pubToAddress = function (pubKey: Buffer, sanitize: boolean = false): Buffer { assertIsBuffer(pubKey) if (sanitize && pubKey.length !== 64) { pubKey = Buffer.from(secp256k1.ProjectivePoint.fromHex(pubKey).toRawBytes(false).slice(1)) } if (pubKey.length !== 64) { throw new Error('Expected pubKey to be of length 64') } // Only take the lower 160bits of the hash return Buffer.from(keccak256(pubKey)).slice(-20) } export const publicToAddress = pubToAddress /** * Returns the ethereum public key of a given private key. * @param privateKey A private key must be 256 bits wide */ export const privateToPublic = function (privateKey: Buffer): Buffer { assertIsBuffer(privateKey) // skip the type flag and use the X, Y points return Buffer.from( secp256k1.ProjectivePoint.fromPrivateKey(privateKey).toRawBytes(false).slice(1) ) } /** * Returns the ethereum address of a given private key. * @param privateKey A private key must be 256 bits wide */ export const privateToAddress = function (privateKey: Buffer): Buffer { return publicToAddress(privateToPublic(privateKey)) } /** * Converts a public key to the Ethereum format. */ export const importPublic = function (publicKey: Buffer): Buffer { assertIsBuffer(publicKey) if (publicKey.length !== 64) { publicKey = Buffer.from(secp256k1.ProjectivePoint.fromHex(publicKey).toRawBytes(false).slice(1)) } return publicKey } /** * Returns the zero address. */ export const zeroAddress = function (): string { const addressLength = 20 const addr = zeros(addressLength) return bufferToHex(addr) } /** * Checks if a given address is the zero address. */ export const isZeroAddress = function (hexAddress: string): boolean { try { assertIsString(hexAddress) } catch (e: any) { return false } const zeroAddr = zeroAddress() return zeroAddr === hexAddress } export function accountBodyFromSlim(body: AccountBodyBuffer) { const [nonce, balance, storageRoot, codeHash] = body return [ nonce, balance, arrToBufArr(storageRoot).length === 0 ? KECCAK256_RLP : storageRoot, arrToBufArr(codeHash).length === 0 ? KECCAK256_NULL : codeHash, ] } const emptyUint8Arr = new Uint8Array(0) export function accountBodyToSlim(body: AccountBodyBuffer) { const [nonce, balance, storageRoot, codeHash] = body return [ nonce, balance, arrToBufArr(storageRoot).equals(KECCAK256_RLP) ? emptyUint8Arr : storageRoot, arrToBufArr(codeHash).equals(KECCAK256_NULL) ? emptyUint8Arr : codeHash, ] } /** * Converts a slim account (per snap protocol spec) to the RLP encoded version of the account * @param body Array of 4 Buffer-like items to represent the account * @returns RLP encoded version of the account */ export function accountBodyToRLP(body: AccountBodyBuffer, couldBeSlim = true) { const accountBody = couldBeSlim ? accountBodyFromSlim(body) : body return arrToBufArr(RLP.encode(accountBody)) }