fix: MMPay fiat payment fixes #8987
Conversation
MoneyAccountDeposit
3f77b67 to
0560a35
Compare
b3b05dd to
8ca96e8
Compare
MoneyAccountDeposit - Consolidate duplicate changelog entries for #8987 - Alphabetize destructured properties in updateQuotes - Rename getPaymentAmount to getSourceAmount for clarity - Simplify walletAddress in fiat-quotes to use transaction.txParams.from
- Consolidate duplicate changelog entries for #8987 - Alphabetize destructured properties in updateQuotes - Rename getPaymentAmount to getSourceAmount for clarity - Simplify walletAddress in fiat-quotes to use transaction.txParams.from
5ce7386 to
d9d35fd
Compare
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
Pass fiatPayment.amountFiat from quote context into calculateTotals so the fiat flow uses the user-entered fiat amount for the total instead of deriving it from token amounts or targetAmount.
- Consolidate duplicate changelog entries for #8987 - Alphabetize destructured properties in updateQuotes - Rename getPaymentAmount to getSourceAmount for clarity - Simplify walletAddress in fiat-quotes to use transaction.txParams.from
Transactions without nested calldata (e.g. Perps, Predict deposits) now use a single EXACT_INPUT relay quote after fiat settlement instead of the three-phase discovery + re-encoding + delegation flow. This reduces relay calls from 3 to 1, uses cheaper EXACT_INPUT fees, and avoids leftover dust on the source chain. The three-phase flow is preserved for moneyAccountDeposit and other transactions with nested calldata that requires re-encoding.
…iat submit - Add fee-as-buffer strategy to prevent EXACT_OUTPUT cost overruns after fiat settlement by reserving the original relay fee from the discovery source amount and adding the discovery fee back to the final target. - Simple deposits (Perps, Predict) skip the three-phase discovery flow and use a single EXACT_INPUT relay quote for cheaper fees and no dust. - Split fiat-submit.ts into focused modules: - fiat-submit.ts: orchestration (polling, validation, routing) - fiat-submit-simple.ts: single EXACT_INPUT relay path - fiat-submit-with-calldata.ts: three-phase flow with fee buffer - utils.ts: shared validateRelayRateDrift, extractProviderCode - Add configurable feeReserveMultiplier and maxRateDriftPercent via confirmations_pay_fiat feature flag with safe defaults (1 and 10%).
…-submit-with-calldata
…ubmit-with-calldata
124114b to
57a14f2
Compare
56a5321 to
d4d107c
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4808a41. Configure here.
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
Replaces the temporary yarn patch with the preview package from MetaMask/core#8987 for local testing. Not intended to merge. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| return { | ||
| data, | ||
| to: from, | ||
| ...(totalValue > BigInt(0) ? { value: toHex(totalValue) } : {}), |
There was a problem hiding this comment.
This isn't how EIP-7702 works. The to is the from so you'd just be passing value to yourself, plus each call is empowered to pass value as it uses a solidity call that includes its own data, to, and value.
What issue were you trying to fix?
|
|
||
| ### Fixed | ||
|
|
||
| - Fix fiat `moneyAccountDeposit` failing after on-ramp settlement by adding `getAmountData` callback for calldata re-encoding and correcting wallet address, quote amount, and slippage validation ([#8987](https://github.com/MetaMask/core/pull/8987)) |
There was a problem hiding this comment.
Is this not the same as in changed, could we combine into one?
| const transactionData = state.transactionData[transactionId]; | ||
| const amountFiat = transactionData?.fiatPayment?.amountFiat; | ||
| const walletAddress = transaction.txParams.from as Hex; | ||
| const walletAddress = (transactionData?.accountOverride ?? |
There was a problem hiding this comment.
This is still not needed right as quotes.ts does this in the request?
| const skipBalanceCheck = | ||
| Boolean(quote.request.isPostQuote) || | ||
| Boolean(quote.request.paymentOverride) || | ||
| Boolean(quote.original.metamask.isExecute); |
There was a problem hiding this comment.
We still want to validate the balance before doing the execute flow, as it's final validation before the user spends their funds.
| : ({ | ||
| data: transaction.txParams.data as Hex | undefined, | ||
| from: transaction.txParams.from, | ||
| maxFeePerGas: normalizedParams[0]?.maxFeePerGas, |
There was a problem hiding this comment.
Good spot, we use the first params in addTransaction later, but if we prepended, that will now be undefined so it will just figure it out on the fly during addTransaction even though Relay may have provided some good fees.
| * @param options.transaction - The transaction metadata. | ||
| * @returns An object containing the relay transaction hash if available. | ||
| */ | ||
| export async function submitWithCalldataReEncoding({ |
There was a problem hiding this comment.
Minor, could just be submitWithTransactionData for clarity?
| ...baseRequest, | ||
| isMaxAmount: false, | ||
| isPostQuote: true, | ||
| sourceBalanceRaw: sourceAmountRaw, |
There was a problem hiding this comment.
Minor, I don't think the relay strategy uses sourceBalanceRaw anywhere, but I see this is pre-existing.
| } | ||
|
|
||
| // Fee in USD = what the relay consumed beyond the target value | ||
| const feeUsd = sourceUsd.minus(targetUsd); |
There was a problem hiding this comment.
Why are we re-calculating this if this is fees.provider.usd on our normalized quote from Relay?
|
|
||
| const adjusted = targetMinRaw.plus(discoveryFeeInTargetRaw); | ||
|
|
||
| log('calculateAdjustedTarget', { |
| } | ||
|
|
||
| // Convert fee from USD to raw target token units | ||
| const usdPerTargetRaw = targetUsd.dividedBy(targetMinRaw); |
There was a problem hiding this comment.
Might be enough for our purposes, but just a warning we're using the estimated target USD but the minimum target raw, so that may skew the target USD rate.

Explanation
Fiat payments for
moneyAccountDeposittransactions fail with multiple sequential errors after the on-ramp order settles:isMaxAmount: truein the fiat re-quote — after Transak settles,submitRelayAfterFiatCompletionre-quotes withisMaxAmount: true, isPostQuote: false. This callsprocessTransactions, which throws"Max amount quotes do not support included transactions"because moneyAccountDeposit has nested txs (approve + deposit) that require delegation.validateRelaySlippagecompares wrong amounts — it comparescurrencyOut.amountfrom two relay quotes made with different source amounts (theoretical $5 quoting phase vs actual ~$4.95 post-Transak settlement). This produces ~25% apparent "slippage" that is not real slippage — it is just a smaller input producing a smaller output.Nested calldata has zero amounts — the fiat path in
handleDoneonly setsamountFiatand never callsupdateTokenAmount(), sorequiredAssets.amountstays0x0and the nested approve + deposit calldata encodes zero amounts.Wrong wallet address in fiat flow —
fiat-quotes.tsandfiat-submit.tsusedtransaction.txParams.fromas the wallet address. FormoneyAccountDeposit,txParams.fromis the money account address on the target chain (Monad), not the user's EOA. This caused Ramps/Transak to receive the wrong deposit address,resolveSourceAmountRawto look for on-chain ETH at the wrong address, and all relay quotes to use the wrongfrom/useraddress — resulting inTRANSFER_FROM_FAILEDreverts.Fiat total calculation using wrong amount —
calculateTotalsderived the payment amount from token amounts ortargetAmount, which is incorrect for fiat flows where the user enters a specific fiat amount.Changes
fiat-quotes.ts— usesaccountOverridewhen available forwalletAddress, matching the existing pattern inquotes.ts. This ensures Ramps/Transak receives the user's actual EOA address, not the money account address.fiat-submit.ts— two wallet address fixes plus three-phase relay flow:submitFiatQuotes: usesaccountOverride ?? txParams.fromfor order polling wallet addresssubmitRelayAfterFiatCompletion: usesbaseRequest.from(already accountOverride-aware from the quote) for on-chain amount lookupisPostQuote: true, EXACT_INPUT) with the settled source amount to learncurrencyOut.minimumAmountgetAmountDatavia messenger to delegate calldata re-encoding to the client, then patches nested tx data +requiredAssets[0].amountisPostQuote: false, EXACT_OUTPUT) with delegationrelay-submit.ts— reverts theisExecutebalance skip (no longer needed with correct wallet address). The balance check now correctly validates the user's EOA balance.validateRelaySlippage→validateRelayRateDrift— compares USD exchange rate ratios (output_usd / input_usd) between original and discovery quotes instead of absolute amounts, normalising for different source amounts.TransactionPayController— adds optionalgetAmountDatacallback on the constructor, exposed via messenger asTransactionPayController:getAmountData. Keeps ABI knowledge on the client side.totals.ts— addsfiatPaymentAmountparameter tocalculateTotals. When a fiat strategy quote is present, uses the user-entered fiat payment amount directly instead of deriving it from token amounts or relaytargetAmount. Falls back to'0'if fiat amount is unavailable.quotes.ts— passesfiatPayment.amountFiatfrom the transaction pay state intocalculateTotalsasfiatPaymentAmount.relay-quotes.ts(recipient routing fix) — addsskipProcessTransactionstoQuoteRequesttype. Defaults toisPostQuotewhen not set. The simple fiat path (fiat-submit-simple.ts) setsskipProcessTransactions: falseto forceprocessTransactionsto run and extract thetransfer(to, amount)recipient from calldata — fixing Predict/Perps deposits where Relay sent swapped tokens to the user's EOA instead of the target contract.relay-quotes.ts(7702 batch gas estimation) — for post-quote flows, includes the original transaction in batch gas estimation alongside relay params. Previously, a single relay step was estimated alone, gotis7702: false, and the batch fell back to separate type-0x2 transactions — breaking zero-balance fiat-funded accounts that need all native tokens for the swap.relay-quotes.ts(native gas subtraction) — extends the phase-2 gas subtraction to trigger when the source token is the native gas token (e.g. POL on Polygon), not only whenisSourceGasFeeTokenis true. For zero-balance accounts where the source IS the native token, gas must be reserved from the source amount to avoidinsufficient funds for gas * price + value.relay-submit.ts(gas price alignment) — the prepended original transaction in post-quote batch submissions now carries the relay step'smaxFeePerGasandmaxPriorityFeePerGas. This ensures the top-level 7702 batch uses the same gas price as the relay quote, matching the gas cost computed during the phase-2 gas subtraction.eip7702.ts(transaction-controller) —generateEIP7702BatchTransactionnow sums native values from all nested calls and sets the result as the top-level transactionvalue. Previously the top-level value was always0x0, causing 7702 batches with native value transfers (e.g. POL swap) to revert because the delegation contract had no native tokens to forward.fiat-quotes.ts(accountOverride fix) — usesaccountOverride ?? txParams.fromfor the wallet address, matching the pattern infiat-submit.ts. For moneyAccountDeposit,txParams.fromis the money account on the target chain, not the user's EOA. This caused relay quotes to use the wrongfrom/useraddress, and Relay Execute to pull funds from an empty account.relay-submit.ts(isExecute balance skip) — skips the source balance validation when the Relay Execute flow is active (isExecute: true). In Execute flows, Relay's relayer handles the source-side transaction — the user's account doesn't need to hold the source tokens at submit time.fiat-submit-with-calldata.ts(rate drift moved) — moves thevalidateRelayRateDriftcheck from after the discovery quote (phase 1) to after the final EXACT_OUTPUT quote (phase 3). The discovery quote uses a reduced source amount (after fee reserve), causing an unfair rate comparison with the original quote on cross-chain routes where relay fees are a large percentage. The final quote uses the full source amount, making the comparison accurate.feature-flags.ts(fee reserve default) — bumpsDEFAULT_FEE_RESERVE_MULTIPLIERfrom1to1.2. On cross-chain routes with high relay fees (ETH→Monad), a 1x reserve was too tight — relay fees could increase slightly between the discovery and final quotes, causing the final EXACT_OUTPUT to overshoot the settled balance. 1.2x provides 20% headroom.Feature Flags (LaunchDarkly)
This PR reads optional properties from two feature flags. No new LaunchDarkly flags need to be created — these are properties on existing flag objects.
confirmations_pay_fiatfeeReserveMultipliernumber1.21.5) if EXACT_OUTPUT quotes consistently exceed the settled balance after settlement on cross-chain routes.maxRateDriftPercentnumber10confirmations_pay_post_quotegasBuffernumber1.1insufficient funds.No action required for launch — all properties default to safe values when absent. They exist as remote safety valves to tune post-settlement behavior without a code release.
References
ogp/fiat-money-account-deposit-fix)Checklist
Note
High Risk
Changes payment settlement, relay quoting/submission, and EIP-7702 batch value handling across fiat and zero-balance paths; regressions could block deposits or mis-route funds.
Overview
Fixes MMPay fiat flows after on-ramp settlement and tightens relay / EIP-7702 behavior for zero-balance and nested-tx deposits.
Fiat submit & quoting — After the ramp order completes, post-settlement relay is split: simple deposits (e.g. Perps/Predict) use one EXACT_INPUT post-quote with
skipProcessTransactions: falseso recipients come from calldata; nested calldata (e.g. money account approve+deposit) uses a three-phase path (discovery EXACT_INPUT with fee reserve →TransactionPayController:getAmountDatato patch nested data → final EXACT_OUTPUT). Wallet/fromnow prefersaccountOverrideovertxParams.from. Slippage checks move tovalidateRelayRateDrift(USD rate ratio) with tunablefeeReserveMultiplier/maxRateDriftPercent.calculateTotalsuses the user’sfiatPaymentAmountwhen a fiat strategy quote is present.Transaction Pay API — Optional
getAmountDataconstructor callback and messenger action;skipProcessTransactionsonQuoteRequest.Relay quotes & submit — Post-quote gas handling reserves native gas (incl. Polygon
0x1010) withgetPostQuoteGasBuffer; batch gas estimation includes the original tx for 7702 post-quote paths; prepended batch txs inherit relay fee fields. Relay submit skips EOA balance checks for post-quote, payment override, and Relay Execute.Transaction controller —
generateEIP7702BatchTransactionsets top-levelvalueto the sum of nested call values when non-zero.Reviewed by Cursor Bugbot for commit 4808a41. Bugbot is set up for automated code reviews on this repo. Configure here.