Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add verifyContract address normalization #309

Merged
merged 12 commits into from
Jun 26, 2024
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"@metamask/json-rpc-engine": "^8.0.2",
"@metamask/rpc-errors": "^6.0.0",
"@metamask/utils": "^8.1.0",
"@types/bn.js": "^5.1.5",
"bn.js": "^5.2.1",
"klona": "^2.0.6",
"pify": "^5.0.0",
"safe-stable-stringify": "^2.4.3"
Expand Down
124 changes: 124 additions & 0 deletions src/utils/normalize.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { normalizeTypedMessage } from './normalize';

const MESSAGE_DATA_MOCK = {
types: {
Permit: [
{
name: 'owner',
type: 'address',
},
{
name: 'spender',
type: 'address',
},
{
name: 'value',
type: 'uint256',
},
{
name: 'nonce',
type: 'uint256',
},
{
name: 'deadline',
type: 'uint256',
},
],
EIP712Domain: [
{
name: 'name',
type: 'string',
},
{
name: 'version',
type: 'string',
},
{
name: 'chainId',
type: 'uint256',
},
{
name: 'verifyingContract',
type: 'address',
},
],
},
domain: {
name: 'Liquid staked Ether 2.0',
version: '2',
chainId: '0x1',
verifyingContract: '996101235222674412020337938588541139382869425796',
},
primaryType: 'Permit',
message: {
owner: '0x6d404afe1a6a07aa3cbcbf9fd027671df628ebfc',
spender: '0x63605E53D422C4F1ac0e01390AC59aAf84C44A51',
value:
'115792089237316195423570985008687907853269984665640564039457584007913129639935',
nonce: '0',
deadline: '4482689033',
},
};

describe('normalizeTypedMessage', () => {
function parseNormalizerResult(data: Record<string, unknown>) {
return JSON.parse(normalizeTypedMessage(JSON.stringify(data)));
}

it('should normalize verifyingContract address in domain', () => {
const normalizedData = parseNormalizerResult(MESSAGE_DATA_MOCK);
expect(normalizedData.domain.verifyingContract).toBe(
'0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
);
});

it('should handle octal verifyingContract address by normalizing it', () => {
const expectedNormalizedOctalAddress = '0x53';
const messageDataWithOctalAddress = {
...MESSAGE_DATA_MOCK,
domain: {
...MESSAGE_DATA_MOCK.domain,
verifyingContract: '0o123',
},
};

const normalizedData = parseNormalizerResult(messageDataWithOctalAddress);

expect(normalizedData.domain.verifyingContract).toBe(
expectedNormalizedOctalAddress,
);
});

it('should not modify if verifyingContract is already hexadecimal', () => {
const expectedVerifyingContract =
'0xae7ab96520de3a18e5e111b5eaab095312d7fe84';
const messageDataWithHexAddress = {
...MESSAGE_DATA_MOCK,
domain: {
...MESSAGE_DATA_MOCK.domain,
verifyingContract: expectedVerifyingContract,
},
};

const normalizedData = parseNormalizerResult(messageDataWithHexAddress);

expect(normalizedData.domain.verifyingContract).toBe(
expectedVerifyingContract,
);
});

it('should not modify other parts of the message data', () => {
const normalizedData = parseNormalizerResult(MESSAGE_DATA_MOCK);
expect(normalizedData.message).toStrictEqual(MESSAGE_DATA_MOCK.message);
expect(normalizedData.types).toStrictEqual(MESSAGE_DATA_MOCK.types);
expect(normalizedData.primaryType).toStrictEqual(
MESSAGE_DATA_MOCK.primaryType,
);
});

it('should return data as is if not parsable', () => {
expect(normalizeTypedMessage('Not parsable data')).toBe(
'Not parsable data',
);
});
});
94 changes: 94 additions & 0 deletions src/utils/normalize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { add0x, isValidHexAddress } from '@metamask/utils';
import type { Hex } from '@metamask/utils';
import BN from 'bn.js';

type EIP712Domain = {
verifyingContract: string;
};

