mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
The fee split braided two independent concerns into one line: how much of a
fee is kept (a protocol constant, floor(fee/2)) and where the kept part goes
(compiled-in extras.FeeRewardVault = 0x0100..02). The second half made the
destination ungovernable and silently bypassed the RewardManager precompile
the moment FeeSplit activated, so turning the burn on would have DISABLED the
one on-chain knob the chain already has.
Split the concerns:
ratio -> stays a right shift. Fixed, auditable, not settable by any
transaction. Changing it remains a release plus a coordinated
fork, deliberately: monetary policy should not move at the
speed of a multisig.
destination -> the configured coinbase, i.e. BlockChain.GetCoinbaseAt, i.e.
the RewardManager precompile's stored address. Already part of
header validity (dummy.verifyCoinbase), so it is verifiable
from the header and movable by one admin transaction.
creditTxFee collapses to one credit site; extras.FeeRewardVault is deleted.
Also adds the ordering gate this created a need for: ChainConfig.Verify now
refuses a config that schedules feeSplitTimestamp while rewardManagerConfig is
not enabled at that time (and allowFeeRecipients is off). Without RewardManager
GetCoinbaseAt falls back to the keyless blackhole, so such a config would burn
half of every fee and strand the other half forever -- the C-Chain already has
~3867 LUX stuck at that address. mainnet/cchain.json currently ships exactly
that shape (feeSplitTimestamp 1785715200, no rewardManagerConfig in any of the
49 precompileUpgrades); the node will now refuse to start on it instead.
This is a no-op on every live chain: mainnet 96369's running config has no
feeSplitTimestamp at all (eth_getChainConfig), the vault balance is 0, and the
branch has never executed. The correction is free today and costs a fork later.
Tests:
core: destination follows the configured coinbase; a redirect mid-chain moves
the whole reward stream while the burn is unchanged; supply conservation
on executed blocks; genesis refuses the ungoverned shape.
dummy: a proposer naming any other coinbase is rejected with ErrInvalidCoinbase,
so crediting the header coinbase is not a validator-capture hole.
extras: the verbatim upgrade.json fragment activating rewardManagerConfig and
feeManagerConfig with the DAO Safe as adminAddresses parses through the
registry, and feeManager's governable surface contains no ratio.
Co-authored-by: Hanzo Dev <[email protected]>
54 lines
2.7 KiB
Go
54 lines
2.7 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package core
|
|
|
|
import (
|
|
"github.com/holiman/uint256"
|
|
"github.com/luxfi/evm/params"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/geth/core/tracing"
|
|
"github.com/luxfi/geth/core/vm"
|
|
ethparams "github.com/luxfi/geth/params"
|
|
)
|
|
|
|
// creditTxFee disburses the fee paid by a single transaction.
|
|
//
|
|
// Two concerns, deliberately kept apart:
|
|
//
|
|
// - HOW MUCH is kept — a protocol constant. FeeSplit inactive: all of it.
|
|
// FeeSplit active: floor(fee/2); the remainder is credited to no account,
|
|
// which reduces the sum of all balances (a true burn, identical in mechanism
|
|
// to the EIP-1559 base-fee burn — not a transfer to a dead address, which
|
|
// would leave supply unchanged). The ratio is a single right shift: fixed,
|
|
// auditable, and not settable by any transaction. Changing it is a node
|
|
// release plus a coordinated fork, by design — monetary policy should not
|
|
// move at the speed of a multisig.
|
|
//
|
|
// - WHERE it goes — chain state, never a compiled-in constant. [coinbase] is
|
|
// the fee sink configured at the parent block, resolved by
|
|
// BlockChain.GetCoinbaseAt: the RewardManager precompile's stored reward
|
|
// address when that precompile is enabled, otherwise the blackhole. It is
|
|
// already part of header validity (dummy.verifyCoinbase rejects a block whose
|
|
// header coinbase disagrees with the configured one), so the destination is
|
|
// verifiable from the header alone and is changeable on-chain, by transaction,
|
|
// through the RewardManager admin — i.e. governable — without touching this
|
|
// code or shipping a binary.
|
|
//
|
|
// Braiding the two — crediting the kept half to an address baked into the binary
|
|
// — would make the destination un-governable and would silently bypass
|
|
// RewardManager the moment FeeSplit activated. It does not, and must not.
|
|
//
|
|
// Determinism: fee is a consensus value (gasUsed * effectiveGasPrice); the split
|
|
// is pure uint256 integer arithmetic to an address already agreed by consensus,
|
|
// so every validator computes an identical post-state and the split cannot fork
|
|
// the chain. The odd wei on an odd fee is assigned to the burn (conservative: it
|
|
// never over-credits the reward side, and it keeps burn >= reward exactly).
|
|
func creditTxFee(stateDB vm.StateDB, config *ethparams.ChainConfig, coinbase common.Address, blockTime uint64, fee *uint256.Int) {
|
|
reward := fee
|
|
if cfg := params.GetExtra(config); cfg != nil && cfg.IsFeeSplit(blockTime) {
|
|
reward = new(uint256.Int).Rsh(fee, 1) // floor(fee/2); fee-reward is the burn
|
|
}
|
|
stateDB.AddBalance(coinbase, reward, tracing.BalanceIncreaseRewardTransactionFee)
|
|
}
|