mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
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.
568 lines
17 KiB
Go
568 lines
17 KiB
Go
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package dead implements the Dead Precompile (LP-0150) that intercepts
|
|
// transfers to dead addresses (0x0, 0xdead) and routes them to a configurable
|
|
// split between burning and DAO treasury.
|
|
package dead
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"math/big"
|
|
"slices"
|
|
|
|
"github.com/holiman/uint256"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/geth/core/tracing"
|
|
"github.com/luxfi/precompile/contract"
|
|
"golang.org/x/crypto/sha3"
|
|
)
|
|
|
|
// keccak returns the Keccak-256 digest of s as a storage slot.
|
|
func keccak(s string) common.Hash {
|
|
h := sha3.NewLegacyKeccak256()
|
|
h.Write([]byte(s))
|
|
var out common.Hash
|
|
copy(out[:], h.Sum(nil))
|
|
return out
|
|
}
|
|
|
|
// Standard dead addresses that trigger this precompile
|
|
var (
|
|
ZeroAddress = common.HexToAddress("0x0000000000000000000000000000000000000000")
|
|
DeadAddress = common.HexToAddress("0x000000000000000000000000000000000000dEaD")
|
|
DeadFullAddress = common.HexToAddress("0xdEaD000000000000000000000000000000000000")
|
|
|
|
AllDeadAddresses = []common.Address{ZeroAddress, DeadAddress, DeadFullAddress}
|
|
)
|
|
|
|
// Default values (can be changed via governance)
|
|
var (
|
|
DefaultDAOTreasury = common.HexToAddress("0x9011E888251AB053B7bD1cdB598Db4f9DEd94714")
|
|
DefaultBurnBPS = uint64(5000) // 50% burn
|
|
DefaultTreasuryBPS = uint64(5000) // 50% treasury
|
|
)
|
|
|
|
// Storage slots.
|
|
//
|
|
// The four configuration slots below are fixed constants chosen to be far from
|
|
// the low words a Solidity layout occupies. They are NOT digests of anything,
|
|
// despite once being commented as keccak256 of "dead.admin" and friends — none
|
|
// of them is. That comment was a hazard rather than a description: regenerating
|
|
// a slot from the string it claimed would silently move it, orphaning the value
|
|
// stored at the old address and leaving the reader an empty word. Their values
|
|
// are part of the storage layout, so they are pinned by TestStorageSlotsAre-
|
|
// PinnedAndDistinct and must not be recomputed.
|
|
var (
|
|
AdminSlot = common.HexToHash("0x8f4e7e7c9a5b3d1e2f6a4c8b0e9d7f3a2c5b8e1d4f7a0c3b6e9d2f5a8c1b4e7d")
|
|
TreasurySlot = common.HexToHash("0x3a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b")
|
|
BurnBPSSlot = common.HexToHash("0x7f8e9d0c1b2a3f4e5d6c7b8a9f0e1d2c3b4a5f6e7d8c9b0a1f2e3d4c5b6a7f8e")
|
|
EnabledSlot = common.HexToHash("0x2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b")
|
|
|
|
// BurnedSlot records how much of a dead address's balance has already been
|
|
// through the split. Unlike the four above it holds no legacy state, so it
|
|
// is derived the way a slot should be. It is read and written under the dead
|
|
// address being credited, not under ZeroAddress: each address carries its
|
|
// own balance and so its own progress.
|
|
BurnedSlot = keccak("dead.burned")
|
|
)
|
|
|
|
// Function selectors (first 4 bytes of keccak256 of function signature)
|
|
var (
|
|
// Admin functions
|
|
SelectorSetAdmin = [4]byte{0x70, 0x4b, 0x6c, 0x02} // setAdmin(address)
|
|
SelectorSetTreasury = [4]byte{0xf0, 0xb3, 0x7c, 0x04} // setTreasury(address)
|
|
SelectorSetBurnRatio = [4]byte{0x8d, 0x14, 0xe1, 0x27} // setBurnRatio(uint256)
|
|
SelectorSetEnabled = [4]byte{0x32, 0x8d, 0x8b, 0x42} // setEnabled(bool)
|
|
|
|
// View functions
|
|
SelectorGetAdmin = [4]byte{0x6e, 0x9d, 0xf3, 0xd2} // getAdmin()
|
|
SelectorGetTreasury = [4]byte{0x3b, 0x19, 0xe8, 0x4a} // getTreasury()
|
|
SelectorGetBurnRatio = [4]byte{0xb5, 0xc5, 0xf6, 0x72} // getBurnRatio()
|
|
SelectorGetSplit = [4]byte{0x1a, 0x86, 0x1d, 0x26} // getSplit(uint256)
|
|
SelectorIsEnabled = [4]byte{0x2f, 0x6f, 0x98, 0x0a} // isEnabled()
|
|
)
|
|
|
|
// Gas costs
|
|
const (
|
|
GasBase uint64 = 10000 // Base cost for receive
|
|
GasAdminRead uint64 = 200 // Reading admin state
|
|
GasAdminWrite uint64 = 5000 // Writing admin state
|
|
)
|
|
|
|
// Basis points constant
|
|
const BasisPoints uint64 = 10000
|
|
|
|
// Errors
|
|
var (
|
|
ErrUnauthorized = errors.New("unauthorized: caller is not admin")
|
|
ErrInvalidRatio = errors.New("invalid ratio: must be <= 10000 BPS")
|
|
ErrInvalidAddress = errors.New("invalid address: cannot be zero")
|
|
ErrDisabled = errors.New("dead precompile is disabled")
|
|
ErrInsufficientGas = errors.New("insufficient gas")
|
|
ErrInvalidInput = contract.ErrInvalidInput
|
|
)
|
|
|
|
// DeadPrecompile implements the stateful precompiled contract interface
|
|
var DeadPrecompile = &deadPrecompile{}
|
|
|
|
type deadPrecompile struct{}
|
|
|
|
// Run executes the dead precompile
|
|
func (d *deadPrecompile) Run(
|
|
accessibleState contract.AccessibleState,
|
|
caller common.Address,
|
|
addr common.Address,
|
|
input []byte,
|
|
suppliedGas uint64,
|
|
readOnly bool,
|
|
) ([]byte, uint64, error) {
|
|
stateDB := accessibleState.GetStateDB()
|
|
|
|
// If no input, this is a value transfer - handle as receive
|
|
if len(input) == 0 {
|
|
return d.handleReceive(stateDB, caller, addr, suppliedGas, readOnly)
|
|
}
|
|
|
|
// Otherwise, parse function selector. A call too short to carry one is
|
|
// still a call, so it is charged; returning the supplied gas untouched
|
|
// would make refused input free to repeat.
|
|
if len(input) < 4 {
|
|
if suppliedGas < GasBase {
|
|
return nil, 0, ErrInsufficientGas
|
|
}
|
|
return nil, suppliedGas - GasBase, ErrInvalidInput
|
|
}
|
|
|
|
var selector [4]byte
|
|
copy(selector[:], input[:4])
|
|
args := input[4:]
|
|
|
|
switch selector {
|
|
// Admin write functions
|
|
case SelectorSetAdmin:
|
|
return d.setAdmin(stateDB, caller, args, suppliedGas, readOnly)
|
|
case SelectorSetTreasury:
|
|
return d.setTreasury(stateDB, caller, args, suppliedGas, readOnly)
|
|
case SelectorSetBurnRatio:
|
|
return d.setBurnRatio(stateDB, caller, args, suppliedGas, readOnly)
|
|
case SelectorSetEnabled:
|
|
return d.setEnabled(stateDB, caller, args, suppliedGas, readOnly)
|
|
|
|
// View functions
|
|
case SelectorGetAdmin:
|
|
return d.getAdmin(stateDB, suppliedGas)
|
|
case SelectorGetTreasury:
|
|
return d.getTreasury(stateDB, suppliedGas)
|
|
case SelectorGetBurnRatio:
|
|
return d.getBurnRatio(stateDB, suppliedGas)
|
|
case SelectorGetSplit:
|
|
return d.getSplit(stateDB, args, suppliedGas)
|
|
case SelectorIsEnabled:
|
|
return d.isEnabled(stateDB, suppliedGas)
|
|
|
|
default:
|
|
// Unknown selector - treat as receive
|
|
return d.handleReceive(stateDB, caller, addr, suppliedGas, readOnly)
|
|
}
|
|
}
|
|
|
|
// handleReceive processes a value transfer to a dead address
|
|
func (d *deadPrecompile) handleReceive(
|
|
stateDB contract.StateDB,
|
|
caller common.Address,
|
|
addr common.Address,
|
|
suppliedGas uint64,
|
|
readOnly bool,
|
|
) ([]byte, uint64, error) {
|
|
if suppliedGas < GasBase {
|
|
return nil, 0, ErrInsufficientGas
|
|
}
|
|
remainingGas := suppliedGas - GasBase
|
|
|
|
// Check if enabled
|
|
if !d.isEnabledInternal(stateDB) {
|
|
return nil, remainingGas, ErrDisabled
|
|
}
|
|
|
|
// Only funds that have not already been through the split are eligible. The
|
|
// burned share stays at the dead address forever, so the balance never
|
|
// returns to zero and cannot itself mark progress; splitting the whole
|
|
// balance on every call would re-split what was already burned and walk it
|
|
// into the treasury a fraction at a time, one cheap call after another,
|
|
// until the burn was undone. `burned` is that high-water mark.
|
|
balance := stateDB.GetBalance(addr)
|
|
burned := d.getBurnedInternal(stateDB, addr)
|
|
if balance.Cmp(burned) <= 0 {
|
|
return nil, remainingGas, nil
|
|
}
|
|
fresh := new(uint256.Int).Sub(balance, burned)
|
|
|
|
if readOnly {
|
|
// Decline without recording progress, so the split stays pending for a
|
|
// writable call rather than being lost.
|
|
return nil, remainingGas, nil
|
|
}
|
|
|
|
// Get configured treasury and burn ratio
|
|
treasury := d.getTreasuryInternal(stateDB)
|
|
burnBPS := d.getBurnRatioInternal(stateDB)
|
|
|
|
burnAmount, treasuryAmount := CalculateSplitUint256(fresh, burnBPS)
|
|
|
|
// The burn amount stays at the dead address (effectively burned).
|
|
// Transfer treasury amount to the DAO treasury.
|
|
if !treasuryAmount.IsZero() {
|
|
stateDB.SubBalance(addr, treasuryAmount, tracing.BalanceChangeTransfer)
|
|
stateDB.AddBalance(treasury, treasuryAmount, tracing.BalanceChangeTransfer)
|
|
}
|
|
d.setBurned(stateDB, addr, new(uint256.Int).Add(burned, burnAmount))
|
|
|
|
return nil, remainingGas, nil
|
|
}
|
|
|
|
// getBurnedInternal reads the amount already burned at addr.
|
|
func (d *deadPrecompile) getBurnedInternal(stateDB contract.StateDB, addr common.Address) *uint256.Int {
|
|
val := stateDB.GetState(addr, BurnedSlot)
|
|
return new(uint256.Int).SetBytes(val[:])
|
|
}
|
|
|
|
// setBurned records the amount burned at addr.
|
|
func (d *deadPrecompile) setBurned(stateDB contract.StateDB, addr common.Address, v *uint256.Int) {
|
|
stateDB.SetState(addr, BurnedSlot, common.BytesToHash(v.Bytes()))
|
|
}
|
|
|
|
// Admin functions
|
|
|
|
func (d *deadPrecompile) setAdmin(
|
|
stateDB contract.StateDB,
|
|
caller common.Address,
|
|
args []byte,
|
|
suppliedGas uint64,
|
|
readOnly bool,
|
|
) ([]byte, uint64, error) {
|
|
if readOnly {
|
|
return nil, suppliedGas, ErrUnauthorized
|
|
}
|
|
if suppliedGas < GasAdminWrite {
|
|
return nil, 0, ErrInsufficientGas
|
|
}
|
|
remainingGas := suppliedGas - GasAdminWrite
|
|
|
|
// Check authorization
|
|
if !d.isAdmin(stateDB, caller) {
|
|
return nil, remainingGas, ErrUnauthorized
|
|
}
|
|
|
|
// Parse new admin address
|
|
if len(args) < 32 {
|
|
return nil, remainingGas, ErrInvalidInput
|
|
}
|
|
newAdmin := common.BytesToAddress(args[12:32])
|
|
if newAdmin == ZeroAddress {
|
|
return nil, remainingGas, ErrInvalidAddress
|
|
}
|
|
|
|
// Store new admin
|
|
d.setStateAddress(stateDB, AdminSlot, newAdmin)
|
|
|
|
return nil, remainingGas, nil
|
|
}
|
|
|
|
func (d *deadPrecompile) setTreasury(
|
|
stateDB contract.StateDB,
|
|
caller common.Address,
|
|
args []byte,
|
|
suppliedGas uint64,
|
|
readOnly bool,
|
|
) ([]byte, uint64, error) {
|
|
if readOnly {
|
|
return nil, suppliedGas, ErrUnauthorized
|
|
}
|
|
if suppliedGas < GasAdminWrite {
|
|
return nil, 0, ErrInsufficientGas
|
|
}
|
|
remainingGas := suppliedGas - GasAdminWrite
|
|
|
|
if !d.isAdmin(stateDB, caller) {
|
|
return nil, remainingGas, ErrUnauthorized
|
|
}
|
|
|
|
if len(args) < 32 {
|
|
return nil, remainingGas, ErrInvalidInput
|
|
}
|
|
newTreasury := common.BytesToAddress(args[12:32])
|
|
if newTreasury == ZeroAddress {
|
|
return nil, remainingGas, ErrInvalidAddress
|
|
}
|
|
|
|
d.setStateAddress(stateDB, TreasurySlot, newTreasury)
|
|
|
|
return nil, remainingGas, nil
|
|
}
|
|
|
|
func (d *deadPrecompile) setBurnRatio(
|
|
stateDB contract.StateDB,
|
|
caller common.Address,
|
|
args []byte,
|
|
suppliedGas uint64,
|
|
readOnly bool,
|
|
) ([]byte, uint64, error) {
|
|
if readOnly {
|
|
return nil, suppliedGas, ErrUnauthorized
|
|
}
|
|
if suppliedGas < GasAdminWrite {
|
|
return nil, 0, ErrInsufficientGas
|
|
}
|
|
remainingGas := suppliedGas - GasAdminWrite
|
|
|
|
if !d.isAdmin(stateDB, caller) {
|
|
return nil, remainingGas, ErrUnauthorized
|
|
}
|
|
|
|
if len(args) < 32 {
|
|
return nil, remainingGas, ErrInvalidInput
|
|
}
|
|
|
|
// Parse burn ratio (last 8 bytes of 32-byte word)
|
|
newBurnBPS := binary.BigEndian.Uint64(args[24:32])
|
|
if newBurnBPS > BasisPoints {
|
|
return nil, remainingGas, ErrInvalidRatio
|
|
}
|
|
|
|
d.setStateUint64(stateDB, BurnBPSSlot, newBurnBPS)
|
|
|
|
return nil, remainingGas, nil
|
|
}
|
|
|
|
func (d *deadPrecompile) setEnabled(
|
|
stateDB contract.StateDB,
|
|
caller common.Address,
|
|
args []byte,
|
|
suppliedGas uint64,
|
|
readOnly bool,
|
|
) ([]byte, uint64, error) {
|
|
if readOnly {
|
|
return nil, suppliedGas, ErrUnauthorized
|
|
}
|
|
if suppliedGas < GasAdminWrite {
|
|
return nil, 0, ErrInsufficientGas
|
|
}
|
|
remainingGas := suppliedGas - GasAdminWrite
|
|
|
|
if !d.isAdmin(stateDB, caller) {
|
|
return nil, remainingGas, ErrUnauthorized
|
|
}
|
|
|
|
if len(args) < 32 {
|
|
return nil, remainingGas, ErrInvalidInput
|
|
}
|
|
|
|
// Parse bool (non-zero = true)
|
|
enabled := args[31] != 0
|
|
var val uint64
|
|
if enabled {
|
|
val = 1
|
|
}
|
|
d.setStateUint64(stateDB, EnabledSlot, val)
|
|
|
|
return nil, remainingGas, nil
|
|
}
|
|
|
|
// View functions
|
|
|
|
func (d *deadPrecompile) getAdmin(stateDB contract.StateDB, suppliedGas uint64) ([]byte, uint64, error) {
|
|
if suppliedGas < GasAdminRead {
|
|
return nil, 0, ErrInsufficientGas
|
|
}
|
|
remainingGas := suppliedGas - GasAdminRead
|
|
|
|
admin := d.getAdminInternal(stateDB)
|
|
result := make([]byte, 32)
|
|
copy(result[12:], admin.Bytes())
|
|
|
|
return result, remainingGas, nil
|
|
}
|
|
|
|
func (d *deadPrecompile) getTreasury(stateDB contract.StateDB, suppliedGas uint64) ([]byte, uint64, error) {
|
|
if suppliedGas < GasAdminRead {
|
|
return nil, 0, ErrInsufficientGas
|
|
}
|
|
remainingGas := suppliedGas - GasAdminRead
|
|
|
|
treasury := d.getTreasuryInternal(stateDB)
|
|
result := make([]byte, 32)
|
|
copy(result[12:], treasury.Bytes())
|
|
|
|
return result, remainingGas, nil
|
|
}
|
|
|
|
func (d *deadPrecompile) getBurnRatio(stateDB contract.StateDB, suppliedGas uint64) ([]byte, uint64, error) {
|
|
if suppliedGas < GasAdminRead {
|
|
return nil, 0, ErrInsufficientGas
|
|
}
|
|
remainingGas := suppliedGas - GasAdminRead
|
|
|
|
burnBPS := d.getBurnRatioInternal(stateDB)
|
|
result := make([]byte, 32)
|
|
binary.BigEndian.PutUint64(result[24:], burnBPS)
|
|
|
|
return result, remainingGas, nil
|
|
}
|
|
|
|
func (d *deadPrecompile) getSplit(stateDB contract.StateDB, args []byte, suppliedGas uint64) ([]byte, uint64, error) {
|
|
if suppliedGas < GasAdminRead {
|
|
return nil, 0, ErrInsufficientGas
|
|
}
|
|
remainingGas := suppliedGas - GasAdminRead
|
|
|
|
if len(args) < 32 {
|
|
return nil, remainingGas, ErrInvalidInput
|
|
}
|
|
|
|
// Parse value as uint256
|
|
value := new(uint256.Int).SetBytes(args[:32])
|
|
burnBPS := d.getBurnRatioInternal(stateDB)
|
|
|
|
burn, treasury := CalculateSplitUint256(value, burnBPS)
|
|
|
|
// Return burn (32 bytes) + treasury (32 bytes)
|
|
result := make([]byte, 64)
|
|
burn.WriteToSlice(result[:32])
|
|
treasury.WriteToSlice(result[32:])
|
|
|
|
return result, remainingGas, nil
|
|
}
|
|
|
|
func (d *deadPrecompile) isEnabled(stateDB contract.StateDB, suppliedGas uint64) ([]byte, uint64, error) {
|
|
if suppliedGas < GasAdminRead {
|
|
return nil, 0, ErrInsufficientGas
|
|
}
|
|
remainingGas := suppliedGas - GasAdminRead
|
|
|
|
enabled := d.isEnabledInternal(stateDB)
|
|
result := make([]byte, 32)
|
|
if enabled {
|
|
result[31] = 1
|
|
}
|
|
|
|
return result, remainingGas, nil
|
|
}
|
|
|
|
// Internal helper functions
|
|
|
|
func (d *deadPrecompile) isAdmin(stateDB contract.StateDB, caller common.Address) bool {
|
|
admin := d.getAdminInternal(stateDB)
|
|
// An empty slot means no administrator has been provisioned, which is the
|
|
// state of every chain at genesis. Admitting the caller here would hand the
|
|
// first account to send a transaction control of the treasury address and
|
|
// the burn ratio for all three dead addresses, so it refuses instead. An
|
|
// administrator arrives through Configure, from the chain's genesis config;
|
|
// it is provisioned, never claimed.
|
|
if admin == ZeroAddress {
|
|
return false
|
|
}
|
|
return caller == admin
|
|
}
|
|
|
|
func (d *deadPrecompile) getAdminInternal(stateDB contract.StateDB) common.Address {
|
|
val := stateDB.GetState(ZeroAddress, AdminSlot)
|
|
return common.BytesToAddress(val[12:])
|
|
}
|
|
|
|
func (d *deadPrecompile) getTreasuryInternal(stateDB contract.StateDB) common.Address {
|
|
val := stateDB.GetState(ZeroAddress, TreasurySlot)
|
|
addr := common.BytesToAddress(val[12:])
|
|
if addr == ZeroAddress {
|
|
return DefaultDAOTreasury
|
|
}
|
|
return addr
|
|
}
|
|
|
|
func (d *deadPrecompile) getBurnRatioInternal(stateDB contract.StateDB) uint64 {
|
|
val := stateDB.GetState(ZeroAddress, BurnBPSSlot)
|
|
// Check if value was explicitly set (byte 0 is marker)
|
|
if val[0] == 0 {
|
|
// Not explicitly set, return default
|
|
return DefaultBurnBPS
|
|
}
|
|
// Explicitly set, return the value (even if 0)
|
|
return binary.BigEndian.Uint64(val[24:])
|
|
}
|
|
|
|
func (d *deadPrecompile) isEnabledInternal(stateDB contract.StateDB) bool {
|
|
val := stateDB.GetState(ZeroAddress, EnabledSlot)
|
|
// Check if value was explicitly set (byte 0 is marker)
|
|
if val[0] == 0 {
|
|
// Not explicitly set, default to enabled
|
|
return true
|
|
}
|
|
// Explicitly set, return the value
|
|
return val[31] != 0
|
|
}
|
|
|
|
func (d *deadPrecompile) setStateAddress(stateDB contract.StateDB, slot common.Hash, addr common.Address) {
|
|
var val common.Hash
|
|
copy(val[12:], addr.Bytes())
|
|
stateDB.SetState(ZeroAddress, slot, val)
|
|
}
|
|
|
|
func (d *deadPrecompile) setStateUint64(stateDB contract.StateDB, slot common.Hash, v uint64) {
|
|
var val common.Hash
|
|
val[0] = 1 // Marker: explicitly set
|
|
binary.BigEndian.PutUint64(val[24:], v)
|
|
stateDB.SetState(ZeroAddress, slot, val)
|
|
}
|
|
|
|
func (d *deadPrecompile) setStateBool(stateDB contract.StateDB, slot common.Hash, v bool) {
|
|
var val common.Hash
|
|
val[0] = 1 // Marker: explicitly set
|
|
if v {
|
|
val[31] = 1
|
|
}
|
|
stateDB.SetState(ZeroAddress, slot, val)
|
|
}
|
|
|
|
// CalculateSplitUint256 calculates the burn and treasury amounts using uint256
|
|
func CalculateSplitUint256(value *uint256.Int, burnBPS uint64) (burn *uint256.Int, treasury *uint256.Int) {
|
|
if value.IsZero() {
|
|
return uint256.NewInt(0), uint256.NewInt(0)
|
|
}
|
|
|
|
// burn = value * burnBPS / 10000
|
|
burn = new(uint256.Int).Mul(value, uint256.NewInt(burnBPS))
|
|
burn = burn.Div(burn, uint256.NewInt(BasisPoints))
|
|
|
|
// treasury = value - burn
|
|
treasury = new(uint256.Int).Sub(value, burn)
|
|
|
|
return burn, treasury
|
|
}
|
|
|
|
// CalculateSplit calculates the burn and treasury amounts using big.Int (for tests/stats)
|
|
func CalculateSplit(value *big.Int) (burn *big.Int, treasury *big.Int) {
|
|
return CalculateSplitBig(value, DefaultBurnBPS)
|
|
}
|
|
|
|
// CalculateSplitBig calculates split with configurable burn ratio
|
|
func CalculateSplitBig(value *big.Int, burnBPS uint64) (burn *big.Int, treasury *big.Int) {
|
|
if value.Sign() == 0 {
|
|
return big.NewInt(0), big.NewInt(0)
|
|
}
|
|
|
|
// burn = value * burnBPS / 10000
|
|
burn = new(big.Int).Mul(value, big.NewInt(int64(burnBPS)))
|
|
burn = burn.Div(burn, big.NewInt(int64(BasisPoints)))
|
|
|
|
// treasury = value - burn
|
|
treasury = new(big.Int).Sub(value, burn)
|
|
|
|
return burn, treasury
|
|
}
|
|
|
|
// IsDeadAddress returns true if the given address is a registered dead address
|
|
func IsDeadAddress(addr common.Address) bool {
|
|
return slices.Contains(AllDeadAddresses, addr)
|
|
}
|