UNPKG

check-ethereum-scanner

Version:

Safety checks for new Ethereum tokens

490 lines (434 loc) 14.1 kB
import Common, { Chain, Hardfork } from '@ethereumjs/common' import { Address, BN, toBuffer, MAX_INTEGER, MAX_UINT64, unpadBuffer, ecsign, publicToAddress, BNLike, bufferToHex, } from 'ethereumjs-util' import { TxData, JsonTx, AccessListEIP2930ValuesArray, AccessListEIP2930TxData, FeeMarketEIP1559ValuesArray, FeeMarketEIP1559TxData, TxValuesArray, Capability, TxOptions, } from './types' interface TransactionCache { hash: Buffer | undefined dataFee?: { value: BN hardfork: string | Hardfork } } /** * This base class will likely be subject to further * refactoring along the introduction of additional tx types * on the Ethereum network. * * It is therefore not recommended to use directly. */ export abstract class BaseTransaction<TransactionObject> { private readonly _type: number public readonly nonce: BN public readonly gasLimit: BN public readonly to?: Address public readonly value: BN public readonly data: Buffer public readonly v?: BN public readonly r?: BN public readonly s?: BN public readonly common!: Common protected cache: TransactionCache = { hash: undefined, dataFee: undefined, } protected readonly txOptions: TxOptions /** * List of tx type defining EIPs, * e.g. 1559 (fee market) and 2930 (access lists) * for FeeMarketEIP1559Transaction objects */ protected activeCapabilities: number[] = [] /** * The default chain the tx falls back to if no Common * is provided and if the chain can't be derived from * a passed in chainId (only EIP-2718 typed txs) or * EIP-155 signature (legacy txs). * * @hidden */ protected DEFAULT_CHAIN = Chain.Mainnet /** * The default HF if the tx type is active on that HF * or the first greater HF where the tx is active. * * @hidden */ protected DEFAULT_HARDFORK: string | Hardfork = Hardfork.Istanbul constructor(txData: TxData | AccessListEIP2930TxData | FeeMarketEIP1559TxData, opts: TxOptions) { const { nonce, gasLimit, to, value, data, v, r, s, type } = txData this._type = new BN(toBuffer(type)).toNumber() this.txOptions = opts const toB = toBuffer(to === '' ? '0x' : to) const vB = toBuffer(v === '' ? '0x' : v) const rB = toBuffer(r === '' ? '0x' : r) const sB = toBuffer(s === '' ? '0x' : s) this.nonce = new BN(toBuffer(nonce === '' ? '0x' : nonce)) this.gasLimit = new BN(toBuffer(gasLimit === '' ? '0x' : gasLimit)) this.to = toB.length > 0 ? new Address(toB) : undefined this.value = new BN(toBuffer(value === '' ? '0x' : value)) this.data = toBuffer(data === '' ? '0x' : data) this.v = vB.length > 0 ? new BN(vB) : undefined this.r = rB.length > 0 ? new BN(rB) : undefined this.s = sB.length > 0 ? new BN(sB) : undefined this._validateCannotExceedMaxInteger({ value: this.value, r: this.r, s: this.s }) // geth limits gasLimit to 2^64-1 this._validateCannotExceedMaxInteger({ gasLimit: this.gasLimit }, 64) // EIP-2681 limits nonce to 2^64-1 (cannot equal 2^64-1) this._validateCannotExceedMaxInteger({ nonce: this.nonce }, 64, true) } /** * Alias for {@link BaseTransaction.type} * * @deprecated Use `type` instead */ get transactionType(): number { return this.type } /** * Returns the transaction type. * * Note: legacy txs will return tx type `0`. */ get type() { return this._type } /** * Checks if a tx type defining capability is active * on a tx, for example the EIP-1559 fee market mechanism * or the EIP-2930 access list feature. * * Note that this is different from the tx type itself, * so EIP-2930 access lists can very well be active * on an EIP-1559 tx for example. * * This method can be useful for feature checks if the * tx type is unknown (e.g. when instantiated with * the tx factory). * * See `Capabilites` in the `types` module for a reference * on all supported capabilities. */ supports(capability: Capability) { return this.activeCapabilities.includes(capability) } /** * Checks if the transaction has the minimum amount of gas required * (DataFee + TxFee + Creation Fee). */ validate(): boolean validate(stringError: false): boolean validate(stringError: true): string[] validate(stringError: boolean = false): boolean | string[] { const errors = [] if (this.getBaseFee().gt(this.gasLimit)) { errors.push(`gasLimit is too low. given ${this.gasLimit}, need at least ${this.getBaseFee()}`) } if (this.isSigned() && !this.verifySignature()) { errors.push('Invalid Signature') } return stringError ? errors : errors.length === 0 } /** * The minimum amount of gas the tx must have (DataFee + TxFee + Creation Fee) */ getBaseFee(): BN { const fee = this.getDataFee().addn(this.common.param('gasPrices', 'tx')) if (this.common.gteHardfork('homestead') && this.toCreationAddress()) { fee.iaddn(this.common.param('gasPrices', 'txCreation')) } return fee } /** * The amount of gas paid for the data in this tx */ getDataFee(): BN { const txDataZero = this.common.param('gasPrices', 'txDataZero') const txDataNonZero = this.common.param('gasPrices', 'txDataNonZero') let cost: number | BN = 0 for (let i = 0; i < this.data.length; i++) { this.data[i] === 0 ? (cost += txDataZero) : (cost += txDataNonZero) } cost = new BN(cost) if ((this.to === undefined || this.to === null) && this.common.isActivatedEIP(3860)) { const dataLength = Math.ceil(this.data.length / 32) const initCodeCost = new BN(this.common.param('gasPrices', 'initCodeWordCost')).imuln( dataLength ) cost.iadd(initCodeCost) } return cost } /** * The up front amount that an account must have for this transaction to be valid */ abstract getUpfrontCost(): BN /** * If the tx's `to` is to the creation address */ toCreationAddress(): boolean { return this.to === undefined || this.to.buf.length === 0 } /** * Returns a Buffer Array of the raw Buffers of this transaction, in order. * * Use {@link BaseTransaction.serialize} to add a transaction to a block * with {@link Block.fromValuesArray}. * * For an unsigned tx this method uses the empty Buffer values for the * signature parameters `v`, `r` and `s` for encoding. For an EIP-155 compliant * representation for external signing use {@link BaseTransaction.getMessageToSign}. */ abstract raw(): TxValuesArray | AccessListEIP2930ValuesArray | FeeMarketEIP1559ValuesArray /** * Returns the encoding of the transaction. */ abstract serialize(): Buffer // Returns the unsigned tx (hashed or raw), which is used to sign the transaction. // // Note: do not use code docs here since VS Studio is then not able to detect the // comments from the inherited methods abstract getMessageToSign(hashMessage: false): Buffer | Buffer[] abstract getMessageToSign(hashMessage?: true): Buffer abstract hash(): Buffer abstract getMessageToVerifySignature(): Buffer public isSigned(): boolean { const { v, r, s } = this if (this.type === 0) { if (!v || !r || !s) { return false } else { return true } } else { if (v === undefined || !r || !s) { return false } else { return true } } } /** * Determines if the signature is valid */ verifySignature(): boolean { try { // Main signature verification is done in `getSenderPublicKey()` const publicKey = this.getSenderPublicKey() return unpadBuffer(publicKey).length !== 0 } catch (e: any) { return false } } /** * Returns the sender's address */ getSenderAddress(): Address { return new Address(publicToAddress(this.getSenderPublicKey())) } /** * Returns the public key of the sender */ abstract getSenderPublicKey(): Buffer /** * Signs a transaction. * * Note that the signed tx is returned as a new object, * use as follows: * ```javascript * const signedTx = tx.sign(privateKey) * ``` */ sign(privateKey: Buffer): TransactionObject { if (privateKey.length !== 32) { const msg = this._errorMsg('Private key must be 32 bytes in length.') throw new Error(msg) } // Hack for the constellation that we have got a legacy tx after spuriousDragon with a non-EIP155 conforming signature // and want to recreate a signature (where EIP155 should be applied) // Leaving this hack lets the legacy.spec.ts -> sign(), verifySignature() test fail // 2021-06-23 let hackApplied = false if ( this.type === 0 && this.common.gteHardfork('spuriousDragon') && !this.supports(Capability.EIP155ReplayProtection) ) { this.activeCapabilities.push(Capability.EIP155ReplayProtection) hackApplied = true } const msgHash = this.getMessageToSign(true) const { v, r, s } = ecsign(msgHash, privateKey) const tx = this._processSignature(v, r, s) // Hack part 2 if (hackApplied) { const index = this.activeCapabilities.indexOf(Capability.EIP155ReplayProtection) if (index > -1) { this.activeCapabilities.splice(index, 1) } } return tx } /** * Returns an object with the JSON representation of the transaction */ abstract toJSON(): JsonTx // Accept the v,r,s values from the `sign` method, and convert this into a TransactionObject protected abstract _processSignature(v: number, r: Buffer, s: Buffer): TransactionObject /** * Does chain ID checks on common and returns a common * to be used on instantiation * @hidden * * @param common - {@link Common} instance from tx options * @param chainId - Chain ID from tx options (typed txs) or signature (legacy tx) */ protected _getCommon(common?: Common, chainId?: BNLike) { // Chain ID provided if (chainId) { const chainIdBN = new BN(toBuffer(chainId)) if (common) { if (!common.chainIdBN().eq(chainIdBN)) { const msg = this._errorMsg('The chain ID does not match the chain ID of Common') throw new Error(msg) } // Common provided, chain ID does match // -> Return provided Common return common.copy() } else { if (Common.isSupportedChainId(chainIdBN)) { // No Common, chain ID supported by Common // -> Instantiate Common with chain ID return new Common({ chain: chainIdBN, hardfork: this.DEFAULT_HARDFORK }) } else { // No Common, chain ID not supported by Common // -> Instantiate custom Common derived from DEFAULT_CHAIN return Common.forCustomChain( this.DEFAULT_CHAIN, { name: 'custom-chain', networkId: chainIdBN, chainId: chainIdBN, }, this.DEFAULT_HARDFORK ) } } } else { // No chain ID provided // -> return Common provided or create new default Common return ( common?.copy() ?? new Common({ chain: this.DEFAULT_CHAIN, hardfork: this.DEFAULT_HARDFORK }) ) } } /** * Validates that an object with BN values cannot exceed the specified bit limit. * @param values Object containing string keys and BN values * @param bits Number of bits to check (64 or 256) * @param cannotEqual Pass true if the number also cannot equal one less the maximum value */ protected _validateCannotExceedMaxInteger( values: { [key: string]: BN | undefined }, bits = 256, cannotEqual = false ) { for (const [key, value] of Object.entries(values)) { switch (bits) { case 64: if (cannotEqual) { if (value?.gte(MAX_UINT64)) { const msg = this._errorMsg( `${key} cannot equal or exceed MAX_UINT64 (2^64-1), given ${value}` ) throw new Error(msg) } } else { if (value?.gt(MAX_UINT64)) { const msg = this._errorMsg(`${key} cannot exceed MAX_UINT64 (2^64-1), given ${value}`) throw new Error(msg) } } break case 256: if (cannotEqual) { if (value?.gte(MAX_INTEGER)) { const msg = this._errorMsg( `${key} cannot equal or exceed MAX_INTEGER (2^256-1), given ${value}` ) throw new Error(msg) } } else { if (value?.gt(MAX_INTEGER)) { const msg = this._errorMsg( `${key} cannot exceed MAX_INTEGER (2^256-1), given ${value}` ) throw new Error(msg) } } break default: { const msg = this._errorMsg('unimplemented bits value') throw new Error(msg) } } } } /** * Return a compact error string representation of the object */ public abstract errorStr(): string /** * Internal helper function to create an annotated error message * * @param msg Base error message * @hidden */ protected abstract _errorMsg(msg: string): string /** * Returns the shared error postfix part for _error() method * tx type implementations. */ protected _getSharedErrorPostfix() { let hash = '' try { hash = this.isSigned() ? bufferToHex(this.hash()) : 'not available (unsigned)' } catch (e: any) { hash = 'error' } let isSigned = '' try { isSigned = this.isSigned().toString() } catch (e: any) { hash = 'error' } let hf = '' try { hf = this.common.hardfork() } catch (e: any) { hf = 'error' } let postfix = `tx type=${this.type} hash=${hash} nonce=${this.nonce} value=${this.value} ` postfix += `signed=${isSigned} hf=${hf}` return postfix } }