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_limitin1..policy.max_gasmax_fee_per_gasin1..policy.max_fee_per_gasgas_limit * max_fee_per_gas <= policy.max_total_fee(worst-case budget cap)max_priority_fee_per_gas <= max_fee_per_gasand<= policy.max_priority_fee_per_gas- expiring nonce key required;
valid_beforepresent, in the future, and withinpolicy.max_validity_window_secondsof 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
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
@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
@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).
@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.
@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}
@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.
@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.