harden: bound the total network fee by gasLimit × fee, on both send paths (closes #399)
The two per-field ceilings in approvalVerify.js were checked independently, but the fee a validator is paid is gasLimit × fee per gas: a gas limit and a fee each under their own ceiling still multiply to thousands of ETH, which a gas-consuming contract really collects. assertWithinCeilings now also bounds that product against MAX_TOTAL_FEE (1 ETH), so both callers — populating the dApp transaction and verifying the signed artifact — refuse it with a full sentence naming the fee and the limit. The wallet's own send in confirmTx.js pinned no fee fields, so ethers filled them from the node with no bound; it now populates the transaction and runs the same check before signing, showing the same error in the confirmation screen's reserved errors box so nothing on screen moves. Model: opus-4-8
This commit was merged in pull request #411.
This commit is contained in:
@@ -51,6 +51,7 @@
|
||||
const {
|
||||
Transaction,
|
||||
accessListify,
|
||||
formatEther,
|
||||
getAddress,
|
||||
getBytes,
|
||||
verifyMessage,
|
||||
@@ -134,10 +135,19 @@ const FORBIDDEN_FIELDS = [
|
||||
const MAX_GAS_LIMIT = 100000000n;
|
||||
|
||||
// 100,000 gwei per gas: orders of magnitude above the highest fee either
|
||||
// supported network has produced, and low enough to catch a fee that would
|
||||
// hand the validator the balance.
|
||||
// supported network has produced.
|
||||
const MAX_FEE_PER_GAS = 100000000000000n;
|
||||
|
||||
// The largest total fee this wallet will sign, in wei. The two ceilings above
|
||||
// bound the gas limit and the price per gas each on its own, but the fee a
|
||||
// validator is actually paid is their product, and a gas limit and a price
|
||||
// that are each under their own ceiling still multiply to thousands of ETH —
|
||||
// 30,000,000 gas at 100,000 gwei is about 3,000 ETH. Bounding the product is
|
||||
// what catches a fee that would hand the validator the balance; the per-field
|
||||
// ceilings alone do not. A full 30,000,000-gas block at 33 gwei reaches this,
|
||||
// which no ordinary wallet transaction approaches.
|
||||
const MAX_TOTAL_FEE = 1000000000000000000n; // 1 ETH
|
||||
|
||||
// A refusal to act on an artifact: it is not the thing that was approved, so
|
||||
// the approval it was offered against is spent and must not be retried. Every
|
||||
// throw in this module is one of these; the background distinguishes them from
|
||||
@@ -384,6 +394,28 @@ function assertWithinCeilings(tx) {
|
||||
);
|
||||
}
|
||||
}
|
||||
// The product: gasLimit × the most this transaction could pay per gas —
|
||||
// maxFeePerGas for a type-2 transaction, gasPrice for a legacy or type-1
|
||||
// one. This is the fee a gas-consuming contract can really extract, and it
|
||||
// is the bound the two per-field ceilings above cannot express.
|
||||
if (present(tx.gasLimit)) {
|
||||
const gasLimit = normalizeQuantity(tx.gasLimit, "gas limit");
|
||||
let price = null;
|
||||
if (present(tx.maxFeePerGas)) {
|
||||
price = normalizeQuantity(tx.maxFeePerGas, "maximum fee per gas");
|
||||
} else if (present(tx.gasPrice)) {
|
||||
price = normalizeQuantity(tx.gasPrice, "gas price");
|
||||
}
|
||||
if (price !== null && gasLimit * price > MAX_TOTAL_FEE) {
|
||||
throw refuse(
|
||||
"This transaction would allow a network fee of up to " +
|
||||
formatEther(gasLimit * price) +
|
||||
" ETH, which is more than the " +
|
||||
formatEther(MAX_TOTAL_FEE) +
|
||||
" ETH this wallet will sign for.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refuse a field only a transaction type this wallet does not sign can carry.
|
||||
@@ -775,4 +807,5 @@ module.exports = {
|
||||
TX_STAGE_NONCE,
|
||||
MAX_GAS_LIMIT,
|
||||
MAX_FEE_PER_GAS,
|
||||
MAX_TOTAL_FEE,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user