MPP.Methods.Tempo.FeePayerPolicy (mpp v0.6.2)

Copy Markdown View Source

Sponsor (fee-payer) gas-economics policy for Tempo transactions.

When the server acts as fee payer it co-signs and broadcasts a client-signed 0x76 envelope, paying the gas from its own wallet. Without bounds, a malicious client embeds arbitrary gas parameters and drains that wallet (GHSA-vv77-66rf-pm86 — unbounded max_fee_per_gas/max_priority_fee_per_gas; GHSA-qpxh-ff8m-c62v — access-list padding). This module bounds the client-supplied gas fields, the total fee budget, and the access list before the server co-signs.

The model mirrors the mppx (TypeScript) and mpp-rs (Rust) reference SDKs: absolute ceilings with per-chain defaults, overridable per server. Checks (all comparisons match the references):

  • gas_limit in 1..policy.max_gas
  • max_fee_per_gas in 1..policy.max_fee_per_gas
  • gas_limit * max_fee_per_gas <= policy.max_total_fee (worst-case budget cap)
  • max_priority_fee_per_gas <= max_fee_per_gas and <= policy.max_priority_fee_per_gas
  • expiring nonce key required; valid_before present, in the future, and within policy.max_validity_window_seconds of now (bounds how long a sponsorship the server co-signs can sit broadcastable)
  • access list empty (fee-payer call scopes never need one)

Any field that is not a well-formed RLP scalar (e.g. a list where a number is expected, or a truncated envelope) is rejected — the policy fails closed on malformed input rather than coercing it to zero.

The validity window is an absolute max_validity_window_seconds cap (matching mpp-rs). mppx additionally ties the ceiling to challengeExpires + 60s; we do not, so a co-signed sponsorship stays broadcastable up to the full window even past challenge expiry — lower max_validity_window_seconds if you need a tighter, challenge-relative bound.

Overrides

Pass a "fee_payer_policy" map in :method_config to raise or lower any ceiling (max_validity_window_seconds is in seconds; the rest are wei). Each unset key falls back to the per-chain default:

"fee_payer_policy" => %{
  "max_gas" => 2_000_000,
  "max_fee_per_gas" => 100_000_000_000,
  "max_priority_fee_per_gas" => 10_000_000_000,
  "max_total_fee" => 50_000_000_000_000_000,
  "max_validity_window_seconds" => 900
}

Not a substitute for simulation

This policy bounds the price the server will pay. It does not catch a gas_limit set too low to complete the call (GHSA-vj8p-hp9x-gh47): that requires pre-broadcast simulation of the co-signed transaction.

Summary

Types

t()

Resolved sponsor gas-economics and validity-window ceilings.

Functions

Return the default sponsor fee-token allowlist for chain_id.

Return true when fee_token is on the allowlist for chain_id.

Resolve the policy for chain_id, applying optional overrides.

Validate a transaction's gas economics and validity window against policy.

Like validate/2, but evaluates the validity window against now (unix seconds) instead of the system clock.

Types

t()

@type t() :: %MPP.Methods.Tempo.FeePayerPolicy{
  max_fee_per_gas: non_neg_integer(),
  max_gas: non_neg_integer(),
  max_priority_fee_per_gas: non_neg_integer(),
  max_total_fee: non_neg_integer(),
  max_validity_window_seconds: non_neg_integer()
}

Resolved sponsor gas-economics and validity-window ceilings.

Functions

default_allowed_fee_tokens(chain_id)

@spec default_allowed_fee_tokens(non_neg_integer()) :: [String.t()]

Return the default sponsor fee-token allowlist for chain_id.

Matches mppx defaultAllowedFeeTokens / mpp-rs default_allowed_fee_tokens: pathUSD plus the chain's default currency (USDC on mainnet, pathUSD on Moderato).

fee_token_allowed?(chain_id, fee_token, overrides \\ nil)

@spec fee_token_allowed?(non_neg_integer(), String.t(), [String.t()] | nil) ::
  boolean()

Return true when fee_token is on the allowlist for chain_id.

overrides is an optional list of hex addresses replacing the per-chain default.

resolve(chain_id, overrides)

@spec resolve(non_neg_integer(), map() | nil) :: t()

Resolve the policy for chain_id, applying optional overrides.

overrides is a string-keyed map ("max_gas", "max_fee_per_gas", "max_priority_fee_per_gas", "max_total_fee", "max_validity_window_seconds"); non-integer or negative values are ignored in favor of the per-chain default.

Examples

iex> p = MPP.Methods.Tempo.FeePayerPolicy.resolve(42_431, nil)
iex> {p.max_fee_per_gas, p.max_priority_fee_per_gas}
{100_000_000_000, 50_000_000_000}

iex> p = MPP.Methods.Tempo.FeePayerPolicy.resolve(4217, %{"max_gas" => 500_000})
iex> {p.max_gas, p.max_priority_fee_per_gas}
{500_000, 10_000_000_000}

validate(tx, policy)

@spec validate(Onchain.Tempo.Transaction.t(), t()) :: :ok | {:error, String.t()}

Validate a transaction's gas economics and validity window against policy.

Returns :ok or {:error, reason} (binary). Fields are read from the parsed 0x76 envelope. The validity-window check is evaluated against the current system time; pass validate/3 with an explicit now (unix seconds) to make the comparison deterministic.

validate(tx, policy, now)

@spec validate(Onchain.Tempo.Transaction.t(), t(), integer()) ::
  :ok | {:error, String.t()}

Like validate/2, but evaluates the validity window against now (unix seconds) instead of the system clock.