# `MPP.Methods.Tempo.FeePayerPolicy`
[🔗](https://github.com/ZenHive/mpp/blob/v0.10.0/lib/mpp/methods/tempo/fee_payer_policy.ex#L1)

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). The same family covers *intrinsic*
gas: padded/non-canonical calldata or a nonzero call `value` inflate what the
sponsor pays without changing the decoded payment intent (mppx #602). This
module bounds the client-supplied gas fields, the total fee budget, the access
list, per-call value, and calldata canonicality 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)
  * every call carries zero native `value`
  * calldata for each recognized TIP-20 / DEX call is byte-exact canonical — no
    trailing padding or non-canonical high-order bytes that raise intrinsic gas
    without changing the decoded intent (ports mppx #602; unknown selectors are
    left to `Transaction.validate_call_scope/1`)

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 {: .warning}
>
> 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.

# `t`

```elixir
@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.

# `default_allowed_fee_tokens`

```elixir
@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?`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
