Skip to content

Commit

Permalink
Enforce checksum address user inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
rileystephens28 committed Jun 5, 2024
1 parent 02670e9 commit c696899
Show file tree
Hide file tree
Showing 14 changed files with 377 additions and 230 deletions.
29 changes: 11 additions & 18 deletions src/address/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {

import type { SignatureLike } from '../crypto/index.js';

function getChecksumAddress(address: string): string {
export function formatMixedCaseChecksumAddress(address: string): string {
address = address.toLowerCase();

const chars = address.substring(2).split('');
Expand All @@ -39,11 +39,11 @@ function getChecksumAddress(address: string): string {
}

/**
* Returns a normalized and checksumed address for `address`. This accepts non-checksum addresses, checksum addresses
* and [[getIcapAddress]] formats.
* Returns a normalized and checksumed address for `address`. This accepts non-checksum addressesa and checksum
* addresses.
*
* The checksum in Ethereum uses the capitalization (upper-case vs lower-case) of the characters within an address to
* encode its checksum, which offers, on average, a checksum of 15-bits.
* The checksum in Quai uses the capitalization (upper-case vs lower-case) of the characters within an address to encode
* its checksum, which offers, on average, a checksum of 15-bits.
*
* If `address` contains both upper-case and lower-case, it is assumed to already be a checksum address and its checksum
* is validated, and if the address fails its expected checksum an error is thrown.
Expand All @@ -60,19 +60,11 @@ function getChecksumAddress(address: string): string {
* getAddress('0x8ba1f109551bd432803012645ac136ddd64dba72');
* //_result:
*
* // Converts ICAP address and adds checksum
* getAddress('XE65GB6LDNXYOFTX0NSV3FUWKOWIXAMJK36');
* //_result:
*
* // Throws an error if an address contains mixed case,
* // but the checksum fails
* getAddress('0x8Ba1f109551bD432803012645Ac136ddd64DBA72');
* //_error:
* ```
*
* @todo Revise this documentation as ICAP addresses are not supported
*
* @todo GetIcapAddress has been removed, link must be revised or removed
*/
export function getAddress(address: string): string {
assertArgument(typeof address === 'string', 'invalid address', 'address', address);
Expand All @@ -83,20 +75,21 @@ export function getAddress(address: string): string {
address = '0x' + address;
}

const result = getChecksumAddress(address);
const result = formatMixedCaseChecksumAddress(address);

// It is a checksummed address with a bad checksum
// If original address is mix cased and recomputed version doesn't
// match the original this could indicate a potential typo or mispaste.
assertArgument(
!address.match(/([A-F].*[a-f])|([a-f].*[A-F])/) || result === address,
'bad address checksum',
'invalid address checksum',
'address',
address,
);

return result;
}

assertArgument(false, 'invalid address', 'address', address);
assertArgument(false, 'invalid address string format', 'address', address);
}

export function getContractAddress(from: string, nonce: BigNumberish, data: BytesLike): string {
Expand Down Expand Up @@ -135,4 +128,4 @@ export function computeAddress(key: string | SigningKey): string {
*/
export function recoverAddress(digest: BytesLike, signature: SignatureLike): string {
return computeAddress(SigningKey.recoverPublicKey(digest, signature));
}
}
27 changes: 22 additions & 5 deletions src/address/checks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { assertArgument } from '../utils/index.js';

import { getAddress } from './address.js';
import { formatMixedCaseChecksumAddress, getAddress } from './address.js';

import type { Addressable, AddressLike } from './index.js';

Expand Down Expand Up @@ -63,7 +63,7 @@ async function checkAddress(target: any, promise: Promise<null | string>): Promi
if (result == null || result === '0x0000000000000000000000000000000000000000') {
assertArgument(false, 'invalid AddressLike value; did not resolve to a value address', 'target', target);
}
return getAddress(result);
return result;
}

/**
Expand All @@ -88,8 +88,6 @@ async function checkAddress(target: any, promise: Promise<null | string>): Promi
* contract = new Contract(addr, []);
* resolveAddress(contract, provider);
* //_result:
*
*
* ```
*
* @param {AddressLike} target - The target to resolve to an address.
Expand All @@ -100,7 +98,7 @@ async function checkAddress(target: any, promise: Promise<null | string>): Promi
export function resolveAddress(target: AddressLike): string | Promise<string> {
if (typeof target === 'string') {
if (target.match(/^0x[0-9a-f]{40}$/i)) {
return getAddress(target);
return target;
}
} else if (isAddressable(target)) {
return checkAddress(target, target.getAddress());
Expand All @@ -110,3 +108,22 @@ export function resolveAddress(target: AddressLike): string | Promise<string> {

assertArgument(false, 'unsupported addressable value', 'target', target);
}

/**
* Checks if the address is a valid mixed case checksummed address.
*
* @category Address
* @param address - The address to validate.
*
* @returns True if the address is a valid mixed case checksummed address.
*/
export function validateAddress(address: string): void {
assertArgument(typeof address === 'string', 'address must be string', 'address', address);
assertArgument(
Boolean(address.match(/^(0x)?[0-9a-fA-F]{40}$/)),
'invalid address string format',
'address',
address,
);
assertArgument(formatMixedCaseChecksumAddress(address) === address, 'invalid address checksum', 'address', address);
}
2 changes: 1 addition & 1 deletion src/address/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ export { getAddress, computeAddress, recoverAddress } from './address.js';

export { getCreateAddress, getCreate2Address } from './contract-address.js';

export { isAddressable, isAddress, resolveAddress } from './checks.js';
export { isAddressable, isAddress, resolveAddress, validateAddress } from './checks.js';
8 changes: 5 additions & 3 deletions src/contract/contract.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Interface, Typed } from '../abi/index.js';
import { isAddressable, resolveAddress } from '../address/index.js';
import { isAddressable, resolveAddress, validateAddress } from '../address/index.js';
// import from provider.ts instead of index.ts to prevent circular dep
// from quaiscanProvider
import {
Expand Down Expand Up @@ -204,9 +204,11 @@ function buildWrappedFallback(contract: BaseContract): WrappedFallback {

const tx: ContractTransaction = <any>await copyOverrides<'data'>(overrides, ['data']);
tx.to = await contract.getAddress();
validateAddress(tx.to);

if (tx.from) {
tx.from = await resolveAddress(tx.from);
validateAddress(tx.from);
}

const iface = contract.interface;
Expand Down Expand Up @@ -748,8 +750,8 @@ export class BaseContract implements Addressable, EventEmitterable<ContractEvent
/**
* The target to connect to.
*
* This can be an address or any [Addressable](../interfaces/Addressable), such as another contract. To
* get the resovled address, use the `getAddress` method.
* This can be an address or any [Addressable](../interfaces/Addressable), such as another contract. To get the
* resovled address, use the `getAddress` method.
*/
readonly target!: string | Addressable;

Expand Down
10 changes: 5 additions & 5 deletions src/contract/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { concat, defineProperties, getBytes, hexlify, assert, assertArgument } f
import { BaseContract, copyOverrides, resolveArgs } from './contract.js';

import type { InterfaceAbi } from '../abi/index.js';
import type { Addressable } from '../address/index.js';
import { validateAddress, type Addressable } from '../address/index.js';
import type { BytesLike } from '../utils/index.js';
import { getShardForAddress, isQiAddress } from '../utils/index.js';
import type { ContractInterface, ContractMethodArgs, ContractDeployTransaction, ContractRunner } from './types.js';
Expand Down Expand Up @@ -133,6 +133,7 @@ export class ContractFactory<A extends Array<any> = Array<any>, I = BaseContract
);

if (this.runner instanceof Wallet) {
validateAddress(this.runner.address);
tx.from = this.runner.address;
}
const grindedTx = await this.grindContractAddress(tx);
Expand Down Expand Up @@ -168,10 +169,9 @@ export class ContractFactory<A extends Array<any> = Array<any>, I = BaseContract
let i = 0;
const startingData = tx.data;
while (i < 10000) {
var contractAddress = getContractAddress(sender, BigInt(tx.nonce || 0), tx.data || '');
var contractShard = getShardForAddress(contractAddress);
console.log("contractAddress ", contractAddress);
var utxo = isQiAddress(contractAddress);
const contractAddress = getContractAddress(sender, BigInt(tx.nonce || 0), tx.data || '');
const contractShard = getShardForAddress(contractAddress);
const utxo = isQiAddress(contractAddress);
if (contractShard === toShard && !utxo) {
return tx;
}
Expand Down
2 changes: 1 addition & 1 deletion src/hash/typed-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ const domainChecks: Record<string, (value: any) => any> = {
},
verifyingContract: function (value: any) {
try {
return getAddress(value).toLowerCase();
return getAddress(value);
// eslint-disable-next-line no-empty
} catch (error) {}
assertArgument(false, `invalid domain value "verifyingContract"`, 'domain.verifyingContract', value);
Expand Down
32 changes: 15 additions & 17 deletions src/providers/abstract-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

// @TODO
// Event coalescence
// When we register an event with an async value (e.g. address is a Signer),
// When we register an event with an async value (e.g. address is a Signer),
// we need to add it immeidately for the Event API, but also
// need time to resolve the address. Upon resolving the address, we need to
// migrate the listener to the static event. We also need to maintain a map
Expand Down Expand Up @@ -74,11 +74,11 @@ import type {
Provider,
ProviderEvent,
TransactionRequest,
} from "./provider.js";
import { WorkObjectLike } from "../transaction/work-object.js";
import {QiTransaction, QuaiTransaction} from "../transaction/index.js";
import {QuaiTransactionResponseParams} from "./formatting.js";
import {keccak256, SigningKey} from "../crypto/index.js";
} from './provider.js';
import { WorkObjectLike } from '../transaction/work-object.js';
import { QiTransaction, QuaiTransaction } from '../transaction/index.js';
import { QuaiTransactionResponseParams } from './formatting.js';
import { keccak256, SigningKey } from '../crypto/index.js';

type Timer = ReturnType<typeof setTimeout>;

Expand Down Expand Up @@ -925,8 +925,8 @@ export class AbstractProvider<C = FetchRequest> implements Provider {
}

/**
* Returns or resolves to the address for `address`, resolving {@link Addressable | **Addressable**}
* objects and returning if already an address.
* Returns or resolves to the address for `address`, resolving {@link Addressable | **Addressable**} objects and
* returning if already an address.
*
* @param {AddressLike} address - The address to normalize.
*
Expand Down Expand Up @@ -985,8 +985,8 @@ export class AbstractProvider<C = FetchRequest> implements Provider {
}

/**
* Returns or resolves to a filter for `filter`, resolving any {@link Addressable | **Addressable**}
* object and returning if already a valid filter.
* Returns or resolves to a filter for `filter`, resolving any {@link Addressable | **Addressable**} object and
* returning if already a valid filter.
*
* @param {Filter | FilterByBlockHash} filter - The filter to normalize.
*
Expand Down Expand Up @@ -1086,8 +1086,8 @@ export class AbstractProvider<C = FetchRequest> implements Provider {
}

/**
* Returns or resovles to a transaction for `request`, resolving any
* {@link Addressable | **Addressable**} and returning if already a valid transaction.
* Returns or resovles to a transaction for `request`, resolving any {@link Addressable | **Addressable**} and
* returning if already a valid transaction.
*
* @param {PerformActionTransaction} _request - The transaction to normalize.
*
Expand All @@ -1107,10 +1107,8 @@ export class AbstractProvider<C = FetchRequest> implements Provider {
? 'address' in <any>request[key][0]
? (<TxOutput[]>(<any>request)[key]).map((it) => resolveAddress(hexlify(it.address)))
: (<TxInput[]>(<any>request)[key]).map((it) =>
resolveAddress(
getAddress(
keccak256('0x' + SigningKey.computePublicKey(it.pub_key).substring(4)).substring(26),
),
getAddress(
keccak256('0x' + SigningKey.computePublicKey(it.pub_key).substring(4)).substring(26),
),
)
: resolveAddress((<any>request)[key]);
Expand Down Expand Up @@ -1279,7 +1277,7 @@ export class AbstractProvider<C = FetchRequest> implements Provider {
async #call(tx: PerformActionTransaction, blockTag: string, attempt: number, shard?: string): Promise<string> {
// This came in as a PerformActionTransaction, so to/from are safe; we can cast
const transaction = <PerformActionTransaction>copyRequest(tx);
return hexlify(await this._perform({ method: "call", transaction, blockTag, shard }));
return hexlify(await this._perform({ method: 'call', transaction, blockTag, shard }));
}

// TODO: `shard` is not used, remove or re-write
Expand Down
1 change: 0 additions & 1 deletion src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ export class FeeData {
}

export function addressFromTransactionRequest(tx: TransactionRequest): AddressLike {
//return 'from' in tx ? tx.from : tx.inputs[0].address;
if ('from' in tx) {
return tx.from;
}
Expand Down
Loading

0 comments on commit c696899

Please sign in to comment.