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]>
59 lines
2.6 KiB
Go
59 lines
2.6 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package extras
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/luxfi/evm/precompile/modules"
|
|
)
|
|
|
|
// RewardManagerConfigKey names the precompile that owns the fee destination.
|
|
// Referenced by key rather than by import so params keeps no dependency on any
|
|
// individual precompile package.
|
|
const RewardManagerConfigKey = "rewardManagerConfig"
|
|
|
|
// verifyFeeSplit refuses a configuration that burns half of every fee while the
|
|
// other half has nowhere governed to land.
|
|
//
|
|
// FeeSplit decides HOW MUCH of a fee is kept. WHERE the kept part goes is always
|
|
// the configured coinbase, which BlockChain.GetCoinbaseAt resolves from the
|
|
// RewardManager precompile's state — and, when that precompile is not enabled,
|
|
// falls back to the keyless blackhole 0x0100..00. Scheduling the split without
|
|
// RewardManager therefore burns half of every fee (irreversible, by design) and
|
|
// strands the other half at an address nobody holds a key to (irreversible, by
|
|
// accident). The C-Chain has ~3867 LUX stuck at that blackhole today, which is
|
|
// exactly what this check exists to stop repeating at twice the rate.
|
|
//
|
|
// This is a configuration invariant, not a consensus rule: it runs at genesis
|
|
// setup and VM init (ChainConfig.Verify), so a node refuses to start on such a
|
|
// config rather than producing blocks that quietly destroy value. It can never
|
|
// fork a chain.
|
|
//
|
|
// Two ways to satisfy it, both governed:
|
|
// - enable rewardManagerConfig at or before the split, with the DAO as
|
|
// adminAddresses and a real rewardAddress; or
|
|
// - set allowFeeRecipients, the explicit "block builders keep the fees" policy.
|
|
func (c *ChainConfig) verifyFeeSplit() error {
|
|
if c.FeeSplitTimestamp == nil {
|
|
return nil // dormant: nothing to check
|
|
}
|
|
if c.AllowFeeRecipients {
|
|
return nil // destination is the block builder, by explicit policy
|
|
}
|
|
module, ok := modules.GetPrecompileModule(RewardManagerConfigKey)
|
|
if !ok {
|
|
return fmt.Errorf(
|
|
"feeSplitTimestamp is set to %d but %s is not registered in this build, so the reward half has no governed destination",
|
|
*c.FeeSplitTimestamp, RewardManagerConfigKey)
|
|
}
|
|
if !c.IsPrecompileEnabled(module.Address, *c.FeeSplitTimestamp) {
|
|
return fmt.Errorf(
|
|
"feeSplitTimestamp is set to %d but %s (%s) is not enabled at that time: half of every fee would be burned and the other half stranded at the keyless blackhole. Enable %s at or before %d, or set allowFeeRecipients",
|
|
*c.FeeSplitTimestamp, RewardManagerConfigKey, module.Address,
|
|
RewardManagerConfigKey, *c.FeeSplitTimestamp)
|
|
}
|
|
return nil
|
|
}
|