type SignTypedMessageDataV3V4 = {
types: Record<string, unknown>;
domain: EIP712Domain;
primaryType: string;
message: unknown;
};

/**
* Normalizes the messageData for the eth_signTypedData

Check warning on line 17 in src/utils/normalize.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (16.x)

JSDoc description does not satisfy the regex pattern

Check warning on line 17 in src/utils/normalize.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

JSDoc description does not satisfy the regex pattern

Check warning on line 17 in src/utils/normalize.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

JSDoc description does not satisfy the regex pattern
*
* @param messageData - The messageData to normalize.
* @returns The normalized messageData.
*/
export function normalizeTypedMessage(messageData: string) {
let data;
try {
data = parseTypedMessage(
messageData,
) as unknown as SignTypedMessageDataV3V4;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using a type assertion it's good to know why it's necessary. After looking at parseTypedMessage I can see it's because of the JSON.parse. What are your thoughts on putting this type assertion in that function instead so it's more associated with the JSON.parse visually?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That make sense @mcmire, fixed in 2ebe685

} catch (e) {
// Ignore normalization errors and pass the message as is
return messageData;
}

const { verifyingContract } = data.domain ?? {};
digiwand marked this conversation as resolved.
Show resolved Hide resolved

if (!verifyingContract) {
return messageData;
}

data.domain.verifyingContract = normalizeContractAddress(verifyingContract);

return JSON.stringify(data);
}

/**
* Parses the messageData to obtain the data object for EIP712 normalization

Check warning on line 45 in src/utils/normalize.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (16.x)

JSDoc description does not satisfy the regex pattern

Check warning on line 45 in src/utils/normalize.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

JSDoc description does not satisfy the regex pattern

Check warning on line 45 in src/utils/normalize.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

JSDoc description does not satisfy the regex pattern
*
* @param data - The messageData to parse.
* @returns The data object for EIP712 normalization.
*/
function parseTypedMessage(data: string) {
if (typeof data !== 'string') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a test for this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in af2a8c2

return data;
}

