Repository navigation
Tags: avnu-labs/paymaster
Tags
🐛 Tell refused submissions apart from uncertain ones (171) (#95) * 🐛 Persist the relayer nonce on delayed release in the Redis lock unlock_with_expiry only extended the lock key: the nonce invalidated before a delayed release was dropped, so the next lock reused the stale cached nonce after the cooldown. Store the nonce and extend the lock in one atomic pipeline, so the cooldown never applies with a stale nonce. * 🐛 Tell refused submissions apart from uncertain ones (171) Every execution failure answered 156 (TRANSACTION_EXECUTION_ERROR), including a timeout or gateway error while submitting the transaction and duplicate requests. A client could not know whether the transaction might still land, and acting on a 156 (e.g. pushing a refund) could race it. - Add TRANSACTION_SUBMISSION_UNCERTAIN (171), the only error meaning the transaction may have been broadcast: any other error guarantees the request broadcast nothing. Its data tells a duplicate request apart from an uncertain broadcast, which carries the relayer transaction hash - Classify submission errors: only the refusals declared by starknet_addInvokeTransaction and requests that never reached a node (connection failure, every endpoint circuit broken) are refusals, anything else becomes an UncertainSubmission carrying the prepared transaction hash - Propagate UncertainSubmission through the relayer and execution errors - On an uncertain submission, invalidate the relayer nonce and release the relayer with a delay, as for an invalid nonce. Treat an underpriced replacement as an invalid nonce, as a pending transaction holds it - Make the duplicate filter admission atomic and return a reservation, released only when the request fails without anything broadcast - Share the estimate and execute step between both execute endpoints - Document 171 in the OpenRPC specification
🐛 Read the privacy pool fee from the pool instead of configuration (#93) * 🐛 Read the privacy pool fee from the pool instead of configuration The relayer pre-transferred privacy.pool_fee_amount from configuration while the forwarder approves the pool for get_fee_amount() read on-chain. If the pool raised its fee, every private transaction reverted; if it lowered it (mainnet went from 6 to 4 STRK in September 2026), the difference stayed in the forwarder on every transaction. Build and execute now resolve the fee from the pool for private transactions. The configured amount keeps its role as the on/off switch (0 disables pool fee handling) and becomes the fallback when the pool cannot be read. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ♻️ Cache the privacy pool fee and remove its configuration - Add SyncValue::read_and_revalidate: serve a stale value and refresh it in the background (single in-flight refresh), fetch inline only once expired - Read get_fee_amount() through the starknet clients, typed, instrumented and cached per pool for 30s - Propagate errors instead of silently falling back to a configured fee - Remove privacy.pool_fee_amount: the fee comes from the pool and a pool without fee returns 0; existing configurations still parse - Resolve the fee in one place for build and execute, rejecting private transactions when no privacy pool is configured - Drop the parsing unit tests and redundant comments --------- Co-authored-by: l-henri <22731646+l-henri@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
⬆️ Bump moka from 0.12.13 to 0.12.15 (#84) Bumps [moka](https://github.com/moka-rs/moka) from 0.12.13 to 0.12.15. - [Release notes](https://github.com/moka-rs/moka/releases) - [Changelog](https://github.com/moka-rs/moka/blob/main/CHANGELOG.md) - [Commits](moka-rs/moka@v0.12.13...v0.12.15) --- updated-dependencies: - dependency-name: moka dependency-version: 0.12.15 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
🐛 Tolerate screening attestation suffix in apply_actions calldata (#82) The privacy pool upgrade added a trailing `screening: Option<ScreeningAttestation>` argument to `apply_actions`, so the calldata now ends with an extra `[0x1]` (None) or `[0x0, issued_at, sig_r, sig_s]` (Some) after the action span. `parse_server_actions` assumed the calldata was only `Span<ServerAction>` and rejected any trailing felt with `TrailingData`, so every private transaction that reaches the parser failed with CALLDATA_PARSING (rpc code 166). Consume the optional suffix after the action span instead of rejecting it. The paymaster only relays the attestation verbatim (the contract verifies it), so we just skip it. Pre-screening pools emit no suffix (remaining() == 0), so the old behaviour is unchanged — backward compatible, and forward compatible for when mainnet receives the same upgrade.
🐛 Cap declared L2 gas bound at the sequencer gateway limit (#81) Heavy sponsored transactions (e.g. a Garaga UltraHonk proof verification) were rejected pre-broadcast by the Starknet sequencer gateway with "Max gas amount is too high ... maximum allowed gas amount: 1200000000" (surfaced as RPC code 156), even though they only consume a fraction of that. The declared `max_l2_gas` bound is the estimate padded by the x1.5 gas multiplier, which can exceed the sequencer's stateless `max_l2_gas_amount` cap (1.2B on mainnet) while actual consumption stays well under it. - Cap the declared L2 gas bound at a configurable `max_l2_gas_amount` (default 1_200_000_000, exactly the mainnet sequencer limit; the gateway check is strict `>`, so declaring exactly it is accepted): min(estimate * 1.5, cap). L1 gas and L1 data gas keep their full x1.5 padding. - Reject early, before locking a relayer and broadcasting, when even the bare estimate (+10% safety) cannot fit under the cap, surfacing a clear MaxL2GasAmountExceeded error (new RPC code 169) instead of a paid out-of-gas revert. - Make the cap configurable via the `starknet` config block (serde default) so it can be tuned per network without a redeploy, since the sequencer value may change over time.
🐛 Don't cache validation overhead when contract is not yet deployed (#79 ) `ValidationGasOverhead::fetch` used to convert every error from `client.call(get_signers)` into `Ok(Self::none())`, and the caller (`resolve_gas_overhead`) cached the result indefinitely. As a side effect, any address queried while its contract did not yet exist — typically during a `deploy_and_invoke` flow that deploys the wallet in the same transaction it is estimating — got permanently marked as "no overhead". Once the wallet was deployed and the user did follow-up sponsored invokes, the cached `none()` was returned instead of the Braavos overhead, the resource bound on `l2_gas` was set ~5x too low, and Braavos passkey transactions reverted with `Insufficient max L2Gas`. Distinguish error variants in `fetch`: - successful response → cache (Braavos or none, based on shape) - contract-level execution error (account exists but does not expose `get_signers`, e.g. Argent/OZ) → cache as `none()` - `ContractNotFound` (wallet not yet deployed, the case behind this bug) → return `none()` for the current request but skip the cache so the next request retries the detection - any other transient RPC error (timeout, rate-limit, ...) → same as `ContractNotFound`: do not cache Also extract the response-shape classification into a pure function (`from_get_signers_response`) and add unit tests covering the on-chain shapes observed for a classic Braavos (4 felts) and a Braavos with a passkey/secp256r1 signer (5 felts).
⚡ Speed up paymaster_getSupportedTokens fanout (#78) With 32 supported_tokens the fanout in `Client::fetch_tokens` used a concurrency of 8, requiring 4 sequential rounds. Bumped the executor limit to 32 so all token prices fetch in a single round. Also pre-fetch STRK once before spawning the workers: every per-token price computation needs the STRK price, and on a cold cache the 32 concurrent workers were each issuing their own redundant STRK call. Reduces the endpoint p95 from ~2s to ~500-700ms in observed traces.
🐛 Fix Insufficient pool fee payment when gas_token == STRK (#77) When the user pays the privacy pool fee in STRK as gas_token, the forwarder's balance_before snapshot already includes the relayer's pre-transferred STRK. The pool's transferFrom then pulls that exact amount back, so `received` ends up `pool_fee` short of `gas_amount` and the assertion reverts with "Insufficient pool fee payment". USDC / ETH gas tokens are unaffected because the snapshot tracks a different token from the pretransfer. Fix in `forwarder.cairo`: when `gas_token_address == STRK_ADDRESS`, subtract the pool fee (now returned by `approve_pool_fee`) from `balance_before` so the delta correctly represents only the user's payment. Behaviour for non-STRK gas tokens is unchanged. Also drop the provider_fee_overhead on the pool fee when paying in STRK (`build.rs`): the overhead exists to cover STRK / gas-token price drift between build and execute, which is irrelevant when the gas token is STRK itself. Saves the user ~0.6 STRK per private tx on mainnet.
🐛 Fix privacy pool fee flow (approve + overhead + STRK pre-fund) (#76) * Approve privacy pool to pull STRK fee in forwarder The last call of a private batch targets a privacy pool whose `apply_actions` collects its fee via `transferFrom(forwarder, fee_collector, fee)` on STRK, independent of the user's gas token. Without an allowance, that transfer reverts on the ERC20 check. - Add `IPrivacyPool` dispatcher (`get_fee_amount`) and a 10_000 STRK safety cap. - Hardcode the canonical STRK address on Starknet; `approve_pool_fee` always approves the pool on STRK, never on the user's gas token. - Wire the helper into `execute_private` and `execute_private_sponsored`. - Add a minimal `MockPrivacyPool` and tests covering the fee-limit guard. * Apply only provider overhead to the privacy pool fee PR #72 applied `max_fee_multiplier` (~3x) to the pool fee, which caused users to approve triple the amount they actually owed and hit "Insufficient ERC20 balance" on execute. The pool fee is a fixed amount in STRK read from the pool, so it does not need the large safety multiplier that guards against gas-fee volatility — only a small overhead to cover STRK / gas-token price drift between build and execute. Use `compute_paid_fee_in_strk` (~1.1x via `provider_fee_overhead`) for the pool portion; keep `compute_max_fee_in_strk` on the gas portion. * Pre-fund forwarder with STRK from relayer for privacy pool fee The privacy pool collects its fee via `transferFrom(forwarder, fee_collector, fee)` in STRK. When the user pays in a non-STRK gas token (USDC, etc.), the forwarder has no STRK balance for the pool to pull, so `apply_actions` reverts. The relayer already holds STRK (refilled by the gas-tank → relayers rebalancing), so we prepend a `strk.transfer(forwarder, pool_fee_amount)` call before the forwarder call in both private and private-sponsored flows. The relayer is reimbursed when the forwarder forwards the user's gas-token payment to the gas tank, closing the loop via the existing rebalancing. Only emitted when `privacy_pool_fee_amount > 0`, so non-pool-fee transactions are unchanged.
🐛 Apply max fee overhead to privacy pool fee (#72) Pool fee was added raw on top of the multiplied gas fee, leaving no headroom for STRK/token price drift between build and execute. With a non-STRK pool_fee_token (e.g. USDC), this caused POOL_FEE_TOO_LOW (167) on paymaster_executeTransaction. Fold pool fee into the total before applying the max_fee_multiplier, and apply the same overhead to the sponsored-mode fee_action_amount.
PreviousNext