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 7, 2024
1 parent d1c805b commit 9632a40
Show file tree
Hide file tree
Showing 14 changed files with 295 additions and 165 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 @@ -205,9 +205,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 @@ -749,8 +751,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
4 changes: 2 additions & 2 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 { getZoneForAddress, 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 @@ -170,7 +171,6 @@ export class ContractFactory<A extends Array<any> = Array<any>, I = BaseContract
while (i < 10000) {
const contractAddress = getContractAddress(sender, BigInt(tx.nonce || 0), tx.data || '');
const contractShard = getZoneForAddress(contractAddress);
console.log('contractAddress ', 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
6 changes: 2 additions & 4 deletions src/providers/abstract-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1100,10 +1100,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
1 change: 0 additions & 1 deletion src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,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 9632a40

Please sign in to comment.