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

chore: validate that custodians did not change parameters when signing #5

Merged
merged 1 commit into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/site/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Initial commit

[Unreleased]: https://github.com/MetaMask/snap-insitutional-wallet/compare/v0.0.1...HEAD
[0.0.1]: https://github.com/MetaMask/snap-insitutional-wallet/releases/tag/v0.0.1
[Unreleased]: https://github.com/MetaMask/snap-institutional-wallet/compare/v0.0.1...HEAD
[0.0.1]: https://github.com/MetaMask/snap-institutional-wallet/releases/tag/v0.0.1
4 changes: 2 additions & 2 deletions packages/snap/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Initial commit

[Unreleased]: https://github.com/MetaMask/snap-insitutional-wallet/compare/v0.0.1...HEAD
[0.0.1]: https://github.com/MetaMask/snap-insitutional-wallet/releases/tag/v0.0.1
[Unreleased]: https://github.com/MetaMask/snap-institutional-wallet/compare/v0.0.1...HEAD
[0.0.1]: https://github.com/MetaMask/snap-institutional-wallet/releases/tag/v0.0.1
3 changes: 2 additions & 1 deletion packages/snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "git+https://github.com/MetaMask/snap-institutional-wallet.git"
},
"source": {
"shasum": "pAM+McrqnMYKY0ctbnqvhb2POFwcXh5wdz8DnklnoXc=",
"shasum": "6KG5Omb+KFNCRvJ5uSSrIuAgJAGprnKFqj1PDPzsaN0=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down Expand Up @@ -35,6 +35,7 @@
"https://apps-portal.safe.global": {}
},
"initialPermissions": {
"snap_notify": {},
"endowment:network-access": {},
"endowment:ethereum-provider": {},
"endowment:keyring": {
Expand Down
15 changes: 15 additions & 0 deletions packages/snap/src/features/error-message/render.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Renders the onboarding interface.
*
* @param errorMessage - The error message to display.
* @returns The result of the dialog.
*/
export async function renderErrorMessage(errorMessage: string) {
await snap.request({
method: 'snap_notify',
params: {
type: 'inApp',
message: errorMessage,
},
});
}
99 changes: 89 additions & 10 deletions packages/snap/src/lib/helpers/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { TypedTransaction } from '@ethereumjs/tx';
import { TransactionFactory } from '@ethereumjs/tx';

import logger from '../../logger';
import { createCommon, formatTransactionData } from '../../util';
import { hexlify } from '../../util/hexlify';
import { TRANSACTION_TYPES } from '../constants';
import type { EthSignTransactionRequest } from '../types/EthSignTransactionRequest';
import type { ITransactionDetails } from '../types/ITransactionDetails';
import type { IEIP1559TxParams, ILegacyTXParams } from '../types/ITXParams';

Expand Down Expand Up @@ -38,19 +40,10 @@ export class TransactionHelper {
transaction: ITransactionDetails,
chainId: string,
): Promise<{ v: string; r: string; s: string }> {
const common = createCommon(transaction, chainId);

if (transaction.signedRawTransaction) {
logger.info('Transaction is signed', transaction.signedRawTransaction);
const signedRawTransaction = Buffer.from(
transaction.signedRawTransaction.substring(2),
'hex',
);

const tx = TransactionFactory.fromSerializedData(signedRawTransaction, {
common,
});

const tx = this.getTypedTransaction(transaction, chainId);
return {
v: hexlify(tx.v?.toString() ?? '0'),
r: hexlify(tx.r?.toString() ?? '0'),
Expand Down Expand Up @@ -78,4 +71,90 @@ export class TransactionHelper {
s: tx.s,
};
}

static getTypedTransaction(
transactionDetails: ITransactionDetails,
chainId: string,
): TypedTransaction {
if (!transactionDetails.signedRawTransaction) {
throw new Error('Transaction is not signed');
}

const common = createCommon(transactionDetails, chainId);

const signedRawTransaction = Buffer.from(
transactionDetails.signedRawTransaction.substring(2),
'hex',
);

const tx = TransactionFactory.fromSerializedData(signedRawTransaction, {
common,
});

return tx;
}

/**
* We need to be certain that the custodian did not alter any of the fields
* which was previously allowed in MMI but cannot work in the institutional snap
* because the snap keyring does not allow us to return anything other than the signature
*
* @param request - The original request we got from the keyring
* @param transactionDetails - The transaction details we got from the custodian
*/

static validateTransaction(
request: EthSignTransactionRequest,
transactionDetails: ITransactionDetails,
): {
isValid: boolean;
error?: string;
} {
const tx = this.getTypedTransaction(transactionDetails, request.chainId);
// Validated nonce, gas price, maxFeePerGas, maxPriorityFeePerGas and gas limit
if (Number(request.nonce) !== Number(tx.nonce)) {
return {
isValid: false,
error: `Custodian altered the nonce from the request: ${Number(
request.nonce,
)} to ${Number(tx.nonce)}`,
};
}
if (
'gasPrice' in request &&
'gasPrice' in tx &&
Number(request.gasPrice) !== Number(tx.gasPrice)
) {
return {
isValid: false,
error: `Custodian altered the gas price from the request: ${request.gasPrice} to ${tx.gasPrice}`,
};
}

if (
'maxFeePerGas' in request &&
'maxFeePerGas' in tx &&
Number(request.maxFeePerGas) !== Number(tx.maxFeePerGas)
) {
return {
isValid: false,
error: `Custodian altered the maxFeePerGas from the request: ${request.maxFeePerGas} to ${tx.maxFeePerGas}`,
};
}

if (
'maxPriorityFeePerGas' in request &&
'maxPriorityFeePerGas' in tx &&
Number(request.maxPriorityFeePerGas) !== Number(tx.maxPriorityFeePerGas)
) {
return {
isValid: false,
error: `Custodian altered the maxPriorityFeePerGas from the request: ${request.maxPriorityFeePerGas} to ${tx.maxPriorityFeePerGas}`,
};
}

return {
isValid: true,
};
}
}
33 changes: 27 additions & 6 deletions packages/snap/src/requestsManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { emitSnapKeyringEvent, KeyringEvent } from '@metamask/keyring-api';
import type { Json } from '@metamask/snaps-sdk';

import { renderErrorMessage } from './features/error-message/render';
import { TransactionHelper } from './lib/helpers/transaction';
import type { ICustodianApi } from './lib/types';
import type {
Expand Down Expand Up @@ -48,6 +49,15 @@ export class RequestManager {
}

async getChainIdFromPendingRequest(id: string): Promise<string> {
const transactionRequest = this.getRequestParams(id);
if (!transactionRequest.chainId) {
throw new Error(`Request ${id} has no chainId`);
}

return transactionRequest.chainId;
}

getRequestParams(id: string): EthSignTransactionRequest {
if (!this.#state.requests[id]) {
throw new Error(`Request ${id} not found`);
}
Expand All @@ -59,12 +69,7 @@ export class RequestManager {
throw new Error(`Request ${id} has invalid params`);
}

const transactionRequest = requestParams[0] as EthSignTransactionRequest;
if (!transactionRequest.chainId) {
throw new Error(`Request ${id} has no chainId`);
}

return transactionRequest.chainId;
return requestParams[0] as EthSignTransactionRequest;
}

async clearAllRequests(): Promise<void> {
Expand Down Expand Up @@ -145,6 +150,22 @@ export class RequestManager {
chainId,
);

const validationResult = TransactionHelper.validateTransaction(
this.getRequestParams(requestId),
transactionResponse,
);

if (!validationResult.isValid) {
// First show a dialog with the error message
if (validationResult.error) {
const errorMessage = `Transaction ${custodianTransactionId} was signed by custodian but failed validation: ${validationResult.error}`;
await renderErrorMessage(errorMessage);
}
await this.emitRejectedEvent(requestId);
await this.removePendingRequest(requestId);
return;
}

const updatedTransaction = {
...request,
fulfilled: true,
Expand Down
Loading