harden: verify all approval fields and make failed signing retryable (closes #174)
All checks were successful
check / check (push) Successful in 30s
All checks were successful
check / check (push) Successful in 30s
approvalVerify now compares every field of the signed artifact against the approval, not a subset. Transaction types are allowlisted to 0/1/2 and any field the module does not check is refused outright, so a future transaction type cannot smuggle consequential fields past verification -- an EIP-7702 type-4 artifact that delegates the signer's own EOA while matching every displayed field was accepted before this change. The serialized bytes handed to broadcastTransaction are compared against the parsed artifact, so the guarantee covers the bytes that actually go to the node. Signing failures in the popup are retryable again. To make that safe, an approval is claimed synchronously before the first await and every path that resolves or removes one goes through a single chokepoint that refuses a claimed approval. Without it, closing the approval window, switching the active address or a late reject would report "User rejected the request." to the dApp while the broadcast completed -- the user then redoes the transfer at a fresh nonce and it sends twice. Failure copy distinguishes the stage reached, so a user is never told to start again from the site when the first attempt may already have reached the network.
This commit was merged in pull request #205.
This commit is contained in:
@@ -24,6 +24,7 @@ const { decryptWithPassword } = require("../../shared/vault");
|
||||
const { getSignerForAddress } = require("../../shared/wallet");
|
||||
const { walletDefect } = require("../../shared/walletDefects");
|
||||
const { getProvider } = require("../../shared/balances");
|
||||
const { describeSigningFailure } = require("../../shared/approvalVerify");
|
||||
const txStatus = require("./txStatus");
|
||||
const uniswap = require("../../shared/uniswap");
|
||||
const runtime =
|
||||
@@ -589,10 +590,20 @@ function init(ctx) {
|
||||
runtime.sendMessage(payload, (response) => {
|
||||
if (response && response.txHash) {
|
||||
txStatus.showWait(pendingTxDetails, response.txHash);
|
||||
return;
|
||||
}
|
||||
// A retryable failure leaves the approval pending in the
|
||||
// background, so stay on this screen with a live button rather
|
||||
// than sending the user to a dead end.
|
||||
const outcome = describeSigningFailure(
|
||||
response,
|
||||
"The transaction could not be sent.",
|
||||
);
|
||||
if (outcome.retryable) {
|
||||
showError("approve-tx-error", outcome.message);
|
||||
setTxButtonBusy(false);
|
||||
} else {
|
||||
const msg =
|
||||
(response && response.error) || "Transaction failed.";
|
||||
txStatus.showError(pendingTxDetails, null, msg);
|
||||
txStatus.showError(pendingTxDetails, null, outcome.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -695,11 +706,18 @@ function init(ctx) {
|
||||
runtime.sendMessage(payload, (response) => {
|
||||
if (response && response.signature) {
|
||||
window.close();
|
||||
} else {
|
||||
const msg = (response && response.error) || "Signing failed.";
|
||||
showError("approve-sign-error", msg);
|
||||
setSignButtonBusy(false);
|
||||
return;
|
||||
}
|
||||
// The button comes back only when the approval is still pending in
|
||||
// the background; otherwise it stays disabled and the message says
|
||||
// why, because a control that cannot succeed must not look like it
|
||||
// can.
|
||||
const outcome = describeSigningFailure(
|
||||
response,
|
||||
"The message could not be signed.",
|
||||
);
|
||||
showError("approve-sign-error", outcome.message);
|
||||
if (outcome.retryable) setSignButtonBusy(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user