Skip to content

fix(runtime): price Substrate weight and bytes like EVM gas (50_000 wei per ref_time, 16 gwei per byte) — transfers cost ~1.6e-5 ORB instead of ~3e-10, so account and block spam are no longer free (spec 20) - #178

Merged
nol4lej merged 1 commit into
mainfrom
fix/substrate-fee-pricing
Oct 22, 2026

Conversation

@nol4lej

@nol4lej nol4lej commented Oct 22, 2026

Copy link
Copy Markdown
Member

Summary

Substrate extrinsic fees were almost zero. WeightToFee and LengthToFee were IdentityFee, so one unit of ref_time or one byte cost 1 wei (1e-18 ORB). A balance transfer cost about 3e-10 ORB: creating a million accounts cost 0.0003 ORB, and filling every block for a day about 0.03 ORB. The same transfer over EVM costs about 1.6e-5 ORB.

Substrate fees now price weight and bytes like EVM gas, so an extrinsic costs what the same work costs as an EVM transaction. Ships in spec 20.

Changes

  • configs/system.rs:
    • WeightToFee = ConstantMultiplier<WeightFee>, where WeightFee = DefaultBaseFeePerGas / WeightPerGas = 1 gwei / 20_000 = 50_000 wei per unit of ref_time.
    • LengthToFee = ConstantMultiplier<LengthFee>, where LengthFee = 16 gas per byte at the base fee = 16 gwei per byte, EVM's calldata price.
    • Both are derived from the EVM config, not hard-coded.
  • runtime_tests/weights.rs: substrate_fees_match_evm_gas pins both constants and checks that the weight of 21_000 gas costs 21_000 × base fee.
  • RUNTIME_VERSIONS.md: spec 20 entry.

Unchanged: unsigned shielded spends (they pay MinSpendFee, 0.01 ORB), Root calls (Pays::No), and the SlowAdjustingFeeUpdate multiplier.

Why ExistentialDeposit stays 0

Raising it was tested on a dev node with ED = 0.001 ORB, against an ED = 0 control. It does stop 1-planck accounts, but it breaks three things:

  • EVM replay. An EVM account emptied by a Substrate transfer_all(keep_alive = false) signed with its own key is reaped and its nonce returns to 0. An earlier signed EVM transaction can then be sent again and moves funds again.
  • Shielded pool insolvency. A fee claim that leaves less than the ED in the pool account burns the remainder. The pool's books then exceed its real balance, and the last pending fee can no longer be paid.
  • Unshield below the ED. An unshield to a new account below the ED passes pool admission, then fails in the block.

Fees are what made spam free, so fees are what this changes.

Testing

  • Runtime tests: 86 pass, including the new one. Clippy and fmt are clean.
  • Dev node, old runtime against new, fee actually charged (TransactionFeePaid):
Extrinsic Before After
transfer to a new account 2.8e-10 ORB 1.65e-5 ORB
transfer_keep_alive 2.7e-10 1.60e-5
EVM transfer, for reference (21_000 gas × eth_gasPrice) 1.64e-5 1.64e-5
system.remark, 1 KiB 1.2e-10 2.4e-5
system.remark, 64 KiB 8.3e-10 1.1e-3
shieldedPool.shield 1.6e-8 8.1e-4
shieldedPool.claim_fees 1.0e-9 5.4e-5
unshield (unsigned) 1.0e-2 1.0e-2
sudo(..) 0 0
Spam cost Before After
1M new accounts 0.0003 ORB 16.5 ORB
Every block full of transfers for a day 0.03 ORB 1,770 ORB

Notes

  • shield now pays about 0.0008 ORB in Substrate fees on top of the amount shielded. Faucet amounts and the app's balance checks may need a look.
  • No weights change, only their price, so nothing is added to the pending re-benchmark.

…ei per ref_time, 16 gwei per byte) — transfers cost ~1.6e-5 ORB instead of ~3e-10, so account and block spam are no longer free (spec 20)
@nol4lej
nol4lej merged commit 2c1cc78 into main Oct 22, 2026
5 checks passed
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