Files
zeekay 96ffac7ef4 dead: refuse an unclaimed admin, and stop re-splitting funds already burned
Two defects in the LP-0150 dead precompile, both reachable from calldata the
moment a chain activates it. Nothing imports the package today, so its init()
never runs and neither is live — but it ships in the module that supplies
luxfi/evm's precompiles, one blank import from being reachable.

isAdmin returned true for EVERY caller while the admin slot was empty, which is
the state of a chain at genesis. The comment called it "allow deployer/genesis
to set initial admin", but nothing restricted it to a deployer or to genesis:
the first account to send a transaction became administrator of all three dead
addresses, and from there could point the treasury at itself, set the burn ratio
to zero, and take the balance. The role is now provisioned through Configure
from the chain's genesis config and never claimed; an empty slot means nobody is
administrator, so a chain that names none has a precompile frozen at its
defaults.

handleReceive split stateDB.GetBalance(addr) — the address's whole accumulated
balance — rather than the funds that had just arrived. Since the burned share
stays at the dead address by design, the balance never returns to zero, so every
subsequent call re-split what was already burned and moved another fraction of
it to the treasury. Ten calls at 10,000 gas each left a thousandth of the burn
standing. It now tracks a per-address high-water mark and splits only what is
new, which also makes the call idempotent.

The existing tests passed throughout: TestAdminAuthorization installed an admin
on its first line and so never exercised the empty slot, and
TestHandleReceiveWithConfigurableRatio called handleReceive exactly once.

Also: the four storage slots were commented as keccak256 of "dead.admin" and
friends. None of them is that digest — they are hand-typed nibble walks. The
comment invited a maintainer to regenerate them from the strings, which would
have moved every slot and orphaned the stored admin. Comment corrected, values
pinned by test. A malformed input shorter than a selector no longer returns the
supplied gas untouched.

Coverage 30.2% -> 99.1%. The remaining gap is init()'s panic on a registration
failure, which a healthy build cannot reach.
2026-08-28 09:04:35 -07:00

127 lines
3.3 KiB
Go

// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dead
import (
"fmt"
"github.com/luxfi/geth/common"
"github.com/luxfi/precompile/contract"
"github.com/luxfi/precompile/modules"
"github.com/luxfi/precompile/precompileconfig"
)
var _ contract.Configurator = (*configurator)(nil)
// Config keys for each dead address
const (
ConfigKeyZero = "deadZeroConfig"
ConfigKeyDead = "deadConfig"
ConfigKeyFull = "deadFullConfig"
)
// Modules for each dead address
var (
ModuleZero = modules.Module{
ConfigKey: ConfigKeyZero,
Address: ZeroAddress,
Contract: DeadPrecompile,
Configurator: &configurator{key: ConfigKeyZero},
}
ModuleDead = modules.Module{
ConfigKey: ConfigKeyDead,
Address: DeadAddress,
Contract: DeadPrecompile,
Configurator: &configurator{key: ConfigKeyDead},
}
ModuleFull = modules.Module{
ConfigKey: ConfigKeyFull,
Address: DeadFullAddress,
Contract: DeadPrecompile,
Configurator: &configurator{key: ConfigKeyFull},
}
)
type configurator struct {
key string
}
func init() {
// Register the Dead Precompile at ALL dead addresses
// This way transfers to 0x0 or 0xdead call the precompile directly
for _, m := range []modules.Module{ModuleZero, ModuleDead, ModuleFull} {
if err := modules.RegisterModule(m); err != nil {
panic(err)
}
}
}
func (c *configurator) MakeConfig() precompileconfig.Config {
return &Config{key: c.key}
}
// Configure provisions the administrator from the chain's genesis config.
//
// This is the only way an administrator is installed. isAdmin refuses every
// caller while the slot is empty, so a chain that names no admin here has a
// dead precompile whose ratio and treasury are frozen at their defaults —
// which is the safe reading of "nobody was put in charge".
func (*configurator) Configure(
chainConfig precompileconfig.ChainConfig,
cfg precompileconfig.Config,
state contract.StateDB,
blockContext contract.ConfigurationBlockContext,
) error {
config, ok := cfg.(*Config)
if !ok {
return fmt.Errorf("dead: expected *Config, got %T", cfg)
}
if config.AdminAddress == nil {
return nil
}
if *config.AdminAddress == ZeroAddress {
// The zero address is how "unprovisioned" is spelled in the slot, so
// accepting it here would write a value indistinguishable from absence.
return ErrInvalidAddress
}
DeadPrecompile.setStateAddress(state, AdminSlot, *config.AdminAddress)
return nil
}
// Config implements the precompileconfig.Config interface
type Config struct {
key string
Upgrade precompileconfig.Upgrade `json:"upgrade"`
// AdminAddress names the account permitted to change the treasury address,
// the burn ratio and the enabled flag. Omitted means no administrator.
AdminAddress *common.Address `json:"adminAddress,omitempty"`
}
func (c *Config) Key() string {
return c.key
}
func (c *Config) Timestamp() *uint64 {
return c.Upgrade.Timestamp()
}
func (c *Config) IsDisabled() bool {
return c.Upgrade.Disable
}
func (c *Config) Equal(cfg precompileconfig.Config) bool {
other, ok := cfg.(*Config)
if !ok {
return false
}
return c.key == other.key && c.Upgrade.Equal(&other.Upgrade)
}
func (c *Config) Verify(chainConfig precompileconfig.ChainConfig) error {
return nil
}