The signed artifact was compared with the dApp's request object, so every
field the dApp left out — normally the nonce, the gas limit and every fee
field, because populateTransaction() filled them in the popup — was checked
by nothing but the absolute ceilings. A bare transfer at the fee ceiling
hands the validator 2.1 ETH. The ceilings were never the defect: the thing
being verified was not the thing the user approved.
The transaction is now populated in the background, before the approval
window opens, and that populated object is what is displayed, what the popup
signs, and what the artifact is verified against. Every consequential field
is compared exactly.
- src/shared/approvalTx.js populates the request through a VoidSigner over
the configured RPC and serializes the result to the fields its type
serializes, as hex quantities that survive the JSON messaging boundary.
Fields the wallet does not act on are dropped before ethers sees the
page's object.
- Population failure raises no approval and opens no window: the error goes
back to the requesting page, bounded by a 20-second timeout. A
half-initialised approval record would be exactly the state the settle
interlock exists to keep out of that record, and the same estimate
previously failed after the user had typed their password.
- verifySignedTx compares the artifact field by field over
SERIALIZED_FIELDS[type], plus the type itself. A quantity the approval
does not fix is a refusal rather than a skipped comparison. The ceilings
stay as a documented backstop and now also apply at population, where they
bound what an RPC node can talk the wallet into displaying.
- The approval pins the address it was raised for. Verification uses that
address, not getActiveAddress(), and an address switch between approval and
signing refuses rather than signing from an account the screen never named
— including a switch during population, and on the message-signing path. A
request naming an address that is not the active one is refused outright.
- The approval screen shows the network, gas limit, fee per gas, maximum fee
and nonce it now vouches for, and the popup signs the object it was given
with no provider and no population of its own.
The settle chokepoint is untouched: one delete of pendingApprovals and one
approval.resolve(), both inside settleApproval(), the claim taken
synchronously before the first await, and a refused settle still leaving the
approval window standing.
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.