fix: harden fee-path panic sites against malicious transactions#2
Open
deblanco wants to merge 1 commit into
Open
fix: harden fee-path panic sites against malicious transactions#2deblanco wants to merge 1 commit into
deblanco wants to merge 1 commit into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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::newunwrappedmax_fee_per_gas.or(gas_price). Rebuilding frommainalready 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 withBadProof.High — storage unwraps (panic on every tx if storage unset)
dnt-fee-controller: 5 unwraps onFeeVaultPrecompileAddressStorage/ValidatorPercentageStorage(fee withdraw/refund/pay path). Storages converted toValueQuerywithtype_valuedefaults (0x…0807,50). Encoding-compatible — no migration.validator-fee-selector: nestedDefaultController::get().unwrap()inside an eagerunwrap_or(panicked even when the outer value wasSome). Nowunwrap_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_limitused asymmetricchecked_muloverflow fallbacks (user-side overflow auto-accepted; attacker controls the user numerator viamax_fee_per_gas). Replaced with exactU256::full_mul(U512) comparison — overflow branch removed.spec_versionbefore deploying to a live chain.fee-rewards-vault-controllerprecompile:set_validator_percentage(...).unwrap()→revert.Hardening
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_usedtoo on non-pallet crates; FRAME macros expand toexpect()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 clippyclean on all 6 crates.cargo check --workspaceclean.Follow-up (out of scope)
FeeOverflow— masks insufficient-balance vs arithmetic vs impl error.U256::max_value()onvalidator_fee + dapp_feeoverflow (silent masking).max_fee_per_gas == 0) skips the fee event log entirely — no operator record.