try {
return JSON.parse(data);
} catch (e) {
throw new Error(`Invalid message data for normalization. data: ${data}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there value in throwing a custom error here since we catch it above anyway?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 4baeb92

}
}

/**
* Normalizes the address to a hexadecimal format

Check warning on line 63 in src/utils/normalize.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (16.x)

JSDoc description does not satisfy the regex pattern

Check warning on line 63 in src/utils/normalize.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

JSDoc description does not satisfy the regex pattern

Check warning on line 63 in src/utils/normalize.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

JSDoc description does not satisfy the regex pattern
*
* @param address - The address to normalize.
* @returns The normalized address.
*/
function normalizeContractAddress(address: string): Hex {
const addressHex = address as Hex;
if (isValidHexAddress(addressHex)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears that we need to use a type assertion because isValidHexAddress takes a Hex. However, we should be able to avoid the type assertion by using isStrictHexString, which narrows its argument to a Hex if given a hex string:

Suggested change
const addressHex = address as Hex;
if (isValidHexAddress(addressHex)) {
if (isStrictHexString(address) && isValidHexAddress(address)) {

That means we shouldn't need to use addressHex below, or upcast it to a string.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, done in a8e8e71

return addressHex;
}

// Check if the address is in octal format, convert to hexadecimal
if (addressHex.startsWith('0o')) {
// If octal, convert to hexadecimal
return octalToHex(addressHex as string);
}

// Check if the address is in decimal format, convert to hexadecimal
const parsedAddress = parseInt(addressHex, 10);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to be worried that this will be a large number? I see that you use BN below, but you seem to only use it for conversion (and then, only for decimal -> hex and not octal -> hex).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ba7ec73

if (!isNaN(parsedAddress)) {
const hexString = new BN(addressHex.toString(), 10).toString(16);
return add0x(hexString);
}

// Returning the original address without normalization
mcmire marked this conversation as resolved.
Show resolved Hide resolved
return addressHex;
}

function octalToHex(octalAddress: string): Hex {
const decimalAddress = parseInt(octalAddress.slice(2), 8).toString(16);
return add0x(decimalAddress);
}
4 changes: 4 additions & 0 deletions src/wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import pify from 'pify';
import type { TransactionParams, MessageParams, TypedMessageV1Params } from '.';
import { createWalletMiddleware } from '.';

jest.mock('./utils/normalize', () => ({
normalizeTypedMessage: jest.fn().mockImplementation((data) => data),
}));

const testAddresses = [
'0xbe93f9bacbcffc8ee6663f2647917ed7a20a57bb',
'0x1234362ef32bcd26d3dd18ca749378213625ba0b',
Expand Down
10 changes: 8 additions & 2 deletions src/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
} from '@metamask/utils';

import type { Block } from './types';
import { normalizeTypedMessage } from './utils/normalize';

/*
export type TransactionParams = {
Expand Down Expand Up @@ -276,7 +277,7 @@
const params = req.params as [string, string];

const address = await validateAndNormalizeKeyholder(params[0], req);
const message = params[1];
const message = normalizeTypedMessage(params[1]);
mcmire marked this conversation as resolved.
Show resolved Hide resolved
const version = 'V3';
const msgParams: TypedMessageParams = {
data: message,
Expand Down Expand Up @@ -306,7 +307,12 @@
const params = req.params as [string, string];

const address = await validateAndNormalizeKeyholder(params[0], req);
const message = params[1];
let message = params[1];
try {
message = normalizeTypedMessage(message);
} catch (e) {
// Ignore normalization errors and pass the message as is
}
const version = 'V4';
const msgParams: TypedMessageParams = {
data: message,
Expand Down Expand Up @@ -458,7 +464,7 @@
*
* @param address - The address to validate and normalize.
* @param req - The request object.
* @returns {string} - The normalized address, if valid. Otherwise, throws

Check warning on line 467 in src/wallet.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (16.x)

There must be no hyphen before @returns description

Check warning on line 467 in src/wallet.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

There must be no hyphen before @returns description

Check warning on line 467 in src/wallet.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

There must be no hyphen before @returns description
* an error
*/
async function validateAndNormalizeKeyholder(
Expand Down
18 changes: 18 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -958,12 +958,14 @@ __metadata:
"@metamask/json-rpc-engine": ^8.0.2
"@metamask/rpc-errors": ^6.0.0
"@metamask/utils": ^8.1.0
"@types/bn.js": ^5.1.5
"@types/btoa": ^1.2.3
"@types/jest": ^27.4.1
"@types/node": ^17.0.23
"@types/pify": ^5.0.2
"@typescript-eslint/eslint-plugin": ^5.42.1
"@typescript-eslint/parser": ^5.42.1
bn.js: ^5.2.1
eslint: ^8.44.0
eslint-config-prettier: ^8.1.0
eslint-plugin-import: ^2.27.5
Expand Down Expand Up @@ -1299,6 +1301,15 @@ __metadata:
languageName: node
linkType: hard

"@types/bn.js@npm:^5.1.5":
version: 5.1.5
resolution: "@types/bn.js@npm:5.1.5"
dependencies:
"@types/node": "*"
checksum: c87b28c4af74545624f8a3dae5294b16aa190c222626e8d4b2e327b33b1a3f1eeb43e7a24d914a9774bca43d8cd6e1cb0325c1f4b3a244af6693a024e1d918e6
languageName: node
linkType: hard

"@types/btoa@npm:^1.2.3":
version: 1.2.3
resolution: "@types/btoa@npm:1.2.3"
Expand Down Expand Up @@ -1982,6 +1993,13 @@ __metadata:
languageName: node
linkType: hard

"bn.js@npm:^5.2.1":
version: 5.2.1
resolution: "bn.js@npm:5.2.1"
checksum: 3dd8c8d38055fedfa95c1d5fc3c99f8dd547b36287b37768db0abab3c239711f88ff58d18d155dd8ad902b0b0cee973747b7ae20ea12a09473272b0201c9edd3
languageName: node
linkType: hard

"brace-expansion@npm:^1.1.7":
version: 1.1.11
resolution: "brace-expansion@npm:1.1.11"
Expand Down
Loading