Skip to content

fix: harden fee-path panic sites against malicious transactions#2

Open
deblanco wants to merge 1 commit into
mainfrom
fix/fee-path-panic-hardening
Open

fix: harden fee-path panic sites against malicious transactions#2
deblanco wants to merge 1 commit into
mainfrom
fix/fee-path-panic-hardening

Conversation

@deblanco

Copy link
Copy Markdown
Contributor

Context

Production node panicked: called Option::unwrap() on a None value, custom_fee.rs:29:39.

That exact unwrap is not in current source — checked every commit/branch. The deployed binary was built from older code where CustomFeeInfo::new unwrapped max_fee_per_gas.or(gas_price). Rebuilding from main already fixes that specific panic. This PR removes the ~10 other live panic sites still on the fee-handling path, reachable from user-submitted transactions.

Changes

Critical — remote DoS (attacker-reachable)

  • sponsored-transactions, zero-gas-transactions: signature.as_slice().try_into().unwrap() panicked on any signature ≠ 65 bytes, reachable from the tx pool. Now validates length and rejects with BadProof.

High — storage unwraps (panic on every tx if storage unset)

  • dnt-fee-controller: 5 unwraps on FeeVaultPrecompileAddressStorage / ValidatorPercentageStorage (fee withdraw/refund/pay path). Storages converted to ValueQuery with type_value defaults (0x…0807, 50). Encoding-compatible — no migration.
  • validator-fee-selector: nested DefaultController::get().unwrap() inside an eager unwrap_or (panicked even when the outer value was Some). Now unwrap_or_else; a missing controller degrades to the existing (1,1) rate fallback.

Medium

  • sponsored:188 / zgt:186: dispatch.actual_weight.unwrap() → falls back to the declared gas limit (worst case, user-safe).
  • custom_fee.rs: match_validator_conversion_rate_limit used asymmetric checked_mul overflow fallbacks (user-side overflow auto-accepted; attacker controls the user numerator via max_fee_per_gas). Replaced with exact U256::full_mul (U512) comparison — overflow branch removed. ⚠️ Consensus-relevant in overflow edge cases — bump spec_version before deploying to a live chain.
  • fee-rewards-vault-controller precompile: set_validator_percentage(...).unwrap()revert.

Hardening

  • Runner substate expect()s kept (mirror upstream Frontier executor invariant, not user-reachable) — documented with an invariant comment + #[allow(clippy::expect_used)].
  • #![cfg_attr(not(test), deny(clippy::unwrap_used))] on the 6 fee-path crates as a regression guard (expect_used too on non-pallet crates; FRAME macros expand to expect() internally).

Tests

New cases: short/empty signatures, killed storage falling back to defaults without panic, zero denominators, U256::max_value() cross-product overflow on both sides.

  • cargo test: stbl-tools 12, dnt 13, validator-fee-selector 10, sponsored 9, zgt 4, runner 24, precompile 7 — all pass.
  • cargo clippy clean on all 6 crates.
  • cargo check --workspace clean.

Follow-up (out of scope)

  • Runner maps all fee errors to generic FeeOverflow — masks insufficient-balance vs arithmetic vs impl error.
  • Fee log emits U256::max_value() on validator_fee + dapp_fee overflow (silent masking).
  • ZGT (max_fee_per_gas == 0) skips the fee event log entirely — no operator record.

Eliminate ~10 unwrap/expect panics reachable from user-submitted
transactions on the fee-handling path.

Critical (remote DoS):
- sponsored-transactions, zero-gas-transactions: signature.try_into()
  .unwrap() panicked on any signature != 65 bytes from the tx pool;
  now reject with BadProof.

High (panic on unset storage, every tx):
- dnt-fee-controller: 5 unwraps on fee-vault/validator-percentage
  storage -> ValueQuery with defaults (encoding-compatible, no
  migration).
- validator-fee-selector: nested DefaultController.unwrap() inside an
  eager unwrap_or -> unwrap_or_else; missing controller degrades to
  (1,1) rate.

Medium:
- sponsored/zero-gas: actual_weight.unwrap() -> fall back to declared
  gas limit.
- custom_fee: match_validator_conversion_rate_limit had asymmetric
  checked_mul overflow fallbacks (attacker controls user numerator via
  max_fee_per_gas); replaced with exact U512 full_mul comparison.
  Consensus-relevant in overflow edge cases -> bump spec_version
  before live deploy.
- fee-rewards-vault-controller precompile: unwrap -> revert.

Hardening:
- Runner substate expect()s kept (upstream Frontier invariant, not
  user-reachable), documented + allow.
- deny(clippy::unwrap_used) on the 6 fee-path crates.

Adds tests for short/empty signatures, killed storage falling back to
defaults, zero denominators, and U256::max_value() cross-product
overflow on both sides.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant