diff --git a/accounts/abi/bind/precompile_bind.go b/accounts/abi/bind/precompile_bind.go index d31d9d0fef..20581b0336 100644 --- a/accounts/abi/bind/precompile_bind.go +++ b/accounts/abi/bind/precompile_bind.go @@ -43,11 +43,13 @@ func PrecompileBind(types []string, abis []string, bytecodes []string, fsigs []m configBind, err := bindHelper(types, abis, bytecodes, fsigs, pkg, lang, libs, aliases, configHook) if err != nil { - return "", "", err + return "", "", fmt.Errorf("failed to generate config binding: %w", err) } contractBind, err := bindHelper(types, abis, bytecodes, fsigs, pkg, lang, libs, aliases, contractHook) - - return configBind, contractBind, err + if err != nil { + return "", "", fmt.Errorf("failed to generate contract binding: %w", err) + } + return configBind, contractBind, nil } func createPrecompileHook(abifilename string, template string) BindHook { diff --git a/accounts/abi/bind/precompile_bind_test.go b/accounts/abi/bind/precompile_bind_test.go index 23f1503142..84e15b242a 100644 --- a/accounts/abi/bind/precompile_bind_test.go +++ b/accounts/abi/bind/precompile_bind_test.go @@ -28,6 +28,8 @@ package bind import ( "testing" + + "github.com/stretchr/testify/require" ) var bindFailedTests = []struct { @@ -100,10 +102,7 @@ func golangBindingsFailure(t *testing.T) { if err == nil { t.Fatalf("test %d: no error occurred but was expected", i) } - - if tt.errorMsg != err.Error() { - t.Fatalf("test %d: expected Err %s but got actual Err: %s", i, tt.errorMsg, err.Error()) - } + require.ErrorContains(t, err, tt.errorMsg) }) } } diff --git a/accounts/abi/bind/precompile_config_template.go b/accounts/abi/bind/precompile_config_template.go index 52ff02d268..dabd5fed86 100644 --- a/accounts/abi/bind/precompile_config_template.go +++ b/accounts/abi/bind/precompile_config_template.go @@ -14,14 +14,14 @@ const tmplSourcePrecompileConfigGo = ` // For testing take a look at other precompile tests in core/stateful_precompile_test.go and config_test.go in other precompile folders. /* General guidelines for precompile development: -1- Read the comment and set a suitable contract address in precompile/params.go. E.g: - {{.Contract.Type}}Address = common.HexToAddress("ASUITABLEHEXADDRESS") -2- Set gas costs in contract.go -3- It is recommended to only modify code in the highlighted areas marked with "CUSTOM CODE STARTS HERE". Modifying code outside of these areas should be done with caution and with a deep understanding of how these changes may impact the EVM. +1- Read the comment and set a suitable contract address in generated contract.go. E.g: + ContractAddress = common.HexToAddress("ASUITABLEHEXADDRESS") +2- It is recommended to only modify code in the highlighted areas marked with "CUSTOM CODE STARTS HERE". Modifying code outside of these areas should be done with caution and with a deep understanding of how these changes may impact the EVM. Typically, custom codes are required in only those areas. -4- Add your upgradable config in params/precompile_config.go -5- Add your precompile upgrade in params/config.go -6- Add your config unit test in {generatedpkg}/config_test.go +3- Set gas costs in generated contract.go +4- Register your precompile module in params/precompile_modules.go +5- Add your config unit tests under generated package config_test.go +6- Add your contract unit tests under tests/statefulprecompiles/{precompilename}_test.go 7- Add your solidity interface and test contract to contract-examples/contracts 8- Write solidity tests for your precompile in contract-examples/test 9- Create your genesis with your precompile enabled in tests/e2e/genesis/ @@ -33,24 +33,27 @@ Typically, custom codes are required in only those areas. package {{.Package}} import ( - "encoding/json" "math/big" "github.com/ava-labs/subnet-evm/precompile" + {{- if .Contract.AllowList}} + "github.com/ava-labs/subnet-evm/precompile/allowlist" + {{- end}} "github.com/ethereum/go-ethereum/common" ) -{{$contract := .Contract}} -var ( - _ precompile.StatefulPrecompileConfig = &{{.Contract.Type}}Config{} -) +var _ precompile.StatefulPrecompileConfig = &{{.Contract.Type}}Config{} + +// ConfigKey is the key used in json config files to specify this precompile config. +// Must be unique across all precompiles. +const ConfigKey = "{{decapitalise .Contract.Type}}Config" // {{.Contract.Type}}Config implements the StatefulPrecompileConfig // interface while adding in the {{.Contract.Type}} specific precompile address. type {{.Contract.Type}}Config struct { {{- if .Contract.AllowList}} - precompile.AllowListConfig + allowlist.AllowListConfig {{- end}} precompile.UpgradeableConfig } @@ -81,7 +84,7 @@ type {{capitalise .Normalized.Name}}Output struct{ // {{.Contract.Type}} {{if .Contract.AllowList}} with the given [admins] as members of the allowlist {{end}}. func New{{.Contract.Type}}Config(blockTimestamp *big.Int{{if .Contract.AllowList}}, admins []common.Address{{end}}) *{{.Contract.Type}}Config { return &{{.Contract.Type}}Config{ - {{if .Contract.AllowList}}AllowListConfig: precompile.AllowListConfig{AllowListAdmins: admins},{{end}} + {{if .Contract.AllowList}}AllowListConfig: allowlist.AllowListConfig{AdminAddresses: admins},{{end}} UpgradeableConfig: precompile.UpgradeableConfig{BlockTimestamp: blockTimestamp}, } } @@ -125,27 +128,41 @@ func (c *{{.Contract.Type}}Config) Equal(s precompile.StatefulPrecompileConfig) return equals } -// Address returns the address of the {{.Contract.Type}}. Addresses reside under the precompile/params.go -// Select a non-conflicting address and set it in the params.go. -func (c *{{.Contract.Type}}Config) Address() common.Address { - return {{.Contract.Type}}Address -} - // Configure configures [state] with the initial configuration. func (c *{{.Contract.Type}}Config) Configure(_ precompile.ChainConfig, state precompile.StateDB, _ precompile.BlockContext) error { - {{if .Contract.AllowList}}c.AllowListConfig.Configure(state, {{.Contract.Type}}Address){{end}} + {{if .Contract.AllowList}}c.AllowListConfig.Configure(state, ContractAddress){{end}} // CUSTOM CODE STARTS HERE return nil } + +// Required module functions for {{.Contract.Type}}Config +// These functions mostly do not require any custom code. + +// NewModule returns a new module for {{.Contract.Type}}. +func NewModule() precompile.StatefulPrecompileModule { + return &{{.Contract.Type}}Config{} +} + +// Address returns the address of the {{.Contract.Type}}. +// Select a non-conflicting address and set it in generated contract.go +func ({{.Contract.Type}}Config) Address() common.Address { + return ContractAddress +} + // Contract returns the singleton stateful precompiled contract to be used for {{.Contract.Type}}. -func (c *{{.Contract.Type}}Config) Contract() precompile.StatefulPrecompiledContract { +func ({{.Contract.Type}}Config) Contract() precompile.StatefulPrecompiledContract { return {{.Contract.Type}}Precompile } -// String returns a string representation of the {{.Contract.Type}}Config. -func (c *{{.Contract.Type}}Config) String() string { - bytes, _ := json.Marshal(c) - return string(bytes) +// Key returns the key used in json config files to specify this precompile config. +func ({{.Contract.Type}}Config) Key() string { + return ConfigKey +} + +// New returns a new {{.Contract.Type}}Config. +// This is used by the json parser to create a new instance of the {{.Contract.Type}}Config. +func ({{.Contract.Type}}Config) NewConfig() precompile.StatefulPrecompileConfig { + return new({{.Contract.Type}}Config) } ` diff --git a/accounts/abi/bind/precompile_contract_template.go b/accounts/abi/bind/precompile_contract_template.go index 6dd7967b3e..47c3a04188 100644 --- a/accounts/abi/bind/precompile_contract_template.go +++ b/accounts/abi/bind/precompile_contract_template.go @@ -20,23 +20,23 @@ type tmplPrecompileContract struct { // tmplSourcePrecompileContractGo is the Go precompiled contract source template. const tmplSourcePrecompileContractGo = ` // Code generated -// This file is a generated precompile contract with stubbed abstract functions. +// This file is a generated precompile contract config with stubbed abstract functions. // The file is generated by a template. Please inspect every code and comment in this file before use. // There are some must-be-done changes waiting in the file. Each area requiring you to add your code is marked with CUSTOM CODE to make them easy to find and modify. // Additionally there are other files you need to edit to activate your precompile. // These areas are highlighted with comments "ADD YOUR PRECOMPILE HERE". -// For testing take a look at other precompile tests in core/stateful_precompile_test.go +// For testing take a look at other precompile tests in tests/statefulprecompiles/ and config_test.go in other precompile folders. /* General guidelines for precompile development: -1- Read the comment and set a suitable contract address in precompile/params.go. E.g: - {{.Contract.Type}}Address = common.HexToAddress("ASUITABLEHEXADDRESS") -2- Set gas costs in contract.go +1- Read the comment and set a suitable contract address in generated contract.go. E.g: + ContractAddress = common.HexToAddress("ASUITABLEHEXADDRESS") +2- Set gas costs in generated contract.go 3- It is recommended to only modify code in the highlighted areas marked with "CUSTOM CODE STARTS HERE". Modifying code outside of these areas should be done with caution and with a deep understanding of how these changes may impact the EVM. Typically, custom codes are required in only those areas. -4- Add your upgradable config in params/precompile_config.go -5- Add your precompile upgrade in params/config.go -6- Add your config unit test in {generatedpkg}/config_test.go +4- Register your precompile module in params/precompile_modules.go +5- Add your config unit tests under generated package config_test.go +6- Add your contract unit tests under tests/statefulprecompiles/{precompilename}_test.go 7- Add your solidity interface and test contract to contract-examples/contracts 8- Write solidity tests for your precompile in contract-examples/test 9- Create your genesis with your precompile enabled in tests/e2e/genesis/ @@ -48,7 +48,6 @@ Typically, custom codes are required in only those areas. package {{.Package}} import ( - "encoding/json" "math/big" "errors" "fmt" @@ -56,6 +55,9 @@ import ( "github.com/ava-labs/subnet-evm/accounts/abi" "github.com/ava-labs/subnet-evm/precompile" + {{- if .Contract.AllowList}} + "github.com/ava-labs/subnet-evm/precompile/allowlist" + {{- end}} "github.com/ava-labs/subnet-evm/vmerrs" _ "embed" @@ -77,14 +79,16 @@ const ( var ( _ = errors.New _ = big.NewInt - _ = strings.NewReader - _ = fmt.Printf - _ = json.Unmarshal ) {{$contract := .Contract}} // Singleton StatefulPrecompiledContract and signatures. var ( + // ContractAddress is the defined address of the precompile contract. + // This should be unique across all precompile contracts. + // See params/precompile_modules.go for registered precompile contracts and more information. + ContractAddress = common.HexToAddress("{ASUITABLEHEXADDRESS}") // SET A SUITABLE HEX ADDRESS HERE + {{- range .Contract.Funcs}} {{- if not .Original.IsConstant | and $contract.AllowList}} @@ -107,10 +111,6 @@ var ( {{.Contract.Type}}ABI abi.ABI // will be initialized by init function {{.Contract.Type}}Precompile precompile.StatefulPrecompiledContract // will be initialized by init function - - // CUSTOM CODE STARTS HERE - // THIS SHOULD BE MOVED TO precompile/params.go with a suitable hex address. - {{.Contract.Type}}Address = common.HexToAddress("ASUITABLEHEXADDRESS") ) {{$structs := .Structs}} @@ -142,7 +142,7 @@ func init() { } {{.Contract.Type}}ABI = parsed - {{.Contract.Type}}Precompile, err = create{{.Contract.Type}}Precompile({{.Contract.Type}}Address) + {{.Contract.Type}}Precompile, err = create{{.Contract.Type}}Precompile() if err != nil { panic(err) } @@ -150,18 +150,18 @@ func init() { {{if .Contract.AllowList}} // Get{{.Contract.Type}}AllowListStatus returns the role of [address] for the {{.Contract.Type}} list. -func Get{{.Contract.Type}}AllowListStatus(stateDB precompile.StateDB, address common.Address) precompile.AllowListRole { - return precompile.GetAllowListStatus(stateDB, {{.Contract.Type}}Address, address) +func Get{{.Contract.Type}}AllowListStatus(stateDB precompile.StateDB, address common.Address) allowlist.AllowListRole { + return allowlist.GetAllowListStatus(stateDB, ContractAddress, address) } // Set{{.Contract.Type}}AllowListStatus sets the permissions of [address] to [role] for the // {{.Contract.Type}} list. Assumes [role] has already been verified as valid. -// This stores the [role] in the contract storage with address [{{.Contract.Type}}Address] +// This stores the [role] in the contract storage with address [ContractAddress] // and [address] hash. It means that any reusage of the [address] key for different value // conflicts with the same slot [role] is stored. // Precompile implementations must use a different key than [address] for their storage. -func Set{{.Contract.Type}}AllowListStatus(stateDB precompile.StateDB, address common.Address, role precompile.AllowListRole) { - precompile.SetAllowListRole(stateDB, {{.Contract.Type}}Address, address, role) +func Set{{.Contract.Type}}AllowListStatus(stateDB precompile.StateDB, address common.Address, role allowlist.AllowListRole) { + allowlist.SetAllowListRole(stateDB, ContractAddress, address, role) } {{end}} @@ -257,8 +257,8 @@ func {{decapitalise .Normalized.Name}}(accessibleState precompile.PrecompileAcce // This part of the code restricts the function to be called only by enabled/admin addresses in the allow list. // You can modify/delete this code if you don't want this function to be restricted by the allow list. stateDB := accessibleState.GetStateDB() - // Verify that the caller is in the allow list and therefore has the right to modify it - callerStatus := precompile.GetAllowListStatus(stateDB, {{$contract.Type}}Address, caller) + // Verify that the caller is in the allow list and therefore has the right to call this function. + callerStatus := allowlist.GetAllowListStatus(stateDB, ContractAddress, caller) if !callerStatus.IsEnabled() { return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannot{{.Normalized.Name}}, caller) } @@ -308,8 +308,8 @@ func {{decapitalise $contract.Type}}Fallback (accessibleState precompile.Precomp // This part of the code restricts the function to be called only by enabled/admin addresses in the allow list. // You can modify/delete this code if you don't want this function to be restricted by the allow list. stateDB := accessibleState.GetStateDB() - // Verify that the caller is in the allow list and therefore has the right to modify it - callerStatus := precompile.GetAllowListStatus(stateDB, {{$contract.Type}}Address, caller) + // Verify that the caller is in the allow list and therefore has the right to call this function. + callerStatus := allowlist.GetAllowListStatus(stateDB, ContractAddress, caller) if !callerStatus.IsEnabled() { return nil, remainingGas, fmt.Errorf("%w: %s", Err{{$contract.Type}}CannotFallback, caller) } @@ -329,11 +329,11 @@ func {{decapitalise $contract.Type}}Fallback (accessibleState precompile.Precomp {{- end}} // create{{.Contract.Type}}Precompile returns a StatefulPrecompiledContract with getters and setters for the precompile. -{{if .Contract.AllowList}} // Access to the getters/setters is controlled by an allow list for [precompileAddr].{{end}} -func create{{.Contract.Type}}Precompile(precompileAddr common.Address) (precompile.StatefulPrecompiledContract, error) { +{{if .Contract.AllowList}} // Access to the getters/setters is controlled by an allow list for ContractAddress.{{end}} +func create{{.Contract.Type}}Precompile() (precompile.StatefulPrecompiledContract, error) { var functions []*precompile.StatefulPrecompileFunction {{- if .Contract.AllowList}} - functions = append(functions, precompile.CreateAllowListFunctions(precompileAddr)...) + functions = append(functions, allowlist.CreateAllowListFunctions(ContractAddress)...) {{- end}} abiFunctionMap := map[string]precompile.RunStatefulPrecompileFunc{ diff --git a/cmd/precompilegen/main.go b/cmd/precompilegen/main.go index 0a60ed187f..7679d2c3e6 100644 --- a/cmd/precompilegen/main.go +++ b/cmd/precompilegen/main.go @@ -147,7 +147,7 @@ func precompilegen(c *cli.Context) error { // Generate the contract precompile configCode, contractCode, err := bind.PrecompileBind(types, abis, bins, sigs, pkg, lang, libs, aliases, abifilename) if err != nil { - utils.Fatalf("Failed to generate ABI precompile: %v", err) + utils.Fatalf("Failed to generate precompile: %v", err) } // Either flush it out to a file or display on the standard output @@ -165,13 +165,13 @@ func precompilegen(c *cli.Context) error { configCodeOut := filepath.Join(outFlagStr, "config.go") if err := os.WriteFile(configCodeOut, []byte(configCode), 0o600); err != nil { - utils.Fatalf("Failed to write generated precompile: %v", err) + utils.Fatalf("Failed to write generated config code: %v", err) } contractCodeOut := filepath.Join(outFlagStr, "contract.go") if err := os.WriteFile(contractCodeOut, []byte(contractCode), 0o600); err != nil { - utils.Fatalf("Failed to write generated precompile: %v", err) + utils.Fatalf("Failed to write generated contract code: %v", err) } if err := os.WriteFile(abipath, []byte(abis[0]), 0o600); err != nil { diff --git a/contract-examples/contracts/ExampleTxAllowList.sol b/contract-examples/contracts/ExampleTxAllowList.sol index 5c17fe1169..9d5c80095f 100644 --- a/contract-examples/contracts/ExampleTxAllowList.sol +++ b/contract-examples/contracts/ExampleTxAllowList.sol @@ -4,11 +4,11 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/access/Ownable.sol"; import "./AllowList.sol"; -// ExampleDeployerList shows how ContractDeployerAllowList precompile can be used in a smart contract +// ExampleTxAllowList shows how TxAllowList precompile can be used in a smart contract // All methods of [allowList] can be directly called. There are example calls as tasks in hardhat.config.ts file. contract ExampleTxAllowList is AllowList { // Precompiled Allow List Contract Address - address constant DEPLOYER_LIST = 0x0200000000000000000000000000000000000002; + address constant TX_ALLOW_LIST = 0x0200000000000000000000000000000000000002; - constructor() AllowList(DEPLOYER_LIST) {} + constructor() AllowList(TX_ALLOW_LIST) {} } diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index ca3dbc528f..6d1b56f77a 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -39,9 +39,8 @@ import ( "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/core/vm" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/precompile/feemanager" - "github.com/ava-labs/subnet-evm/precompile/rewardmanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/feemanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/rewardmanager" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/event" ) @@ -350,7 +349,7 @@ func (bc *BlockChain) SubscribeAcceptedTransactionEvent(ch chan<- NewTxsEvent) e func (bc *BlockChain) GetFeeConfigAt(parent *types.Header) (commontype.FeeConfig, *big.Int, error) { config := bc.Config() bigTime := new(big.Int).SetUint64(parent.Time) - if !config.IsPrecompileEnabled(precompile.FeeManagerAddress, bigTime) { + if !config.IsPrecompileEnabled(feemanager.ContractAddress, bigTime) { return config.FeeConfig, common.Big0, nil } @@ -394,7 +393,7 @@ func (bc *BlockChain) GetCoinbaseAt(parent *types.Header) (common.Address, bool, return constants.BlackholeAddr, false, nil } - if !config.IsPrecompileEnabled(precompile.RewardManagerAddress, bigTime) { + if !config.IsPrecompileEnabled(rewardmanager.ContractAddress, bigTime) { if bc.chainConfig.AllowFeeRecipients { return common.Address{}, true, nil } else { diff --git a/core/genesis.go b/core/genesis.go index 1ab970ad2e..07a0ac1bc0 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -307,7 +307,7 @@ func (g *Genesis) ToBlock(db ethdb.Database) *types.Block { } // Configure any stateful precompiles that should be enabled in the genesis. - err = g.Config.ConfigurePrecompiles(nil, types.NewBlockWithHeader(head), statedb) + err = ApplyPrecompileActivations(g.Config, nil, types.NewBlockWithHeader(head), statedb) if err != nil { panic(fmt.Sprintf("unable to configure precompiles in genesis block: %v", err)) } diff --git a/core/genesis_test.go b/core/genesis_test.go index 61baeed669..3c2b57d1c0 100644 --- a/core/genesis_test.go +++ b/core/genesis_test.go @@ -38,9 +38,8 @@ import ( "github.com/ava-labs/subnet-evm/core/vm" "github.com/ava-labs/subnet-evm/ethdb" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" "github.com/ava-labs/subnet-evm/precompile/allowlist" - "github.com/ava-labs/subnet-evm/precompile/deployerallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/deployerallowlist" "github.com/davecgh/go-spew/spew" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" @@ -192,12 +191,14 @@ func TestStatefulPrecompilesConfigure(t *testing.T) { "allow list enabled in genesis": { getConfig: func() *params.ChainConfig { config := *params.TestChainConfig - config.ContractDeployerAllowListConfig = deployerallowlist.NewContractDeployerAllowListConfig(big.NewInt(0), []common.Address{addr}, nil) + config.GenesisPrecompiles = params.ChainConfigPrecompiles{ + deployerallowlist.ConfigKey: deployerallowlist.NewConfig(big.NewInt(0), []common.Address{addr}, nil), + } return &config }, assertState: func(t *testing.T, sdb *state.StateDB) { - assert.Equal(t, allowlist.AllowListAdmin, deployerallowlist.GetContractDeployerAllowListStatus(sdb, addr), "unexpected allow list status for modified address") - assert.Equal(t, uint64(1), sdb.GetNonce(precompile.ContractDeployerAllowListAddress)) + assert.Equal(t, allowlist.AdminRole, deployerallowlist.GetContractDeployerAllowListStatus(sdb, addr), "unexpected allow list status for modified address") + assert.Equal(t, uint64(1), sdb.GetNonce(deployerallowlist.ContractAddress)) }, }, } { @@ -267,11 +268,10 @@ func TestPrecompileActivationAfterHeaderBlock(t *testing.T) { require.Greater(block.Time(), bc.lastAccepted.Time()) activatedGenesis := customg - contractDeployerConfig := deployerallowlist.NewContractDeployerAllowListConfig(big.NewInt(51), nil, nil) + contractDeployerConfig := deployerallowlist.NewConfig(big.NewInt(51), nil, nil) activatedGenesis.Config.UpgradeConfig.PrecompileUpgrades = []params.PrecompileUpgrade{ { - // Enable ContractDeployerAllowList at timestamp 50 - ContractDeployerAllowListConfig: contractDeployerConfig, + Config: contractDeployerConfig, }, } diff --git a/core/state_processor.go b/core/state_processor.go index 58d7df6fd7..99be0b20f6 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -35,6 +35,8 @@ import ( "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/core/vm" "github.com/ava-labs/subnet-evm/params" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" @@ -79,7 +81,7 @@ func (p *StateProcessor) Process(block *types.Block, parent *types.Header, state ) // Configure any stateful precompiles that should go into effect during this block. - err := p.config.ConfigurePrecompiles(new(big.Int).SetUint64(parent.Time), block, statedb) + err := ApplyPrecompileActivations(p.config, new(big.Int).SetUint64(parent.Time), block, statedb) if err != nil { log.Error("failed to configure precompiles processing block", "hash", block.Hash(), "number", block.NumberU64(), "timestamp", block.Time(), "err", err) return nil, nil, 0, err @@ -168,3 +170,51 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, config, cfg) return applyTransaction(msg, config, author, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv) } + +// ApplyPrecompileActivations checks if any of the precompiles specified by the chain config are enabled or disabled by the block +// transition from [parentTimestamp] to the timestamp set in [blockContext]. If this is the case, it calls [Configure] +// to apply the necessary state transitions for the upgrade. +// This function is called: +// - within genesis setup to configure the starting state for precompiles enabled at genesis, +// - during block processing to update the state before processing the given block. +// - during block producing to apply the precompile upgrades before producing the block. +func ApplyPrecompileActivations(c *params.ChainConfig, parentTimestamp *big.Int, blockContext contract.BlockContext, statedb *state.StateDB) error { + blockTimestamp := blockContext.Timestamp() + // Note: RegisteredModules returns precompiles sorted by module addresses. + // This ensures that the order we call Configure for each precompile is consistent. + // This ensures even if precompiles read/write state other than their own they will observe + // an identical global state in a deterministic order when as they are configured. + for _, module := range modules.RegisteredModules() { + key := module.ConfigKey + for _, activatingConfig := range c.GetActivatingPrecompileConfigs(module.Address, parentTimestamp, blockTimestamp, c.PrecompileUpgrades) { + // If this transition activates the upgrade, configure the stateful precompile. + // (or deconfigure it if it is being disabled.) + if activatingConfig.IsDisabled() { + log.Info("Disabling precompile", "name", key) + statedb.Suicide(module.Address) + // Calling Finalise here effectively commits Suicide call and wipes the contract state. + // This enables re-configuration of the same contract state in the same block. + // Without an immediate Finalise call after the Suicide, a reconfigured precompiled state can be wiped out + // since Suicide will be committed after the reconfiguration. + statedb.Finalise(true) + } else { + module, ok := modules.GetPrecompileModule(key) + if !ok { + return fmt.Errorf("could not find module for activating precompile, name: %s", key) + } + log.Info("Activating new precompile", "name", key, "config", activatingConfig) + // Set the nonce of the precompile's address (as is done when a contract is created) to ensure + // that it is marked as non-empty and will not be cleaned up when the statedb is finalized. + statedb.SetNonce(module.Address, 1) + // Set the code of the precompile's address to a non-zero length byte slice to ensure that the precompile + // can be called from within Solidity contracts. Solidity adds a check before invoking a contract to ensure + // that it does not attempt to invoke a non-existent contract. + statedb.SetCode(module.Address, []byte{0x1}) + if err := module.Configure(c, activatingConfig, statedb, blockContext); err != nil { + return fmt.Errorf("could not configure precompile, name: %s, reason: %w", key, err) + } + } + } + } + return nil +} diff --git a/core/state_processor_test.go b/core/state_processor_test.go index 0c5ce0f50b..d8da0edda5 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -36,7 +36,7 @@ import ( "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/core/vm" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile/txallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" "github.com/ava-labs/subnet-evm/trie" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -315,8 +315,8 @@ func TestBadTxAllowListBlock(t *testing.T) { NetworkUpgrades: params.NetworkUpgrades{ SubnetEVMTimestamp: big.NewInt(0), }, - PrecompileUpgrade: params.PrecompileUpgrade{ - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(0), nil, nil), + GenesisPrecompiles: params.ChainConfigPrecompiles{ + txallowlist.ConfigKey: txallowlist.NewConfig(big.NewInt(0), nil, nil), }, } signer = types.LatestSigner(config) diff --git a/core/state_transition.go b/core/state_transition.go index 5fcfa642b5..ccb72beeaa 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -36,8 +36,7 @@ import ( "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/core/vm" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/precompile/txallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" ) @@ -250,10 +249,10 @@ func (st *StateTransition) preCheck() error { } // Check that the sender is on the tx allow list if enabled - if st.evm.ChainConfig().IsPrecompileEnabled(precompile.TxAllowListAddress, st.evm.Context.Time) { + if st.evm.ChainConfig().IsPrecompileEnabled(txallowlist.ContractAddress, st.evm.Context.Time) { txAllowListRole := txallowlist.GetTxAllowListStatus(st.state, st.msg.From()) if !txAllowListRole.IsEnabled() { - return fmt.Errorf("%w: %s", txallowlist.ErrSenderAddressNotAllowListed, st.msg.From()) + return fmt.Errorf("%w: %s", vmerrs.ErrSenderAddressNotAllowListed, st.msg.From()) } } } diff --git a/core/stateful_precompile_test.go b/core/stateful_precompile_test.go deleted file mode 100644 index 5f4f868918..0000000000 --- a/core/stateful_precompile_test.go +++ /dev/null @@ -1,1296 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package core - -import ( - "math/big" - "testing" - - "github.com/ava-labs/avalanchego/snow" - "github.com/ava-labs/subnet-evm/commontype" - "github.com/ava-labs/subnet-evm/constants" - "github.com/ava-labs/subnet-evm/core/rawdb" - "github.com/ava-labs/subnet-evm/core/state" - "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/precompile/allowlist" - "github.com/ava-labs/subnet-evm/precompile/deployerallowlist" - "github.com/ava-labs/subnet-evm/precompile/feemanager" - "github.com/ava-labs/subnet-evm/precompile/nativeminter" - "github.com/ava-labs/subnet-evm/precompile/rewardmanager" - "github.com/ava-labs/subnet-evm/precompile/txallowlist" - "github.com/ava-labs/subnet-evm/vmerrs" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/math" - "github.com/stretchr/testify/require" -) - -// TODO: move this to precompile package once cross-import is resolved - -var ( - _ precompile.BlockContext = &mockBlockContext{} - _ precompile.PrecompileAccessibleState = &mockAccessibleState{} - - testFeeConfig = commontype.FeeConfig{ - GasLimit: big.NewInt(8_000_000), - TargetBlockRate: 2, // in seconds - - MinBaseFee: big.NewInt(25_000_000_000), - TargetGas: big.NewInt(15_000_000), - BaseFeeChangeDenominator: big.NewInt(36), - - MinBlockGasCost: big.NewInt(0), - MaxBlockGasCost: big.NewInt(1_000_000), - BlockGasCostStep: big.NewInt(200_000), - } - - testBlockNumber = big.NewInt(7) -) - -type mockBlockContext struct { - blockNumber *big.Int - timestamp uint64 -} - -func (mb *mockBlockContext) Number() *big.Int { return mb.blockNumber } -func (mb *mockBlockContext) Timestamp() *big.Int { return new(big.Int).SetUint64(mb.timestamp) } - -type mockAccessibleState struct { - state *state.StateDB - blockContext *mockBlockContext - snowContext *snow.Context -} - -func (m *mockAccessibleState) GetStateDB() precompile.StateDB { return m.state } - -func (m *mockAccessibleState) GetBlockContext() precompile.BlockContext { return m.blockContext } - -func (m *mockAccessibleState) GetSnowContext() *snow.Context { return m.snowContext } - -func (m *mockAccessibleState) CallFromPrecompile(caller common.Address, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) { - return nil, 0, nil -} - -// This test is added within the core package so that it can import all of the required code -// without creating any import cycles -func TestContractDeployerAllowListRun(t *testing.T) { - type test struct { - caller common.Address - input func() []byte - suppliedGas uint64 - readOnly bool - - expectedRes []byte - expectedErr string - - assertState func(t *testing.T, state *state.StateDB) - } - - adminAddr := common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC") - noRoleAddr := common.HexToAddress("0xF60C45c607D0f41687c94C314d300f483661E13a") - - for name, test := range map[string]test{ - "set admin": { - caller: adminAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(noRoleAddr, allowlist.AllowListAdmin) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := deployerallowlist.GetContractDeployerAllowListStatus(state, noRoleAddr) - require.Equal(t, allowlist.AllowListAdmin, res) - }, - }, - "set deployer": { - caller: adminAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(noRoleAddr, allowlist.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := deployerallowlist.GetContractDeployerAllowListStatus(state, noRoleAddr) - require.Equal(t, allowlist.AllowListEnabled, res) - }, - }, - "set no role": { - caller: adminAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.AllowListNoRole) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := deployerallowlist.GetContractDeployerAllowListStatus(state, adminAddr) - require.Equal(t, allowlist.AllowListNoRole, res) - }, - }, - "set no role from non-admin": { - caller: noRoleAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.AllowListNoRole) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedErr: allowlist.ErrCannotModifyAllowList.Error(), - }, - "set deployer from non-admin": { - caller: noRoleAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedErr: allowlist.ErrCannotModifyAllowList.Error(), - }, - "set admin from non-admin": { - caller: noRoleAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.AllowListAdmin) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedErr: allowlist.ErrCannotModifyAllowList.Error(), - }, - "set no role with readOnly enabled": { - caller: adminAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.AllowListNoRole) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "set no role insufficient gas": { - caller: adminAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.AllowListNoRole) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "read allow list no role": { - caller: noRoleAddr, - input: func() []byte { - return allowlist.PackReadAllowList(noRoleAddr) - }, - suppliedGas: allowlist.ReadAllowListGasCost, - readOnly: false, - expectedRes: common.Hash(allowlist.AllowListNoRole).Bytes(), - assertState: nil, - }, - "read allow list admin role": { - caller: adminAddr, - input: func() []byte { - return allowlist.PackReadAllowList(noRoleAddr) - }, - suppliedGas: allowlist.ReadAllowListGasCost, - readOnly: false, - expectedRes: common.Hash(allowlist.AllowListNoRole).Bytes(), - assertState: nil, - }, - "read allow list with readOnly enabled": { - caller: adminAddr, - input: func() []byte { - return allowlist.PackReadAllowList(noRoleAddr) - }, - suppliedGas: allowlist.ReadAllowListGasCost, - readOnly: true, - expectedRes: common.Hash(allowlist.AllowListNoRole).Bytes(), - assertState: nil, - }, - "read allow list out of gas": { - caller: adminAddr, - input: func() []byte { - return allowlist.PackReadAllowList(noRoleAddr) - }, - suppliedGas: allowlist.ReadAllowListGasCost - 1, - readOnly: true, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - } { - t.Run(name, func(t *testing.T) { - db := rawdb.NewMemoryDatabase() - state, err := state.New(common.Hash{}, state.NewDatabase(db), nil) - require.NoError(t, err) - - // Set up the state so that each address has the expected permissions at the start. - deployerallowlist.SetContractDeployerAllowListStatus(state, adminAddr, allowlist.AllowListAdmin) - deployerallowlist.SetContractDeployerAllowListStatus(state, noRoleAddr, allowlist.AllowListNoRole) - require.Equal(t, allowlist.AllowListAdmin, deployerallowlist.GetContractDeployerAllowListStatus(state, adminAddr)) - require.Equal(t, allowlist.AllowListNoRole, deployerallowlist.GetContractDeployerAllowListStatus(state, noRoleAddr)) - - blockContext := &mockBlockContext{blockNumber: common.Big0} - ret, remainingGas, err := deployerallowlist.ContractDeployerAllowListPrecompile.Run(&mockAccessibleState{state: state, blockContext: blockContext, snowContext: snow.DefaultContextTest()}, test.caller, precompile.ContractDeployerAllowListAddress, test.input(), test.suppliedGas, test.readOnly) - if len(test.expectedErr) != 0 { - require.ErrorContains(t, err, test.expectedErr) - } else { - require.NoError(t, err) - } - - require.Equal(t, uint64(0), remainingGas) - require.Equal(t, test.expectedRes, ret) - - if test.assertState != nil { - test.assertState(t, state) - } - }) - } -} - -func TestTxAllowListRun(t *testing.T) { - type test struct { - caller common.Address - precompileAddr common.Address - input func() []byte - suppliedGas uint64 - readOnly bool - - expectedRes []byte - expectedErr string - - assertState func(t *testing.T, state *state.StateDB) - } - - adminAddr := common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC") - noRoleAddr := common.HexToAddress("0xF60C45c607D0f41687c94C314d300f483661E13a") - - for name, test := range map[string]test{ - "set admin": { - caller: adminAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(noRoleAddr, allowlist.AllowListAdmin) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := txallowlist.GetTxAllowListStatus(state, noRoleAddr) - require.Equal(t, allowlist.AllowListAdmin, res) - }, - }, - "set allowed": { - caller: adminAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(noRoleAddr, allowlist.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := txallowlist.GetTxAllowListStatus(state, noRoleAddr) - require.Equal(t, allowlist.AllowListEnabled, res) - }, - }, - "set no role": { - caller: adminAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.AllowListNoRole) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := txallowlist.GetTxAllowListStatus(state, adminAddr) - require.Equal(t, allowlist.AllowListNoRole, res) - }, - }, - "set no role from non-admin": { - caller: noRoleAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.AllowListNoRole) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedErr: allowlist.ErrCannotModifyAllowList.Error(), - }, - "set allowed from non-admin": { - caller: noRoleAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedErr: allowlist.ErrCannotModifyAllowList.Error(), - }, - "set admin from non-admin": { - caller: noRoleAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.AllowListAdmin) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedErr: allowlist.ErrCannotModifyAllowList.Error(), - }, - "set no role with readOnly enabled": { - caller: adminAddr, - precompileAddr: precompile.TxAllowListAddress, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.AllowListNoRole) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "set no role insufficient gas": { - caller: adminAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.AllowListNoRole) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "read allow list no role": { - caller: noRoleAddr, - input: func() []byte { - return allowlist.PackReadAllowList(noRoleAddr) - }, - suppliedGas: allowlist.ReadAllowListGasCost, - readOnly: false, - expectedRes: common.Hash(allowlist.AllowListNoRole).Bytes(), - assertState: nil, - }, - "read allow list admin role": { - caller: adminAddr, - input: func() []byte { - return allowlist.PackReadAllowList(noRoleAddr) - }, - suppliedGas: allowlist.ReadAllowListGasCost, - readOnly: false, - expectedRes: common.Hash(allowlist.AllowListNoRole).Bytes(), - assertState: nil, - }, - "read allow list with readOnly enabled": { - caller: adminAddr, - input: func() []byte { - return allowlist.PackReadAllowList(noRoleAddr) - }, - suppliedGas: allowlist.ReadAllowListGasCost, - readOnly: true, - expectedRes: common.Hash(allowlist.AllowListNoRole).Bytes(), - assertState: nil, - }, - "read allow list out of gas": { - caller: adminAddr, - input: func() []byte { - return allowlist.PackReadAllowList(noRoleAddr) - }, - suppliedGas: allowlist.ReadAllowListGasCost - 1, - readOnly: true, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - } { - t.Run(name, func(t *testing.T) { - db := rawdb.NewMemoryDatabase() - state, err := state.New(common.Hash{}, state.NewDatabase(db), nil) - require.NoError(t, err) - - // Set up the state so that each address has the expected permissions at the start. - txallowlist.SetTxAllowListStatus(state, adminAddr, allowlist.AllowListAdmin) - require.Equal(t, allowlist.AllowListAdmin, txallowlist.GetTxAllowListStatus(state, adminAddr)) - - blockContext := &mockBlockContext{blockNumber: common.Big0} - ret, remainingGas, err := txallowlist.TxAllowListPrecompile.Run(&mockAccessibleState{state: state, blockContext: blockContext, snowContext: snow.DefaultContextTest()}, test.caller, precompile.TxAllowListAddress, test.input(), test.suppliedGas, test.readOnly) - if len(test.expectedErr) != 0 { - require.ErrorContains(t, err, test.expectedErr) - } else { - require.NoError(t, err) - } - - require.Equal(t, uint64(0), remainingGas) - require.Equal(t, test.expectedRes, ret) - - if test.assertState != nil { - test.assertState(t, state) - } - }) - } -} - -func TestContractNativeMinterRun(t *testing.T) { - type test struct { - caller common.Address - input func() []byte - suppliedGas uint64 - readOnly bool - config *nativeminter.ContractNativeMinterConfig - - expectedRes []byte - expectedErr string - - assertState func(t *testing.T, state *state.StateDB) - } - - adminAddr := common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC") - enabledAddr := common.HexToAddress("0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B") - noRoleAddr := common.HexToAddress("0xF60C45c607D0f41687c94C314d300f483661E13a") - testAddr := common.HexToAddress("0x123456789") - - for name, test := range map[string]test{ - "mint funds from no role fails": { - caller: noRoleAddr, - input: func() []byte { - input, err := nativeminter.PackMintInput(noRoleAddr, common.Big1) - require.NoError(t, err) - - return input - }, - suppliedGas: nativeminter.MintGasCost, - readOnly: false, - expectedErr: nativeminter.ErrCannotMint.Error(), - }, - "mint funds from enabled address": { - caller: enabledAddr, - input: func() []byte { - input, err := nativeminter.PackMintInput(enabledAddr, common.Big1) - require.NoError(t, err) - - return input - }, - suppliedGas: nativeminter.MintGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - require.Equal(t, common.Big1, state.GetBalance(enabledAddr), "expected minted funds") - }, - }, - "enabled role by config": { - caller: noRoleAddr, - input: func() []byte { - return allowlist.PackReadAllowList(testAddr) - }, - suppliedGas: allowlist.ReadAllowListGasCost, - readOnly: false, - expectedRes: common.Hash(allowlist.AllowListEnabled).Bytes(), - assertState: func(t *testing.T, state *state.StateDB) { - require.Equal(t, allowlist.AllowListEnabled, nativeminter.GetContractNativeMinterStatus(state, testAddr)) - }, - config: &nativeminter.ContractNativeMinterConfig{ - AllowListConfig: allowlist.AllowListConfig{EnabledAddresses: []common.Address{testAddr}}, - }, - }, - "initial mint funds": { - caller: enabledAddr, - config: &nativeminter.ContractNativeMinterConfig{ - InitialMint: map[common.Address]*math.HexOrDecimal256{ - enabledAddr: math.NewHexOrDecimal256(2), - }, - }, - input: func() []byte { - return allowlist.PackReadAllowList(noRoleAddr) - }, - suppliedGas: allowlist.ReadAllowListGasCost, - readOnly: false, - expectedRes: common.Hash(allowlist.AllowListNoRole).Bytes(), - assertState: func(t *testing.T, state *state.StateDB) { - require.Equal(t, common.Big2, state.GetBalance(enabledAddr), "expected minted funds") - }, - }, - "mint funds from admin address": { - caller: adminAddr, - input: func() []byte { - input, err := nativeminter.PackMintInput(adminAddr, common.Big1) - require.NoError(t, err) - - return input - }, - suppliedGas: nativeminter.MintGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - require.Equal(t, common.Big1, state.GetBalance(adminAddr), "expected minted funds") - }, - }, - "mint max big funds": { - caller: adminAddr, - input: func() []byte { - input, err := nativeminter.PackMintInput(adminAddr, math.MaxBig256) - require.NoError(t, err) - - return input - }, - suppliedGas: nativeminter.MintGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - require.Equal(t, math.MaxBig256, state.GetBalance(adminAddr), "expected minted funds") - }, - }, - "readOnly mint with noRole fails": { - caller: noRoleAddr, - input: func() []byte { - input, err := nativeminter.PackMintInput(adminAddr, common.Big1) - require.NoError(t, err) - - return input - }, - suppliedGas: nativeminter.MintGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "readOnly mint with allow role fails": { - caller: enabledAddr, - input: func() []byte { - input, err := nativeminter.PackMintInput(enabledAddr, common.Big1) - require.NoError(t, err) - - return input - }, - suppliedGas: nativeminter.MintGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "readOnly mint with admin role fails": { - caller: adminAddr, - input: func() []byte { - input, err := nativeminter.PackMintInput(adminAddr, common.Big1) - require.NoError(t, err) - - return input - }, - suppliedGas: nativeminter.MintGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "insufficient gas mint from admin": { - caller: adminAddr, - input: func() []byte { - input, err := nativeminter.PackMintInput(enabledAddr, common.Big1) - require.NoError(t, err) - - return input - }, - suppliedGas: nativeminter.MintGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "read from noRole address": { - caller: noRoleAddr, - input: func() []byte { - return allowlist.PackReadAllowList(noRoleAddr) - }, - suppliedGas: allowlist.ReadAllowListGasCost, - readOnly: false, - expectedRes: common.Hash(allowlist.AllowListNoRole).Bytes(), - assertState: func(t *testing.T, state *state.StateDB) {}, - }, - "read from noRole address readOnly enabled": { - caller: noRoleAddr, - input: func() []byte { - return allowlist.PackReadAllowList(noRoleAddr) - }, - suppliedGas: allowlist.ReadAllowListGasCost, - readOnly: true, - expectedRes: common.Hash(allowlist.AllowListNoRole).Bytes(), - assertState: func(t *testing.T, state *state.StateDB) {}, - }, - "read from noRole address with insufficient gas": { - caller: noRoleAddr, - input: func() []byte { - return allowlist.PackReadAllowList(noRoleAddr) - }, - suppliedGas: allowlist.ReadAllowListGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "set allow role from admin": { - caller: adminAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(noRoleAddr, allowlist.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := nativeminter.GetContractNativeMinterStatus(state, noRoleAddr) - require.Equal(t, allowlist.AllowListEnabled, res) - }, - }, - "set allow role from non-admin fails": { - caller: enabledAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(noRoleAddr, allowlist.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedErr: allowlist.ErrCannotModifyAllowList.Error(), - }, - } { - t.Run(name, func(t *testing.T) { - db := rawdb.NewMemoryDatabase() - state, err := state.New(common.Hash{}, state.NewDatabase(db), nil) - require.NoError(t, err) - - // Set up the state so that each address has the expected permissions at the start. - nativeminter.SetContractNativeMinterStatus(state, adminAddr, allowlist.AllowListAdmin) - nativeminter.SetContractNativeMinterStatus(state, enabledAddr, allowlist.AllowListEnabled) - nativeminter.SetContractNativeMinterStatus(state, noRoleAddr, allowlist.AllowListNoRole) - require.Equal(t, allowlist.AllowListAdmin, nativeminter.GetContractNativeMinterStatus(state, adminAddr)) - require.Equal(t, allowlist.AllowListEnabled, nativeminter.GetContractNativeMinterStatus(state, enabledAddr)) - require.Equal(t, allowlist.AllowListNoRole, nativeminter.GetContractNativeMinterStatus(state, noRoleAddr)) - - blockContext := &mockBlockContext{blockNumber: common.Big0} - if test.config != nil { - test.config.Configure(params.TestChainConfig, state, blockContext) - } - ret, remainingGas, err := nativeminter.ContractNativeMinterPrecompile.Run(&mockAccessibleState{state: state, blockContext: blockContext, snowContext: snow.DefaultContextTest()}, test.caller, precompile.ContractNativeMinterAddress, test.input(), test.suppliedGas, test.readOnly) - if len(test.expectedErr) != 0 { - require.ErrorContains(t, err, test.expectedErr) - } else { - require.NoError(t, err) - } - - require.Equal(t, uint64(0), remainingGas) - require.Equal(t, test.expectedRes, ret) - - if test.assertState != nil { - test.assertState(t, state) - } - }) - } -} - -func TestFeeManagerRun(t *testing.T) { - type test struct { - caller common.Address - preCondition func(t *testing.T, state *state.StateDB) - input func() []byte - suppliedGas uint64 - readOnly bool - config *feemanager.FeeManagerConfig - - expectedRes []byte - expectedErr string - - assertState func(t *testing.T, state *state.StateDB) - } - - adminAddr := common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC") - enabledAddr := common.HexToAddress("0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B") - noRoleAddr := common.HexToAddress("0xF60C45c607D0f41687c94C314d300f483661E13a") - - for name, test := range map[string]test{ - "set config from no role fails": { - caller: noRoleAddr, - input: func() []byte { - input, err := feemanager.PackSetFeeConfig(testFeeConfig) - require.NoError(t, err) - - return input - }, - suppliedGas: feemanager.SetFeeConfigGasCost, - readOnly: false, - expectedErr: feemanager.ErrCannotChangeFee.Error(), - }, - "set config from enabled address": { - caller: enabledAddr, - input: func() []byte { - input, err := feemanager.PackSetFeeConfig(testFeeConfig) - require.NoError(t, err) - - return input - }, - suppliedGas: feemanager.SetFeeConfigGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - feeConfig := feemanager.GetStoredFeeConfig(state) - require.Equal(t, testFeeConfig, feeConfig) - }, - }, - "set invalid config from enabled address": { - caller: enabledAddr, - input: func() []byte { - feeConfig := testFeeConfig - feeConfig.MinBlockGasCost = new(big.Int).Mul(feeConfig.MaxBlockGasCost, common.Big2) - input, err := feemanager.PackSetFeeConfig(feeConfig) - require.NoError(t, err) - - return input - }, - suppliedGas: feemanager.SetFeeConfigGasCost, - readOnly: false, - expectedRes: nil, - config: &feemanager.FeeManagerConfig{ - InitialFeeConfig: &testFeeConfig, - }, - expectedErr: "cannot be greater than maxBlockGasCost", - assertState: func(t *testing.T, state *state.StateDB) { - feeConfig := feemanager.GetStoredFeeConfig(state) - require.Equal(t, testFeeConfig, feeConfig) - }, - }, - "set config from admin address": { - caller: adminAddr, - input: func() []byte { - input, err := feemanager.PackSetFeeConfig(testFeeConfig) - require.NoError(t, err) - - return input - }, - suppliedGas: feemanager.SetFeeConfigGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - feeConfig := feemanager.GetStoredFeeConfig(state) - require.Equal(t, testFeeConfig, feeConfig) - lastChangedAt := feemanager.GetFeeConfigLastChangedAt(state) - require.EqualValues(t, testBlockNumber, lastChangedAt) - }, - }, - "get fee config from non-enabled address": { - caller: noRoleAddr, - preCondition: func(t *testing.T, state *state.StateDB) { - err := feemanager.StoreFeeConfig(state, testFeeConfig, &mockBlockContext{blockNumber: big.NewInt(6)}) - require.NoError(t, err) - }, - input: func() []byte { - return feemanager.PackGetFeeConfigInput() - }, - suppliedGas: feemanager.GetFeeConfigGasCost, - readOnly: true, - expectedRes: func() []byte { - res, err := feemanager.PackFeeConfig(testFeeConfig) - require.NoError(t, err) - return res - }(), - assertState: func(t *testing.T, state *state.StateDB) { - feeConfig := feemanager.GetStoredFeeConfig(state) - lastChangedAt := feemanager.GetFeeConfigLastChangedAt(state) - require.Equal(t, testFeeConfig, feeConfig) - require.EqualValues(t, big.NewInt(6), lastChangedAt) - }, - }, - "get initial fee config": { - caller: noRoleAddr, - input: func() []byte { - return feemanager.PackGetFeeConfigInput() - }, - suppliedGas: feemanager.GetFeeConfigGasCost, - config: &feemanager.FeeManagerConfig{ - InitialFeeConfig: &testFeeConfig, - }, - readOnly: true, - expectedRes: func() []byte { - res, err := feemanager.PackFeeConfig(testFeeConfig) - require.NoError(t, err) - return res - }(), - assertState: func(t *testing.T, state *state.StateDB) { - feeConfig := feemanager.GetStoredFeeConfig(state) - lastChangedAt := feemanager.GetFeeConfigLastChangedAt(state) - require.Equal(t, testFeeConfig, feeConfig) - require.EqualValues(t, testBlockNumber, lastChangedAt) - }, - }, - "get last changed at from non-enabled address": { - caller: noRoleAddr, - preCondition: func(t *testing.T, state *state.StateDB) { - err := feemanager.StoreFeeConfig(state, testFeeConfig, &mockBlockContext{blockNumber: testBlockNumber}) - require.NoError(t, err) - }, - input: func() []byte { - return feemanager.PackGetLastChangedAtInput() - }, - suppliedGas: feemanager.GetLastChangedAtGasCost, - readOnly: true, - expectedRes: common.BigToHash(testBlockNumber).Bytes(), - assertState: func(t *testing.T, state *state.StateDB) { - feeConfig := feemanager.GetStoredFeeConfig(state) - lastChangedAt := feemanager.GetFeeConfigLastChangedAt(state) - require.Equal(t, testFeeConfig, feeConfig) - require.Equal(t, testBlockNumber, lastChangedAt) - }, - }, - "readOnly setFeeConfig with noRole fails": { - caller: noRoleAddr, - input: func() []byte { - input, err := feemanager.PackSetFeeConfig(testFeeConfig) - require.NoError(t, err) - - return input - }, - suppliedGas: feemanager.SetFeeConfigGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "readOnly setFeeConfig with allow role fails": { - caller: enabledAddr, - input: func() []byte { - input, err := feemanager.PackSetFeeConfig(testFeeConfig) - require.NoError(t, err) - - return input - }, - suppliedGas: feemanager.SetFeeConfigGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "readOnly setFeeConfig with admin role fails": { - caller: adminAddr, - input: func() []byte { - input, err := feemanager.PackSetFeeConfig(testFeeConfig) - require.NoError(t, err) - - return input - }, - suppliedGas: feemanager.SetFeeConfigGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "insufficient gas setFeeConfig from admin": { - caller: adminAddr, - input: func() []byte { - input, err := feemanager.PackSetFeeConfig(testFeeConfig) - require.NoError(t, err) - - return input - }, - suppliedGas: feemanager.SetFeeConfigGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "set allow role from admin": { - caller: adminAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(noRoleAddr, allowlist.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := feemanager.GetFeeManagerStatus(state, noRoleAddr) - require.Equal(t, allowlist.AllowListEnabled, res) - }, - }, - "set allow role from non-admin fails": { - caller: enabledAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(noRoleAddr, allowlist.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedErr: allowlist.ErrCannotModifyAllowList.Error(), - }, - } { - t.Run(name, func(t *testing.T) { - db := rawdb.NewMemoryDatabase() - state, err := state.New(common.Hash{}, state.NewDatabase(db), nil) - require.NoError(t, err) - - // Set up the state so that each address has the expected permissions at the start. - feemanager.SetFeeManagerStatus(state, adminAddr, allowlist.AllowListAdmin) - feemanager.SetFeeManagerStatus(state, enabledAddr, allowlist.AllowListEnabled) - feemanager.SetFeeManagerStatus(state, noRoleAddr, allowlist.AllowListNoRole) - - if test.preCondition != nil { - test.preCondition(t, state) - } - - blockContext := &mockBlockContext{blockNumber: testBlockNumber} - if test.config != nil { - test.config.Configure(params.TestChainConfig, state, blockContext) - } - ret, remainingGas, err := feemanager.FeeManagerPrecompile.Run(&mockAccessibleState{state: state, blockContext: blockContext, snowContext: snow.DefaultContextTest()}, test.caller, precompile.FeeManagerAddress, test.input(), test.suppliedGas, test.readOnly) - if len(test.expectedErr) != 0 { - require.ErrorContains(t, err, test.expectedErr) - } else { - require.NoError(t, err) - } - - require.Equal(t, uint64(0), remainingGas) - require.Equal(t, test.expectedRes, ret) - - if test.assertState != nil { - test.assertState(t, state) - } - }) - } -} - -func TestRewardManagerRun(t *testing.T) { - type test struct { - caller common.Address - preCondition func(t *testing.T, state *state.StateDB) - input func() []byte - suppliedGas uint64 - readOnly bool - config *rewardmanager.RewardManagerConfig - - expectedRes []byte - expectedErr string - - assertState func(t *testing.T, state *state.StateDB) - } - - adminAddr := common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC") - enabledAddr := common.HexToAddress("0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B") - noRoleAddr := common.HexToAddress("0xF60C45c607D0f41687c94C314d300f483661E13a") - testAddr := common.HexToAddress("0x0123") - - for name, test := range map[string]test{ - "set allow fee recipients from no role fails": { - caller: noRoleAddr, - input: func() []byte { - input, err := rewardmanager.PackAllowFeeRecipients() - require.NoError(t, err) - - return input - }, - suppliedGas: rewardmanager.AllowFeeRecipientsGasCost, - readOnly: false, - expectedErr: rewardmanager.ErrCannotAllowFeeRecipients.Error(), - }, - "set reward address from no role fails": { - caller: noRoleAddr, - input: func() []byte { - input, err := rewardmanager.PackSetRewardAddress(testAddr) - require.NoError(t, err) - - return input - }, - suppliedGas: rewardmanager.SetRewardAddressGasCost, - readOnly: false, - expectedErr: rewardmanager.ErrCannotSetRewardAddress.Error(), - }, - "disable rewards from no role fails": { - caller: noRoleAddr, - input: func() []byte { - input, err := rewardmanager.PackDisableRewards() - require.NoError(t, err) - - return input - }, - suppliedGas: rewardmanager.DisableRewardsGasCost, - readOnly: false, - expectedErr: rewardmanager.ErrCannotDisableRewards.Error(), - }, - "set allow fee recipients from enabled succeeds": { - caller: enabledAddr, - input: func() []byte { - input, err := rewardmanager.PackAllowFeeRecipients() - require.NoError(t, err) - - return input - }, - suppliedGas: rewardmanager.AllowFeeRecipientsGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - _, isFeeRecipients := rewardmanager.GetStoredRewardAddress(state) - require.True(t, isFeeRecipients) - }, - }, - "set reward address from enabled succeeds": { - caller: enabledAddr, - input: func() []byte { - input, err := rewardmanager.PackSetRewardAddress(testAddr) - require.NoError(t, err) - - return input - }, - suppliedGas: rewardmanager.SetRewardAddressGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - address, isFeeRecipients := rewardmanager.GetStoredRewardAddress(state) - require.Equal(t, testAddr, address) - require.False(t, isFeeRecipients) - }, - }, - "disable rewards from enabled succeeds": { - caller: enabledAddr, - input: func() []byte { - input, err := rewardmanager.PackDisableRewards() - require.NoError(t, err) - - return input - }, - suppliedGas: rewardmanager.DisableRewardsGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - address, isFeeRecipients := rewardmanager.GetStoredRewardAddress(state) - require.False(t, isFeeRecipients) - require.Equal(t, constants.BlackholeAddr, address) - }, - }, - "get current reward address from no role succeeds": { - caller: noRoleAddr, - preCondition: func(t *testing.T, state *state.StateDB) { - rewardmanager.StoreRewardAddress(state, testAddr) - }, - input: func() []byte { - input, err := rewardmanager.PackCurrentRewardAddress() - require.NoError(t, err) - - return input - }, - suppliedGas: rewardmanager.CurrentRewardAddressGasCost, - readOnly: false, - expectedRes: func() []byte { - res, err := rewardmanager.PackCurrentRewardAddressOutput(testAddr) - require.NoError(t, err) - return res - }(), - }, - "get are fee recipients allowed from no role succeeds": { - caller: noRoleAddr, - preCondition: func(t *testing.T, state *state.StateDB) { - rewardmanager.EnableAllowFeeRecipients(state) - }, - input: func() []byte { - input, err := rewardmanager.PackAreFeeRecipientsAllowed() - require.NoError(t, err) - return input - }, - suppliedGas: rewardmanager.AreFeeRecipientsAllowedGasCost, - readOnly: false, - expectedRes: func() []byte { - res, err := rewardmanager.PackAreFeeRecipientsAllowedOutput(true) - require.NoError(t, err) - return res - }(), - }, - "get initial config with address": { - caller: noRoleAddr, - input: func() []byte { - input, err := rewardmanager.PackCurrentRewardAddress() - require.NoError(t, err) - return input - }, - suppliedGas: rewardmanager.CurrentRewardAddressGasCost, - config: &rewardmanager.RewardManagerConfig{ - InitialRewardConfig: &rewardmanager.InitialRewardConfig{ - RewardAddress: testAddr, - }, - }, - readOnly: false, - expectedRes: func() []byte { - res, err := rewardmanager.PackCurrentRewardAddressOutput(testAddr) - require.NoError(t, err) - return res - }(), - }, - "get initial config with allow fee recipients enabled": { - caller: noRoleAddr, - input: func() []byte { - input, err := rewardmanager.PackAreFeeRecipientsAllowed() - require.NoError(t, err) - return input - }, - suppliedGas: rewardmanager.AreFeeRecipientsAllowedGasCost, - config: &rewardmanager.RewardManagerConfig{ - InitialRewardConfig: &rewardmanager.InitialRewardConfig{ - AllowFeeRecipients: true, - }, - }, - readOnly: false, - expectedRes: func() []byte { - res, err := rewardmanager.PackAreFeeRecipientsAllowedOutput(true) - require.NoError(t, err) - return res - }(), - }, - "readOnly allow fee recipients with allowed role fails": { - caller: enabledAddr, - input: func() []byte { - input, err := rewardmanager.PackAllowFeeRecipients() - require.NoError(t, err) - - return input - }, - suppliedGas: rewardmanager.AllowFeeRecipientsGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "readOnly set reward addresss with allowed role fails": { - caller: enabledAddr, - input: func() []byte { - input, err := rewardmanager.PackSetRewardAddress(testAddr) - require.NoError(t, err) - - return input - }, - suppliedGas: rewardmanager.SetRewardAddressGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "insufficient gas set reward address from allowed role": { - caller: enabledAddr, - input: func() []byte { - input, err := rewardmanager.PackSetRewardAddress(testAddr) - require.NoError(t, err) - - return input - }, - suppliedGas: rewardmanager.SetRewardAddressGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "insufficient gas allow fee recipients from allowed role": { - caller: enabledAddr, - input: func() []byte { - input, err := rewardmanager.PackAllowFeeRecipients() - require.NoError(t, err) - - return input - }, - suppliedGas: rewardmanager.AllowFeeRecipientsGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "insufficient gas read current reward address from allowed role": { - caller: enabledAddr, - input: func() []byte { - input, err := rewardmanager.PackCurrentRewardAddress() - require.NoError(t, err) - - return input - }, - suppliedGas: rewardmanager.CurrentRewardAddressGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "insufficient gas are fee recipients allowed from allowed role": { - caller: enabledAddr, - input: func() []byte { - input, err := rewardmanager.PackAreFeeRecipientsAllowed() - require.NoError(t, err) - - return input - }, - suppliedGas: rewardmanager.AreFeeRecipientsAllowedGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "set allow role from admin": { - caller: adminAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(noRoleAddr, allowlist.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := rewardmanager.GetRewardManagerAllowListStatus(state, noRoleAddr) - require.Equal(t, allowlist.AllowListEnabled, res) - }, - }, - "set allow role from non-admin fails": { - caller: enabledAddr, - input: func() []byte { - input, err := allowlist.PackModifyAllowList(noRoleAddr, allowlist.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: allowlist.ModifyAllowListGasCost, - readOnly: false, - expectedErr: allowlist.ErrCannotModifyAllowList.Error(), - }, - } { - t.Run(name, func(t *testing.T) { - db := rawdb.NewMemoryDatabase() - state, err := state.New(common.Hash{}, state.NewDatabase(db), nil) - require.NoError(t, err) - - // Set up the state so that each address has the expected permissions at the start. - rewardmanager.SetRewardManagerAllowListStatus(state, adminAddr, allowlist.AllowListAdmin) - rewardmanager.SetRewardManagerAllowListStatus(state, enabledAddr, allowlist.AllowListEnabled) - rewardmanager.SetRewardManagerAllowListStatus(state, noRoleAddr, allowlist.AllowListNoRole) - - if test.preCondition != nil { - test.preCondition(t, state) - } - - blockContext := &mockBlockContext{blockNumber: testBlockNumber} - if test.config != nil { - test.config.Configure(params.TestChainConfig, state, blockContext) - } - ret, remainingGas, err := rewardmanager.RewardManagerPrecompile.Run(&mockAccessibleState{state: state, blockContext: blockContext, snowContext: snow.DefaultContextTest()}, test.caller, precompile.RewardManagerAddress, test.input(), test.suppliedGas, test.readOnly) - if len(test.expectedErr) != 0 { - require.ErrorContains(t, err, test.expectedErr) - } else { - require.NoError(t, err) - } - - require.Equal(t, uint64(0), remainingGas) - require.Equal(t, test.expectedRes, ret) - - if test.assertState != nil { - test.assertState(t, state) - } - }) - } -} diff --git a/core/test_blockchain.go b/core/test_blockchain.go index 2284fee91b..2acbf65eae 100644 --- a/core/test_blockchain.go +++ b/core/test_blockchain.go @@ -16,10 +16,9 @@ import ( "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/ethdb" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" "github.com/ava-labs/subnet-evm/precompile/allowlist" - "github.com/ava-labs/subnet-evm/precompile/deployerallowlist" - "github.com/ava-labs/subnet-evm/precompile/feemanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/deployerallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/feemanager" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/assert" @@ -1549,8 +1548,10 @@ func TestStatefulPrecompiles(t *testing.T, create func(db ethdb.Database, chainC genesisBalance := new(big.Int).Mul(big.NewInt(1000000), big.NewInt(params.Ether)) config := *params.TestChainConfig // Set all of the required config parameters - config.ContractDeployerAllowListConfig = deployerallowlist.NewContractDeployerAllowListConfig(big.NewInt(0), []common.Address{addr1}, nil) - config.FeeManagerConfig = feemanager.NewFeeManagerConfig(big.NewInt(0), []common.Address{addr1}, nil, nil) + config.GenesisPrecompiles = params.ChainConfigPrecompiles{ + deployerallowlist.ConfigKey: deployerallowlist.NewConfig(big.NewInt(0), []common.Address{addr1}, nil), + feemanager.ConfigKey: feemanager.NewConfig(big.NewInt(0), []common.Address{addr1}, nil, nil), + } gspec := &Genesis{ Config: &config, Alloc: GenesisAlloc{addr1: {Balance: genesisBalance}}, @@ -1590,14 +1591,14 @@ func TestStatefulPrecompiles(t *testing.T, create func(db ethdb.Database, chainC "allow list": { addTx: func(gen *BlockGen) { feeCap := new(big.Int).Add(gen.BaseFee(), tip) - input, err := allowlist.PackModifyAllowList(addr2, allowlist.AllowListAdmin) + input, err := allowlist.PackModifyAllowList(addr2, allowlist.AdminRole) if err != nil { t.Fatal(err) } tx := types.NewTx(&types.DynamicFeeTx{ ChainID: params.TestChainConfig.ChainID, Nonce: gen.TxNonce(addr1), - To: &precompile.ContractDeployerAllowListAddress, + To: &deployerallowlist.ContractAddress, Gas: 3_000_000, Value: common.Big0, GasFeeCap: feeCap, @@ -1613,23 +1614,23 @@ func TestStatefulPrecompiles(t *testing.T, create func(db ethdb.Database, chainC }, verifyState: func(sdb *state.StateDB) error { res := deployerallowlist.GetContractDeployerAllowListStatus(sdb, addr1) - if allowlist.AllowListAdmin != res { - return fmt.Errorf("unexpected allow list status for addr1 %s, expected %s", res, allowlist.AllowListAdmin) + if allowlist.AdminRole != res { + return fmt.Errorf("unexpected allow list status for addr1 %s, expected %s", res, allowlist.AdminRole) } res = deployerallowlist.GetContractDeployerAllowListStatus(sdb, addr2) - if allowlist.AllowListAdmin != res { - return fmt.Errorf("unexpected allow list status for addr2 %s, expected %s", res, allowlist.AllowListAdmin) + if allowlist.AdminRole != res { + return fmt.Errorf("unexpected allow list status for addr2 %s, expected %s", res, allowlist.AdminRole) } return nil }, verifyGenesis: func(sdb *state.StateDB) { res := deployerallowlist.GetContractDeployerAllowListStatus(sdb, addr1) - if allowlist.AllowListAdmin != res { - t.Fatalf("unexpected allow list status for addr1 %s, expected %s", res, allowlist.AllowListAdmin) + if allowlist.AdminRole != res { + t.Fatalf("unexpected allow list status for addr1 %s, expected %s", res, allowlist.AdminRole) } res = deployerallowlist.GetContractDeployerAllowListStatus(sdb, addr2) - if allowlist.AllowListNoRole != res { - t.Fatalf("unexpected allow list status for addr2 %s, expected %s", res, allowlist.AllowListNoRole) + if allowlist.NoRole != res { + t.Fatalf("unexpected allow list status for addr2 %s, expected %s", res, allowlist.NoRole) } }, }, @@ -1643,7 +1644,7 @@ func TestStatefulPrecompiles(t *testing.T, create func(db ethdb.Database, chainC tx := types.NewTx(&types.DynamicFeeTx{ ChainID: params.TestChainConfig.ChainID, Nonce: gen.TxNonce(addr1), - To: &precompile.FeeManagerAddress, + To: &feemanager.ContractAddress, Gas: 3_000_000, Value: common.Big0, GasFeeCap: feeCap, @@ -1659,7 +1660,7 @@ func TestStatefulPrecompiles(t *testing.T, create func(db ethdb.Database, chainC }, verifyState: func(sdb *state.StateDB) error { res := feemanager.GetFeeManagerStatus(sdb, addr1) - assert.Equal(allowlist.AllowListAdmin, res) + assert.Equal(allowlist.AdminRole, res) storedConfig := feemanager.GetStoredFeeConfig(sdb) assert.EqualValues(testFeeConfig, storedConfig) @@ -1671,7 +1672,7 @@ func TestStatefulPrecompiles(t *testing.T, create func(db ethdb.Database, chainC }, verifyGenesis: func(sdb *state.StateDB) { res := feemanager.GetFeeManagerStatus(sdb, addr1) - assert.Equal(allowlist.AllowListAdmin, res) + assert.Equal(allowlist.AdminRole, res) feeConfig, _, err := blockchain.GetFeeConfigAt(blockchain.Genesis().Header()) assert.NoError(err) diff --git a/core/tx_pool.go b/core/tx_pool.go index 1f92bc0ce5..5d498fcddf 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -42,8 +42,10 @@ import ( "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/metrics" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/precompile/txallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/feemanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/prque" "github.com/ethereum/go-ethereum/event" @@ -694,10 +696,10 @@ func (pool *TxPool) checkTxState(from common.Address, tx *types.Transaction) err // If the tx allow list is enabled, return an error if the from address is not allow listed. headTimestamp := big.NewInt(int64(pool.currentHead.Time)) - if pool.chainconfig.IsPrecompileEnabled(precompile.TxAllowListAddress, headTimestamp) { + if pool.chainconfig.IsPrecompileEnabled(txallowlist.ContractAddress, headTimestamp) { txAllowListRole := txallowlist.GetTxAllowListStatus(pool.currentState, from) if !txAllowListRole.IsEnabled() { - return fmt.Errorf("%w: %s", txallowlist.ErrSenderAddressNotAllowListed, from) + return fmt.Errorf("%w: %s", vmerrs.ErrSenderAddressNotAllowListed, from) } } return nil @@ -1446,7 +1448,7 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) { // without requiring FeeManager is enabled. // This is already being set by SetMinFee when gas price updater starts. // However tests are currently failing if we change this check to IsSubnetEVM. - if pool.chainconfig.IsPrecompileEnabled(precompile.FeeManagerAddress, new(big.Int).SetUint64(newHead.Time)) { + if pool.chainconfig.IsPrecompileEnabled(feemanager.ContractAddress, new(big.Int).SetUint64(newHead.Time)) { feeConfig, _, err := pool.chain.GetFeeConfigAt(newHead) if err != nil { log.Error("Failed to get fee config state", "err", err, "root", newHead.Root) diff --git a/core/tx_pool_test.go b/core/tx_pool_test.go index de7bea7273..2bca2c372d 100644 --- a/core/tx_pool_test.go +++ b/core/tx_pool_test.go @@ -56,6 +56,19 @@ var ( // eip1559Config is a chain config with EIP-1559 enabled at block 0. eip1559Config *params.ChainConfig + + testFeeConfig = commontype.FeeConfig{ + GasLimit: big.NewInt(8_000_000), + TargetBlockRate: 2, // in seconds + + MinBaseFee: big.NewInt(25_000_000_000), + TargetGas: big.NewInt(15_000_000), + BaseFeeChangeDenominator: big.NewInt(36), + + MinBlockGasCost: big.NewInt(0), + MaxBlockGasCost: big.NewInt(1_000_000), + BlockGasCostStep: big.NewInt(200_000), + } ) func init() { diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 056f7af1ad..bdeb189fb5 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -35,7 +35,8 @@ import ( "github.com/ava-labs/subnet-evm/constants" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" @@ -57,7 +58,7 @@ type PrecompiledContract interface { // PrecompiledContractsHomestead contains the default set of pre-compiled Ethereum // contracts used in the Frontier and Homestead releases. -var PrecompiledContractsHomestead = map[common.Address]precompile.StatefulPrecompiledContract{ +var PrecompiledContractsHomestead = map[common.Address]contract.StatefulPrecompiledContract{ common.BytesToAddress([]byte{1}): newWrappedPrecompiledContract(&ecrecover{}), common.BytesToAddress([]byte{2}): newWrappedPrecompiledContract(&sha256hash{}), common.BytesToAddress([]byte{3}): newWrappedPrecompiledContract(&ripemd160hash{}), @@ -66,7 +67,7 @@ var PrecompiledContractsHomestead = map[common.Address]precompile.StatefulPrecom // PrecompiledContractsByzantium contains the default set of pre-compiled Ethereum // contracts used in the Byzantium release. -var PrecompiledContractsByzantium = map[common.Address]precompile.StatefulPrecompiledContract{ +var PrecompiledContractsByzantium = map[common.Address]contract.StatefulPrecompiledContract{ common.BytesToAddress([]byte{1}): newWrappedPrecompiledContract(&ecrecover{}), common.BytesToAddress([]byte{2}): newWrappedPrecompiledContract(&sha256hash{}), common.BytesToAddress([]byte{3}): newWrappedPrecompiledContract(&ripemd160hash{}), @@ -79,7 +80,7 @@ var PrecompiledContractsByzantium = map[common.Address]precompile.StatefulPrecom // PrecompiledContractsIstanbul contains the default set of pre-compiled Ethereum // contracts used in the Istanbul release. -var PrecompiledContractsIstanbul = map[common.Address]precompile.StatefulPrecompiledContract{ +var PrecompiledContractsIstanbul = map[common.Address]contract.StatefulPrecompiledContract{ common.BytesToAddress([]byte{1}): newWrappedPrecompiledContract(&ecrecover{}), common.BytesToAddress([]byte{2}): newWrappedPrecompiledContract(&sha256hash{}), common.BytesToAddress([]byte{3}): newWrappedPrecompiledContract(&ripemd160hash{}), @@ -93,7 +94,7 @@ var PrecompiledContractsIstanbul = map[common.Address]precompile.StatefulPrecomp // PrecompiledContractsBerlin contains the default set of pre-compiled Ethereum // contracts used in the Berlin release. -var PrecompiledContractsBerlin = map[common.Address]precompile.StatefulPrecompiledContract{ +var PrecompiledContractsBerlin = map[common.Address]contract.StatefulPrecompiledContract{ common.BytesToAddress([]byte{1}): newWrappedPrecompiledContract(&ecrecover{}), common.BytesToAddress([]byte{2}): newWrappedPrecompiledContract(&sha256hash{}), common.BytesToAddress([]byte{3}): newWrappedPrecompiledContract(&ripemd160hash{}), @@ -107,7 +108,7 @@ var PrecompiledContractsBerlin = map[common.Address]precompile.StatefulPrecompil // PrecompiledContractsBLS contains the set of pre-compiled Ethereum // contracts specified in EIP-2537. These are exported for testing purposes. -var PrecompiledContractsBLS = map[common.Address]precompile.StatefulPrecompiledContract{ +var PrecompiledContractsBLS = map[common.Address]contract.StatefulPrecompiledContract{ common.BytesToAddress([]byte{10}): newWrappedPrecompiledContract(&bls12381G1Add{}), common.BytesToAddress([]byte{11}): newWrappedPrecompiledContract(&bls12381G1Mul{}), common.BytesToAddress([]byte{12}): newWrappedPrecompiledContract(&bls12381G1MultiExp{}), @@ -158,12 +159,13 @@ func init() { // Ensure that this package will panic during init if there is a conflict present with the declared // precompile addresses. - for _, k := range precompile.UsedAddresses { - if _, ok := PrecompileAllNativeAddresses[k]; ok { - panic(fmt.Errorf("precompile address collides with existing native address: %s", k)) + for _, module := range modules.RegisteredModules() { + address := module.Address + if _, ok := PrecompileAllNativeAddresses[address]; ok { + panic(fmt.Errorf("precompile address collides with existing native address: %s", address)) } - if k == constants.BlackholeAddr { - panic(fmt.Errorf("cannot use address %s for stateful precompile - overlaps with blackhole address", k)) + if address == constants.BlackholeAddr { + panic(fmt.Errorf("cannot use address %s for stateful precompile - overlaps with blackhole address", address)) } } } diff --git a/core/vm/contracts_stateful.go b/core/vm/contracts_stateful.go index 49c1aef48d..dc04120979 100644 --- a/core/vm/contracts_stateful.go +++ b/core/vm/contracts_stateful.go @@ -4,7 +4,7 @@ package vm import ( - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/contract" "github.com/ethereum/go-ethereum/common" ) @@ -16,16 +16,16 @@ type wrappedPrecompiledContract struct { // newWrappedPrecompiledContract returns a wrapped version of [PrecompiledContract] to be executed according to the StatefulPrecompiledContract // interface. -func newWrappedPrecompiledContract(p PrecompiledContract) precompile.StatefulPrecompiledContract { +func newWrappedPrecompiledContract(p PrecompiledContract) contract.StatefulPrecompiledContract { return &wrappedPrecompiledContract{p: p} } // Run implements the StatefulPrecompiledContract interface -func (w *wrappedPrecompiledContract) Run(accessibleState precompile.PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { +func (w *wrappedPrecompiledContract) Run(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { return RunPrecompiledContract(w.p, input, suppliedGas) } // RunStatefulPrecompiledContract confirms runs [precompile] with the specified parameters. -func RunStatefulPrecompiledContract(precompile precompile.StatefulPrecompiledContract, accessibleState precompile.PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { +func RunStatefulPrecompiledContract(precompile contract.StatefulPrecompiledContract, accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { return precompile.Run(accessibleState, caller, addr, input, suppliedGas, readOnly) } diff --git a/core/vm/evm.go b/core/vm/evm.go index 86849d05ad..764bce4e65 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -35,8 +35,9 @@ import ( "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/subnet-evm/constants" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/precompile/deployerallowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/contracts/deployerallowlist" + "github.com/ava-labs/subnet-evm/precompile/modules" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -44,8 +45,8 @@ import ( ) var ( - _ precompile.PrecompileAccessibleState = &EVM{} - _ precompile.BlockContext = &BlockContext{} + _ contract.AccessibleState = &EVM{} + _ contract.BlockContext = &BlockContext{} ) // IsProhibited returns true if [addr] is in the prohibited list of addresses which should @@ -55,7 +56,7 @@ func IsProhibited(addr common.Address) bool { return true } - return precompile.ReservedAddress(addr) + return modules.ReservedAddress(addr) } // emptyCodeHash is used by create to ensure deployment is disallowed to already @@ -72,8 +73,8 @@ type ( GetHashFunc func(uint64) common.Hash ) -func (evm *EVM) precompile(addr common.Address) (precompile.StatefulPrecompiledContract, bool) { - var precompiles map[common.Address]precompile.StatefulPrecompiledContract +func (evm *EVM) precompile(addr common.Address) (contract.StatefulPrecompiledContract, bool) { + var precompiles map[common.Address]contract.StatefulPrecompiledContract switch { case evm.chainRules.IsSubnetEVM: precompiles = PrecompiledContractsBerlin @@ -92,8 +93,12 @@ func (evm *EVM) precompile(addr common.Address) (precompile.StatefulPrecompiledC } // Otherwise, check the chain rules for the additionally configured precompiles. - p, ok = evm.chainRules.Precompiles[addr] - return p, ok + if _, ok = evm.chainRules.ActivePrecompiles[addr]; ok { + module, ok := modules.GetPrecompileModuleByAddress(addr) + return module.Contract, ok + } + + return nil, false } // BlockContext provides the EVM with auxiliary information. Once provided @@ -208,12 +213,12 @@ func (evm *EVM) GetSnowContext() *snow.Context { } // GetStateDB returns the evm's StateDB -func (evm *EVM) GetStateDB() precompile.StateDB { +func (evm *EVM) GetStateDB() contract.StateDB { return evm.StateDB } // GetBlockContext returns the evm's BlockContext -func (evm *EVM) GetBlockContext() precompile.BlockContext { +func (evm *EVM) GetBlockContext() contract.BlockContext { return &evm.Context } @@ -508,7 +513,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, return nil, common.Address{}, 0, vmerrs.ErrContractAddressCollision } // If the allow list is enabled, check that [evm.TxContext.Origin] has permission to deploy a contract. - if evm.chainRules.IsPrecompileEnabled(precompile.ContractDeployerAllowListAddress) { + if evm.chainRules.IsPrecompileEnabled(deployerallowlist.ContractAddress) { allowListRole := deployerallowlist.GetContractDeployerAllowListStatus(evm.StateDB, evm.TxContext.Origin) if !allowListRole.IsEnabled() { return nil, common.Address{}, 0, fmt.Errorf("tx.origin %s is not authorized to deploy a contract", evm.TxContext.Origin) diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index 63d1cb3a59..253778c6af 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -39,7 +39,7 @@ import ( "github.com/ava-labs/subnet-evm/core" "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/contracts/feemanager" "github.com/ava-labs/subnet-evm/rpc" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" @@ -318,7 +318,7 @@ func (oracle *Oracle) suggestDynamicFees(ctx context.Context) (*big.Int, *big.In feeLastChangedAt *big.Int feeConfig commontype.FeeConfig ) - if oracle.backend.ChainConfig().IsPrecompileEnabled(precompile.FeeManagerAddress, new(big.Int).SetUint64(head.Time)) { + if oracle.backend.ChainConfig().IsPrecompileEnabled(feemanager.ContractAddress, new(big.Int).SetUint64(head.Time)) { feeConfig, feeLastChangedAt, err = oracle.backend.GetFeeConfigAt(head) if err != nil { return nil, nil, err diff --git a/eth/gasprice/gasprice_test.go b/eth/gasprice/gasprice_test.go index 3acc68d20d..049c154127 100644 --- a/eth/gasprice/gasprice_test.go +++ b/eth/gasprice/gasprice_test.go @@ -40,8 +40,7 @@ import ( "github.com/ava-labs/subnet-evm/core/vm" "github.com/ava-labs/subnet-evm/ethdb" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/precompile/feemanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/feemanager" "github.com/ava-labs/subnet-evm/rpc" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -435,7 +434,9 @@ func TestSuggestGasPriceAfterFeeConfigUpdate(t *testing.T) { // create a chain config with fee manager enabled at genesis with [addr] as the admin chainConfig := *params.TestChainConfig - chainConfig.FeeManagerConfig = feemanager.NewFeeManagerConfig(big.NewInt(0), []common.Address{addr}, nil, nil) + chainConfig.GenesisPrecompiles = params.ChainConfigPrecompiles{ + feemanager.ConfigKey: feemanager.NewConfig(big.NewInt(0), []common.Address{addr}, nil, nil), + } // create a fee config with higher MinBaseFee and prepare it for inclusion in a tx signer := types.LatestSigner(params.TestChainConfig) @@ -463,7 +464,7 @@ func TestSuggestGasPriceAfterFeeConfigUpdate(t *testing.T) { tx := types.NewTx(&types.DynamicFeeTx{ ChainID: chainConfig.ChainID, Nonce: b.TxNonce(addr), - To: &precompile.FeeManagerAddress, + To: &feemanager.ContractAddress, Gas: chainConfig.FeeConfig.GasLimit.Uint64(), Value: common.Big0, GasFeeCap: chainConfig.FeeConfig.MinBaseFee, // give low fee, it should work since we still haven't applied high fees diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index f2a109677f..9cdf0cf7f7 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -626,12 +626,12 @@ func (api *BlockChainAPI) ChainId() *hexutil.Big { } // GetActivePrecompilesAt returns the active precompile configs at the given block timestamp. -func (s *BlockChainAPI) GetActivePrecompilesAt(ctx context.Context, blockTimestamp *big.Int) params.PrecompileUpgrade { +func (s *BlockChainAPI) GetActivePrecompilesAt(ctx context.Context, blockTimestamp *big.Int) params.ChainConfigPrecompiles { if blockTimestamp == nil { - blockTimestampInt := s.b.CurrentHeader().Time - blockTimestamp = new(big.Int).SetUint64(blockTimestampInt) + blockTimestamp = new(big.Int).SetUint64(s.b.CurrentHeader().Time) } - return s.b.ChainConfig().GetActivePrecompileUpgrade(blockTimestamp) + + return s.b.ChainConfig().EnabledStatefulPrecompiles(blockTimestamp) } type FeeConfigResult struct { diff --git a/miner/worker.go b/miner/worker.go index bed65cb814..26a9c31168 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -186,7 +186,7 @@ func (w *worker) commitNewWork() (*types.Block, error) { return nil, fmt.Errorf("failed to create new current environment: %w", err) } // Configure any stateful precompiles that should go into effect during this block. - err = w.chainConfig.ConfigurePrecompiles(new(big.Int).SetUint64(parent.Time()), types.NewBlockWithHeader(header), env.state) + err = core.ApplyPrecompileActivations(w.chainConfig, new(big.Int).SetUint64(parent.Time()), types.NewBlockWithHeader(header), env.state) if err != nil { log.Error("failed to configure precompiles mining new block", "parent", parent.Hash(), "number", header.Number, "timestamp", header.Time, "err", err) return nil, err diff --git a/params/chain_config_precompiles.go b/params/chain_config_precompiles.go new file mode 100644 index 0000000000..5d60ab69b5 --- /dev/null +++ b/params/chain_config_precompiles.go @@ -0,0 +1,36 @@ +// (c) 2023 Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package params + +import ( + "encoding/json" + + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ava-labs/subnet-evm/precompile/modules" +) + +type ChainConfigPrecompiles map[string]config.Config + +// UnmarshalJSON parses the JSON-encoded data into the ChainConfigPrecompiles. +// ChainConfigPrecompiles is a map of precompile module keys to their +// configuration. +func (ccp *ChainConfigPrecompiles) UnmarshalJSON(data []byte) error { + raw := make(map[string]json.RawMessage) + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + *ccp = make(ChainConfigPrecompiles) + for _, module := range modules.RegisteredModules() { + key := module.ConfigKey + if value, ok := raw[key]; ok { + conf := module.NewConfig() + if err := json.Unmarshal(value, conf); err != nil { + return err + } + (*ccp)[key] = conf + } + } + return nil +} diff --git a/params/config.go b/params/config.go index 1407ce1bb8..2a925084dd 100644 --- a/params/config.go +++ b/params/config.go @@ -34,7 +34,8 @@ import ( "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/subnet-evm/commontype" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ava-labs/subnet-evm/precompile/modules" "github.com/ava-labs/subnet-evm/utils" "github.com/ethereum/go-ethereum/common" ) @@ -83,7 +84,7 @@ var ( PetersburgBlock: big.NewInt(0), IstanbulBlock: big.NewInt(0), MuirGlacierBlock: big.NewInt(0), - + GenesisPrecompiles: ChainConfigPrecompiles{}, NetworkUpgrades: NetworkUpgrades{ SubnetEVMTimestamp: big.NewInt(0), }, @@ -105,7 +106,7 @@ var ( IstanbulBlock: big.NewInt(0), MuirGlacierBlock: big.NewInt(0), NetworkUpgrades: NetworkUpgrades{big.NewInt(0)}, - PrecompileUpgrade: PrecompileUpgrade{}, + GenesisPrecompiles: ChainConfigPrecompiles{}, UpgradeConfig: UpgradeConfig{}, } @@ -125,7 +126,7 @@ var ( IstanbulBlock: big.NewInt(0), MuirGlacierBlock: big.NewInt(0), NetworkUpgrades: NetworkUpgrades{}, - PrecompileUpgrade: PrecompileUpgrade{}, + GenesisPrecompiles: ChainConfigPrecompiles{}, UpgradeConfig: UpgradeConfig{}, } ) @@ -157,9 +158,57 @@ type ChainConfig struct { IstanbulBlock *big.Int `json:"istanbulBlock,omitempty"` // Istanbul switch block (nil = no fork, 0 = already on istanbul) MuirGlacierBlock *big.Int `json:"muirGlacierBlock,omitempty"` // Eip-2384 (bomb delay) switch block (nil = no fork, 0 = already activated) - NetworkUpgrades // Config for timestamps that enable avalanche network upgrades - PrecompileUpgrade // Config for enabling precompiles from genesis - UpgradeConfig `json:"-"` // Config specified in upgradeBytes (avalanche network upgrades or enable/disabling precompiles). Skip encoding/decoding directly into ChainConfig. + NetworkUpgrades // Config for timestamps that enable avalanche network upgrades + GenesisPrecompiles ChainConfigPrecompiles `json:"-"` // Config for enabling precompiles from genesis. JSON encode/decode will be handled by the custom marshaler/unmarshaler. + UpgradeConfig `json:"-"` // Config specified in upgradeBytes (avalanche network upgrades or enable/disabling precompiles). Skip encoding/decoding directly into ChainConfig. +} + +// UnmarshalJSON parses the JSON-encoded data and stores the result in the +// object pointed to by c. +// This is a custom unmarshaler to handle the Precompiles field. +// Precompiles was presented as an inline object in the JSON. +// This custom unmarshaler ensures backwards compatibility with the old format. +func (c *ChainConfig) UnmarshalJSON(data []byte) error { + // Alias ChainConfig to avoid recursion + type _ChainConfig ChainConfig + tmp := _ChainConfig{} + if err := json.Unmarshal(data, &tmp); err != nil { + return err + } + + // At this point we have populated all fields except PrecompileUpgrade + *c = ChainConfig(tmp) + + // Unmarshal inlined PrecompileUpgrade + return json.Unmarshal(data, &c.GenesisPrecompiles) +} + +// MarshalJSON returns the JSON encoding of c. +// This is a custom marshaler to handle the Precompiles field. +func (c ChainConfig) MarshalJSON() ([]byte, error) { + // Alias ChainConfig to avoid recursion + type _ChainConfig ChainConfig + tmp, err := json.Marshal(_ChainConfig(c)) + if err != nil { + return nil, err + } + + // To include PrecompileUpgrades, we unmarshal the json representing c + // then directly add the corresponding keys to the json. + raw := make(map[string]json.RawMessage) + if err := json.Unmarshal(tmp, &raw); err != nil { + return nil, err + } + + for key, value := range c.GenesisPrecompiles { + conf, err := json.Marshal(value) + if err != nil { + return nil, err + } + raw[key] = conf + } + + return json.Marshal(raw) } // UpgradeConfig includes the following configs that may be specified in upgradeBytes: @@ -191,7 +240,7 @@ func (c *ChainConfig) String() string { if err != nil { networkUpgradesBytes = []byte("cannot marshal NetworkUpgrades") } - precompileUpgradeBytes, err := json.Marshal(c.PrecompileUpgrade) + precompileUpgradeBytes, err := json.Marshal(c.GenesisPrecompiles) if err != nil { precompileUpgradeBytes = []byte("cannot marshal PrecompileUpgrade") } @@ -274,7 +323,7 @@ func (c *ChainConfig) IsSubnetEVM(blockTimestamp *big.Int) bool { // IsPrecompileEnabled returns whether precompile with [address] is enabled at [blockTimestamp]. func (c *ChainConfig) IsPrecompileEnabled(address common.Address, blockTimestamp *big.Int) bool { - config := c.GetPrecompileConfig(address, blockTimestamp) + config := c.GetActivePrecompileConfig(address, blockTimestamp) return config != nil && !config.IsDisabled() } @@ -305,7 +354,7 @@ func (c *ChainConfig) Verify() error { // Verify the precompile upgrades are internally consistent given the existing chainConfig. if err := c.verifyPrecompileUpgrades(); err != nil { - return err + return fmt.Errorf("invalid precompile upgrades: %w", err) } return nil @@ -505,15 +554,15 @@ type Rules struct { // Rules for Avalanche releases IsSubnetEVM bool - // Precompiles maps addresses to stateful precompiled contracts that are enabled + // ActivePrecompiles maps addresses to stateful precompiled contracts that are enabled // for this rule set. // Note: none of these addresses should conflict with the address space used by // any existing precompiles. - Precompiles map[common.Address]precompile.StatefulPrecompiledContract + ActivePrecompiles map[common.Address]config.Config } func (r *Rules) IsPrecompileEnabled(addr common.Address) bool { - _, ok := r.Precompiles[addr] + _, ok := r.ActivePrecompiles[addr] return ok } @@ -544,12 +593,11 @@ func (c *ChainConfig) AvalancheRules(blockNum, blockTimestamp *big.Int) Rules { rules.IsSubnetEVM = c.IsSubnetEVM(blockTimestamp) // Initialize the stateful precompiles that should be enabled at [blockTimestamp]. - rules.Precompiles = make(map[common.Address]precompile.StatefulPrecompiledContract) - for _, config := range c.EnabledStatefulPrecompiles(blockTimestamp) { - if config.IsDisabled() { - continue + rules.ActivePrecompiles = make(map[common.Address]config.Config) + for _, module := range modules.RegisteredModules() { + if config := c.GetActivePrecompileConfig(module.Address, blockTimestamp); config != nil { + rules.ActivePrecompiles[module.Address] = config } - rules.Precompiles[config.Address()] = config.Contract() } return rules diff --git a/params/config_test.go b/params/config_test.go index 50487f159a..8e1c3f7ada 100644 --- a/params/config_test.go +++ b/params/config_test.go @@ -27,9 +27,15 @@ package params import ( + "encoding/json" "math/big" "reflect" "testing" + + "github.com/ava-labs/subnet-evm/precompile/contracts/nativeminter" + "github.com/ava-labs/subnet-evm/precompile/contracts/rewardmanager" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" ) func TestCheckCompatible(t *testing.T) { @@ -136,3 +142,67 @@ func TestCheckCompatible(t *testing.T) { } } } + +func TestConfigUnmarshalJSON(t *testing.T) { + require := require.New(t) + + testRewardManagerConfig := rewardmanager.NewConfig( + big.NewInt(1671542573), + []common.Address{common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC")}, + nil, + &rewardmanager.InitialRewardConfig{ + AllowFeeRecipients: true, + }) + + testContractNativeMinterConfig := nativeminter.NewConfig( + big.NewInt(0), + []common.Address{common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC")}, + nil, + nil, + ) + + config := []byte(` + { + "chainId": 43214, + "allowFeeRecipients": true, + "rewardManagerConfig": { + "blockTimestamp": 1671542573, + "adminAddresses": [ + "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" + ], + "initialRewardConfig": { + "allowFeeRecipients": true + } + }, + "contractNativeMinterConfig": { + "blockTimestamp": 0, + "adminAddresses": [ + "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" + ] + } + } + `) + c := ChainConfig{} + err := json.Unmarshal(config, &c) + require.NoError(err) + + require.Equal(c.ChainID, big.NewInt(43214)) + require.Equal(c.AllowFeeRecipients, true) + + rewardManagerConfig, ok := c.GenesisPrecompiles[rewardmanager.ConfigKey] + require.True(ok) + require.Equal(rewardManagerConfig.Key(), rewardmanager.ConfigKey) + require.True(rewardManagerConfig.Equal(testRewardManagerConfig)) + + nativeMinterConfig := c.GenesisPrecompiles[nativeminter.ConfigKey] + require.Equal(nativeMinterConfig.Key(), nativeminter.ConfigKey) + require.True(nativeMinterConfig.Equal(testContractNativeMinterConfig)) + + // Marshal and unmarshal again and check that the result is the same + marshaled, err := json.Marshal(c) + require.NoError(err) + c2 := ChainConfig{} + err = json.Unmarshal(marshaled, &c2) + require.NoError(err) + require.Equal(c, c2) +} diff --git a/params/precompile_config.go b/params/precompile_config.go deleted file mode 100644 index a43133f3f9..0000000000 --- a/params/precompile_config.go +++ /dev/null @@ -1,300 +0,0 @@ -// (c) 2022 Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package params - -import ( - "fmt" - "math/big" - - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/precompile/deployerallowlist" - "github.com/ava-labs/subnet-evm/precompile/feemanager" - "github.com/ava-labs/subnet-evm/precompile/nativeminter" - "github.com/ava-labs/subnet-evm/precompile/rewardmanager" - "github.com/ava-labs/subnet-evm/precompile/txallowlist" - "github.com/ava-labs/subnet-evm/utils" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/log" -) - -// PrecompileUpgrade is a helper struct embedded in UpgradeConfig, representing -// each of the possible stateful precompile types that can be activated -// as a network upgrade. -type PrecompileUpgrade struct { - ContractDeployerAllowListConfig *deployerallowlist.ContractDeployerAllowListConfig `json:"contractDeployerAllowListConfig,omitempty"` // Config for the contract deployer allow list precompile - ContractNativeMinterConfig *nativeminter.ContractNativeMinterConfig `json:"contractNativeMinterConfig,omitempty"` // Config for the native minter precompile - TxAllowListConfig *txallowlist.TxAllowListConfig `json:"txAllowListConfig,omitempty"` // Config for the tx allow list precompile - FeeManagerConfig *feemanager.FeeManagerConfig `json:"feeManagerConfig,omitempty"` // Config for the fee manager precompile - RewardManagerConfig *rewardmanager.RewardManagerConfig `json:"rewardManagerConfig,omitempty"` // Config for the reward manager precompile - // ADD YOUR PRECOMPILE HERE - // {YourPrecompile}Config *precompile.{YourPrecompile}Config `json:"{yourPrecompile}Config,omitempty"` -} - -// getByAddress returns the precompile config for the given address. -func (p *PrecompileUpgrade) getByAddress(address common.Address) (precompile.StatefulPrecompileConfig, bool) { - switch address { - case precompile.ContractDeployerAllowListAddress: - return p.ContractDeployerAllowListConfig, p.ContractDeployerAllowListConfig != nil - case precompile.ContractNativeMinterAddress: - return p.ContractNativeMinterConfig, p.ContractNativeMinterConfig != nil - case precompile.TxAllowListAddress: - return p.TxAllowListConfig, p.TxAllowListConfig != nil - case precompile.FeeManagerAddress: - return p.FeeManagerConfig, p.FeeManagerConfig != nil - case precompile.RewardManagerAddress: - return p.RewardManagerConfig, p.RewardManagerConfig != nil - // ADD YOUR PRECOMPILE HERE - /* - case precompile.{YourPrecompile}Address: - return p.{YourPrecompile}Config, p.{YourPrecompile}Config != nil - */ - default: - panic(fmt.Sprintf("unknown precompile address: %v", address)) - } -} - -// verifyPrecompileUpgrades checks [c.PrecompileUpgrades] is well formed: -// - [upgrades] must specify exactly one key per PrecompileUpgrade -// - the specified blockTimestamps must monotonically increase -// - the specified blockTimestamps must be compatible with those -// specified in the chainConfig by genesis. -// - check a precompile is disabled before it is re-enabled -func (c *ChainConfig) verifyPrecompileUpgrades() error { - var lastBlockTimestamp *big.Int - for i, upgrade := range c.PrecompileUpgrades { - hasKey := false // used to verify if there is only one key per Upgrade - - for _, address := range precompile.UsedAddresses { - config, ok := upgrade.getByAddress(address) - if !ok { - continue - } - if hasKey { - return fmt.Errorf("PrecompileUpgrades[%d] has more than one key set", i) - } - configTimestamp := config.Timestamp() - if configTimestamp == nil { - return fmt.Errorf("PrecompileUpgrades[%d] cannot have a nil timestamp", i) - } - // Verify specified timestamps are monotonically increasing across all precompile keys. - // Note: It is OK for multiple configs of different keys to specify the same timestamp. - if lastBlockTimestamp != nil && configTimestamp.Cmp(lastBlockTimestamp) < 0 { - return fmt.Errorf("PrecompileUpgrades[%d] config timestamp (%v) < previous timestamp (%v)", i, configTimestamp, lastBlockTimestamp) - } - lastBlockTimestamp = configTimestamp - hasKey = true - } - if !hasKey { - return fmt.Errorf("empty precompile upgrade at index %d", i) - } - } - - for _, address := range precompile.UsedAddresses { - var ( - lastUpgraded *big.Int - disabled bool - ) - // check the genesis chain config for any enabled upgrade - if config, ok := c.PrecompileUpgrade.getByAddress(address); ok { - if err := config.Verify(); err != nil { - return err - } - disabled = false - lastUpgraded = config.Timestamp() - } else { - disabled = true - } - // next range over upgrades to verify correct use of disabled and blockTimestamps. - for i, upgrade := range c.PrecompileUpgrades { - config, ok := upgrade.getByAddress(address) - // Skip the upgrade if it's not relevant to [address]. - if !ok { - continue - } - - if disabled == config.IsDisabled() { - return fmt.Errorf("PrecompileUpgrades[%d] disable should be [%v]", i, !disabled) - } - if lastUpgraded != nil && (config.Timestamp().Cmp(lastUpgraded) <= 0) { - return fmt.Errorf("PrecompileUpgrades[%d] config timestamp (%v) <= previous timestamp (%v)", i, config.Timestamp(), lastUpgraded) - } - - if err := config.Verify(); err != nil { - return err - } - - disabled = config.IsDisabled() - lastUpgraded = config.Timestamp() - } - } - - return nil -} - -// getActivePrecompileConfig returns the most recent precompile config corresponding to [address]. -// If none have occurred, returns nil. -func (c *ChainConfig) getActivePrecompileConfig(blockTimestamp *big.Int, address common.Address, upgrades []PrecompileUpgrade) precompile.StatefulPrecompileConfig { - configs := c.getActivatingPrecompileConfigs(nil, blockTimestamp, address, upgrades) - if len(configs) == 0 { - return nil - } - return configs[len(configs)-1] // return the most recent config -} - -// getActivatingPrecompileConfigs returns all forks configured to activate during the state transition from a block with timestamp [from] -// to a block with timestamp [to]. -func (c *ChainConfig) getActivatingPrecompileConfigs(from *big.Int, to *big.Int, address common.Address, upgrades []PrecompileUpgrade) []precompile.StatefulPrecompileConfig { - configs := make([]precompile.StatefulPrecompileConfig, 0) - // First check the embedded [upgrade] for precompiles configured - // in the genesis chain config. - if config, ok := c.PrecompileUpgrade.getByAddress(address); ok { - if utils.IsForkTransition(config.Timestamp(), from, to) { - configs = append(configs, config) - } - } - // Loop over all upgrades checking for the requested precompile config. - for _, upgrade := range upgrades { - if config, ok := upgrade.getByAddress(address); ok { - // Check if the precompile activates in the specified range. - if utils.IsForkTransition(config.Timestamp(), from, to) { - configs = append(configs, config) - } - } - } - return configs -} - -func (c *ChainConfig) GetPrecompileConfig(address common.Address, blockTimestamp *big.Int) precompile.StatefulPrecompileConfig { - if val := c.getActivePrecompileConfig(blockTimestamp, address, c.PrecompileUpgrades); val != nil { - return val - } - return nil -} - -// TODO: remove this -func (c *ChainConfig) GetActivePrecompileUpgrade(blockTimestamp *big.Int) PrecompileUpgrade { - pu := PrecompileUpgrade{} - if config := c.GetPrecompileConfig(precompile.ContractDeployerAllowListAddress, blockTimestamp); config != nil && !config.IsDisabled() { - pu.ContractDeployerAllowListConfig = config.(*deployerallowlist.ContractDeployerAllowListConfig) - } - if config := c.GetPrecompileConfig(precompile.ContractNativeMinterAddress, blockTimestamp); config != nil && !config.IsDisabled() { - pu.ContractNativeMinterConfig = config.(*nativeminter.ContractNativeMinterConfig) - } - if config := c.GetPrecompileConfig(precompile.TxAllowListAddress, blockTimestamp); config != nil && !config.IsDisabled() { - pu.TxAllowListConfig = config.(*txallowlist.TxAllowListConfig) - } - if config := c.GetPrecompileConfig(precompile.FeeManagerAddress, blockTimestamp); config != nil && !config.IsDisabled() { - pu.FeeManagerConfig = config.(*feemanager.FeeManagerConfig) - } - if config := c.GetPrecompileConfig(precompile.RewardManagerAddress, blockTimestamp); config != nil && !config.IsDisabled() { - pu.RewardManagerConfig = config.(*rewardmanager.RewardManagerConfig) - } - - // ADD YOUR PRECOMPILE HERE - // if config := c.GetPrecompileConfig(precompile.{YourPrecompile}Address, blockTimestamp); config != nil && !config.IsDisabled() { - // pu.{YourPrecompile}Config = config.(*precompile.{YourPrecompile}Config) - // } - - return pu -} - -// CheckPrecompilesCompatible checks if [precompileUpgrades] are compatible with [c] at [headTimestamp]. -// Returns a ConfigCompatError if upgrades already forked at [headTimestamp] are missing from -// [precompileUpgrades]. Upgrades not already forked may be modified or absent from [precompileUpgrades]. -// Returns nil if [precompileUpgrades] is compatible with [c]. -// Assumes given timestamp is the last accepted block timestamp. -// This ensures that as long as the node has not accepted a block with a different rule set it will allow a new upgrade to be applied as long as it activates after the last accepted block. -func (c *ChainConfig) CheckPrecompilesCompatible(precompileUpgrades []PrecompileUpgrade, lastTimestamp *big.Int) *ConfigCompatError { - for _, address := range precompile.UsedAddresses { - if err := c.checkPrecompileCompatible(address, precompileUpgrades, lastTimestamp); err != nil { - return err - } - } - - return nil -} - -// checkPrecompileCompatible verifies that the precompile specified by [address] is compatible between [c] and [precompileUpgrades] at [headTimestamp]. -// Returns an error if upgrades already forked at [headTimestamp] are missing from [precompileUpgrades]. -// Upgrades that have already gone into effect cannot be modified or absent from [precompileUpgrades]. -func (c *ChainConfig) checkPrecompileCompatible(address common.Address, precompileUpgrades []PrecompileUpgrade, lastTimestamp *big.Int) *ConfigCompatError { - // all active upgrades must match - activeUpgrades := c.getActivatingPrecompileConfigs(nil, lastTimestamp, address, c.PrecompileUpgrades) - newUpgrades := c.getActivatingPrecompileConfigs(nil, lastTimestamp, address, precompileUpgrades) - - // first, check existing upgrades are there - for i, upgrade := range activeUpgrades { - if len(newUpgrades) <= i { - // missing upgrade - return newCompatError( - fmt.Sprintf("missing PrecompileUpgrade[%d]", i), - upgrade.Timestamp(), - nil, - ) - } - // All upgrades that have forked must be identical. - if !upgrade.Equal(newUpgrades[i]) { - return newCompatError( - fmt.Sprintf("PrecompileUpgrade[%d]", i), - upgrade.Timestamp(), - newUpgrades[i].Timestamp(), - ) - } - } - // then, make sure newUpgrades does not have additional upgrades - // that are already activated. (cannot perform retroactive upgrade) - if len(newUpgrades) > len(activeUpgrades) { - return newCompatError( - fmt.Sprintf("cannot retroactively enable PrecompileUpgrade[%d]", len(activeUpgrades)), - nil, - newUpgrades[len(activeUpgrades)].Timestamp(), // this indexes to the first element in newUpgrades after the end of activeUpgrades - ) - } - - return nil -} - -// EnabledStatefulPrecompiles returns a slice of stateful precompile configs that -// have been activated through an upgrade. -func (c *ChainConfig) EnabledStatefulPrecompiles(blockTimestamp *big.Int) []precompile.StatefulPrecompileConfig { - statefulPrecompileConfigs := make([]precompile.StatefulPrecompileConfig, 0) - for _, address := range precompile.UsedAddresses { - if config := c.getActivePrecompileConfig(blockTimestamp, address, c.PrecompileUpgrades); config != nil { - statefulPrecompileConfigs = append(statefulPrecompileConfigs, config) - } - } - - return statefulPrecompileConfigs -} - -// ConfigurePrecompiles checks if any of the precompiles specified by the chain config are enabled or disabled by the block -// transition from [parentTimestamp] to the timestamp set in [blockContext]. If this is the case, it calls [Configure] -// or [Deconfigure] to apply the necessary state transitions for the upgrade. -// This function is called: -// - within genesis setup to configure the starting state for precompiles enabled at genesis, -// - during block processing to update the state before processing the given block. -func (c *ChainConfig) ConfigurePrecompiles(parentTimestamp *big.Int, blockContext precompile.BlockContext, statedb precompile.StateDB) error { - blockTimestamp := blockContext.Timestamp() - for _, address := range precompile.UsedAddresses { // Note: configure precompiles in a deterministic order. - for _, config := range c.getActivatingPrecompileConfigs(parentTimestamp, blockTimestamp, address, c.PrecompileUpgrades) { - // If this transition activates the upgrade, configure the stateful precompile. - // (or deconfigure it if it is being disabled.) - if config.IsDisabled() { - log.Info("Disabling precompile", "precompileAddress", address) // TODO: use proper names for precompiles - statedb.Suicide(config.Address()) - // Calling Finalise here effectively commits Suicide call and wipes the contract state. - // This enables re-configuration of the same contract state in the same block. - // Without an immediate Finalise call after the Suicide, a reconfigured precompiled state can be wiped out - // since Suicide will be committed after the reconfiguration. - statedb.Finalise(true) - } else { - log.Info("Activating new precompile", "precompileAddress", address, "config", config) - if err := precompile.Configure(c, blockContext, config, statedb); err != nil { - return fmt.Errorf("could not configure precompile, precompileAddress: %s, reason: %w", address, err) - } - } - } - } - return nil -} diff --git a/params/precompile_config_test.go b/params/precompile_config_test.go index 4d7a114d14..bb77d912e1 100644 --- a/params/precompile_config_test.go +++ b/params/precompile_config_test.go @@ -4,14 +4,16 @@ package params import ( + "encoding/json" "math/big" "testing" "github.com/ava-labs/subnet-evm/commontype" - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/precompile/deployerallowlist" - "github.com/ava-labs/subnet-evm/precompile/feemanager" - "github.com/ava-labs/subnet-evm/precompile/txallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/deployerallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/feemanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/nativeminter" + "github.com/ava-labs/subnet-evm/precompile/contracts/rewardmanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -21,17 +23,17 @@ func TestVerifyWithChainConfig(t *testing.T) { admins := []common.Address{{1}} baseConfig := *SubnetEVMDefaultChainConfig config := &baseConfig - config.PrecompileUpgrade = PrecompileUpgrade{ - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(2), nil, nil), + config.GenesisPrecompiles = ChainConfigPrecompiles{ + txallowlist.ConfigKey: txallowlist.NewConfig(big.NewInt(2), nil, nil), } config.PrecompileUpgrades = []PrecompileUpgrade{ { // disable TxAllowList at timestamp 4 - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(4)), + txallowlist.NewDisableConfig(big.NewInt(4)), }, { // re-enable TxAllowList at timestamp 5 - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(5), admins, nil), + txallowlist.NewConfig(big.NewInt(5), admins, nil), }, } @@ -44,18 +46,18 @@ func TestVerifyWithChainConfig(t *testing.T) { badConfig.PrecompileUpgrades = append( badConfig.PrecompileUpgrades, PrecompileUpgrade{ - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(5)), + Config: txallowlist.NewDisableConfig(big.NewInt(5)), }, ) err = badConfig.Verify() - assert.ErrorContains(t, err, "config timestamp (5) <= previous timestamp (5)") + assert.ErrorContains(t, err, "config block timestamp (5) <= previous timestamp (5) of same key") // cannot enable a precompile without disabling it first. badConfig = *config badConfig.PrecompileUpgrades = append( badConfig.PrecompileUpgrades, PrecompileUpgrade{ - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(5), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(5), admins, nil), }, ) err = badConfig.Verify() @@ -73,10 +75,10 @@ func TestVerifyPrecompileUpgrades(t *testing.T) { name: "enable and disable tx allow list", upgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(1), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(1), admins, nil), }, { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(2)), + Config: txallowlist.NewDisableConfig(big.NewInt(2)), }, }, expectedError: "", @@ -85,13 +87,13 @@ func TestVerifyPrecompileUpgrades(t *testing.T) { name: "invalid allow list config in tx allowlist", upgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(1), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(1), admins, nil), }, { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(2)), + Config: txallowlist.NewDisableConfig(big.NewInt(2)), }, { - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(3), admins, admins), + Config: txallowlist.NewConfig(big.NewInt(3), admins, admins), }, }, expectedError: "cannot set address", @@ -100,7 +102,7 @@ func TestVerifyPrecompileUpgrades(t *testing.T) { name: "invalid initial fee manager config", upgrades: []PrecompileUpgrade{ { - FeeManagerConfig: feemanager.NewFeeManagerConfig(big.NewInt(3), admins, nil, + Config: feemanager.NewConfig(big.NewInt(3), admins, nil, &commontype.FeeConfig{ GasLimit: big.NewInt(-1), }), @@ -112,7 +114,7 @@ func TestVerifyPrecompileUpgrades(t *testing.T) { name: "invalid initial fee manager config gas limit 0", upgrades: []PrecompileUpgrade{ { - FeeManagerConfig: feemanager.NewFeeManagerConfig(big.NewInt(3), admins, nil, + Config: feemanager.NewConfig(big.NewInt(3), admins, nil, &commontype.FeeConfig{ GasLimit: big.NewInt(0), }), @@ -120,6 +122,42 @@ func TestVerifyPrecompileUpgrades(t *testing.T) { }, expectedError: "gasLimit = 0 cannot be less than or equal to 0", }, + { + name: "different upgrades are allowed to configure same timestamp for different precompiles", + upgrades: []PrecompileUpgrade{ + { + Config: txallowlist.NewConfig(big.NewInt(1), admins, nil), + }, + { + Config: feemanager.NewConfig(big.NewInt(1), admins, nil, nil), + }, + }, + expectedError: "", + }, + { + name: "different upgrades must be monotonically increasing", + upgrades: []PrecompileUpgrade{ + { + Config: txallowlist.NewConfig(big.NewInt(2), admins, nil), + }, + { + Config: feemanager.NewConfig(big.NewInt(1), admins, nil, nil), + }, + }, + expectedError: "config block timestamp (1) < previous timestamp (2)", + }, + { + name: "upgrades with same keys are not allowed to configure same timestamp for same precompiles", + upgrades: []PrecompileUpgrade{ + { + Config: txallowlist.NewConfig(big.NewInt(1), admins, nil), + }, + { + Config: txallowlist.NewDisableConfig(big.NewInt(1)), + }, + }, + expectedError: " config block timestamp (1) <= previous timestamp (1) of same key", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -142,20 +180,20 @@ func TestVerifyPrecompiles(t *testing.T) { admins := []common.Address{{1}} tests := []struct { name string - upgrade PrecompileUpgrade + precompiles ChainConfigPrecompiles expectedError string }{ { name: "invalid allow list config in tx allowlist", - upgrade: PrecompileUpgrade{ - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(3), admins, admins), + precompiles: ChainConfigPrecompiles{ + txallowlist.ConfigKey: txallowlist.NewConfig(big.NewInt(3), admins, admins), }, expectedError: "cannot set address", }, { name: "invalid initial fee manager config", - upgrade: PrecompileUpgrade{ - FeeManagerConfig: feemanager.NewFeeManagerConfig(big.NewInt(3), admins, nil, + precompiles: ChainConfigPrecompiles{ + feemanager.ConfigKey: feemanager.NewConfig(big.NewInt(3), admins, nil, &commontype.FeeConfig{ GasLimit: big.NewInt(-1), }), @@ -168,7 +206,7 @@ func TestVerifyPrecompiles(t *testing.T) { require := require.New(t) baseConfig := *SubnetEVMDefaultChainConfig config := &baseConfig - config.PrecompileUpgrade = tt.upgrade + config.GenesisPrecompiles = tt.precompiles err := config.Verify() if tt.expectedError == "" { @@ -186,35 +224,93 @@ func TestVerifyRequiresSortedTimestamps(t *testing.T) { config := &baseConfig config.PrecompileUpgrades = []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(2), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(2), admins, nil), }, { - ContractDeployerAllowListConfig: deployerallowlist.NewContractDeployerAllowListConfig(big.NewInt(1), admins, nil), + Config: deployerallowlist.NewConfig(big.NewInt(1), admins, nil), }, } // block timestamps must be monotonically increasing, so this config is invalid err := config.Verify() - assert.ErrorContains(t, err, "config timestamp (1) < previous timestamp (2)") + assert.ErrorContains(t, err, "config block timestamp (1) < previous timestamp (2)") } func TestGetPrecompileConfig(t *testing.T) { assert := assert.New(t) baseConfig := *SubnetEVMDefaultChainConfig config := &baseConfig - config.PrecompileUpgrade = PrecompileUpgrade{ - ContractDeployerAllowListConfig: deployerallowlist.NewContractDeployerAllowListConfig(big.NewInt(10), nil, nil), + config.GenesisPrecompiles = ChainConfigPrecompiles{ + deployerallowlist.ConfigKey: deployerallowlist.NewConfig(big.NewInt(10), nil, nil), } - deployerConfig := config.GetPrecompileConfig(precompile.ContractDeployerAllowListAddress, big.NewInt(0)) + deployerConfig := config.GetActivePrecompileConfig(deployerallowlist.ContractAddress, big.NewInt(0)) assert.Nil(deployerConfig) - deployerConfig = config.GetPrecompileConfig(precompile.ContractDeployerAllowListAddress, big.NewInt(10)) + deployerConfig = config.GetActivePrecompileConfig(deployerallowlist.ContractAddress, big.NewInt(10)) assert.NotNil(deployerConfig) - deployerConfig = config.GetPrecompileConfig(precompile.ContractDeployerAllowListAddress, big.NewInt(11)) + deployerConfig = config.GetActivePrecompileConfig(deployerallowlist.ContractAddress, big.NewInt(11)) assert.NotNil(deployerConfig) - txAllowListConfig := config.GetPrecompileConfig(precompile.TxAllowListAddress, big.NewInt(0)) + txAllowListConfig := config.GetActivePrecompileConfig(txallowlist.ContractAddress, big.NewInt(0)) assert.Nil(txAllowListConfig) } + +func TestPrecompileUpgradeUnmarshalJSON(t *testing.T) { + require := require.New(t) + + upgradeBytes := []byte(` + { + "precompileUpgrades": [ + { + "rewardManagerConfig": { + "blockTimestamp": 1671542573, + "adminAddresses": [ + "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" + ], + "initialRewardConfig": { + "allowFeeRecipients": true + } + } + }, + { + "contractNativeMinterConfig": { + "blockTimestamp": 1671543172, + "disable": false + } + } + ] + } + `) + + var upgradeConfig UpgradeConfig + err := json.Unmarshal(upgradeBytes, &upgradeConfig) + require.NoError(err) + + require.Len(upgradeConfig.PrecompileUpgrades, 2) + + rewardManagerConf := upgradeConfig.PrecompileUpgrades[0] + require.Equal(rewardManagerConf.Key(), rewardmanager.ConfigKey) + testRewardManagerConfig := rewardmanager.NewConfig( + big.NewInt(1671542573), + []common.Address{common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC")}, + nil, + &rewardmanager.InitialRewardConfig{ + AllowFeeRecipients: true, + }) + require.True(rewardManagerConf.Equal(testRewardManagerConfig)) + + nativeMinterConfig := upgradeConfig.PrecompileUpgrades[1] + require.Equal(nativeMinterConfig.Key(), nativeminter.ConfigKey) + expectedNativeMinterConfig := nativeminter.NewConfig(big.NewInt(1671543172), nil, nil, nil) + require.True(nativeMinterConfig.Equal(expectedNativeMinterConfig)) + + // Marshal and unmarshal again and check that the result is the same + upgradeBytes2, err := json.Marshal(upgradeConfig) + require.NoError(err) + var upgradeConfig2 UpgradeConfig + err = json.Unmarshal(upgradeBytes2, &upgradeConfig2) + require.NoError(err) + require.Equal(upgradeConfig, upgradeConfig2) +} diff --git a/params/precompile_upgrade.go b/params/precompile_upgrade.go new file mode 100644 index 0000000000..1bcff4e6b4 --- /dev/null +++ b/params/precompile_upgrade.go @@ -0,0 +1,260 @@ +// (c) 2022 Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package params + +import ( + "encoding/json" + "errors" + "fmt" + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/config" + precompileConfig "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/utils" + "github.com/ethereum/go-ethereum/common" +) + +var errNoKey = errors.New("PrecompileUpgrade cannot be empty") + +// PrecompileUpgrade is a helper struct embedded in UpgradeConfig. +// It is used to unmarshal the json into the correct precompile config type +// based on the key. Keys are defined in each precompile module, and registered in +// precompile/registry/registry.go. +type PrecompileUpgrade struct { + config.Config +} + +// UnmarshalJSON unmarshals the json into the correct precompile config type +// based on the key. Keys are defined in each precompile module, and registered in +// params/precompile_modules.go. +// precompile/registry/registry.go. +// Ex: {"feeManagerConfig": {...}} where "feeManagerConfig" is the key +func (u *PrecompileUpgrade) UnmarshalJSON(data []byte) error { + raw := make(map[string]json.RawMessage) + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if len(raw) == 0 { + return errNoKey + } + if len(raw) > 1 { + return fmt.Errorf("PrecompileUpgrade must have exactly one key, got %d", len(raw)) + } + for key, value := range raw { + module, ok := modules.GetPrecompileModule(key) + if !ok { + return fmt.Errorf("unknown precompile config: %s", key) + } + config := module.NewConfig() + if err := json.Unmarshal(value, config); err != nil { + return err + } + u.Config = config + } + return nil +} + +// MarshalJSON marshal the precompile config into json based on the precompile key. +// Ex: {"feeManagerConfig": {...}} where "feeManagerConfig" is the key +func (u *PrecompileUpgrade) MarshalJSON() ([]byte, error) { + res := make(map[string]precompileConfig.Config) + res[u.Key()] = u.Config + return json.Marshal(res) +} + +// verifyPrecompileUpgrades checks [c.PrecompileUpgrades] is well formed: +// - [upgrades] must specify exactly one key per PrecompileUpgrade +// - the specified blockTimestamps must monotonically increase +// - the specified blockTimestamps must be compatible with those +// specified in the chainConfig by genesis. +// - check a precompile is disabled before it is re-enabled +func (c *ChainConfig) verifyPrecompileUpgrades() error { + // Store this struct to keep track of the last upgrade for each precompile key. + // Required for timestamp and disabled checks. + type lastUpgradeData struct { + blockTimestamp *big.Int + disabled bool + } + + lastPrecompileUpgrades := make(map[string]lastUpgradeData) + + // verify genesis precompiles + for key, config := range c.GenesisPrecompiles { + if err := config.Verify(); err != nil { + return err + } + // if the precompile is disabled at genesis, skip it. + if config.Timestamp() == nil { + continue + } + // check the genesis chain config for any enabled upgrade + lastPrecompileUpgrades[key] = lastUpgradeData{ + disabled: false, + blockTimestamp: config.Timestamp(), + } + } + + // next range over upgrades to verify correct use of disabled and blockTimestamps. + // previousUpgradeTimestamp is used to verify monotonically increasing timestamps. + var previousUpgradeTimestamp *big.Int + for i, upgrade := range c.PrecompileUpgrades { + key := upgrade.Key() + + // lastUpgradeByKey is the previous processed upgrade for this precompile key. + lastUpgradeByKey, ok := lastPrecompileUpgrades[key] + var ( + disabled bool + lastTimestamp *big.Int + ) + if !ok { + disabled = true + lastTimestamp = nil + } else { + disabled = lastUpgradeByKey.disabled + lastTimestamp = lastUpgradeByKey.blockTimestamp + } + upgradeTimestamp := upgrade.Timestamp() + + if upgradeTimestamp == nil { + return fmt.Errorf("PrecompileUpgrade (%s) at [%d]: block timestamp cannot be nil ", key, i) + } + // Verify specified timestamps are monotonically increasing across all precompile keys. + // Note: It is OK for multiple configs of DIFFERENT keys to specify the same timestamp. + if previousUpgradeTimestamp != nil && upgradeTimestamp.Cmp(previousUpgradeTimestamp) < 0 { + return fmt.Errorf("PrecompileUpgrade (%s) at [%d]: config block timestamp (%v) < previous timestamp (%v)", key, i, upgradeTimestamp, previousUpgradeTimestamp) + } + + if disabled == upgrade.IsDisabled() { + return fmt.Errorf("PrecompileUpgrade (%s) at [%d]: disable should be [%v]", key, i, !disabled) + } + // Verify specified timestamps are monotonically increasing across same precompile keys. + // Note: It is NOT OK for multiple configs of the SAME key to specify the same timestamp. + if lastTimestamp != nil && (upgradeTimestamp.Cmp(lastTimestamp) <= 0) { + return fmt.Errorf("PrecompileUpgrade (%s) at [%d]: config block timestamp (%v) <= previous timestamp (%v) of same key", key, i, upgradeTimestamp, lastTimestamp) + } + + if err := upgrade.Verify(); err != nil { + return err + } + + lastPrecompileUpgrades[key] = lastUpgradeData{ + disabled: upgrade.IsDisabled(), + blockTimestamp: upgradeTimestamp, + } + + previousUpgradeTimestamp = upgradeTimestamp + } + + return nil +} + +// GetActivePrecompileConfig returns the most recent precompile config corresponding to [address]. +// If none have occurred, returns nil. +func (c *ChainConfig) GetActivePrecompileConfig(address common.Address, blockTimestamp *big.Int) config.Config { + configs := c.GetActivatingPrecompileConfigs(address, nil, blockTimestamp, c.PrecompileUpgrades) + if len(configs) == 0 { + return nil + } + return configs[len(configs)-1] // return the most recent config +} + +// GetActivatingPrecompileConfigs returns all upgrades configured to activate during the state transition from a block with timestamp [from] +// to a block with timestamp [to]. +func (c *ChainConfig) GetActivatingPrecompileConfigs(address common.Address, from *big.Int, to *big.Int, upgrades []PrecompileUpgrade) []config.Config { + // Get key from address. + module, ok := modules.GetPrecompileModuleByAddress(address) + if !ok { + return nil + } + configs := make([]config.Config, 0) + key := module.ConfigKey + // First check the embedded [upgrade] for precompiles configured + // in the genesis chain config. + if config, ok := c.GenesisPrecompiles[key]; ok { + if utils.IsForkTransition(config.Timestamp(), from, to) { + configs = append(configs, config) + } + } + // Loop over all upgrades checking for the requested precompile config. + for _, upgrade := range upgrades { + if upgrade.Key() == key { + // Check if the precompile activates in the specified range. + if utils.IsForkTransition(upgrade.Timestamp(), from, to) { + configs = append(configs, upgrade.Config) + } + } + } + return configs +} + +// CheckPrecompilesCompatible checks if [precompileUpgrades] are compatible with [c] at [headTimestamp]. +// Returns a ConfigCompatError if upgrades already activated at [headTimestamp] are missing from +// [precompileUpgrades]. Upgrades not already activated may be modified or absent from [precompileUpgrades]. +// Returns nil if [precompileUpgrades] is compatible with [c]. +// Assumes given timestamp is the last accepted block timestamp. +// This ensures that as long as the node has not accepted a block with a different rule set it will allow a +// new upgrade to be applied as long as it activates after the last accepted block. +func (c *ChainConfig) CheckPrecompilesCompatible(precompileUpgrades []PrecompileUpgrade, lastTimestamp *big.Int) *ConfigCompatError { + for _, module := range modules.RegisteredModules() { + if err := c.checkPrecompileCompatible(module.Address, precompileUpgrades, lastTimestamp); err != nil { + return err + } + } + + return nil +} + +// checkPrecompileCompatible verifies that the precompile specified by [address] is compatible between [c] +// and [precompileUpgrades] at [headTimestamp]. +// Returns an error if upgrades already activated at [headTimestamp] are missing from [precompileUpgrades]. +// Upgrades that have already gone into effect cannot be modified or absent from [precompileUpgrades]. +func (c *ChainConfig) checkPrecompileCompatible(address common.Address, precompileUpgrades []PrecompileUpgrade, lastTimestamp *big.Int) *ConfigCompatError { + // All active upgrades (from nil to [lastTimestamp]) must match. + activeUpgrades := c.GetActivatingPrecompileConfigs(address, nil, lastTimestamp, c.PrecompileUpgrades) + newUpgrades := c.GetActivatingPrecompileConfigs(address, nil, lastTimestamp, precompileUpgrades) + + // Check activated upgrades are still present. + for i, upgrade := range activeUpgrades { + if len(newUpgrades) <= i { + // missing upgrade + return newCompatError( + fmt.Sprintf("missing PrecompileUpgrade[%d]", i), + upgrade.Timestamp(), + nil, + ) + } + // All upgrades that have activated must be identical. + if !upgrade.Equal(newUpgrades[i]) { + return newCompatError( + fmt.Sprintf("PrecompileUpgrade[%d]", i), + upgrade.Timestamp(), + newUpgrades[i].Timestamp(), + ) + } + } + // then, make sure newUpgrades does not have additional upgrades + // that are already activated. (cannot perform retroactive upgrade) + if len(newUpgrades) > len(activeUpgrades) { + return newCompatError( + fmt.Sprintf("cannot retroactively enable PrecompileUpgrade[%d]", len(activeUpgrades)), + nil, + newUpgrades[len(activeUpgrades)].Timestamp(), // this indexes to the first element in newUpgrades after the end of activeUpgrades + ) + } + + return nil +} + +// EnabledStatefulPrecompiles returns current stateful precompile configs that are enabled at [blockTimestamp]. +func (c *ChainConfig) EnabledStatefulPrecompiles(blockTimestamp *big.Int) ChainConfigPrecompiles { + statefulPrecompileConfigs := make(ChainConfigPrecompiles) + for _, module := range modules.RegisteredModules() { + if config := c.GetActivePrecompileConfig(module.Address, blockTimestamp); config != nil && !config.IsDisabled() { + statefulPrecompileConfigs[module.ConfigKey] = config + } + } + + return statefulPrecompileConfigs +} diff --git a/params/upgrade_config_test.go b/params/precompile_upgrade_test.go similarity index 64% rename from params/upgrade_config_test.go rename to params/precompile_upgrade_test.go index e8e5fb9750..3048693f65 100644 --- a/params/upgrade_config_test.go +++ b/params/precompile_upgrade_test.go @@ -7,8 +7,8 @@ import ( "math/big" "testing" - "github.com/ava-labs/subnet-evm/precompile/deployerallowlist" - "github.com/ava-labs/subnet-evm/precompile/txallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/deployerallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" ) @@ -16,7 +16,9 @@ import ( func TestVerifyUpgradeConfig(t *testing.T) { admins := []common.Address{{1}} chainConfig := *TestChainConfig - chainConfig.TxAllowListConfig = txallowlist.NewTxAllowListConfig(big.NewInt(1), admins, nil) + chainConfig.GenesisPrecompiles = ChainConfigPrecompiles{ + txallowlist.ConfigKey: txallowlist.NewConfig(big.NewInt(1), admins, nil), + } type test struct { upgrades []PrecompileUpgrade @@ -28,23 +30,23 @@ func TestVerifyUpgradeConfig(t *testing.T) { expectedErrorString: "disable should be [true]", upgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(2), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(2), admins, nil), }, }, }, "upgrade bytes conflicts with genesis (disable before enable)": { - expectedErrorString: "config timestamp (0) <= previous timestamp (1)", + expectedErrorString: "config block timestamp (0) <= previous timestamp (1) of same key", upgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(0)), + Config: txallowlist.NewDisableConfig(big.NewInt(0)), }, }, }, "upgrade bytes conflicts with genesis (disable same time as enable)": { - expectedErrorString: "config timestamp (1) <= previous timestamp (1)", + expectedErrorString: "config block timestamp (1) <= previous timestamp (1) of same key", upgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(1)), + Config: txallowlist.NewDisableConfig(big.NewInt(1)), }, }, }, @@ -71,8 +73,10 @@ func TestVerifyUpgradeConfig(t *testing.T) { func TestCheckCompatibleUpgradeConfigs(t *testing.T) { admins := []common.Address{{1}} chainConfig := *TestChainConfig - chainConfig.TxAllowListConfig = txallowlist.NewTxAllowListConfig(big.NewInt(1), admins, nil) - chainConfig.ContractDeployerAllowListConfig = deployerallowlist.NewContractDeployerAllowListConfig(big.NewInt(10), admins, nil) + chainConfig.GenesisPrecompiles = ChainConfigPrecompiles{ + txallowlist.ConfigKey: txallowlist.NewConfig(big.NewInt(1), admins, nil), + deployerallowlist.ConfigKey: deployerallowlist.NewConfig(big.NewInt(10), admins, nil), + } type test struct { configs []*UpgradeConfig @@ -87,10 +91,10 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(7), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(7), admins, nil), }, }, }, @@ -102,20 +106,20 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(7), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(8), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(8), admins, nil), }, }, }, @@ -128,20 +132,20 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(7), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(8), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(8), admins, nil), }, }, }, @@ -153,17 +157,17 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(7), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, }, }, @@ -176,17 +180,17 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(7), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, }, }, @@ -199,21 +203,21 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(7), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { // uses a different (empty) admin list, not allowed - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(7), []common.Address{}, nil), + Config: txallowlist.NewConfig(big.NewInt(7), []common.Address{}, nil), }, }, }, @@ -225,20 +229,20 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(7), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(7), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(7), admins, nil), }, }, }, diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index ae114d010b..1721ed1f25 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -46,6 +46,9 @@ import ( // inside of cmd/geth. _ "github.com/ava-labs/subnet-evm/eth/tracers/native" + // Force-load precompiles to trigger registration + _ "github.com/ava-labs/subnet-evm/precompile/registry" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go index 5010a810c4..d97e23811f 100644 --- a/plugin/evm/vm_test.go +++ b/plugin/evm/vm_test.go @@ -19,12 +19,11 @@ import ( "github.com/ava-labs/subnet-evm/commontype" "github.com/ava-labs/subnet-evm/metrics" - "github.com/ava-labs/subnet-evm/precompile" "github.com/ava-labs/subnet-evm/precompile/allowlist" - "github.com/ava-labs/subnet-evm/precompile/deployerallowlist" - "github.com/ava-labs/subnet-evm/precompile/feemanager" - "github.com/ava-labs/subnet-evm/precompile/rewardmanager" - "github.com/ava-labs/subnet-evm/precompile/txallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/deployerallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/feemanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/rewardmanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" "github.com/ava-labs/subnet-evm/trie" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" @@ -2121,7 +2120,9 @@ func TestBuildAllowListActivationBlock(t *testing.T) { if err := genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM)); err != nil { t.Fatal(err) } - genesis.Config.ContractDeployerAllowListConfig = deployerallowlist.NewContractDeployerAllowListConfig(big.NewInt(time.Now().Unix()), testEthAddrs, nil) + genesis.Config.GenesisPrecompiles = params.ChainConfigPrecompiles{ + deployerallowlist.ConfigKey: deployerallowlist.NewConfig(big.NewInt(time.Now().Unix()), testEthAddrs, nil), + } genesisJSON, err := genesis.MarshalJSON() if err != nil { @@ -2143,8 +2144,8 @@ func TestBuildAllowListActivationBlock(t *testing.T) { t.Fatal(err) } role := deployerallowlist.GetContractDeployerAllowListStatus(genesisState, testEthAddrs[0]) - if role != allowlist.AllowListNoRole { - t.Fatalf("Expected allow list status to be set to no role: %s, but found: %s", allowlist.AllowListNoRole, role) + if role != allowlist.NoRole { + t.Fatalf("Expected allow list status to be set to no role: %s, but found: %s", allowlist.NoRole, role) } // Send basic transaction to construct a simple block and confirm that the precompile state configuration in the worker behaves correctly. @@ -2173,8 +2174,8 @@ func TestBuildAllowListActivationBlock(t *testing.T) { t.Fatal(err) } role = deployerallowlist.GetContractDeployerAllowListStatus(blkState, testEthAddrs[0]) - if role != allowlist.AllowListAdmin { - t.Fatalf("Expected allow list status to be set to Admin: %s, but found: %s", allowlist.AllowListAdmin, role) + if role != allowlist.AdminRole { + t.Fatalf("Expected allow list status to be set role %s, but found: %s", allowlist.AdminRole, role) } } @@ -2185,7 +2186,9 @@ func TestTxAllowListSuccessfulTx(t *testing.T) { if err := genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM)); err != nil { t.Fatal(err) } - genesis.Config.TxAllowListConfig = txallowlist.NewTxAllowListConfig(big.NewInt(0), testEthAddrs[0:1], nil) + genesis.Config.GenesisPrecompiles = params.ChainConfigPrecompiles{ + txallowlist.ConfigKey: txallowlist.NewConfig(big.NewInt(0), testEthAddrs[0:1], nil), + } genesisJSON, err := genesis.MarshalJSON() if err != nil { t.Fatal(err) @@ -2208,12 +2211,12 @@ func TestTxAllowListSuccessfulTx(t *testing.T) { // Check that address 0 is whitelisted and address 1 is not role := txallowlist.GetTxAllowListStatus(genesisState, testEthAddrs[0]) - if role != allowlist.AllowListAdmin { - t.Fatalf("Expected allow list status to be set to admin: %s, but found: %s", allowlist.AllowListAdmin, role) + if role != allowlist.AdminRole { + t.Fatalf("Expected allow list status to be set to admin: %s, but found: %s", allowlist.AdminRole, role) } role = txallowlist.GetTxAllowListStatus(genesisState, testEthAddrs[1]) - if role != allowlist.AllowListNoRole { - t.Fatalf("Expected allow list status to be set to no role: %s, but found: %s", allowlist.AllowListNoRole, role) + if role != allowlist.NoRole { + t.Fatalf("Expected allow list status to be set to no role: %s, but found: %s", allowlist.NoRole, role) } // Submit a successful transaction @@ -2234,7 +2237,7 @@ func TestTxAllowListSuccessfulTx(t *testing.T) { } errs = vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1}) - if err := errs[0]; !errors.Is(err, txallowlist.ErrSenderAddressNotAllowListed) { + if err := errs[0]; !errors.Is(err, vmerrs.ErrSenderAddressNotAllowListed) { t.Fatalf("expected ErrSenderAddressNotAllowListed, got: %s", err) } @@ -2261,7 +2264,9 @@ func TestTxAllowListDisablePrecompile(t *testing.T) { t.Fatal(err) } enableAllowListTimestamp := time.Unix(0, 0) // enable at genesis - genesis.Config.TxAllowListConfig = txallowlist.NewTxAllowListConfig(big.NewInt(enableAllowListTimestamp.Unix()), testEthAddrs[0:1], nil) + genesis.Config.GenesisPrecompiles = params.ChainConfigPrecompiles{ + txallowlist.ConfigKey: txallowlist.NewConfig(big.NewInt(enableAllowListTimestamp.Unix()), testEthAddrs[0:1], nil), + } genesisJSON, err := genesis.MarshalJSON() if err != nil { t.Fatal(err) @@ -2303,12 +2308,12 @@ func TestTxAllowListDisablePrecompile(t *testing.T) { // Check that address 0 is whitelisted and address 1 is not role := txallowlist.GetTxAllowListStatus(genesisState, testEthAddrs[0]) - if role != allowlist.AllowListAdmin { - t.Fatalf("Expected allow list status to be set to admin: %s, but found: %s", allowlist.AllowListAdmin, role) + if role != allowlist.AdminRole { + t.Fatalf("Expected allow list status to be set to admin: %s, but found: %s", allowlist.AdminRole, role) } role = txallowlist.GetTxAllowListStatus(genesisState, testEthAddrs[1]) - if role != allowlist.AllowListNoRole { - t.Fatalf("Expected allow list status to be set to no role: %s, but found: %s", allowlist.AllowListNoRole, role) + if role != allowlist.NoRole { + t.Fatalf("Expected allow list status to be set to no role: %s, but found: %s", allowlist.NoRole, role) } // Submit a successful transaction @@ -2329,7 +2334,7 @@ func TestTxAllowListDisablePrecompile(t *testing.T) { } errs = vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1}) - if err := errs[0]; !errors.Is(err, txallowlist.ErrSenderAddressNotAllowListed) { + if err := errs[0]; !errors.Is(err, vmerrs.ErrSenderAddressNotAllowListed) { t.Fatalf("expected ErrSenderAddressNotAllowListed, got: %s", err) } @@ -2373,7 +2378,9 @@ func TestFeeManagerChangeFee(t *testing.T) { if err := genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM)); err != nil { t.Fatal(err) } - genesis.Config.FeeManagerConfig = feemanager.NewFeeManagerConfig(big.NewInt(0), testEthAddrs[0:1], nil, nil) + genesis.Config.GenesisPrecompiles = params.ChainConfigPrecompiles{ + feemanager.ConfigKey: feemanager.NewConfig(big.NewInt(0), testEthAddrs[0:1], nil, nil), + } // set a lower fee config now testLowFeeConfig := commontype.FeeConfig{ @@ -2412,12 +2419,12 @@ func TestFeeManagerChangeFee(t *testing.T) { // Check that address 0 is whitelisted and address 1 is not role := feemanager.GetFeeManagerStatus(genesisState, testEthAddrs[0]) - if role != allowlist.AllowListAdmin { - t.Fatalf("Expected fee manager list status to be set to admin: %s, but found: %s", precompile.FeeManagerAddress, role) + if role != allowlist.AdminRole { + t.Fatalf("Expected fee manager list status to be set to admin: %s, but found: %s", allowlist.AdminRole, role) } role = feemanager.GetFeeManagerStatus(genesisState, testEthAddrs[1]) - if role != allowlist.AllowListNoRole { - t.Fatalf("Expected fee manager list status to be set to no role: %s, but found: %s", precompile.FeeManagerAddress, role) + if role != allowlist.NoRole { + t.Fatalf("Expected fee manager list status to be set to no role: %s, but found: %s", allowlist.AdminRole, role) } // Contract is initialized but no preconfig is given, reader should return genesis fee config feeConfig, lastChangedAt, err := vm.blockChain.GetFeeConfigAt(vm.blockChain.Genesis().Header()) @@ -2435,7 +2442,7 @@ func TestFeeManagerChangeFee(t *testing.T) { tx := types.NewTx(&types.DynamicFeeTx{ ChainID: genesis.Config.ChainID, Nonce: uint64(0), - To: &precompile.FeeManagerAddress, + To: &feemanager.ContractAddress, Gas: testLowFeeConfig.GasLimit.Uint64(), Value: common.Big0, GasFeeCap: testLowFeeConfig.MinBaseFee, // give low fee, it should work since we still haven't applied high fees @@ -2471,7 +2478,7 @@ func TestFeeManagerChangeFee(t *testing.T) { tx2 := types.NewTx(&types.DynamicFeeTx{ ChainID: genesis.Config.ChainID, Nonce: uint64(1), - To: &precompile.FeeManagerAddress, + To: &feemanager.ContractAddress, Gas: genesis.Config.FeeConfig.GasLimit.Uint64(), Value: common.Big0, GasFeeCap: testLowFeeConfig.MinBaseFee, // this is too low for applied config, should fail @@ -2613,7 +2620,9 @@ func TestRewardManagerPrecompileSetRewardAddress(t *testing.T) { genesis := &core.Genesis{} require.NoError(t, genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM))) - genesis.Config.RewardManagerConfig = rewardmanager.NewRewardManagerConfig(common.Big0, testEthAddrs[0:1], nil, nil) + genesis.Config.GenesisPrecompiles = params.ChainConfigPrecompiles{ + rewardmanager.ConfigKey: rewardmanager.NewConfig(common.Big0, testEthAddrs[0:1], nil, nil), + } genesis.Config.AllowFeeRecipients = true // enable this in genesis to test if this is recognized by the reward manager genesisJSON, err := genesis.MarshalJSON() require.NoError(t, err) @@ -2659,7 +2668,7 @@ func TestRewardManagerPrecompileSetRewardAddress(t *testing.T) { gas := 21000 + 240 + rewardmanager.SetRewardAddressGasCost // 21000 for tx, 240 for tx data - tx := types.NewTransaction(uint64(0), precompile.RewardManagerAddress, big.NewInt(1), gas, big.NewInt(testMinGasPrice), data) + tx := types.NewTransaction(uint64(0), rewardmanager.ContractAddress, big.NewInt(1), gas, big.NewInt(testMinGasPrice), data) signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm.chainConfig.ChainID), testKeys[0]) require.NoError(t, err) @@ -2753,7 +2762,9 @@ func TestRewardManagerPrecompileAllowFeeRecipients(t *testing.T) { genesis := &core.Genesis{} require.NoError(t, genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM))) - genesis.Config.RewardManagerConfig = rewardmanager.NewRewardManagerConfig(common.Big0, testEthAddrs[0:1], nil, nil) + genesis.Config.GenesisPrecompiles = params.ChainConfigPrecompiles{ + rewardmanager.ConfigKey: rewardmanager.NewConfig(common.Big0, testEthAddrs[0:1], nil, nil), + } genesis.Config.AllowFeeRecipients = false // disable this in genesis genesisJSON, err := genesis.MarshalJSON() require.NoError(t, err) @@ -2795,7 +2806,7 @@ func TestRewardManagerPrecompileAllowFeeRecipients(t *testing.T) { gas := 21000 + 240 + rewardmanager.AllowFeeRecipientsGasCost // 21000 for tx, 240 for tx data - tx := types.NewTransaction(uint64(0), precompile.RewardManagerAddress, big.NewInt(1), gas, big.NewInt(testMinGasPrice), data) + tx := types.NewTransaction(uint64(0), rewardmanager.ContractAddress, big.NewInt(1), gas, big.NewInt(testMinGasPrice), data) signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm.chainConfig.ChainID), testKeys[0]) require.NoError(t, err) diff --git a/plugin/evm/vm_upgrade_bytes_test.go b/plugin/evm/vm_upgrade_bytes_test.go index 2adca151fe..a4ac725a59 100644 --- a/plugin/evm/vm_upgrade_bytes_test.go +++ b/plugin/evm/vm_upgrade_bytes_test.go @@ -18,7 +18,8 @@ import ( "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/metrics" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile/txallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" + "github.com/ava-labs/subnet-evm/vmerrs" "github.com/stretchr/testify/assert" ) @@ -28,7 +29,7 @@ func TestVMUpgradeBytesPrecompile(t *testing.T) { upgradeConfig := ¶ms.UpgradeConfig{ PrecompileUpgrades: []params.PrecompileUpgrade{ { - TxAllowListConfig: txallowlist.NewTxAllowListConfig(big.NewInt(enableAllowListTimestamp.Unix()), testEthAddrs[0:1], nil), + Config: txallowlist.NewConfig(big.NewInt(enableAllowListTimestamp.Unix()), testEthAddrs[0:1], nil), }, }, } @@ -57,7 +58,7 @@ func TestVMUpgradeBytesPrecompile(t *testing.T) { t.Fatal(err) } errs = vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1}) - if err := errs[0]; !errors.Is(err, txallowlist.ErrSenderAddressNotAllowListed) { + if err := errs[0]; !errors.Is(err, vmerrs.ErrSenderAddressNotAllowListed) { t.Fatalf("expected ErrSenderAddressNotAllowListed, got: %s", err) } @@ -71,7 +72,7 @@ func TestVMUpgradeBytesPrecompile(t *testing.T) { upgradeConfig.PrecompileUpgrades = append( upgradeConfig.PrecompileUpgrades, params.PrecompileUpgrade{ - TxAllowListConfig: txallowlist.NewDisableTxAllowListConfig(big.NewInt(disableAllowListTimestamp.Unix())), + Config: txallowlist.NewDisableConfig(big.NewInt(disableAllowListTimestamp.Unix())), }, ) upgradeBytesJSON, err = json.Marshal(upgradeConfig) @@ -108,7 +109,7 @@ func TestVMUpgradeBytesPrecompile(t *testing.T) { // Submit a rejected transaction, should throw an error errs = vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1}) - if err := errs[0]; !errors.Is(err, txallowlist.ErrSenderAddressNotAllowListed) { + if err := errs[0]; !errors.Is(err, vmerrs.ErrSenderAddressNotAllowListed) { t.Fatalf("expected ErrSenderAddressNotAllowListed, got: %s", err) } diff --git a/precompile/allowlist/allow_list.go b/precompile/allowlist/allow_list.go deleted file mode 100644 index ced37ed530..0000000000 --- a/precompile/allowlist/allow_list.go +++ /dev/null @@ -1,240 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package allowlist - -import ( - "errors" - "fmt" - "math/big" - - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/vmerrs" - "github.com/ethereum/go-ethereum/common" -) - -const ( - SetAdminFuncKey = "setAdmin" - SetEnabledFuncKey = "setEnabled" - SetNoneFuncKey = "setNone" - ReadAllowListFuncKey = "readAllowList" - - ModifyAllowListGasCost = precompile.WriteGasCostPerSlot - ReadAllowListGasCost = precompile.ReadGasCostPerSlot -) - -var ( - AllowListNoRole AllowListRole = AllowListRole(common.BigToHash(big.NewInt(0))) // No role assigned - this is equivalent to common.Hash{} and deletes the key from the DB when set - AllowListEnabled AllowListRole = AllowListRole(common.BigToHash(big.NewInt(1))) // Deployers are allowed to create new contracts - AllowListAdmin AllowListRole = AllowListRole(common.BigToHash(big.NewInt(2))) // Admin - allowed to modify both the admin and deployer list as well as deploy contracts - - // AllowList function signatures - setAdminSignature = precompile.CalculateFunctionSelector("setAdmin(address)") - setEnabledSignature = precompile.CalculateFunctionSelector("setEnabled(address)") - setNoneSignature = precompile.CalculateFunctionSelector("setNone(address)") - readAllowListSignature = precompile.CalculateFunctionSelector("readAllowList(address)") - // Error returned when an invalid write is attempted - ErrCannotModifyAllowList = errors.New("non-admin cannot modify allow list") - - allowListInputLen = common.HashLength -) - -// AllowListConfig specifies the initial set of allow list admins. -type AllowListConfig struct { - AllowListAdmins []common.Address `json:"adminAddresses"` - EnabledAddresses []common.Address `json:"enabledAddresses"` // initial enabled addresses -} - -// Configure initializes the address space of [precompileAddr] by initializing the role of each of -// the addresses in [AllowListAdmins]. -func (c *AllowListConfig) Configure(state precompile.StateDB, precompileAddr common.Address) error { - for _, enabledAddr := range c.EnabledAddresses { - SetAllowListRole(state, precompileAddr, enabledAddr, AllowListEnabled) - } - for _, adminAddr := range c.AllowListAdmins { - SetAllowListRole(state, precompileAddr, adminAddr, AllowListAdmin) - } - return nil -} - -// Equal returns true iff [other] has the same admins in the same order in its allow list. -func (c *AllowListConfig) Equal(other *AllowListConfig) bool { - if other == nil { - return false - } - if !areEqualAddressLists(c.AllowListAdmins, other.AllowListAdmins) { - return false - } - - return areEqualAddressLists(c.EnabledAddresses, other.EnabledAddresses) -} - -// areEqualAddressLists returns true iff [a] and [b] have the same addresses in the same order. -func areEqualAddressLists(current []common.Address, other []common.Address) bool { - if len(current) != len(other) { - return false - } - for i, address := range current { - if address != other[i] { - return false - } - } - return true -} - -// Verify returns an error if there is an overlapping address between admin and enabled roles -func (c *AllowListConfig) Verify() error { - // return early if either list is empty - if len(c.EnabledAddresses) == 0 || len(c.AllowListAdmins) == 0 { - return nil - } - - addressMap := make(map[common.Address]bool) - for _, enabledAddr := range c.EnabledAddresses { - // check for duplicates - if _, ok := addressMap[enabledAddr]; ok { - return fmt.Errorf("duplicate address %s in enabled list", enabledAddr) - } - addressMap[enabledAddr] = false - } - - for _, adminAddr := range c.AllowListAdmins { - // check for overlap between enabled and admin lists - if inAdmin, ok := addressMap[adminAddr]; ok { - if inAdmin { - return fmt.Errorf("duplicate address %s in admin list", adminAddr) - } else { - return fmt.Errorf("cannot set address %s as both admin and enabled", adminAddr) - } - } - addressMap[adminAddr] = true - } - - return nil -} - -// GetAllowListStatus returns the allow list role of [address] for the precompile -// at [precompileAddr] -func GetAllowListStatus(state precompile.StateDB, precompileAddr common.Address, address common.Address) AllowListRole { - // Generate the state key for [address] - addressKey := address.Hash() - return AllowListRole(state.GetState(precompileAddr, addressKey)) -} - -// SetAllowListRole sets the permissions of [address] to [role] for the precompile -// at [precompileAddr]. -// assumes [role] has already been verified as valid. -func SetAllowListRole(stateDB precompile.StateDB, precompileAddr, address common.Address, role AllowListRole) { - // Generate the state key for [address] - addressKey := address.Hash() - // Assign [role] to the address - // This stores the [role] in the contract storage with address [precompileAddr] - // and [addressKey] hash. It means that any reusage of the [addressKey] for different value - // conflicts with the same slot [role] is stored. - // Precompile implementations must use a different key than [addressKey] - stateDB.SetState(precompileAddr, addressKey, common.Hash(role)) -} - -// PackModifyAllowList packs [address] and [role] into the appropriate arguments for modifying the allow list. -// Note: [role] is not packed in the input value returned, but is instead used as a selector for the function -// selector that should be encoded in the input. -func PackModifyAllowList(address common.Address, role AllowListRole) ([]byte, error) { - // function selector (4 bytes) + hash for address - input := make([]byte, 0, precompile.SelectorLen+common.HashLength) - - switch role { - case AllowListAdmin: - input = append(input, setAdminSignature...) - case AllowListEnabled: - input = append(input, setEnabledSignature...) - case AllowListNoRole: - input = append(input, setNoneSignature...) - default: - return nil, fmt.Errorf("cannot pack modify list input with invalid role: %s", role) - } - - input = append(input, address.Hash().Bytes()...) - return input, nil -} - -// PackReadAllowList packs [address] into the input data to the read allow list function -func PackReadAllowList(address common.Address) []byte { - input := make([]byte, 0, precompile.SelectorLen+common.HashLength) - input = append(input, readAllowListSignature...) - input = append(input, address.Hash().Bytes()...) - return input -} - -// createAllowListRoleSetter returns an execution function for setting the allow list status of the input address argument to [role]. -// This execution function is speciifc to [precompileAddr]. -func createAllowListRoleSetter(precompileAddr common.Address, role AllowListRole) precompile.RunStatefulPrecompileFunc { - return func(evm precompile.PrecompileAccessibleState, callerAddr, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = precompile.DeductGas(suppliedGas, ModifyAllowListGasCost); err != nil { - return nil, 0, err - } - - if len(input) != allowListInputLen { - return nil, remainingGas, fmt.Errorf("invalid input length for modifying allow list: %d", len(input)) - } - - modifyAddress := common.BytesToAddress(input) - - if readOnly { - return nil, remainingGas, vmerrs.ErrWriteProtection - } - - stateDB := evm.GetStateDB() - - // Verify that the caller is in the allow list and therefore has the right to modify it - callerStatus := GetAllowListStatus(stateDB, precompileAddr, callerAddr) - if !callerStatus.IsAdmin() { - return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotModifyAllowList, callerAddr) - } - - SetAllowListRole(stateDB, precompileAddr, modifyAddress, role) - // Return an empty output and the remaining gas - return []byte{}, remainingGas, nil - } -} - -// createReadAllowList returns an execution function that reads the allow list for the given [precompileAddr]. -// The execution function parses the input into a single address and returns the 32 byte hash that specifies the -// designated role of that address -func createReadAllowList(precompileAddr common.Address) precompile.RunStatefulPrecompileFunc { - return func(evm precompile.PrecompileAccessibleState, callerAddr common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = precompile.DeductGas(suppliedGas, ReadAllowListGasCost); err != nil { - return nil, 0, err - } - - if len(input) != allowListInputLen { - return nil, remainingGas, fmt.Errorf("invalid input length for read allow list: %d", len(input)) - } - - readAddress := common.BytesToAddress(input) - role := GetAllowListStatus(evm.GetStateDB(), precompileAddr, readAddress) - roleBytes := common.Hash(role).Bytes() - return roleBytes, remainingGas, nil - } -} - -// createAllowListPrecompile returns a StatefulPrecompiledContract with R/W control of an allow list at [precompileAddr] -func CreateAllowListPrecompile(precompileAddr common.Address) precompile.StatefulPrecompiledContract { - // Construct the contract with no fallback function. - allowListFuncs := CreateAllowListFunctions(precompileAddr) - contract, err := precompile.NewStatefulPrecompileContract(nil, allowListFuncs) - // TODO Change this to be returned as an error after refactoring this precompile - // to use the new precompile template. - if err != nil { - panic(err) - } - return contract -} - -func CreateAllowListFunctions(precompileAddr common.Address) []*precompile.StatefulPrecompileFunction { - setAdmin := precompile.NewStatefulPrecompileFunction(setAdminSignature, createAllowListRoleSetter(precompileAddr, AllowListAdmin)) - setEnabled := precompile.NewStatefulPrecompileFunction(setEnabledSignature, createAllowListRoleSetter(precompileAddr, AllowListEnabled)) - setNone := precompile.NewStatefulPrecompileFunction(setNoneSignature, createAllowListRoleSetter(precompileAddr, AllowListNoRole)) - read := precompile.NewStatefulPrecompileFunction(readAllowListSignature, createReadAllowList(precompileAddr)) - - return []*precompile.StatefulPrecompileFunction{setAdmin, setEnabled, setNone, read} -} diff --git a/precompile/allowlist/allow_list_test.go b/precompile/allowlist/allow_list_test.go new file mode 100644 index 0000000000..3a9f4f2dd3 --- /dev/null +++ b/precompile/allowlist/allow_list_test.go @@ -0,0 +1,283 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package allowlist + +import ( + "testing" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/subnet-evm/core/rawdb" + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestAllowListRun(t *testing.T) { + type test struct { + caller common.Address + input func() []byte + suppliedGas uint64 + readOnly bool + + expectedRes []byte + expectedErr string + + config *Config + + assertState func(t *testing.T, state *state.StateDB) + } + + adminAddr := common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC") + enabledAddr := common.HexToAddress("0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B") + noRoleAddr := common.HexToAddress("0xF60C45c607D0f41687c94C314d300f483661E13a") + dummyContractAddr := common.HexToAddress("0x0000000000000000000000000000000000000000") + testAllowListPrecompile := CreateAllowListPrecompile(dummyContractAddr) + + for name, test := range map[string]test{ + "set admin": { + caller: adminAddr, + input: func() []byte { + input, err := PackModifyAllowList(noRoleAddr, AdminRole) + require.NoError(t, err) + + return input + }, + suppliedGas: ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetAllowListStatus(state, dummyContractAddr, noRoleAddr) + require.Equal(t, AdminRole, res) + }, + }, + "set enabled": { + caller: adminAddr, + input: func() []byte { + input, err := PackModifyAllowList(noRoleAddr, EnabledRole) + require.NoError(t, err) + + return input + }, + suppliedGas: ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetAllowListStatus(state, dummyContractAddr, noRoleAddr) + require.Equal(t, EnabledRole, res) + }, + }, + "set no role": { + caller: adminAddr, + input: func() []byte { + input, err := PackModifyAllowList(enabledAddr, NoRole) + require.NoError(t, err) + + return input + }, + suppliedGas: ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetAllowListStatus(state, dummyContractAddr, enabledAddr) + require.Equal(t, NoRole, res) + }, + }, + "set no role from no role": { + caller: noRoleAddr, + input: func() []byte { + input, err := PackModifyAllowList(enabledAddr, NoRole) + require.NoError(t, err) + + return input + }, + suppliedGas: ModifyAllowListGasCost, + readOnly: false, + expectedErr: ErrCannotModifyAllowList.Error(), + }, + "set enabled from no role": { + caller: noRoleAddr, + input: func() []byte { + input, err := PackModifyAllowList(noRoleAddr, EnabledRole) + require.NoError(t, err) + + return input + }, + suppliedGas: ModifyAllowListGasCost, + readOnly: false, + expectedErr: ErrCannotModifyAllowList.Error(), + }, + "set admin from no role": { + caller: noRoleAddr, + input: func() []byte { + input, err := PackModifyAllowList(enabledAddr, AdminRole) + require.NoError(t, err) + + return input + }, + suppliedGas: ModifyAllowListGasCost, + readOnly: false, + expectedErr: ErrCannotModifyAllowList.Error(), + }, + "set no role from enabled": { + caller: enabledAddr, + input: func() []byte { + input, err := PackModifyAllowList(adminAddr, NoRole) + require.NoError(t, err) + + return input + }, + suppliedGas: ModifyAllowListGasCost, + readOnly: false, + expectedErr: ErrCannotModifyAllowList.Error(), + }, + "set enabled from enabled": { + caller: enabledAddr, + input: func() []byte { + input, err := PackModifyAllowList(noRoleAddr, EnabledRole) + require.NoError(t, err) + + return input + }, + suppliedGas: ModifyAllowListGasCost, + readOnly: false, + expectedErr: ErrCannotModifyAllowList.Error(), + }, + "set admin from enabled": { + caller: enabledAddr, + input: func() []byte { + input, err := PackModifyAllowList(noRoleAddr, AdminRole) + require.NoError(t, err) + + return input + }, + suppliedGas: ModifyAllowListGasCost, + readOnly: false, + expectedErr: ErrCannotModifyAllowList.Error(), + }, + "set no role with readOnly enabled": { + caller: adminAddr, + input: func() []byte { + input, err := PackModifyAllowList(enabledAddr, NoRole) + require.NoError(t, err) + + return input + }, + suppliedGas: ModifyAllowListGasCost, + readOnly: true, + expectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "set no role insufficient gas": { + caller: adminAddr, + input: func() []byte { + input, err := PackModifyAllowList(enabledAddr, NoRole) + require.NoError(t, err) + + return input + }, + suppliedGas: ModifyAllowListGasCost - 1, + readOnly: false, + expectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "read allow list no role": { + caller: noRoleAddr, + input: func() []byte { + return PackReadAllowList(noRoleAddr) + }, + suppliedGas: ReadAllowListGasCost, + readOnly: false, + expectedRes: common.Hash(NoRole).Bytes(), + assertState: nil, + }, + "read allow list admin role": { + caller: adminAddr, + input: func() []byte { + return PackReadAllowList(adminAddr) + }, + suppliedGas: ReadAllowListGasCost, + readOnly: false, + expectedRes: common.Hash(AdminRole).Bytes(), + assertState: nil, + }, + "read allow list with readOnly enabled": { + caller: adminAddr, + input: func() []byte { + return PackReadAllowList(noRoleAddr) + }, + suppliedGas: ReadAllowListGasCost, + readOnly: true, + expectedRes: common.Hash(NoRole).Bytes(), + assertState: nil, + }, + "read allow list out of gas": { + caller: adminAddr, + input: func() []byte { + return PackReadAllowList(noRoleAddr) + }, + suppliedGas: ReadAllowListGasCost - 1, + readOnly: true, + expectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "initial config sets admins": { + config: &Config{ + AdminAddresses: []common.Address{noRoleAddr, enabledAddr}, + }, + suppliedGas: 0, + readOnly: false, + expectedErr: "", + assertState: func(t *testing.T, state *state.StateDB) { + require.Equal(t, AdminRole, GetAllowListStatus(state, dummyContractAddr, noRoleAddr)) + require.Equal(t, AdminRole, GetAllowListStatus(state, dummyContractAddr, enabledAddr)) + }, + }, + "initial config sets enabled": { + config: &Config{ + EnabledAddresses: []common.Address{noRoleAddr, adminAddr}, + }, + suppliedGas: 0, + readOnly: false, + expectedErr: "", + assertState: func(t *testing.T, state *state.StateDB) { + require.Equal(t, EnabledRole, GetAllowListStatus(state, dummyContractAddr, adminAddr)) + require.Equal(t, EnabledRole, GetAllowListStatus(state, dummyContractAddr, noRoleAddr)) + }, + }, + } { + t.Run(name, func(t *testing.T) { + db := rawdb.NewMemoryDatabase() + state, err := state.New(common.Hash{}, state.NewDatabase(db), nil) + require.NoError(t, err) + + // Set up the state so that each address has the expected permissions at the start. + SetAllowListRole(state, dummyContractAddr, adminAddr, AdminRole) + SetAllowListRole(state, dummyContractAddr, enabledAddr, EnabledRole) + require.Equal(t, AdminRole, GetAllowListStatus(state, dummyContractAddr, adminAddr)) + require.Equal(t, EnabledRole, GetAllowListStatus(state, dummyContractAddr, enabledAddr)) + + if test.config != nil { + test.config.Configure(state, dummyContractAddr) + } + + blockContext := contract.NewMockBlockContext(common.Big0, 0) + accesibleState := contract.NewMockAccessibleState(state, blockContext, snow.DefaultContextTest()) + if test.input != nil { + ret, remainingGas, err := testAllowListPrecompile.Run(accesibleState, test.caller, dummyContractAddr, test.input(), test.suppliedGas, test.readOnly) + + if len(test.expectedErr) != 0 { + require.ErrorContains(t, err, test.expectedErr) + } else { + require.NoError(t, err) + } + + require.Equal(t, uint64(0), remainingGas) + require.Equal(t, test.expectedRes, ret) + } + + if test.assertState != nil { + test.assertState(t, state) + } + }) + } +} diff --git a/precompile/allowlist/allowlist.go b/precompile/allowlist/allowlist.go new file mode 100644 index 0000000000..357855b419 --- /dev/null +++ b/precompile/allowlist/allowlist.go @@ -0,0 +1,172 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package allowlist + +import ( + "errors" + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" +) + +// AllowList is an abstraction that allows other precompiles to manage +// which addresses can call the precompile by maintaining an allowlist +// in the storage trie. Each account may have one of the following roles: +// 1. NoRole - this is equivalent to common.Hash{} and deletes the key from the DB when set +// 2. EnabledRole - allowed to call the precompile +// 3. Admin - allowed to modify both the allowlist and call the precompile + +const ( + SetAdminFuncKey = "setAdmin" + SetEnabledFuncKey = "setEnabled" + SetNoneFuncKey = "setNone" + ReadAllowListFuncKey = "readAllowList" + + ModifyAllowListGasCost = contract.WriteGasCostPerSlot + ReadAllowListGasCost = contract.ReadGasCostPerSlot +) + +const allowListInputLen = common.HashLength + +var ( + NoRole = Role(common.BigToHash(common.Big0)) // NoRole - this is equivalent to common.Hash{} and deletes the key from the DB when set + EnabledRole = Role(common.BigToHash(common.Big1)) // EnabledRole - allowed to call the precompile + AdminRole = Role(common.BigToHash(common.Big2)) // Admin - allowed to modify both the allowlist and call the precompile + + // AllowList function signatures + setAdminSignature = contract.CalculateFunctionSelector("setAdmin(address)") + setEnabledSignature = contract.CalculateFunctionSelector("setEnabled(address)") + setNoneSignature = contract.CalculateFunctionSelector("setNone(address)") + readAllowListSignature = contract.CalculateFunctionSelector("readAllowList(address)") + // Error returned when an invalid write is attempted + ErrCannotModifyAllowList = errors.New("non-admin cannot modify allow list") +) + +// GetAllowListStatus returns the allow list role of [address] for the precompile +// at [precompileAddr] +func GetAllowListStatus(state contract.StateDB, precompileAddr common.Address, address common.Address) Role { + // Generate the state key for [address] + addressKey := address.Hash() + return Role(state.GetState(precompileAddr, addressKey)) +} + +// SetAllowListRole sets the permissions of [address] to [role] for the precompile +// at [precompileAddr]. +// assumes [role] has already been verified as valid. +func SetAllowListRole(stateDB contract.StateDB, precompileAddr, address common.Address, role Role) { + // Generate the state key for [address] + addressKey := address.Hash() + // Assign [role] to the address + // This stores the [role] in the contract storage with address [precompileAddr] + // and [addressKey] hash. It means that any reusage of the [addressKey] for different value + // conflicts with the same slot [role] is stored. + // Precompile implementations must use a different key than [addressKey] + stateDB.SetState(precompileAddr, addressKey, common.Hash(role)) +} + +// PackModifyAllowList packs [address] and [role] into the appropriate arguments for modifying the allow list. +// Note: [role] is not packed in the input value returned, but is instead used as a selector for the function +// selector that should be encoded in the input. +func PackModifyAllowList(address common.Address, role Role) ([]byte, error) { + // function selector (4 bytes) + hash for address + input := make([]byte, 0, contract.SelectorLen+common.HashLength) + + switch role { + case AdminRole: + input = append(input, setAdminSignature...) + case EnabledRole: + input = append(input, setEnabledSignature...) + case NoRole: + input = append(input, setNoneSignature...) + default: + return nil, fmt.Errorf("cannot pack modify list input with invalid role: %s", role) + } + + input = append(input, address.Hash().Bytes()...) + return input, nil +} + +// PackReadAllowList packs [address] into the input data to the read allow list function +func PackReadAllowList(address common.Address) []byte { + input := make([]byte, 0, contract.SelectorLen+common.HashLength) + input = append(input, readAllowListSignature...) + input = append(input, address.Hash().Bytes()...) + return input +} + +// createAllowListRoleSetter returns an execution function for setting the allow list status of the input address argument to [role]. +// This execution function is speciifc to [precompileAddr]. +func createAllowListRoleSetter(precompileAddr common.Address, role Role) contract.RunStatefulPrecompileFunc { + return func(evm contract.AccessibleState, callerAddr, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, ModifyAllowListGasCost); err != nil { + return nil, 0, err + } + + if len(input) != allowListInputLen { + return nil, remainingGas, fmt.Errorf("invalid input length for modifying allow list: %d", len(input)) + } + + modifyAddress := common.BytesToAddress(input) + + if readOnly { + return nil, remainingGas, vmerrs.ErrWriteProtection + } + + stateDB := evm.GetStateDB() + + // Verify that the caller is an admin with permission to modify the allow list + callerStatus := GetAllowListStatus(stateDB, precompileAddr, callerAddr) + if !callerStatus.IsAdmin() { + return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotModifyAllowList, callerAddr) + } + + SetAllowListRole(stateDB, precompileAddr, modifyAddress, role) + // Return an empty output and the remaining gas + return []byte{}, remainingGas, nil + } +} + +// createReadAllowList returns an execution function that reads the allow list for the given [precompileAddr]. +// The execution function parses the input into a single address and returns the 32 byte hash that specifies the +// designated role of that address +func createReadAllowList(precompileAddr common.Address) contract.RunStatefulPrecompileFunc { + return func(evm contract.AccessibleState, callerAddr common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, ReadAllowListGasCost); err != nil { + return nil, 0, err + } + + if len(input) != allowListInputLen { + return nil, remainingGas, fmt.Errorf("invalid input length for read allow list: %d", len(input)) + } + + readAddress := common.BytesToAddress(input) + role := GetAllowListStatus(evm.GetStateDB(), precompileAddr, readAddress) + roleBytes := common.Hash(role).Bytes() + return roleBytes, remainingGas, nil + } +} + +// CreateAllowListPrecompile returns a StatefulPrecompiledContract with R/W control of an allow list at [precompileAddr] +func CreateAllowListPrecompile(precompileAddr common.Address) contract.StatefulPrecompiledContract { + // Construct the contract with no fallback function. + allowListFuncs := CreateAllowListFunctions(precompileAddr) + contract, err := contract.NewStatefulPrecompileContract(nil, allowListFuncs) + // TODO Change this to be returned as an error after refactoring this precompile + // to use the new precompile template. + if err != nil { + panic(err) + } + return contract +} + +func CreateAllowListFunctions(precompileAddr common.Address) []*contract.StatefulPrecompileFunction { + setAdmin := contract.NewStatefulPrecompileFunction(setAdminSignature, createAllowListRoleSetter(precompileAddr, AdminRole)) + setEnabled := contract.NewStatefulPrecompileFunction(setEnabledSignature, createAllowListRoleSetter(precompileAddr, EnabledRole)) + setNone := contract.NewStatefulPrecompileFunction(setNoneSignature, createAllowListRoleSetter(precompileAddr, NoRole)) + read := contract.NewStatefulPrecompileFunction(readAllowListSignature, createReadAllowList(precompileAddr)) + + return []*contract.StatefulPrecompileFunction{setAdmin, setEnabled, setNone, read} +} diff --git a/precompile/allowlist/config.go b/precompile/allowlist/config.go new file mode 100644 index 0000000000..c11f86afc2 --- /dev/null +++ b/precompile/allowlist/config.go @@ -0,0 +1,79 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package allowlist + +import ( + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" +) + +// Config specifies the initial set of addresses with Admin or Enabled roles. +type Config struct { + AdminAddresses []common.Address `json:"adminAddresses,omitempty"` // initial admin addresses + EnabledAddresses []common.Address `json:"enabledAddresses,omitempty"` // initial enabled addresses +} + +// Configure initializes the address space of [precompileAddr] by initializing the role of each of +// the addresses in [AllowListAdmins]. +func (c *Config) Configure(state contract.StateDB, precompileAddr common.Address) error { + for _, enabledAddr := range c.EnabledAddresses { + SetAllowListRole(state, precompileAddr, enabledAddr, EnabledRole) + } + for _, adminAddr := range c.AdminAddresses { + SetAllowListRole(state, precompileAddr, adminAddr, AdminRole) + } + return nil +} + +// Equal returns true iff [other] has the same admins in the same order in its allow list. +func (c *Config) Equal(other *Config) bool { + if other == nil { + return false + } + + return areEqualAddressLists(c.AdminAddresses, other.AdminAddresses) && + areEqualAddressLists(c.EnabledAddresses, other.EnabledAddresses) +} + +// areEqualAddressLists returns true iff [a] and [b] have the same addresses in the same order. +func areEqualAddressLists(current []common.Address, other []common.Address) bool { + if len(current) != len(other) { + return false + } + for i, address := range current { + if address != other[i] { + return false + } + } + return true +} + +// Verify returns an error if there is an overlapping address between admin and enabled roles +func (c *Config) Verify() error { + addressMap := make(map[common.Address]Role) // tracks which addresses we have seen and their role + + // check for duplicates in enabled list + for _, enabledAddr := range c.EnabledAddresses { + if _, ok := addressMap[enabledAddr]; ok { + return fmt.Errorf("duplicate address %s in enabled list", enabledAddr) + } + addressMap[enabledAddr] = EnabledRole + } + + // check for overlap between enabled and admin lists or duplicates in admin list + for _, adminAddr := range c.AdminAddresses { + if role, ok := addressMap[adminAddr]; ok { + if role == AdminRole { + return fmt.Errorf("duplicate address %s in admin list", adminAddr) + } else { + return fmt.Errorf("cannot set address %s as both admin and enabled", adminAddr) + } + } + addressMap[adminAddr] = AdminRole + } + + return nil +} diff --git a/precompile/allowlist/config_test.go b/precompile/allowlist/config_test.go new file mode 100644 index 0000000000..b9fdb24053 --- /dev/null +++ b/precompile/allowlist/config_test.go @@ -0,0 +1,97 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package allowlist + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestVerifyAllowlistConfig(t *testing.T) { + admins := []common.Address{{1}} + enableds := []common.Address{{2}} + tests := []struct { + name string + config Config + expectedError string + }{ + { + name: "invalid allow list config in allowlist", + config: Config{admins, admins}, + expectedError: "cannot set address", + }, + { + name: "nil member allow list config in allowlist", + config: Config{nil, nil}, + expectedError: "", + }, + { + name: "empty member allow list config in allowlist", + config: Config{[]common.Address{}, []common.Address{}}, + expectedError: "", + }, + { + name: "valid allow list config in allowlist", + config: Config{admins, enableds}, + expectedError: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + + err := tt.config.Verify() + if tt.expectedError == "" { + require.NoError(err) + } else { + require.ErrorContains(err, tt.expectedError) + } + }) + } +} + +func TestEqualAllowListConfig(t *testing.T) { + admins := []common.Address{{1}} + enableds := []common.Address{{2}} + tests := []struct { + name string + config *Config + other *Config + expected bool + }{ + { + name: "non-nil config and nil other", + config: &Config{admins, enableds}, + other: nil, + expected: false, + }, + { + name: "different admin", + config: &Config{admins, enableds}, + other: &Config{[]common.Address{{3}}, enableds}, + expected: false, + }, + { + name: "different enabled", + config: &Config{admins, enableds}, + other: &Config{admins, []common.Address{{3}}}, + expected: false, + }, + { + name: "same config", + config: &Config{admins, enableds}, + other: &Config{admins, enableds}, + expected: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + + require.Equal(tt.expected, tt.config.Equal(tt.other)) + }) + } +} diff --git a/precompile/allowlist/allow_list_role.go b/precompile/allowlist/role.go similarity index 56% rename from precompile/allowlist/allow_list_role.go rename to precompile/allowlist/role.go index 7e59f8420e..aa55007662 100644 --- a/precompile/allowlist/allow_list_role.go +++ b/precompile/allowlist/role.go @@ -5,13 +5,13 @@ package allowlist import "github.com/ethereum/go-ethereum/common" -// Enum constants for valid AllowListRole -type AllowListRole common.Hash +// Enum constants for valid Role +type Role common.Hash // Valid returns true iff [s] represents a valid role. -func (s AllowListRole) Valid() bool { +func (s Role) Valid() bool { switch s { - case AllowListNoRole, AllowListEnabled, AllowListAdmin: + case NoRole, EnabledRole, AdminRole: return true default: return false @@ -19,9 +19,9 @@ func (s AllowListRole) Valid() bool { } // IsNoRole returns true if [s] indicates no specific role. -func (s AllowListRole) IsNoRole() bool { +func (s Role) IsNoRole() bool { switch s { - case AllowListNoRole: + case NoRole: return true default: return false @@ -29,9 +29,9 @@ func (s AllowListRole) IsNoRole() bool { } // IsAdmin returns true if [s] indicates the permission to modify the allow list. -func (s AllowListRole) IsAdmin() bool { +func (s Role) IsAdmin() bool { switch s { - case AllowListAdmin: + case AdminRole: return true default: return false @@ -39,11 +39,25 @@ func (s AllowListRole) IsAdmin() bool { } // IsEnabled returns true if [s] indicates that it has permission to access the resource. -func (s AllowListRole) IsEnabled() bool { +func (s Role) IsEnabled() bool { switch s { - case AllowListAdmin, AllowListEnabled: + case AdminRole, EnabledRole: return true default: return false } } + +// String returns a string representation of [s]. +func (s Role) String() string { + switch s { + case NoRole: + return "NoRole" + case EnabledRole: + return "EnabledRole" + case AdminRole: + return "AdminRole" + default: + return "UnknownRole" + } +} diff --git a/precompile/config/config.go b/precompile/config/config.go new file mode 100644 index 0000000000..63ccf43dad --- /dev/null +++ b/precompile/config/config.go @@ -0,0 +1,27 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Defines the stateless interface for unmarshalling an arbitrary config of a precompile +package config + +import ( + "math/big" +) + +// StatefulPrecompileConfig defines the interface for a stateful precompile to +// be enabled via a network upgrade. +type Config interface { + // Key returns the unique key for the stateful precompile. + Key() string + // Timestamp returns the timestamp at which this stateful precompile should be enabled. + // 1) 0 indicates that the precompile should be enabled from genesis. + // 2) n indicates that the precompile should be enabled in the first block with timestamp >= [n]. + // 3) nil indicates that the precompile is never enabled. + Timestamp() *big.Int + // IsDisabled returns true if this network upgrade should disable the precompile. + IsDisabled() bool + // Equal returns true if the provided argument configures the same precompile with the same parameters. + Equal(Config) bool + // Verify is called on startup and an error is treated as fatal. Configure can assume the Config has passed verification. + Verify() error +} diff --git a/precompile/config/mock_config.go b/precompile/config/mock_config.go new file mode 100644 index 0000000000..cb1f1f9a91 --- /dev/null +++ b/precompile/config/mock_config.go @@ -0,0 +1,45 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// TODO: replace with gomock + +package config + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +var _ Config = &noopStatefulPrecompileConfig{} + +type noopStatefulPrecompileConfig struct { +} + +func NewNoopStatefulPrecompileConfig() *noopStatefulPrecompileConfig { + return &noopStatefulPrecompileConfig{} +} + +func (n *noopStatefulPrecompileConfig) Key() string { + return "" +} + +func (n *noopStatefulPrecompileConfig) Address() common.Address { + return common.Address{} +} + +func (n *noopStatefulPrecompileConfig) Timestamp() *big.Int { + return new(big.Int) +} + +func (n *noopStatefulPrecompileConfig) IsDisabled() bool { + return false +} + +func (n *noopStatefulPrecompileConfig) Equal(Config) bool { + return false +} + +func (n *noopStatefulPrecompileConfig) Verify() error { + return nil +} diff --git a/precompile/upgradeable.go b/precompile/config/upgradeable.go similarity index 59% rename from precompile/upgradeable.go rename to precompile/config/upgradeable.go index a35d575de0..66a4381694 100644 --- a/precompile/upgradeable.go +++ b/precompile/config/upgradeable.go @@ -1,7 +1,7 @@ // (c) 2022 Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package precompile +package config import ( "math/big" @@ -9,30 +9,29 @@ import ( "github.com/ava-labs/subnet-evm/utils" ) -// UpgradeableConfig contains the timestamp for the upgrade along with +// Upgrade contains the timestamp for the upgrade along with // a boolean [Disable]. If [Disable] is set, the upgrade deactivates -// the precompile and resets its storage. -// TODO: convert to interface -type UpgradeableConfig struct { +// the precompile and clears its storage. +type Upgrade struct { BlockTimestamp *big.Int `json:"blockTimestamp"` Disable bool `json:"disable,omitempty"` } // Timestamp returns the timestamp this network upgrade goes into effect. -func (c *UpgradeableConfig) Timestamp() *big.Int { - return c.BlockTimestamp +func (u *Upgrade) Timestamp() *big.Int { + return u.BlockTimestamp } // IsDisabled returns true if the network upgrade deactivates the precompile. -func (c *UpgradeableConfig) IsDisabled() bool { - return c.Disable +func (u *Upgrade) IsDisabled() bool { + return u.Disable } // Equal returns true iff [other] has the same blockTimestamp and has the // same on value for the Disable flag. -func (c *UpgradeableConfig) Equal(other *UpgradeableConfig) bool { +func (u *Upgrade) Equal(other *Upgrade) bool { if other == nil { return false } - return c.Disable == other.Disable && utils.BigNumEqual(c.BlockTimestamp, other.BlockTimestamp) + return u.Disable == other.Disable && utils.BigNumEqual(u.BlockTimestamp, other.BlockTimestamp) } diff --git a/precompile/contract.go b/precompile/contract/contract.go similarity index 80% rename from precompile/contract.go rename to precompile/contract/contract.go index 596ea6dc00..82bb5fe21c 100644 --- a/precompile/contract.go +++ b/precompile/contract/contract.go @@ -1,7 +1,7 @@ // (c) 2019-2020, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package precompile +package contract import ( "fmt" @@ -13,13 +13,7 @@ const ( SelectorLen = 4 ) -type RunStatefulPrecompileFunc func(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) - -// StatefulPrecompiledContract is the interface for executing a precompiled contract -type StatefulPrecompiledContract interface { - // Run executes the precompiled contract. - Run(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) -} +type RunStatefulPrecompileFunc func(accessibleState AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) // StatefulPrecompileFunction defines a function implemented by a stateful precompile type StatefulPrecompileFunction struct { @@ -67,7 +61,7 @@ func NewStatefulPrecompileContract(fallback RunStatefulPrecompileFunc, functions // Run selects the function using the 4 byte function selector at the start of the input and executes the underlying function on the // given arguments. -func (s *statefulPrecompileWithFunctionSelectors) Run(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { +func (s *statefulPrecompileWithFunctionSelectors) Run(accessibleState AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { // If there is no input data present, call the fallback function if present. if len(input) == 0 && s.fallback != nil { return s.fallback(accessibleState, caller, addr, nil, suppliedGas, readOnly) diff --git a/precompile/interface.go b/precompile/contract/interfaces.go similarity index 67% rename from precompile/interface.go rename to precompile/contract/interfaces.go index 82be0f749a..75f48980a1 100644 --- a/precompile/interface.go +++ b/precompile/contract/interfaces.go @@ -1,30 +1,22 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// (c) 2023, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package precompile +// Defines the interface for the configuration and execution of a precompile contract +package contract import ( "math/big" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/subnet-evm/commontype" + "github.com/ava-labs/subnet-evm/precompile/config" "github.com/ethereum/go-ethereum/common" ) -// PrecompileAccessibleState defines the interface exposed to stateful precompile contracts -type PrecompileAccessibleState interface { - GetStateDB() StateDB - GetBlockContext() BlockContext - GetSnowContext() *snow.Context - CallFromPrecompile(caller common.Address, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) -} - -// BlockContext defines an interface that provides information to a stateful precompile -// about the block that activates the upgrade. The precompile can access this information -// to initialize its state. -type BlockContext interface { - Number() *big.Int - Timestamp() *big.Int +// StatefulPrecompiledContract is the interface for executing a precompiled contract +type StatefulPrecompiledContract interface { + // Run executes the precompiled contract. + Run(accessibleState AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) } // ChainContext defines an interface that provides information to a stateful precompile @@ -59,3 +51,29 @@ type StateDB interface { Suicide(common.Address) bool Finalise(deleteEmptyObjects bool) } + +// AccessibleState defines the interface exposed to stateful precompile contracts +type AccessibleState interface { + GetStateDB() StateDB + GetBlockContext() BlockContext + GetSnowContext() *snow.Context + CallFromPrecompile(caller common.Address, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) +} + +// BlockContext defines an interface that provides information to a stateful precompile +// about the block that activates the upgrade. The precompile can access this information +// to initialize its state. +type BlockContext interface { + Number() *big.Int + Timestamp() *big.Int +} + +type Configurator interface { + NewConfig() config.Config + Configure( + chainConfig ChainConfig, + precompileConfig config.Config, + state StateDB, + blockContext BlockContext, + ) error +} diff --git a/precompile/contract/mock_interfaces.go b/precompile/contract/mock_interfaces.go new file mode 100644 index 0000000000..fe8cf5bf87 --- /dev/null +++ b/precompile/contract/mock_interfaces.go @@ -0,0 +1,57 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package contract + +import ( + "math/big" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ethereum/go-ethereum/common" +) + +// TODO: replace with gomock library + +var ( + _ BlockContext = &mockBlockContext{} + _ AccessibleState = &mockAccessibleState{} +) + +type mockBlockContext struct { + blockNumber *big.Int + timestamp uint64 +} + +func NewMockBlockContext(blockNumber *big.Int, timestamp uint64) *mockBlockContext { + return &mockBlockContext{ + blockNumber: blockNumber, + timestamp: timestamp, + } +} + +func (mb *mockBlockContext) Number() *big.Int { return mb.blockNumber } +func (mb *mockBlockContext) Timestamp() *big.Int { return new(big.Int).SetUint64(mb.timestamp) } + +type mockAccessibleState struct { + state StateDB + blockContext *mockBlockContext + snowContext *snow.Context +} + +func NewMockAccessibleState(state StateDB, blockContext *mockBlockContext, snowContext *snow.Context) *mockAccessibleState { + return &mockAccessibleState{ + state: state, + blockContext: blockContext, + snowContext: snowContext, + } +} + +func (m *mockAccessibleState) GetStateDB() StateDB { return m.state } + +func (m *mockAccessibleState) GetBlockContext() BlockContext { return m.blockContext } + +func (m *mockAccessibleState) GetSnowContext() *snow.Context { return m.snowContext } + +func (m *mockAccessibleState) CallFromPrecompile(caller common.Address, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) { + return nil, 0, nil +} diff --git a/precompile/utils.go b/precompile/contract/utils.go similarity index 85% rename from precompile/utils.go rename to precompile/contract/utils.go index f03e89bc50..9cc50d3155 100644 --- a/precompile/utils.go +++ b/precompile/contract/utils.go @@ -1,17 +1,25 @@ // (c) 2019-2020, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package precompile +package contract import ( "fmt" "regexp" + "strings" + "github.com/ava-labs/subnet-evm/accounts/abi" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) +// Gas costs for stateful precompiles +const ( + WriteGasCostPerSlot = 20_000 + ReadGasCostPerSlot = 5_000 +) + var functionSignatureRegex = regexp.MustCompile(`[\w]+\(((([\w]+)?)|((([\w]+),)+([\w]+)))\)`) // CalculateFunctionSelector returns the 4 byte function selector that results from [functionSignature] @@ -69,3 +77,14 @@ func PackedHash(packed []byte, index int) []byte { end := start + common.HashLength return packed[start:end] } + +// ParseABI parses the given ABI string and returns the parsed ABI. +// If the ABI is invalid, it panics. +func ParseABI(rawABI string) abi.ABI { + parsed, err := abi.JSON(strings.NewReader(rawABI)) + if err != nil { + panic(err) + } + + return parsed +} diff --git a/precompile/utils_test.go b/precompile/contract/utils_test.go similarity index 98% rename from precompile/utils_test.go rename to precompile/contract/utils_test.go index 3414bc341c..6220af95a8 100644 --- a/precompile/utils_test.go +++ b/precompile/contract/utils_test.go @@ -1,7 +1,7 @@ // (c) 2019-2020, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package precompile +package contract import ( "testing" diff --git a/precompile/contracts/deployerallowlist/config.go b/precompile/contracts/deployerallowlist/config.go new file mode 100644 index 0000000000..4f624de1f6 --- /dev/null +++ b/precompile/contracts/deployerallowlist/config.go @@ -0,0 +1,58 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package deployerallowlist + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ethereum/go-ethereum/common" +) + +var _ config.Config = &Config{} + +// Config contains the configuration for the ContractDeployerAllowList precompile, +// consisting of the initial allowlist and the timestamp for the network upgrade. +type Config struct { + allowlist.Config + config.Upgrade +} + +// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables +// ContractDeployerAllowList with [admins] and [enableds] as members of the allowlist. +func NewConfig(blockTimestamp *big.Int, admins []common.Address, enableds []common.Address) *Config { + return &Config{ + Config: allowlist.Config{ + AdminAddresses: admins, + EnabledAddresses: enableds, + }, + Upgrade: config.Upgrade{BlockTimestamp: blockTimestamp}, + } +} + +// NewDisableConfig returns config for a network upgrade at [blockTimestamp] +// that disables ContractDeployerAllowList. +func NewDisableConfig(blockTimestamp *big.Int) *Config { + return &Config{ + Upgrade: config.Upgrade{ + BlockTimestamp: blockTimestamp, + Disable: true, + }, + } +} + +func (*Config) Key() string { return ConfigKey } + +// Equal returns true if [cfg] is a [*ContractDeployerAllowListConfig] and it has been configured identical to [c]. +func (c *Config) Equal(cfg config.Config) bool { + // typecast before comparison + other, ok := (cfg).(*Config) + if !ok { + return false + } + return c.Upgrade.Equal(&other.Upgrade) && c.Config.Equal(&other.Config) +} + +func (c *Config) Verify() error { return c.Config.Verify() } diff --git a/precompile/deployerallowlist/config_test.go b/precompile/contracts/deployerallowlist/config_test.go similarity index 56% rename from precompile/deployerallowlist/config_test.go rename to precompile/contracts/deployerallowlist/config_test.go index 2ffb0be77c..c1245b0b09 100644 --- a/precompile/deployerallowlist/config_test.go +++ b/precompile/contracts/deployerallowlist/config_test.go @@ -7,7 +7,7 @@ import ( "math/big" "testing" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/config" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) @@ -16,12 +16,12 @@ func TestVerifyContractDeployerConfig(t *testing.T) { admins := []common.Address{{1}} tests := []struct { name string - config precompile.StatefulPrecompileConfig + config config.Config expectedError string }{ { name: "invalid allow list config in deployer allowlist", - config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, admins), + config: NewConfig(big.NewInt(3), admins, admins), expectedError: "cannot set address", }, } @@ -44,44 +44,44 @@ func TestEqualContractDeployerAllowListConfig(t *testing.T) { enableds := []common.Address{{2}} tests := []struct { name string - config precompile.StatefulPrecompileConfig - other precompile.StatefulPrecompileConfig + config config.Config + other config.Config expected bool }{ { name: "non-nil config and nil other", - config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), + config: NewConfig(big.NewInt(3), admins, enableds), other: nil, expected: false, }, { name: "different type", - config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), - other: precompile.NewNoopStatefulPrecompileConfig(), + config: NewConfig(big.NewInt(3), admins, enableds), + other: config.NewNoopStatefulPrecompileConfig(), expected: false, }, { name: "different admin", - config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), - other: NewContractDeployerAllowListConfig(big.NewInt(3), []common.Address{{3}}, enableds), + config: NewConfig(big.NewInt(3), admins, enableds), + other: NewConfig(big.NewInt(3), []common.Address{{3}}, enableds), expected: false, }, { name: "different enabled", - config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), - other: NewContractDeployerAllowListConfig(big.NewInt(3), admins, []common.Address{{3}}), + config: NewConfig(big.NewInt(3), admins, enableds), + other: NewConfig(big.NewInt(3), admins, []common.Address{{3}}), expected: false, }, { name: "different timestamp", - config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), - other: NewContractDeployerAllowListConfig(big.NewInt(4), admins, enableds), + config: NewConfig(big.NewInt(3), admins, enableds), + other: NewConfig(big.NewInt(4), admins, enableds), expected: false, }, { name: "same config", - config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), - other: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), + config: NewConfig(big.NewInt(3), admins, enableds), + other: NewConfig(big.NewInt(3), admins, enableds), expected: true, }, } diff --git a/precompile/contracts/deployerallowlist/contract.go b/precompile/contracts/deployerallowlist/contract.go new file mode 100644 index 0000000000..bb4b97e95b --- /dev/null +++ b/precompile/contracts/deployerallowlist/contract.go @@ -0,0 +1,26 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package deployerallowlist + +import ( + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" +) + +// Singleton StatefulPrecompiledContract for W/R access to the contract deployer allow list. +var ContractDeployerAllowListPrecompile contract.StatefulPrecompiledContract = allowlist.CreateAllowListPrecompile(ContractAddress) + +// GetContractDeployerAllowListStatus returns the role of [address] for the contract deployer +// allow list. +func GetContractDeployerAllowListStatus(stateDB contract.StateDB, address common.Address) allowlist.Role { + return allowlist.GetAllowListStatus(stateDB, ContractAddress, address) +} + +// SetContractDeployerAllowListStatus sets the permissions of [address] to [role] for the +// contract deployer allow list. +// assumes [role] has already been verified as valid. +func SetContractDeployerAllowListStatus(stateDB contract.StateDB, address common.Address, role allowlist.Role) { + allowlist.SetAllowListRole(stateDB, ContractAddress, address, role) +} diff --git a/precompile/contracts/deployerallowlist/contract_test.go b/precompile/contracts/deployerallowlist/contract_test.go new file mode 100644 index 0000000000..245fea438a --- /dev/null +++ b/precompile/contracts/deployerallowlist/contract_test.go @@ -0,0 +1,216 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package deployerallowlist + +import ( + "testing" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/subnet-evm/core/rawdb" + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +type precompileTest struct { + caller common.Address + input func() []byte + suppliedGas uint64 + readOnly bool + + config config.Config + + preCondition func(t *testing.T, state *state.StateDB) + assertState func(t *testing.T, state *state.StateDB) + + expectedRes []byte + expectedErr string +} + +func TestContractDeployerAllowListRun(t *testing.T) { + adminAddr := common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC") + noRoleAddr := common.HexToAddress("0xF60C45c607D0f41687c94C314d300f483661E13a") + + for name, test := range map[string]precompileTest{ + "set admin": { + caller: adminAddr, + input: func() []byte { + input, err := allowlist.PackModifyAllowList(noRoleAddr, allowlist.AdminRole) + require.NoError(t, err) + + return input + }, + suppliedGas: allowlist.ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetContractDeployerAllowListStatus(state, noRoleAddr) + require.Equal(t, allowlist.AdminRole, res) + }, + }, + "set deployer": { + caller: adminAddr, + input: func() []byte { + input, err := allowlist.PackModifyAllowList(noRoleAddr, allowlist.EnabledRole) + require.NoError(t, err) + + return input + }, + suppliedGas: allowlist.ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetContractDeployerAllowListStatus(state, noRoleAddr) + require.Equal(t, allowlist.EnabledRole, res) + }, + }, + "set no role": { + caller: adminAddr, + input: func() []byte { + input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.EnabledRole) + require.NoError(t, err) + + return input + }, + suppliedGas: allowlist.ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetContractDeployerAllowListStatus(state, adminAddr) + require.Equal(t, allowlist.EnabledRole, res) + }, + }, + "set no role from non-admin": { + caller: noRoleAddr, + input: func() []byte { + input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.EnabledRole) + require.NoError(t, err) + + return input + }, + suppliedGas: allowlist.ModifyAllowListGasCost, + readOnly: false, + expectedErr: allowlist.ErrCannotModifyAllowList.Error(), + }, + "set deployer from non-admin": { + caller: noRoleAddr, + input: func() []byte { + input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.EnabledRole) + require.NoError(t, err) + + return input + }, + suppliedGas: allowlist.ModifyAllowListGasCost, + readOnly: false, + expectedErr: allowlist.ErrCannotModifyAllowList.Error(), + }, + "set admin from non-admin": { + caller: noRoleAddr, + input: func() []byte { + input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.AdminRole) + require.NoError(t, err) + + return input + }, + suppliedGas: allowlist.ModifyAllowListGasCost, + readOnly: false, + expectedErr: allowlist.ErrCannotModifyAllowList.Error(), + }, + "set no role with readOnly enabled": { + caller: adminAddr, + input: func() []byte { + input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.EnabledRole) + require.NoError(t, err) + + return input + }, + suppliedGas: allowlist.ModifyAllowListGasCost, + readOnly: true, + expectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "set no role insufficient gas": { + caller: adminAddr, + input: func() []byte { + input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.EnabledRole) + require.NoError(t, err) + + return input + }, + suppliedGas: allowlist.ModifyAllowListGasCost - 1, + readOnly: false, + expectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "read allow list no role": { + caller: noRoleAddr, + input: func() []byte { + return allowlist.PackReadAllowList(noRoleAddr) + }, + suppliedGas: allowlist.ReadAllowListGasCost, + readOnly: false, + expectedRes: common.Hash(allowlist.EnabledRole).Bytes(), + assertState: nil, + }, + "read allow list admin role": { + caller: adminAddr, + input: func() []byte { + return allowlist.PackReadAllowList(noRoleAddr) + }, + suppliedGas: allowlist.ReadAllowListGasCost, + readOnly: false, + expectedRes: common.Hash(allowlist.EnabledRole).Bytes(), + assertState: nil, + }, + "read allow list with readOnly enabled": { + caller: adminAddr, + input: func() []byte { + return allowlist.PackReadAllowList(noRoleAddr) + }, + suppliedGas: allowlist.ReadAllowListGasCost, + readOnly: true, + expectedRes: common.Hash(allowlist.EnabledRole).Bytes(), + assertState: nil, + }, + "read allow list out of gas": { + caller: adminAddr, + input: func() []byte { + return allowlist.PackReadAllowList(noRoleAddr) + }, + suppliedGas: allowlist.ReadAllowListGasCost - 1, + readOnly: true, + expectedErr: vmerrs.ErrOutOfGas.Error(), + }, + } { + t.Run(name, func(t *testing.T) { + db := rawdb.NewMemoryDatabase() + state, err := state.New(common.Hash{}, state.NewDatabase(db), nil) + require.NoError(t, err) + + // Set up the state so that each address has the expected permissions at the start. + SetContractDeployerAllowListStatus(state, adminAddr, allowlist.AdminRole) + SetContractDeployerAllowListStatus(state, noRoleAddr, allowlist.EnabledRole) + require.Equal(t, allowlist.AdminRole, GetContractDeployerAllowListStatus(state, adminAddr)) + require.Equal(t, allowlist.EnabledRole, GetContractDeployerAllowListStatus(state, noRoleAddr)) + + blockContext := contract.NewMockBlockContext(common.Big0, 0) + accesibleState := contract.NewMockAccessibleState(state, blockContext, snow.DefaultContextTest()) + ret, remainingGas, err := ContractDeployerAllowListPrecompile.Run(accesibleState, test.caller, ContractAddress, test.input(), test.suppliedGas, test.readOnly) + if len(test.expectedErr) != 0 { + require.ErrorContains(t, err, test.expectedErr) + } else { + require.NoError(t, err) + } + + require.Equal(t, uint64(0), remainingGas) + require.Equal(t, test.expectedRes, ret) + + if test.assertState != nil { + test.assertState(t, state) + } + }) + } +} diff --git a/precompile/contracts/deployerallowlist/module.go b/precompile/contracts/deployerallowlist/module.go new file mode 100644 index 0000000000..47e4bc7269 --- /dev/null +++ b/precompile/contracts/deployerallowlist/module.go @@ -0,0 +1,49 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package deployerallowlist + +import ( + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ethereum/go-ethereum/common" +) + +var _ contract.Configurator = &configurator{} + +// ConfigKey is the key used in json config files to specify this precompile config. +// must be unique across all precompiles. +const ConfigKey = "contractDeployerAllowListConfig" + +var ContractAddress = common.HexToAddress("0x0200000000000000000000000000000000000000") + +var Module = modules.Module{ + ConfigKey: ConfigKey, + Address: ContractAddress, + Contract: ContractDeployerAllowListPrecompile, + Configurator: &configurator{}, +} + +type configurator struct{} + +func init() { + if err := modules.RegisterModule(Module); err != nil { + panic(err) + } +} + +func (*configurator) NewConfig() config.Config { + return &Config{} +} + +// Configure configures [state] with the initial state for the precompile. +func (c *configurator) Configure(_ contract.ChainConfig, cfg config.Config, state contract.StateDB, _ contract.BlockContext) error { + config, ok := cfg.(*Config) + if !ok { + return fmt.Errorf("incorrect config %T: %v", config, config) + } + return config.Config.Configure(state, ContractAddress) +} diff --git a/precompile/contracts/feemanager/config.go b/precompile/contracts/feemanager/config.go new file mode 100644 index 0000000000..9db36ffeee --- /dev/null +++ b/precompile/contracts/feemanager/config.go @@ -0,0 +1,79 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package feemanager + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/commontype" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ethereum/go-ethereum/common" +) + +var _ config.Config = &Config{} + +// Config implements the StatefulPrecompileConfig interface while adding in the +// FeeManager specific precompile config. +type Config struct { + allowlist.Config // Config for the fee config manager allow list + config.Upgrade + InitialFeeConfig *commontype.FeeConfig `json:"initialFeeConfig,omitempty"` // initial fee config to be immediately activated +} + +// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables +// FeeManager with the given [admins] and [enableds] as members of the allowlist with [initialConfig] as initial fee config if specified. +func NewConfig(blockTimestamp *big.Int, admins []common.Address, enableds []common.Address, initialConfig *commontype.FeeConfig) *Config { + return &Config{ + Config: allowlist.Config{ + AdminAddresses: admins, + EnabledAddresses: enableds, + }, + Upgrade: config.Upgrade{BlockTimestamp: blockTimestamp}, + InitialFeeConfig: initialConfig, + } +} + +// NewDisableConfig returns config for a network upgrade at [blockTimestamp] +// that disables FeeManager. +func NewDisableConfig(blockTimestamp *big.Int) *Config { + return &Config{ + Upgrade: config.Upgrade{ + BlockTimestamp: blockTimestamp, + Disable: true, + }, + } +} + +func (*Config) Key() string { return ConfigKey } + +// Equal returns true if [cfg] is a [*FeeManagerConfig] and it has been configured identical to [c]. +func (c *Config) Equal(cfg config.Config) bool { + // typecast before comparison + other, ok := (cfg).(*Config) + if !ok { + return false + } + eq := c.Upgrade.Equal(&other.Upgrade) && c.Config.Equal(&other.Config) + if !eq { + return false + } + + if c.InitialFeeConfig == nil { + return other.InitialFeeConfig == nil + } + + return c.InitialFeeConfig.Equal(other.InitialFeeConfig) +} + +func (c *Config) Verify() error { + if err := c.Config.Verify(); err != nil { + return err + } + if c.InitialFeeConfig == nil { + return nil + } + + return c.InitialFeeConfig.Verify() +} diff --git a/precompile/feemanager/config_test.go b/precompile/contracts/feemanager/config_test.go similarity index 65% rename from precompile/feemanager/config_test.go rename to precompile/contracts/feemanager/config_test.go index 1761c3109d..861c713023 100644 --- a/precompile/feemanager/config_test.go +++ b/precompile/contracts/feemanager/config_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/ava-labs/subnet-evm/commontype" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/config" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) @@ -30,17 +30,17 @@ func TestVerifyFeeManagerConfig(t *testing.T) { admins := []common.Address{{1}} tests := []struct { name string - config precompile.StatefulPrecompileConfig + config config.Config expectedError string }{ { name: "invalid allow list config in fee manager allowlist", - config: NewFeeManagerConfig(big.NewInt(3), admins, admins, nil), + config: NewConfig(big.NewInt(3), admins, admins, nil), expectedError: "cannot set address", }, { name: "invalid initial fee manager config", - config: NewFeeManagerConfig(big.NewInt(3), admins, nil, + config: NewConfig(big.NewInt(3), admins, nil, &commontype.FeeConfig{ GasLimit: big.NewInt(0), }), @@ -66,44 +66,44 @@ func TestEqualFeeManagerConfig(t *testing.T) { enableds := []common.Address{{2}} tests := []struct { name string - config precompile.StatefulPrecompileConfig - other precompile.StatefulPrecompileConfig + config config.Config + other config.Config expected bool }{ { name: "non-nil config and nil other", - config: NewFeeManagerConfig(big.NewInt(3), admins, enableds, nil), + config: NewConfig(big.NewInt(3), admins, enableds, nil), other: nil, expected: false, }, { name: "different type", - config: NewFeeManagerConfig(big.NewInt(3), admins, enableds, nil), - other: precompile.NewNoopStatefulPrecompileConfig(), + config: NewConfig(big.NewInt(3), admins, enableds, nil), + other: config.NewNoopStatefulPrecompileConfig(), expected: false, }, { name: "different timestamp", - config: NewFeeManagerConfig(big.NewInt(3), admins, nil, nil), - other: NewFeeManagerConfig(big.NewInt(4), admins, nil, nil), + config: NewConfig(big.NewInt(3), admins, nil, nil), + other: NewConfig(big.NewInt(4), admins, nil, nil), expected: false, }, { name: "different enabled", - config: NewFeeManagerConfig(big.NewInt(3), admins, nil, nil), - other: NewFeeManagerConfig(big.NewInt(3), admins, enableds, nil), + config: NewConfig(big.NewInt(3), admins, nil, nil), + other: NewConfig(big.NewInt(3), admins, enableds, nil), expected: false, }, { name: "non-nil initial config and nil initial config", - config: NewFeeManagerConfig(big.NewInt(3), admins, nil, &validFeeConfig), - other: NewFeeManagerConfig(big.NewInt(3), admins, nil, nil), + config: NewConfig(big.NewInt(3), admins, nil, &validFeeConfig), + other: NewConfig(big.NewInt(3), admins, nil, nil), expected: false, }, { name: "different initial config", - config: NewFeeManagerConfig(big.NewInt(3), admins, nil, &validFeeConfig), - other: NewFeeManagerConfig(big.NewInt(3), admins, nil, + config: NewConfig(big.NewInt(3), admins, nil, &validFeeConfig), + other: NewConfig(big.NewInt(3), admins, nil, func() *commontype.FeeConfig { c := validFeeConfig c.GasLimit = big.NewInt(123) @@ -113,8 +113,8 @@ func TestEqualFeeManagerConfig(t *testing.T) { }, { name: "same config", - config: NewFeeManagerConfig(big.NewInt(3), admins, nil, &validFeeConfig), - other: NewFeeManagerConfig(big.NewInt(3), admins, nil, &validFeeConfig), + config: NewConfig(big.NewInt(3), admins, nil, &validFeeConfig), + other: NewConfig(big.NewInt(3), admins, nil, &validFeeConfig), expected: true, }, } diff --git a/precompile/feemanager/contract.go b/precompile/contracts/feemanager/contract.go similarity index 68% rename from precompile/feemanager/contract.go rename to precompile/contracts/feemanager/contract.go index 5fdc3fb656..0d08338d36 100644 --- a/precompile/feemanager/contract.go +++ b/precompile/contracts/feemanager/contract.go @@ -9,8 +9,8 @@ import ( "math/big" "github.com/ava-labs/subnet-evm/commontype" - "github.com/ava-labs/subnet-evm/precompile" "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" ) @@ -33,35 +33,34 @@ const ( // [numFeeConfigField] fields in FeeConfig struct feeConfigInputLen = common.HashLength * numFeeConfigField - SetFeeConfigGasCost = precompile.WriteGasCostPerSlot * (numFeeConfigField + 1) // plus one for setting last changed at - GetFeeConfigGasCost = precompile.ReadGasCostPerSlot * numFeeConfigField - GetLastChangedAtGasCost = precompile.ReadGasCostPerSlot + SetFeeConfigGasCost = contract.WriteGasCostPerSlot * (numFeeConfigField + 1) // plus one for setting last changed at + GetFeeConfigGasCost = contract.ReadGasCostPerSlot * numFeeConfigField + GetLastChangedAtGasCost = contract.ReadGasCostPerSlot ) var ( - _ precompile.StatefulPrecompileConfig = &FeeManagerConfig{} // Singleton StatefulPrecompiledContract for setting fee configs by permissioned callers. - FeeManagerPrecompile precompile.StatefulPrecompiledContract = createFeeManagerPrecompile(precompile.FeeManagerAddress) + FeeManagerPrecompile contract.StatefulPrecompiledContract = createFeeManagerPrecompile() - setFeeConfigSignature = precompile.CalculateFunctionSelector("setFeeConfig(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)") - getFeeConfigSignature = precompile.CalculateFunctionSelector("getFeeConfig()") - getFeeConfigLastChangedAtSignature = precompile.CalculateFunctionSelector("getFeeConfigLastChangedAt()") + setFeeConfigSignature = contract.CalculateFunctionSelector("setFeeConfig(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)") + getFeeConfigSignature = contract.CalculateFunctionSelector("getFeeConfig()") + getFeeConfigLastChangedAtSignature = contract.CalculateFunctionSelector("getFeeConfigLastChangedAt()") feeConfigLastChangedAtKey = common.Hash{'l', 'c', 'a'} ErrCannotChangeFee = errors.New("non-enabled cannot change fee config") ) -// GetFeeManagerStatus returns the role of [address] for the FeeManager allowlist. -func GetFeeManagerStatus(stateDB precompile.StateDB, address common.Address) allowlist.AllowListRole { - return allowlist.GetAllowListStatus(stateDB, precompile.FeeManagerAddress, address) +// GetFeeManagerStatus returns the role of [address] for the fee config manager list. +func GetFeeManagerStatus(stateDB contract.StateDB, address common.Address) allowlist.Role { + return allowlist.GetAllowListStatus(stateDB, ContractAddress, address) } // SetFeeManagerStatus sets the permissions of [address] to [role] for the -// FeeManager allowlist. -func SetFeeManagerStatus(stateDB precompile.StateDB, address common.Address, role allowlist.AllowListRole) { - allowlist.SetAllowListRole(stateDB, precompile.FeeManagerAddress, address, role) +// fee config manager list. assumes [role] has already been verified as valid. +func SetFeeManagerStatus(stateDB contract.StateDB, address common.Address, role allowlist.Role) { + allowlist.SetAllowListRole(stateDB, ContractAddress, address, role) } // PackGetFeeConfigInput packs the getFeeConfig signature @@ -100,12 +99,12 @@ func packFeeConfigHelper(feeConfig commontype.FeeConfig, useSelector bool) ([]by if useSelector { res := make([]byte, len(setFeeConfigSignature)+feeConfigInputLen) - err := precompile.PackOrderedHashesWithSelector(res, setFeeConfigSignature, hashes) + err := contract.PackOrderedHashesWithSelector(res, setFeeConfigSignature, hashes) return res, err } res := make([]byte, len(hashes)*common.HashLength) - err := precompile.PackOrderedHashes(res, hashes) + err := contract.PackOrderedHashes(res, hashes) return res, err } @@ -118,7 +117,7 @@ func UnpackFeeConfigInput(input []byte) (commontype.FeeConfig, error) { feeConfig := commontype.FeeConfig{} for i := minFeeConfigFieldKey; i <= numFeeConfigField; i++ { listIndex := i - 1 - packedElement := precompile.PackedHash(input, listIndex) + packedElement := contract.PackedHash(input, listIndex) switch i { case gasLimitKey: feeConfig.GasLimit = new(big.Int).SetBytes(packedElement) @@ -145,10 +144,10 @@ func UnpackFeeConfigInput(input []byte) (commontype.FeeConfig, error) { } // GetStoredFeeConfig returns fee config from contract storage in given state -func GetStoredFeeConfig(stateDB precompile.StateDB) commontype.FeeConfig { +func GetStoredFeeConfig(stateDB contract.StateDB) commontype.FeeConfig { feeConfig := commontype.FeeConfig{} for i := minFeeConfigFieldKey; i <= numFeeConfigField; i++ { - val := stateDB.GetState(precompile.FeeManagerAddress, common.Hash{byte(i)}) + val := stateDB.GetState(ContractAddress, common.Hash{byte(i)}) switch i { case gasLimitKey: feeConfig.GasLimit = new(big.Int).Set(val.Big()) @@ -174,14 +173,14 @@ func GetStoredFeeConfig(stateDB precompile.StateDB) commontype.FeeConfig { return feeConfig } -func GetFeeConfigLastChangedAt(stateDB precompile.StateDB) *big.Int { - val := stateDB.GetState(precompile.FeeManagerAddress, feeConfigLastChangedAtKey) +func GetFeeConfigLastChangedAt(stateDB contract.StateDB) *big.Int { + val := stateDB.GetState(ContractAddress, feeConfigLastChangedAtKey) return val.Big() } // StoreFeeConfig stores given [feeConfig] and block number in the [blockContext] to the [stateDB]. // A validation on [feeConfig] is done before storing. -func StoreFeeConfig(stateDB precompile.StateDB, feeConfig commontype.FeeConfig, blockContext precompile.BlockContext) error { +func StoreFeeConfig(stateDB contract.StateDB, feeConfig commontype.FeeConfig, blockContext contract.BlockContext) error { if err := feeConfig.Verify(); err != nil { return fmt.Errorf("cannot verify fee config: %w", err) } @@ -209,22 +208,21 @@ func StoreFeeConfig(stateDB precompile.StateDB, feeConfig commontype.FeeConfig, // This should never encounter an unknown fee config key panic(fmt.Sprintf("unknown fee config key: %d", i)) } - stateDB.SetState(precompile.FeeManagerAddress, common.Hash{byte(i)}, input) + stateDB.SetState(ContractAddress, common.Hash{byte(i)}, input) } blockNumber := blockContext.Number() if blockNumber == nil { return fmt.Errorf("blockNumber cannot be nil") } - stateDB.SetState(precompile.FeeManagerAddress, feeConfigLastChangedAtKey, common.BigToHash(blockNumber)) - + stateDB.SetState(ContractAddress, feeConfigLastChangedAtKey, common.BigToHash(blockNumber)) return nil } // setFeeConfig checks if the caller has permissions to set the fee config. // The execution function parses [input] into FeeConfig structure and sets contract storage accordingly. -func setFeeConfig(accessibleState precompile.PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = precompile.DeductGas(suppliedGas, SetFeeConfigGasCost); err != nil { +func setFeeConfig(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, SetFeeConfigGasCost); err != nil { return nil, 0, err } @@ -238,8 +236,8 @@ func setFeeConfig(accessibleState precompile.PrecompileAccessibleState, caller c } stateDB := accessibleState.GetStateDB() - // Verify that the caller is in the allow list and therefore has the right to modify it - callerStatus := allowlist.GetAllowListStatus(stateDB, precompile.FeeManagerAddress, caller) + // Verify that the caller is in the allow list and therefore has the right to call this function. + callerStatus := GetFeeManagerStatus(stateDB, caller) if !callerStatus.IsEnabled() { return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotChangeFee, caller) } @@ -254,8 +252,8 @@ func setFeeConfig(accessibleState precompile.PrecompileAccessibleState, caller c // getFeeConfig returns the stored fee config as an output. // The execution function reads the contract state for the stored fee config and returns the output. -func getFeeConfig(accessibleState precompile.PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = precompile.DeductGas(suppliedGas, GetFeeConfigGasCost); err != nil { +func getFeeConfig(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, GetFeeConfigGasCost); err != nil { return nil, 0, err } @@ -272,8 +270,8 @@ func getFeeConfig(accessibleState precompile.PrecompileAccessibleState, caller c // getFeeConfigLastChangedAt returns the block number that fee config was last changed in. // The execution function reads the contract state for the stored block number and returns the output. -func getFeeConfigLastChangedAt(accessibleState precompile.PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = precompile.DeductGas(suppliedGas, GetLastChangedAtGasCost); err != nil { +func getFeeConfigLastChangedAt(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, GetLastChangedAtGasCost); err != nil { return nil, 0, err } @@ -285,17 +283,17 @@ func getFeeConfigLastChangedAt(accessibleState precompile.PrecompileAccessibleSt // createFeeManagerPrecompile returns a StatefulPrecompiledContract // with getters and setters for the chain's fee config. Access to the getters/setters -// is controlled by an allow list for [precompileAddr]. -func createFeeManagerPrecompile(precompileAddr common.Address) precompile.StatefulPrecompiledContract { - FeeManagerFunctions := allowlist.CreateAllowListFunctions(precompileAddr) +// is controlled by an allow list for ContractAddress. +func createFeeManagerPrecompile() contract.StatefulPrecompiledContract { + feeManagerFunctions := allowlist.CreateAllowListFunctions(ContractAddress) - setFeeConfigFunc := precompile.NewStatefulPrecompileFunction(setFeeConfigSignature, setFeeConfig) - getFeeConfigFunc := precompile.NewStatefulPrecompileFunction(getFeeConfigSignature, getFeeConfig) - getFeeConfigLastChangedAtFunc := precompile.NewStatefulPrecompileFunction(getFeeConfigLastChangedAtSignature, getFeeConfigLastChangedAt) + setFeeConfigFunc := contract.NewStatefulPrecompileFunction(setFeeConfigSignature, setFeeConfig) + getFeeConfigFunc := contract.NewStatefulPrecompileFunction(getFeeConfigSignature, getFeeConfig) + getFeeConfigLastChangedAtFunc := contract.NewStatefulPrecompileFunction(getFeeConfigLastChangedAtSignature, getFeeConfigLastChangedAt) - FeeManagerFunctions = append(FeeManagerFunctions, setFeeConfigFunc, getFeeConfigFunc, getFeeConfigLastChangedAtFunc) + feeManagerFunctions = append(feeManagerFunctions, setFeeConfigFunc, getFeeConfigFunc, getFeeConfigLastChangedAtFunc) // Construct the contract with no fallback function. - contract, err := precompile.NewStatefulPrecompileContract(nil, FeeManagerFunctions) + contract, err := contract.NewStatefulPrecompileContract(nil, feeManagerFunctions) // TODO Change this to be returned as an error after refactoring this precompile // to use the new precompile template. if err != nil { diff --git a/precompile/contracts/feemanager/contract_test.go b/precompile/contracts/feemanager/contract_test.go new file mode 100644 index 0000000000..698fce802d --- /dev/null +++ b/precompile/contracts/feemanager/contract_test.go @@ -0,0 +1,273 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package feemanager + +import ( + "math/big" + "testing" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/subnet-evm/commontype" + "github.com/ava-labs/subnet-evm/core/rawdb" + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +type precompileTest struct { + caller common.Address + input func() []byte + suppliedGas uint64 + readOnly bool + + config config.Config + + preCondition func(t *testing.T, state *state.StateDB) + assertState func(t *testing.T, state *state.StateDB) + + expectedRes []byte + expectedErr string +} + +var testFeeConfig = commontype.FeeConfig{ + GasLimit: big.NewInt(8_000_000), + TargetBlockRate: 2, // in seconds + + MinBaseFee: big.NewInt(25_000_000_000), + TargetGas: big.NewInt(15_000_000), + BaseFeeChangeDenominator: big.NewInt(36), + + MinBlockGasCost: big.NewInt(0), + MaxBlockGasCost: big.NewInt(1_000_000), + BlockGasCostStep: big.NewInt(200_000), +} + +func TestFeeManagerRun(t *testing.T) { + testBlockNumber := big.NewInt(7) + + adminAddr := common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC") + enabledAddr := common.HexToAddress("0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B") + noRoleAddr := common.HexToAddress("0xF60C45c607D0f41687c94C314d300f483661E13a") + + for name, test := range map[string]precompileTest{ + "set config from no role fails": { + caller: noRoleAddr, + input: func() []byte { + input, err := PackSetFeeConfig(testFeeConfig) + require.NoError(t, err) + + return input + }, + suppliedGas: SetFeeConfigGasCost, + readOnly: false, + expectedErr: ErrCannotChangeFee.Error(), + }, + "set config from enabled address": { + caller: enabledAddr, + input: func() []byte { + input, err := PackSetFeeConfig(testFeeConfig) + require.NoError(t, err) + + return input + }, + suppliedGas: SetFeeConfigGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + feeConfig := GetStoredFeeConfig(state) + require.Equal(t, testFeeConfig, feeConfig) + }, + }, + "set invalid config from enabled address": { + caller: enabledAddr, + input: func() []byte { + feeConfig := testFeeConfig + feeConfig.MinBlockGasCost = new(big.Int).Mul(feeConfig.MaxBlockGasCost, common.Big2) + input, err := PackSetFeeConfig(feeConfig) + require.NoError(t, err) + + return input + }, + suppliedGas: SetFeeConfigGasCost, + readOnly: false, + expectedRes: nil, + config: &Config{ + InitialFeeConfig: &testFeeConfig, + }, + expectedErr: "cannot be greater than maxBlockGasCost", + assertState: func(t *testing.T, state *state.StateDB) { + feeConfig := GetStoredFeeConfig(state) + require.Equal(t, testFeeConfig, feeConfig) + }, + }, + "set config from admin address": { + caller: adminAddr, + input: func() []byte { + input, err := PackSetFeeConfig(testFeeConfig) + require.NoError(t, err) + + return input + }, + suppliedGas: SetFeeConfigGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + feeConfig := GetStoredFeeConfig(state) + require.Equal(t, testFeeConfig, feeConfig) + lastChangedAt := GetFeeConfigLastChangedAt(state) + require.EqualValues(t, testBlockNumber, lastChangedAt) + }, + }, + "get fee config from non-enabled address": { + caller: noRoleAddr, + preCondition: func(t *testing.T, state *state.StateDB) { + err := StoreFeeConfig(state, testFeeConfig, contract.NewMockBlockContext(big.NewInt(6), 0)) + require.NoError(t, err) + }, + input: func() []byte { + return PackGetFeeConfigInput() + }, + suppliedGas: GetFeeConfigGasCost, + readOnly: true, + expectedRes: func() []byte { + res, err := PackFeeConfig(testFeeConfig) + require.NoError(t, err) + return res + }(), + assertState: func(t *testing.T, state *state.StateDB) { + feeConfig := GetStoredFeeConfig(state) + lastChangedAt := GetFeeConfigLastChangedAt(state) + require.Equal(t, testFeeConfig, feeConfig) + require.EqualValues(t, big.NewInt(6), lastChangedAt) + }, + }, + "get initial fee config": { + caller: noRoleAddr, + input: func() []byte { + return PackGetFeeConfigInput() + }, + suppliedGas: GetFeeConfigGasCost, + config: &Config{ + InitialFeeConfig: &testFeeConfig, + }, + readOnly: true, + expectedRes: func() []byte { + res, err := PackFeeConfig(testFeeConfig) + require.NoError(t, err) + return res + }(), + assertState: func(t *testing.T, state *state.StateDB) { + feeConfig := GetStoredFeeConfig(state) + lastChangedAt := GetFeeConfigLastChangedAt(state) + require.Equal(t, testFeeConfig, feeConfig) + require.EqualValues(t, testBlockNumber, lastChangedAt) + }, + }, + "get last changed at from non-enabled address": { + caller: noRoleAddr, + preCondition: func(t *testing.T, state *state.StateDB) { + err := StoreFeeConfig(state, testFeeConfig, contract.NewMockBlockContext(testBlockNumber, 0)) + require.NoError(t, err) + }, + input: func() []byte { + return PackGetLastChangedAtInput() + }, + suppliedGas: GetLastChangedAtGasCost, + readOnly: true, + expectedRes: common.BigToHash(testBlockNumber).Bytes(), + assertState: func(t *testing.T, state *state.StateDB) { + feeConfig := GetStoredFeeConfig(state) + lastChangedAt := GetFeeConfigLastChangedAt(state) + require.Equal(t, testFeeConfig, feeConfig) + require.Equal(t, testBlockNumber, lastChangedAt) + }, + }, + "readOnly setFeeConfig with noRole fails": { + caller: noRoleAddr, + input: func() []byte { + input, err := PackSetFeeConfig(testFeeConfig) + require.NoError(t, err) + + return input + }, + suppliedGas: SetFeeConfigGasCost, + readOnly: true, + expectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "readOnly setFeeConfig with allow role fails": { + caller: enabledAddr, + input: func() []byte { + input, err := PackSetFeeConfig(testFeeConfig) + require.NoError(t, err) + + return input + }, + suppliedGas: SetFeeConfigGasCost, + readOnly: true, + expectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "readOnly setFeeConfig with admin role fails": { + caller: adminAddr, + input: func() []byte { + input, err := PackSetFeeConfig(testFeeConfig) + require.NoError(t, err) + + return input + }, + suppliedGas: SetFeeConfigGasCost, + readOnly: true, + expectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "insufficient gas setFeeConfig from admin": { + caller: adminAddr, + input: func() []byte { + input, err := PackSetFeeConfig(testFeeConfig) + require.NoError(t, err) + + return input + }, + suppliedGas: SetFeeConfigGasCost - 1, + readOnly: false, + expectedErr: vmerrs.ErrOutOfGas.Error(), + }, + } { + t.Run(name, func(t *testing.T) { + db := rawdb.NewMemoryDatabase() + state, err := state.New(common.Hash{}, state.NewDatabase(db), nil) + require.NoError(t, err) + + // Set up the state so that each address has the expected permissions at the start. + SetFeeManagerStatus(state, adminAddr, allowlist.AdminRole) + SetFeeManagerStatus(state, enabledAddr, allowlist.EnabledRole) + require.Equal(t, allowlist.AdminRole, GetFeeManagerStatus(state, adminAddr)) + require.Equal(t, allowlist.EnabledRole, GetFeeManagerStatus(state, enabledAddr)) + + if test.preCondition != nil { + test.preCondition(t, state) + } + blockContext := contract.NewMockBlockContext(testBlockNumber, 0) + accesibleState := contract.NewMockAccessibleState(state, blockContext, snow.DefaultContextTest()) + if test.config != nil { + Module.Configure(nil, test.config, state, blockContext) + } + ret, remainingGas, err := FeeManagerPrecompile.Run(accesibleState, test.caller, ContractAddress, test.input(), test.suppliedGas, test.readOnly) + if len(test.expectedErr) != 0 { + require.ErrorContains(t, err, test.expectedErr) + } else { + require.NoError(t, err) + } + + require.Equal(t, uint64(0), remainingGas) + require.Equal(t, test.expectedRes, ret) + + if test.assertState != nil { + test.assertState(t, state) + } + }) + } +} diff --git a/precompile/contracts/feemanager/module.go b/precompile/contracts/feemanager/module.go new file mode 100644 index 0000000000..a725bb3f3d --- /dev/null +++ b/precompile/contracts/feemanager/module.go @@ -0,0 +1,61 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package feemanager + +import ( + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ethereum/go-ethereum/common" +) + +var _ contract.Configurator = &configurator{} + +// ConfigKey is the key used in json config files to specify this precompile config. +// must be unique across all precompiles. +const ConfigKey = "feeManagerConfig" + +var ContractAddress = common.HexToAddress("0x0200000000000000000000000000000000000003") + +var Module = modules.Module{ + ConfigKey: ConfigKey, + Address: ContractAddress, + Contract: FeeManagerPrecompile, + Configurator: &configurator{}, +} + +type configurator struct{} + +func init() { + if err := modules.RegisterModule(Module); err != nil { + panic(err) + } +} + +func (*configurator) NewConfig() config.Config { + return &Config{} +} + +// Configure configures [state] with the desired admins based on [configIface]. +func (*configurator) Configure(chainConfig contract.ChainConfig, cfg config.Config, state contract.StateDB, blockContext contract.BlockContext) error { + config, ok := cfg.(*Config) + if !ok { + return fmt.Errorf("incorrect config %T: %v", config, config) + } + // Store the initial fee config into the state when the fee manager activates. + if config.InitialFeeConfig != nil { + if err := StoreFeeConfig(state, *config.InitialFeeConfig, blockContext); err != nil { + // This should not happen since we already checked this config with Verify() + return fmt.Errorf("cannot configure given initial fee config: %w", err) + } + } else { + if err := StoreFeeConfig(state, chainConfig.GetFeeConfig(), blockContext); err != nil { + // This should not happen since we already checked the chain config in the genesis creation. + return fmt.Errorf("cannot configure fee config in chain config: %w", err) + } + } + return config.Config.Configure(state, ContractAddress) +} diff --git a/precompile/contracts/nativeminter/config.go b/precompile/contracts/nativeminter/config.go new file mode 100644 index 0000000000..21278e8de6 --- /dev/null +++ b/precompile/contracts/nativeminter/config.go @@ -0,0 +1,95 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package nativeminter + +import ( + "fmt" + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ava-labs/subnet-evm/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" +) + +var _ config.Config = &Config{} + +// Config implements the StatefulPrecompileConfig interface while adding in the +// ContractNativeMinter specific precompile config. +type Config struct { + allowlist.Config + config.Upgrade + InitialMint map[common.Address]*math.HexOrDecimal256 `json:"initialMint,omitempty"` // addresses to receive the initial mint mapped to the amount to mint +} + +// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables +// ContractNativeMinter with the given [admins] and [enableds] as members of the allowlist. Also mints balances according to [initialMint] when the upgrade activates. +func NewConfig(blockTimestamp *big.Int, admins []common.Address, enableds []common.Address, initialMint map[common.Address]*math.HexOrDecimal256) *Config { + return &Config{ + Config: allowlist.Config{ + AdminAddresses: admins, + EnabledAddresses: enableds, + }, + Upgrade: config.Upgrade{BlockTimestamp: blockTimestamp}, + InitialMint: initialMint, + } +} + +// NewDisableConfig returns config for a network upgrade at [blockTimestamp] +// that disables ContractNativeMinter. +func NewDisableConfig(blockTimestamp *big.Int) *Config { + return &Config{ + Upgrade: config.Upgrade{ + BlockTimestamp: blockTimestamp, + Disable: true, + }, + } +} +func (*Config) Key() string { return ConfigKey } + +// Equal returns true if [cfg] is a [*ContractNativeMinterConfig] and it has been configured identical to [c]. +func (c *Config) Equal(cfg config.Config) bool { + // typecast before comparison + other, ok := (cfg).(*Config) + if !ok { + return false + } + eq := c.Upgrade.Equal(&other.Upgrade) && c.Config.Equal(&other.Config) + if !eq { + return false + } + + if len(c.InitialMint) != len(other.InitialMint) { + return false + } + + for address, amount := range c.InitialMint { + val, ok := other.InitialMint[address] + if !ok { + return false + } + bigIntAmount := (*big.Int)(amount) + bigIntVal := (*big.Int)(val) + if !utils.BigNumEqual(bigIntAmount, bigIntVal) { + return false + } + } + + return true +} + +func (c *Config) Verify() error { + // ensure that all of the initial mint values in the map are non-nil positive values + for addr, amount := range c.InitialMint { + if amount == nil { + return fmt.Errorf("initial mint cannot contain nil amount for address %s", addr) + } + bigIntAmount := (*big.Int)(amount) + if bigIntAmount.Sign() < 1 { + return fmt.Errorf("initial mint cannot contain invalid amount %v for address %s", bigIntAmount, addr) + } + } + return c.Config.Verify() +} diff --git a/precompile/nativeminter/config_test.go b/precompile/contracts/nativeminter/config_test.go similarity index 67% rename from precompile/nativeminter/config_test.go rename to precompile/contracts/nativeminter/config_test.go index 9eb2441454..d1232b9cc3 100644 --- a/precompile/nativeminter/config_test.go +++ b/precompile/contracts/nativeminter/config_test.go @@ -7,7 +7,7 @@ import ( "math/big" "testing" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/config" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" "github.com/stretchr/testify/require" @@ -18,27 +18,27 @@ func TestVerifyContractNativeMinterConfig(t *testing.T) { enableds := []common.Address{{2}} tests := []struct { name string - config precompile.StatefulPrecompileConfig + config config.Config expectedError string }{ { name: "invalid allow list config in native minter allowlist", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, admins, nil), + config: NewConfig(big.NewInt(3), admins, admins, nil), expectedError: "cannot set address", }, { name: "duplicate admins in config in native minter allowlist", - config: NewContractNativeMinterConfig(big.NewInt(3), append(admins, admins[0]), enableds, nil), + config: NewConfig(big.NewInt(3), append(admins, admins[0]), enableds, nil), expectedError: "duplicate address", }, { name: "duplicate enableds in config in native minter allowlist", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, append(enableds, enableds[0]), nil), + config: NewConfig(big.NewInt(3), admins, append(enableds, enableds[0]), nil), expectedError: "duplicate address", }, { name: "nil amount in native minter config", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, + config: NewConfig(big.NewInt(3), admins, nil, map[common.Address]*math.HexOrDecimal256{ common.HexToAddress("0x01"): math.NewHexOrDecimal256(123), common.HexToAddress("0x02"): nil, @@ -47,7 +47,7 @@ func TestVerifyContractNativeMinterConfig(t *testing.T) { }, { name: "negative amount in native minter config", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, + config: NewConfig(big.NewInt(3), admins, nil, map[common.Address]*math.HexOrDecimal256{ common.HexToAddress("0x01"): math.NewHexOrDecimal256(123), common.HexToAddress("0x02"): math.NewHexOrDecimal256(-1), @@ -74,41 +74,41 @@ func TestEqualContractNativeMinterConfig(t *testing.T) { enableds := []common.Address{{2}} tests := []struct { name string - config precompile.StatefulPrecompileConfig - other precompile.StatefulPrecompileConfig + config config.Config + other config.Config expected bool }{ { name: "non-nil config and nil other", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, enableds, nil), + config: NewConfig(big.NewInt(3), admins, enableds, nil), other: nil, expected: false, }, { name: "different type", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, enableds, nil), - other: precompile.NewNoopStatefulPrecompileConfig(), + config: NewConfig(big.NewInt(3), admins, enableds, nil), + other: config.NewNoopStatefulPrecompileConfig(), expected: false, }, { name: "different timestamps", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, nil), - other: NewContractNativeMinterConfig(big.NewInt(4), admins, nil, nil), + config: NewConfig(big.NewInt(3), admins, nil, nil), + other: NewConfig(big.NewInt(4), admins, nil, nil), expected: false, }, { name: "different enabled", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, nil), - other: NewContractNativeMinterConfig(big.NewInt(3), admins, enableds, nil), + config: NewConfig(big.NewInt(3), admins, nil, nil), + other: NewConfig(big.NewInt(3), admins, enableds, nil), expected: false, }, { name: "different initial mint amounts", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, + config: NewConfig(big.NewInt(3), admins, nil, map[common.Address]*math.HexOrDecimal256{ common.HexToAddress("0x01"): math.NewHexOrDecimal256(1), }), - other: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, + other: NewConfig(big.NewInt(3), admins, nil, map[common.Address]*math.HexOrDecimal256{ common.HexToAddress("0x01"): math.NewHexOrDecimal256(2), }), @@ -116,11 +116,11 @@ func TestEqualContractNativeMinterConfig(t *testing.T) { }, { name: "different initial mint addresses", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, + config: NewConfig(big.NewInt(3), admins, nil, map[common.Address]*math.HexOrDecimal256{ common.HexToAddress("0x01"): math.NewHexOrDecimal256(1), }), - other: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, + other: NewConfig(big.NewInt(3), admins, nil, map[common.Address]*math.HexOrDecimal256{ common.HexToAddress("0x02"): math.NewHexOrDecimal256(1), }), @@ -128,11 +128,11 @@ func TestEqualContractNativeMinterConfig(t *testing.T) { }, { name: "same config", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, + config: NewConfig(big.NewInt(3), admins, nil, map[common.Address]*math.HexOrDecimal256{ common.HexToAddress("0x01"): math.NewHexOrDecimal256(1), }), - other: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, + other: NewConfig(big.NewInt(3), admins, nil, map[common.Address]*math.HexOrDecimal256{ common.HexToAddress("0x01"): math.NewHexOrDecimal256(1), }), diff --git a/precompile/nativeminter/contract.go b/precompile/contracts/nativeminter/contract.go similarity index 60% rename from precompile/nativeminter/contract.go rename to precompile/contracts/nativeminter/contract.go index 7098c252cb..63d6a624da 100644 --- a/precompile/nativeminter/contract.go +++ b/precompile/contracts/nativeminter/contract.go @@ -8,8 +8,8 @@ import ( "fmt" "math/big" - "github.com/ava-labs/subnet-evm/precompile" "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" ) @@ -25,29 +25,29 @@ const ( var ( // Singleton StatefulPrecompiledContract for minting native assets by permissioned callers. - ContractNativeMinterPrecompile precompile.StatefulPrecompiledContract = createNativeMinterPrecompile(precompile.ContractNativeMinterAddress) + ContractNativeMinterPrecompile contract.StatefulPrecompiledContract = createNativeMinterPrecompile() - mintSignature = precompile.CalculateFunctionSelector("mintNativeCoin(address,uint256)") // address, amount + mintSignature = contract.CalculateFunctionSelector("mintNativeCoin(address,uint256)") // address, amount ErrCannotMint = errors.New("non-enabled cannot mint") ) // GetContractNativeMinterStatus returns the role of [address] for the minter list. -func GetContractNativeMinterStatus(stateDB precompile.StateDB, address common.Address) allowlist.AllowListRole { - return allowlist.GetAllowListStatus(stateDB, precompile.ContractNativeMinterAddress, address) +func GetContractNativeMinterStatus(stateDB contract.StateDB, address common.Address) allowlist.Role { + return allowlist.GetAllowListStatus(stateDB, ContractAddress, address) } // SetContractNativeMinterStatus sets the permissions of [address] to [role] for the // minter list. assumes [role] has already been verified as valid. -func SetContractNativeMinterStatus(stateDB precompile.StateDB, address common.Address, role allowlist.AllowListRole) { - allowlist.SetAllowListRole(stateDB, precompile.ContractNativeMinterAddress, address, role) +func SetContractNativeMinterStatus(stateDB contract.StateDB, address common.Address, role allowlist.Role) { + allowlist.SetAllowListRole(stateDB, ContractAddress, address, role) } // PackMintInput packs [address] and [amount] into the appropriate arguments for minting operation. // Assumes that [amount] can be represented by 32 bytes. func PackMintInput(address common.Address, amount *big.Int) ([]byte, error) { // function selector (4 bytes) + input(hash for address + hash for amount) - res := make([]byte, precompile.SelectorLen+mintInputLen) - err := precompile.PackOrderedHashesWithSelector(res, mintSignature, []common.Hash{ + res := make([]byte, contract.SelectorLen+mintInputLen) + err := contract.PackOrderedHashesWithSelector(res, mintSignature, []common.Hash{ address.Hash(), common.BigToHash(amount), }) @@ -61,15 +61,15 @@ func UnpackMintInput(input []byte) (common.Address, *big.Int, error) { if len(input) != mintInputLen { return common.Address{}, nil, fmt.Errorf("invalid input length for minting: %d", len(input)) } - to := common.BytesToAddress(precompile.PackedHash(input, mintInputAddressSlot)) - assetAmount := new(big.Int).SetBytes(precompile.PackedHash(input, mintInputAmountSlot)) + to := common.BytesToAddress(contract.PackedHash(input, mintInputAddressSlot)) + assetAmount := new(big.Int).SetBytes(contract.PackedHash(input, mintInputAmountSlot)) return to, assetAmount, nil } // mintNativeCoin checks if the caller is permissioned for minting operation. // The execution function parses the [input] into native coin amount and receiver address. -func mintNativeCoin(accessibleState precompile.PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = precompile.DeductGas(suppliedGas, MintGasCost); err != nil { +func mintNativeCoin(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, MintGasCost); err != nil { return nil, 0, err } @@ -83,8 +83,8 @@ func mintNativeCoin(accessibleState precompile.PrecompileAccessibleState, caller } stateDB := accessibleState.GetStateDB() - // Verify that the caller is in the allow list - callerStatus := allowlist.GetAllowListStatus(stateDB, precompile.ContractNativeMinterAddress, caller) + // Verify that the caller is in the allow list and therefore has the right to call this function. + callerStatus := allowlist.GetAllowListStatus(stateDB, ContractAddress, caller) if !callerStatus.IsEnabled() { return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotMint, caller) } @@ -101,14 +101,14 @@ func mintNativeCoin(accessibleState precompile.PrecompileAccessibleState, caller // createNativeMinterPrecompile returns a StatefulPrecompiledContract for native coin minting. The precompile // is accessed controlled by an allow list at [precompileAddr]. -func createNativeMinterPrecompile(precompileAddr common.Address) precompile.StatefulPrecompiledContract { - enabledFuncs := allowlist.CreateAllowListFunctions(precompileAddr) +func createNativeMinterPrecompile() contract.StatefulPrecompiledContract { + enabledFuncs := allowlist.CreateAllowListFunctions(ContractAddress) - mintFunc := precompile.NewStatefulPrecompileFunction(mintSignature, mintNativeCoin) + mintFunc := contract.NewStatefulPrecompileFunction(mintSignature, mintNativeCoin) enabledFuncs = append(enabledFuncs, mintFunc) // Construct the contract with no fallback function. - contract, err := precompile.NewStatefulPrecompileContract(nil, enabledFuncs) + contract, err := contract.NewStatefulPrecompileContract(nil, enabledFuncs) // TODO: Change this to be returned as an error after refactoring this precompile // to use the new precompile template. if err != nil { diff --git a/precompile/contracts/nativeminter/contract_test.go b/precompile/contracts/nativeminter/contract_test.go new file mode 100644 index 0000000000..4eb3202495 --- /dev/null +++ b/precompile/contracts/nativeminter/contract_test.go @@ -0,0 +1,193 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package nativeminter + +import ( + "testing" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/subnet-evm/core/rawdb" + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/params" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/stretchr/testify/require" +) + +type precompileTest struct { + caller common.Address + input func() []byte + suppliedGas uint64 + readOnly bool + + config config.Config + + preCondition func(t *testing.T, state *state.StateDB) + assertState func(t *testing.T, state *state.StateDB) + + expectedRes []byte + expectedErr string +} + +func TestContractNativeMinterRun(t *testing.T) { + adminAddr := common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC") + enabledAddr := common.HexToAddress("0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B") + noRoleAddr := common.HexToAddress("0xF60C45c607D0f41687c94C314d300f483661E13a") + + for name, test := range map[string]precompileTest{ + "mint funds from no role fails": { + caller: noRoleAddr, + input: func() []byte { + input, err := PackMintInput(noRoleAddr, common.Big1) + require.NoError(t, err) + + return input + }, + suppliedGas: MintGasCost, + readOnly: false, + expectedErr: ErrCannotMint.Error(), + }, + "mint funds from enabled address": { + caller: enabledAddr, + input: func() []byte { + input, err := PackMintInput(enabledAddr, common.Big1) + require.NoError(t, err) + + return input + }, + suppliedGas: MintGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + require.Equal(t, common.Big1, state.GetBalance(enabledAddr), "expected minted funds") + }, + }, + "initial mint funds": { + caller: enabledAddr, + config: &Config{ + InitialMint: map[common.Address]*math.HexOrDecimal256{ + enabledAddr: math.NewHexOrDecimal256(2), + }, + }, + assertState: func(t *testing.T, state *state.StateDB) { + require.Equal(t, common.Big2, state.GetBalance(enabledAddr), "expected minted funds") + }, + }, + "mint funds from admin address": { + caller: adminAddr, + input: func() []byte { + input, err := PackMintInput(adminAddr, common.Big1) + require.NoError(t, err) + + return input + }, + suppliedGas: MintGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + require.Equal(t, common.Big1, state.GetBalance(adminAddr), "expected minted funds") + }, + }, + "mint max big funds": { + caller: adminAddr, + input: func() []byte { + input, err := PackMintInput(adminAddr, math.MaxBig256) + require.NoError(t, err) + + return input + }, + suppliedGas: MintGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + require.Equal(t, math.MaxBig256, state.GetBalance(adminAddr), "expected minted funds") + }, + }, + "readOnly mint with noRole fails": { + caller: noRoleAddr, + input: func() []byte { + input, err := PackMintInput(adminAddr, common.Big1) + require.NoError(t, err) + + return input + }, + suppliedGas: MintGasCost, + readOnly: true, + expectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "readOnly mint with allow role fails": { + caller: enabledAddr, + input: func() []byte { + input, err := PackMintInput(enabledAddr, common.Big1) + require.NoError(t, err) + + return input + }, + suppliedGas: MintGasCost, + readOnly: true, + expectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "readOnly mint with admin role fails": { + caller: adminAddr, + input: func() []byte { + input, err := PackMintInput(adminAddr, common.Big1) + require.NoError(t, err) + + return input + }, + suppliedGas: MintGasCost, + readOnly: true, + expectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "insufficient gas mint from admin": { + caller: adminAddr, + input: func() []byte { + input, err := PackMintInput(enabledAddr, common.Big1) + require.NoError(t, err) + + return input + }, + suppliedGas: MintGasCost - 1, + readOnly: false, + expectedErr: vmerrs.ErrOutOfGas.Error(), + }, + } { + t.Run(name, func(t *testing.T) { + db := rawdb.NewMemoryDatabase() + state, err := state.New(common.Hash{}, state.NewDatabase(db), nil) + require.NoError(t, err) + + // Set up the state so that each address has the expected permissions at the start. + SetContractNativeMinterStatus(state, adminAddr, allowlist.AdminRole) + SetContractNativeMinterStatus(state, enabledAddr, allowlist.EnabledRole) + require.Equal(t, allowlist.AdminRole, GetContractNativeMinterStatus(state, adminAddr)) + require.Equal(t, allowlist.EnabledRole, GetContractNativeMinterStatus(state, enabledAddr)) + + blockContext := contract.NewMockBlockContext(common.Big0, 0) + accesibleState := contract.NewMockAccessibleState(state, blockContext, snow.DefaultContextTest()) + if test.config != nil { + Module.Configure(params.TestChainConfig, test.config, state, blockContext) + } + if test.input != nil { + ret, remainingGas, err := ContractNativeMinterPrecompile.Run(accesibleState, test.caller, ContractAddress, test.input(), test.suppliedGas, test.readOnly) + if len(test.expectedErr) != 0 { + require.ErrorContains(t, err, test.expectedErr) + } else { + require.NoError(t, err) + } + + require.Equal(t, uint64(0), remainingGas) + require.Equal(t, test.expectedRes, ret) + } + + if test.assertState != nil { + test.assertState(t, state) + } + }) + } +} diff --git a/precompile/contracts/nativeminter/module.go b/precompile/contracts/nativeminter/module.go new file mode 100644 index 0000000000..76987071cd --- /dev/null +++ b/precompile/contracts/nativeminter/module.go @@ -0,0 +1,57 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package nativeminter + +import ( + "fmt" + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ethereum/go-ethereum/common" +) + +var _ contract.Configurator = &configurator{} + +// ConfigKey is the key used in json config files to specify this precompile config. +// must be unique across all precompiles. +const ConfigKey = "contractNativeMinterConfig" + +var ContractAddress = common.HexToAddress("0x0200000000000000000000000000000000000001") + +var Module = modules.Module{ + ConfigKey: ConfigKey, + Address: ContractAddress, + Contract: ContractNativeMinterPrecompile, + Configurator: &configurator{}, +} + +type configurator struct{} + +func init() { + if err := modules.RegisterModule(Module); err != nil { + panic(err) + } +} + +func (*configurator) NewConfig() config.Config { + return &Config{} +} + +// Configure configures [state] with the desired admins based on [cfg]. +func (*configurator) Configure(_ contract.ChainConfig, cfg config.Config, state contract.StateDB, _ contract.BlockContext) error { + config, ok := cfg.(*Config) + if !ok { + return fmt.Errorf("incorrect config %T: %v", config, config) + } + for to, amount := range config.InitialMint { + if amount != nil { + bigIntAmount := (*big.Int)(amount) + state.AddBalance(to, bigIntAmount) + } + } + + return config.Config.Configure(state, ContractAddress) +} diff --git a/precompile/contracts/rewardmanager/config.go b/precompile/contracts/rewardmanager/config.go new file mode 100644 index 0000000000..80adc93429 --- /dev/null +++ b/precompile/contracts/rewardmanager/config.go @@ -0,0 +1,118 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Code generated +// This file is a generated precompile contract with stubbed abstract functions. + +package rewardmanager + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" +) + +var _ config.Config = &Config{} + +type InitialRewardConfig struct { + AllowFeeRecipients bool `json:"allowFeeRecipients"` + RewardAddress common.Address `json:"rewardAddress,omitempty"` +} + +func (i *InitialRewardConfig) Equal(other *InitialRewardConfig) bool { + if other == nil { + return false + } + + return i.AllowFeeRecipients == other.AllowFeeRecipients && i.RewardAddress == other.RewardAddress +} + +func (i *InitialRewardConfig) Verify() error { + switch { + case i.AllowFeeRecipients && i.RewardAddress != (common.Address{}): + return ErrCannotEnableBothRewards + default: + return nil + } +} + +func (i *InitialRewardConfig) Configure(state contract.StateDB) error { + // enable allow fee recipients + if i.AllowFeeRecipients { + EnableAllowFeeRecipients(state) + } else if i.RewardAddress == (common.Address{}) { + // if reward address is empty and allow fee recipients is false + // then disable rewards + DisableFeeRewards(state) + } else { + // set reward address + return StoreRewardAddress(state, i.RewardAddress) + } + return nil +} + +// Config implements the StatefulPrecompileConfig interface while adding in the +// RewardManager specific precompile config. +type Config struct { + allowlist.Config + config.Upgrade + InitialRewardConfig *InitialRewardConfig `json:"initialRewardConfig,omitempty"` +} + +// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables +// RewardManager with the given [admins] and [enableds] as members of the allowlist with [initialConfig] as initial rewards config if specified. +func NewConfig(blockTimestamp *big.Int, admins []common.Address, enableds []common.Address, initialConfig *InitialRewardConfig) *Config { + return &Config{ + Config: allowlist.Config{ + AdminAddresses: admins, + EnabledAddresses: enableds, + }, + Upgrade: config.Upgrade{BlockTimestamp: blockTimestamp}, + InitialRewardConfig: initialConfig, + } +} + +// NewDisableConfig returns config for a network upgrade at [blockTimestamp] +// that disables RewardManager. +func NewDisableConfig(blockTimestamp *big.Int) *Config { + return &Config{ + Upgrade: config.Upgrade{ + BlockTimestamp: blockTimestamp, + Disable: true, + }, + } +} + +func (*Config) Key() string { return ConfigKey } + +func (c *Config) Verify() error { + if c.InitialRewardConfig != nil { + if err := c.InitialRewardConfig.Verify(); err != nil { + return err + } + } + return c.Config.Verify() +} + +// Equal returns true if [cfg] is a [*RewardManagerConfig] and it has been configured identical to [c]. +func (c *Config) Equal(cfg config.Config) bool { + // typecast before comparison + other, ok := (cfg).(*Config) + if !ok { + return false + } + + if c.InitialRewardConfig != nil { + if other.InitialRewardConfig == nil { + return false + } + if !c.InitialRewardConfig.Equal(other.InitialRewardConfig) { + return false + } + } + + return c.Upgrade.Equal(&other.Upgrade) && c.Config.Equal(&other.Config) +} diff --git a/precompile/rewardmanager/config_test.go b/precompile/contracts/rewardmanager/config_test.go similarity index 61% rename from precompile/rewardmanager/config_test.go rename to precompile/contracts/rewardmanager/config_test.go index 7e5fb5c19f..669ec1cb64 100644 --- a/precompile/rewardmanager/config_test.go +++ b/precompile/contracts/rewardmanager/config_test.go @@ -7,7 +7,7 @@ import ( "math/big" "testing" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/config" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) @@ -17,17 +17,17 @@ func TestVerifyRewardManagerConfig(t *testing.T) { enableds := []common.Address{{2}} tests := []struct { name string - config precompile.StatefulPrecompileConfig + config config.Config expectedError string }{ { name: "duplicate enableds in config in reward manager allowlist", - config: NewRewardManagerConfig(big.NewInt(3), admins, append(enableds, enableds[0]), nil), + config: NewConfig(big.NewInt(3), admins, append(enableds, enableds[0]), nil), expectedError: "duplicate address", }, { name: "both reward mechanisms should not be activated at the same time in reward manager", - config: NewRewardManagerConfig(big.NewInt(3), admins, enableds, &InitialRewardConfig{ + config: NewConfig(big.NewInt(3), admins, enableds, &InitialRewardConfig{ AllowFeeRecipients: true, RewardAddress: common.HexToAddress("0x01"), }), @@ -53,48 +53,48 @@ func TestEqualRewardManagerConfig(t *testing.T) { enableds := []common.Address{{2}} tests := []struct { name string - config precompile.StatefulPrecompileConfig - other precompile.StatefulPrecompileConfig + config config.Config + other config.Config expected bool }{ { name: "non-nil config and nil other", - config: NewRewardManagerConfig(big.NewInt(3), admins, enableds, nil), + config: NewConfig(big.NewInt(3), admins, enableds, nil), other: nil, expected: false, }, { name: "different type", - config: NewRewardManagerConfig(big.NewInt(3), admins, enableds, nil), - other: precompile.NewNoopStatefulPrecompileConfig(), + config: NewConfig(big.NewInt(3), admins, enableds, nil), + other: config.NewNoopStatefulPrecompileConfig(), expected: false, }, { name: "different timestamp", - config: NewRewardManagerConfig(big.NewInt(3), admins, nil, nil), - other: NewRewardManagerConfig(big.NewInt(4), admins, nil, nil), + config: NewConfig(big.NewInt(3), admins, nil, nil), + other: NewConfig(big.NewInt(4), admins, nil, nil), expected: false, }, { name: "different enabled", - config: NewRewardManagerConfig(big.NewInt(3), admins, nil, nil), - other: NewRewardManagerConfig(big.NewInt(3), admins, enableds, nil), + config: NewConfig(big.NewInt(3), admins, nil, nil), + other: NewConfig(big.NewInt(3), admins, enableds, nil), expected: false, }, { name: "non-nil initial config and nil initial config", - config: NewRewardManagerConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ + config: NewConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ AllowFeeRecipients: true, }), - other: NewRewardManagerConfig(big.NewInt(3), admins, nil, nil), + other: NewConfig(big.NewInt(3), admins, nil, nil), expected: false, }, { name: "different initial config", - config: NewRewardManagerConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ + config: NewConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ RewardAddress: common.HexToAddress("0x01"), }), - other: NewRewardManagerConfig(big.NewInt(3), admins, nil, + other: NewConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ RewardAddress: common.HexToAddress("0x02"), }), @@ -102,10 +102,10 @@ func TestEqualRewardManagerConfig(t *testing.T) { }, { name: "same config", - config: NewRewardManagerConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ + config: NewConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ RewardAddress: common.HexToAddress("0x01"), }), - other: NewRewardManagerConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ + other: NewConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ RewardAddress: common.HexToAddress("0x01"), }), expected: true, diff --git a/precompile/rewardmanager/contract.abi b/precompile/contracts/rewardmanager/contract.abi similarity index 100% rename from precompile/rewardmanager/contract.abi rename to precompile/contracts/rewardmanager/contract.abi diff --git a/precompile/rewardmanager/contract.go b/precompile/contracts/rewardmanager/contract.go similarity index 67% rename from precompile/rewardmanager/contract.go rename to precompile/contracts/rewardmanager/contract.go index 0c95aee847..8d28e0815d 100644 --- a/precompile/rewardmanager/contract.go +++ b/precompile/contracts/rewardmanager/contract.go @@ -7,27 +7,25 @@ package rewardmanager import ( + _ "embed" "errors" "fmt" - "strings" "github.com/ava-labs/subnet-evm/accounts/abi" "github.com/ava-labs/subnet-evm/constants" - "github.com/ava-labs/subnet-evm/precompile" "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" "github.com/ava-labs/subnet-evm/vmerrs" - _ "embed" - "github.com/ethereum/go-ethereum/common" ) const ( - AllowFeeRecipientsGasCost uint64 = (precompile.WriteGasCostPerSlot) + allowlist.ReadAllowListGasCost // write 1 slot + read allow list + AllowFeeRecipientsGasCost uint64 = (contract.WriteGasCostPerSlot) + allowlist.ReadAllowListGasCost // write 1 slot + read allow list AreFeeRecipientsAllowedGasCost uint64 = allowlist.ReadAllowListGasCost CurrentRewardAddressGasCost uint64 = allowlist.ReadAllowListGasCost - DisableRewardsGasCost uint64 = (precompile.WriteGasCostPerSlot) + allowlist.ReadAllowListGasCost // write 1 slot + read allow list - SetRewardAddressGasCost uint64 = (precompile.WriteGasCostPerSlot) + allowlist.ReadAllowListGasCost // write 1 slot + read allow list + DisableRewardsGasCost uint64 = (contract.WriteGasCostPerSlot) + allowlist.ReadAllowListGasCost // write 1 slot + read allow list + SetRewardAddressGasCost uint64 = (contract.WriteGasCostPerSlot) + allowlist.ReadAllowListGasCost // write 1 slot + read allow list ) // Singleton StatefulPrecompiledContract and signatures. @@ -45,34 +43,22 @@ var ( //go:embed contract.abi RewardManagerRawABI string - RewardManagerABI abi.ABI // will be initialized by init function - RewardManagerPrecompile precompile.StatefulPrecompiledContract // will be initialized by init function + RewardManagerABI = contract.ParseABI(RewardManagerRawABI) + RewardManagerPrecompile = createRewardManagerPrecompile() // will be initialized by init function rewardAddressStorageKey = common.Hash{'r', 'a', 's', 'k'} allowFeeRecipientsAddressValue = common.Hash{'a', 'f', 'r', 'a', 'v'} ) -func init() { - parsed, err := abi.JSON(strings.NewReader(RewardManagerRawABI)) - if err != nil { - panic(err) - } - RewardManagerABI = parsed - RewardManagerPrecompile, err = createRewardManagerPrecompile(precompile.RewardManagerAddress) - if err != nil { - panic(err) - } -} - // GetRewardManagerAllowListStatus returns the role of [address] for the RewardManager list. -func GetRewardManagerAllowListStatus(stateDB precompile.StateDB, address common.Address) allowlist.AllowListRole { - return allowlist.GetAllowListStatus(stateDB, precompile.RewardManagerAddress, address) +func GetRewardManagerAllowListStatus(stateDB contract.StateDB, address common.Address) allowlist.Role { + return allowlist.GetAllowListStatus(stateDB, ContractAddress, address) } // SetRewardManagerAllowListStatus sets the permissions of [address] to [role] for the // RewardManager list. Assumes [role] has already been verified as valid. -func SetRewardManagerAllowListStatus(stateDB precompile.StateDB, address common.Address, role allowlist.AllowListRole) { - allowlist.SetAllowListRole(stateDB, precompile.RewardManagerAddress, address, role) +func SetRewardManagerAllowListStatus(stateDB contract.StateDB, address common.Address, role allowlist.Role) { + allowlist.SetAllowListRole(stateDB, ContractAddress, address, role) } // PackAllowFeeRecipients packs the function selector (first 4 func signature bytes). @@ -82,17 +68,17 @@ func PackAllowFeeRecipients() ([]byte, error) { } // EnableAllowFeeRecipients enables fee recipients. -func EnableAllowFeeRecipients(stateDB precompile.StateDB) { - stateDB.SetState(precompile.RewardManagerAddress, rewardAddressStorageKey, allowFeeRecipientsAddressValue) +func EnableAllowFeeRecipients(stateDB contract.StateDB) { + stateDB.SetState(ContractAddress, rewardAddressStorageKey, allowFeeRecipientsAddressValue) } // DisableRewardAddress disables rewards and burns them by sending to Blackhole Address. -func DisableFeeRewards(stateDB precompile.StateDB) { - stateDB.SetState(precompile.RewardManagerAddress, rewardAddressStorageKey, constants.BlackholeAddr.Hash()) +func DisableFeeRewards(stateDB contract.StateDB) { + stateDB.SetState(ContractAddress, rewardAddressStorageKey, constants.BlackholeAddr.Hash()) } -func allowFeeRecipients(accessibleState precompile.PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = precompile.DeductGas(suppliedGas, AllowFeeRecipientsGasCost); err != nil { +func allowFeeRecipients(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, AllowFeeRecipientsGasCost); err != nil { return nil, 0, err } if readOnly { @@ -104,8 +90,8 @@ func allowFeeRecipients(accessibleState precompile.PrecompileAccessibleState, ca // This part of the code restricts the function to be called only by enabled/admin addresses in the allow list. // You can modify/delete this code if you don't want this function to be restricted by the allow list. stateDB := accessibleState.GetStateDB() - // Verify that the caller is in the allow list and therefore has the right to modify it - callerStatus := allowlist.GetAllowListStatus(stateDB, precompile.RewardManagerAddress, caller) + // Verify that the caller is in the allow list and therefore has the right to call this function. + callerStatus := allowlist.GetAllowListStatus(stateDB, ContractAddress, caller) if !callerStatus.IsEnabled() { return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotAllowFeeRecipients, caller) } @@ -131,8 +117,8 @@ func PackAreFeeRecipientsAllowedOutput(isAllowed bool) ([]byte, error) { return RewardManagerABI.PackOutput("areFeeRecipientsAllowed", isAllowed) } -func areFeeRecipientsAllowed(accessibleState precompile.PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = precompile.DeductGas(suppliedGas, AreFeeRecipientsAllowedGasCost); err != nil { +func areFeeRecipientsAllowed(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, AreFeeRecipientsAllowedGasCost); err != nil { return nil, 0, err } // no input provided for this function @@ -164,18 +150,18 @@ func PackCurrentRewardAddressOutput(rewardAddress common.Address) ([]byte, error // GetStoredRewardAddress returns the current value of the address stored under rewardAddressStorageKey. // Returns an empty address and true if allow fee recipients is enabled, otherwise returns current reward address and false. -func GetStoredRewardAddress(stateDB precompile.StateDB) (common.Address, bool) { - val := stateDB.GetState(precompile.RewardManagerAddress, rewardAddressStorageKey) +func GetStoredRewardAddress(stateDB contract.StateDB) (common.Address, bool) { + val := stateDB.GetState(ContractAddress, rewardAddressStorageKey) return common.BytesToAddress(val.Bytes()), val == allowFeeRecipientsAddressValue } // StoredRewardAddress stores the given [val] under rewardAddressStorageKey. -func StoreRewardAddress(stateDB precompile.StateDB, val common.Address) error { +func StoreRewardAddress(stateDB contract.StateDB, val common.Address) error { // if input is empty, return an error if val == (common.Address{}) { return ErrEmptyRewardAddress } - stateDB.SetState(precompile.RewardManagerAddress, rewardAddressStorageKey, val.Hash()) + stateDB.SetState(ContractAddress, rewardAddressStorageKey, val.Hash()) return nil } @@ -197,8 +183,8 @@ func UnpackSetRewardAddressInput(input []byte) (common.Address, error) { return unpacked, nil } -func setRewardAddress(accessibleState precompile.PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = precompile.DeductGas(suppliedGas, SetRewardAddressGasCost); err != nil { +func setRewardAddress(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, SetRewardAddressGasCost); err != nil { return nil, 0, err } if readOnly { @@ -216,8 +202,8 @@ func setRewardAddress(accessibleState precompile.PrecompileAccessibleState, call // This part of the code restricts the function to be called only by enabled/admin addresses in the allow list. // You can modify/delete this code if you don't want this function to be restricted by the allow list. stateDB := accessibleState.GetStateDB() - // Verify that the caller is in the allow list and therefore has the right to modify it - callerStatus := allowlist.GetAllowListStatus(stateDB, precompile.RewardManagerAddress, caller) + // Verify that the caller is in the allow list and therefore has the right to call this function. + callerStatus := allowlist.GetAllowListStatus(stateDB, ContractAddress, caller) if !callerStatus.IsEnabled() { return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotSetRewardAddress, caller) } @@ -233,8 +219,8 @@ func setRewardAddress(accessibleState precompile.PrecompileAccessibleState, call return packedOutput, remainingGas, nil } -func currentRewardAddress(accessibleState precompile.PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = precompile.DeductGas(suppliedGas, CurrentRewardAddressGasCost); err != nil { +func currentRewardAddress(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, CurrentRewardAddressGasCost); err != nil { return nil, 0, err } @@ -256,8 +242,8 @@ func PackDisableRewards() ([]byte, error) { return RewardManagerABI.Pack("disableRewards") } -func disableRewards(accessibleState precompile.PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = precompile.DeductGas(suppliedGas, DisableRewardsGasCost); err != nil { +func disableRewards(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, DisableRewardsGasCost); err != nil { return nil, 0, err } if readOnly { @@ -269,8 +255,8 @@ func disableRewards(accessibleState precompile.PrecompileAccessibleState, caller // This part of the code restricts the function to be called only by enabled/admin addresses in the allow list. // You can modify/delete this code if you don't want this function to be restricted by the allow list. stateDB := accessibleState.GetStateDB() - // Verify that the caller is in the allow list and therefore has the right to modify it - callerStatus := allowlist.GetAllowListStatus(stateDB, precompile.RewardManagerAddress, caller) + // Verify that the caller is in the allow list and therefore has the right to call this function. + callerStatus := allowlist.GetAllowListStatus(stateDB, ContractAddress, caller) if !callerStatus.IsEnabled() { return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotDisableRewards, caller) } @@ -285,10 +271,10 @@ func disableRewards(accessibleState precompile.PrecompileAccessibleState, caller // createRewardManagerPrecompile returns a StatefulPrecompiledContract with getters and setters for the precompile. // Access to the getters/setters is controlled by an allow list for [precompileAddr]. -func createRewardManagerPrecompile(precompileAddr common.Address) (precompile.StatefulPrecompiledContract, error) { - var functions []*precompile.StatefulPrecompileFunction - functions = append(functions, allowlist.CreateAllowListFunctions(precompileAddr)...) - abiFunctionMap := map[string]precompile.RunStatefulPrecompileFunc{ +func createRewardManagerPrecompile() contract.StatefulPrecompiledContract { + var functions []*contract.StatefulPrecompileFunction + functions = append(functions, allowlist.CreateAllowListFunctions(ContractAddress)...) + abiFunctionMap := map[string]contract.RunStatefulPrecompileFunc{ "allowFeeRecipients": allowFeeRecipients, "areFeeRecipientsAllowed": areFeeRecipientsAllowed, "currentRewardAddress": currentRewardAddress, @@ -299,11 +285,15 @@ func createRewardManagerPrecompile(precompileAddr common.Address) (precompile.St for name, function := range abiFunctionMap { method, ok := RewardManagerABI.Methods[name] if !ok { - return nil, fmt.Errorf("given method (%s) does not exist in the ABI", name) + panic(fmt.Errorf("given method (%s) does not exist in the ABI", name)) } - functions = append(functions, precompile.NewStatefulPrecompileFunction(method.ID, function)) + functions = append(functions, contract.NewStatefulPrecompileFunction(method.ID, function)) } // Construct the contract with no fallback function. - return precompile.NewStatefulPrecompileContract(nil, functions) + statefulContract, err := contract.NewStatefulPrecompileContract(nil, functions) + if err != nil { + panic(err) + } + return statefulContract } diff --git a/precompile/contracts/rewardmanager/contract_test.go b/precompile/contracts/rewardmanager/contract_test.go new file mode 100644 index 0000000000..e2db6b5d5e --- /dev/null +++ b/precompile/contracts/rewardmanager/contract_test.go @@ -0,0 +1,318 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package rewardmanager + +import ( + "math/big" + "testing" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/subnet-evm/constants" + "github.com/ava-labs/subnet-evm/core/rawdb" + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +var testBlockNumber = big.NewInt(7) + +type precompileTest struct { + caller common.Address + input func() []byte + suppliedGas uint64 + readOnly bool + + config config.Config + + preCondition func(t *testing.T, state *state.StateDB) + assertState func(t *testing.T, state *state.StateDB) + + expectedRes []byte + expectedErr string +} + +func TestRewardManagerRun(t *testing.T) { + adminAddr := common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC") + enabledAddr := common.HexToAddress("0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B") + noRoleAddr := common.HexToAddress("0xF60C45c607D0f41687c94C314d300f483661E13a") + testAddr := common.HexToAddress("0x0123") + + for name, test := range map[string]precompileTest{ + "set allow fee recipients from no role fails": { + caller: noRoleAddr, + input: func() []byte { + input, err := PackAllowFeeRecipients() + require.NoError(t, err) + + return input + }, + suppliedGas: AllowFeeRecipientsGasCost, + readOnly: false, + expectedErr: ErrCannotAllowFeeRecipients.Error(), + }, + "set reward address from no role fails": { + caller: noRoleAddr, + input: func() []byte { + input, err := PackSetRewardAddress(testAddr) + require.NoError(t, err) + + return input + }, + suppliedGas: SetRewardAddressGasCost, + readOnly: false, + expectedErr: ErrCannotSetRewardAddress.Error(), + }, + "disable rewards from no role fails": { + caller: noRoleAddr, + input: func() []byte { + input, err := PackDisableRewards() + require.NoError(t, err) + + return input + }, + suppliedGas: DisableRewardsGasCost, + readOnly: false, + expectedErr: ErrCannotDisableRewards.Error(), + }, + "set allow fee recipients from enabled succeeds": { + caller: enabledAddr, + input: func() []byte { + input, err := PackAllowFeeRecipients() + require.NoError(t, err) + + return input + }, + suppliedGas: AllowFeeRecipientsGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + _, isFeeRecipients := GetStoredRewardAddress(state) + require.True(t, isFeeRecipients) + }, + }, + "set reward address from enabled succeeds": { + caller: enabledAddr, + input: func() []byte { + input, err := PackSetRewardAddress(testAddr) + require.NoError(t, err) + + return input + }, + suppliedGas: SetRewardAddressGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + address, isFeeRecipients := GetStoredRewardAddress(state) + require.Equal(t, testAddr, address) + require.False(t, isFeeRecipients) + }, + }, + "disable rewards from enabled succeeds": { + caller: enabledAddr, + input: func() []byte { + input, err := PackDisableRewards() + require.NoError(t, err) + + return input + }, + suppliedGas: DisableRewardsGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + address, isFeeRecipients := GetStoredRewardAddress(state) + require.False(t, isFeeRecipients) + require.Equal(t, constants.BlackholeAddr, address) + }, + }, + "get current reward address from no role succeeds": { + caller: noRoleAddr, + preCondition: func(t *testing.T, state *state.StateDB) { + StoreRewardAddress(state, testAddr) + }, + input: func() []byte { + input, err := PackCurrentRewardAddress() + require.NoError(t, err) + + return input + }, + suppliedGas: CurrentRewardAddressGasCost, + readOnly: false, + expectedRes: func() []byte { + res, err := PackCurrentRewardAddressOutput(testAddr) + require.NoError(t, err) + return res + }(), + }, + "get are fee recipients allowed from no role succeeds": { + caller: noRoleAddr, + preCondition: func(t *testing.T, state *state.StateDB) { + EnableAllowFeeRecipients(state) + }, + input: func() []byte { + input, err := PackAreFeeRecipientsAllowed() + require.NoError(t, err) + return input + }, + suppliedGas: AreFeeRecipientsAllowedGasCost, + readOnly: false, + expectedRes: func() []byte { + res, err := PackAreFeeRecipientsAllowedOutput(true) + require.NoError(t, err) + return res + }(), + }, + "get initial config with address": { + caller: noRoleAddr, + input: func() []byte { + input, err := PackCurrentRewardAddress() + require.NoError(t, err) + return input + }, + suppliedGas: CurrentRewardAddressGasCost, + config: &Config{ + InitialRewardConfig: &InitialRewardConfig{ + RewardAddress: testAddr, + }, + }, + readOnly: false, + expectedRes: func() []byte { + res, err := PackCurrentRewardAddressOutput(testAddr) + require.NoError(t, err) + return res + }(), + }, + "get initial config with allow fee recipients enabled": { + caller: noRoleAddr, + input: func() []byte { + input, err := PackAreFeeRecipientsAllowed() + require.NoError(t, err) + return input + }, + suppliedGas: AreFeeRecipientsAllowedGasCost, + config: &Config{ + InitialRewardConfig: &InitialRewardConfig{ + AllowFeeRecipients: true, + }, + }, + readOnly: false, + expectedRes: func() []byte { + res, err := PackAreFeeRecipientsAllowedOutput(true) + require.NoError(t, err) + return res + }(), + }, + "readOnly allow fee recipients with allowed role fails": { + caller: enabledAddr, + input: func() []byte { + input, err := PackAllowFeeRecipients() + require.NoError(t, err) + + return input + }, + suppliedGas: AllowFeeRecipientsGasCost, + readOnly: true, + expectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "readOnly set reward addresss with allowed role fails": { + caller: enabledAddr, + input: func() []byte { + input, err := PackSetRewardAddress(testAddr) + require.NoError(t, err) + + return input + }, + suppliedGas: SetRewardAddressGasCost, + readOnly: true, + expectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "insufficient gas set reward address from allowed role": { + caller: enabledAddr, + input: func() []byte { + input, err := PackSetRewardAddress(testAddr) + require.NoError(t, err) + + return input + }, + suppliedGas: SetRewardAddressGasCost - 1, + readOnly: false, + expectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "insufficient gas allow fee recipients from allowed role": { + caller: enabledAddr, + input: func() []byte { + input, err := PackAllowFeeRecipients() + require.NoError(t, err) + + return input + }, + suppliedGas: AllowFeeRecipientsGasCost - 1, + readOnly: false, + expectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "insufficient gas read current reward address from allowed role": { + caller: enabledAddr, + input: func() []byte { + input, err := PackCurrentRewardAddress() + require.NoError(t, err) + + return input + }, + suppliedGas: CurrentRewardAddressGasCost - 1, + readOnly: false, + expectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "insufficient gas are fee recipients allowed from allowed role": { + caller: enabledAddr, + input: func() []byte { + input, err := PackAreFeeRecipientsAllowed() + require.NoError(t, err) + + return input + }, + suppliedGas: AreFeeRecipientsAllowedGasCost - 1, + readOnly: false, + expectedErr: vmerrs.ErrOutOfGas.Error(), + }, + } { + t.Run(name, func(t *testing.T) { + db := rawdb.NewMemoryDatabase() + state, err := state.New(common.Hash{}, state.NewDatabase(db), nil) + require.NoError(t, err) + + // Set up the state so that each address has the expected permissions at the start. + SetRewardManagerAllowListStatus(state, adminAddr, allowlist.AdminRole) + SetRewardManagerAllowListStatus(state, enabledAddr, allowlist.EnabledRole) + require.Equal(t, allowlist.AdminRole, GetRewardManagerAllowListStatus(state, adminAddr)) + require.Equal(t, allowlist.EnabledRole, GetRewardManagerAllowListStatus(state, enabledAddr)) + + if test.preCondition != nil { + test.preCondition(t, state) + } + + blockContext := contract.NewMockBlockContext(testBlockNumber, 0) + accesibleState := contract.NewMockAccessibleState(state, blockContext, snow.DefaultContextTest()) + + if test.config != nil { + Module.Configure(nil, test.config, state, blockContext) + } + ret, remainingGas, err := RewardManagerPrecompile.Run(accesibleState, test.caller, ContractAddress, test.input(), test.suppliedGas, test.readOnly) + if len(test.expectedErr) != 0 { + require.ErrorContains(t, err, test.expectedErr) + } else { + require.NoError(t, err) + } + + require.Equal(t, uint64(0), remainingGas) + require.Equal(t, test.expectedRes, ret) + + if test.assertState != nil { + test.assertState(t, state) + } + }) + } +} diff --git a/precompile/contracts/rewardmanager/module.go b/precompile/contracts/rewardmanager/module.go new file mode 100644 index 0000000000..e5f18a7c2b --- /dev/null +++ b/precompile/contracts/rewardmanager/module.go @@ -0,0 +1,61 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package rewardmanager + +import ( + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ethereum/go-ethereum/common" +) + +var _ contract.Configurator = &configurator{} + +// ConfigKey is the key used in json config files to specify this precompile config. +// must be unique across all precompiles. +const ConfigKey = "rewardManagerConfig" + +var ContractAddress = common.HexToAddress("0x0200000000000000000000000000000000000004") + +var Module = modules.Module{ + ConfigKey: ConfigKey, + Address: ContractAddress, + Contract: RewardManagerPrecompile, + Configurator: &configurator{}, +} + +type configurator struct{} + +func init() { + if err := modules.RegisterModule(Module); err != nil { + panic(err) + } +} + +func (*configurator) NewConfig() config.Config { + return &Config{} +} + +// Configure configures [state] with the initial state for the precompile. +func (*configurator) Configure(chainConfig contract.ChainConfig, cfg config.Config, state contract.StateDB, _ contract.BlockContext) error { + config, ok := cfg.(*Config) + if !ok { + return fmt.Errorf("incorrect config %T: %v", config, config) + } + // configure the RewardManager with the given initial configuration + if config.InitialRewardConfig != nil { + return config.InitialRewardConfig.Configure(state) + } else if chainConfig.AllowedFeeRecipients() { + // configure the RewardManager according to chainConfig + EnableAllowFeeRecipients(state) + } else { + // chainConfig does not have any reward address + // if chainConfig does not enable fee recipients + // default to disabling rewards + DisableFeeRewards(state) + } + return config.Config.Configure(state, ContractAddress) +} diff --git a/precompile/contracts/txallowlist/config.go b/precompile/contracts/txallowlist/config.go new file mode 100644 index 0000000000..93042c46da --- /dev/null +++ b/precompile/contracts/txallowlist/config.go @@ -0,0 +1,56 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txallowlist + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ethereum/go-ethereum/common" +) + +var _ config.Config = &Config{} + +// Config implements the StatefulPrecompileConfig interface while adding in the +// TxAllowList specific precompile config. +type Config struct { + allowlist.Config + config.Upgrade +} + +// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables +// TxAllowList with the given [admins] and [enableds] as members of the allowlist. +func NewConfig(blockTimestamp *big.Int, admins []common.Address, enableds []common.Address) *Config { + return &Config{ + Config: allowlist.Config{ + AdminAddresses: admins, + EnabledAddresses: enableds, + }, + Upgrade: config.Upgrade{BlockTimestamp: blockTimestamp}, + } +} + +// NewDisableConfig returns config for a network upgrade at [blockTimestamp] +// that disables TxAllowList. +func NewDisableConfig(blockTimestamp *big.Int) *Config { + return &Config{ + Upgrade: config.Upgrade{ + BlockTimestamp: blockTimestamp, + Disable: true, + }, + } +} + +func (c *Config) Key() string { return ConfigKey } + +// Equal returns true if [cfg] is a [*TxAllowListConfig] and it has been configured identical to [c]. +func (c *Config) Equal(cfg config.Config) bool { + // typecast before comparison + other, ok := (cfg).(*Config) + if !ok { + return false + } + return c.Upgrade.Equal(&other.Upgrade) && c.Config.Equal(&other.Config) +} diff --git a/precompile/txallowlist/config_test.go b/precompile/contracts/txallowlist/config_test.go similarity index 60% rename from precompile/txallowlist/config_test.go rename to precompile/contracts/txallowlist/config_test.go index 651bfa9ba2..51817d8959 100644 --- a/precompile/txallowlist/config_test.go +++ b/precompile/contracts/txallowlist/config_test.go @@ -7,7 +7,7 @@ import ( "math/big" "testing" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/config" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) @@ -17,27 +17,27 @@ func TestVerifyTxAllowlistConfig(t *testing.T) { enableds := []common.Address{{2}} tests := []struct { name string - config precompile.StatefulPrecompileConfig + config config.Config expectedError string }{ { name: "invalid allow list config in tx allowlist", - config: NewTxAllowListConfig(big.NewInt(3), admins, admins), + config: NewConfig(big.NewInt(3), admins, admins), expectedError: "cannot set address", }, { name: "nil member allow list config in tx allowlist", - config: NewTxAllowListConfig(big.NewInt(3), nil, nil), + config: NewConfig(big.NewInt(3), nil, nil), expectedError: "", }, { name: "empty member allow list config in tx allowlist", - config: NewTxAllowListConfig(big.NewInt(3), []common.Address{}, []common.Address{}), + config: NewConfig(big.NewInt(3), []common.Address{}, []common.Address{}), expectedError: "", }, { name: "valid allow list config in tx allowlist", - config: NewTxAllowListConfig(big.NewInt(3), admins, enableds), + config: NewConfig(big.NewInt(3), admins, enableds), expectedError: "", }, } @@ -60,38 +60,38 @@ func TestEqualTxAllowListConfig(t *testing.T) { enableds := []common.Address{{2}} tests := []struct { name string - config precompile.StatefulPrecompileConfig - other precompile.StatefulPrecompileConfig + config config.Config + other config.Config expected bool }{ { name: "non-nil config and nil other", - config: NewTxAllowListConfig(big.NewInt(3), admins, enableds), + config: NewConfig(big.NewInt(3), admins, enableds), other: nil, expected: false, }, { name: "different admin", - config: NewTxAllowListConfig(big.NewInt(3), admins, enableds), - other: NewTxAllowListConfig(big.NewInt(3), []common.Address{{3}}, enableds), + config: NewConfig(big.NewInt(3), admins, enableds), + other: NewConfig(big.NewInt(3), []common.Address{{3}}, enableds), expected: false, }, { name: "different enabled", - config: NewTxAllowListConfig(big.NewInt(3), admins, enableds), - other: NewTxAllowListConfig(big.NewInt(3), admins, []common.Address{{3}}), + config: NewConfig(big.NewInt(3), admins, enableds), + other: NewConfig(big.NewInt(3), admins, []common.Address{{3}}), expected: false, }, { name: "different timestamp", - config: NewTxAllowListConfig(big.NewInt(3), admins, enableds), - other: NewTxAllowListConfig(big.NewInt(4), admins, enableds), + config: NewConfig(big.NewInt(3), admins, enableds), + other: NewConfig(big.NewInt(4), admins, enableds), expected: false, }, { name: "same config", - config: NewTxAllowListConfig(big.NewInt(3), admins, enableds), - other: NewTxAllowListConfig(big.NewInt(3), admins, enableds), + config: NewConfig(big.NewInt(3), admins, enableds), + other: NewConfig(big.NewInt(3), admins, enableds), expected: true, }, } diff --git a/precompile/contracts/txallowlist/contract.go b/precompile/contracts/txallowlist/contract.go new file mode 100644 index 0000000000..e93d53c6a1 --- /dev/null +++ b/precompile/contracts/txallowlist/contract.go @@ -0,0 +1,25 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txallowlist + +import ( + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" +) + +// Singleton StatefulPrecompiledContract for W/R access to the tx allow list. +var TxAllowListPrecompile contract.StatefulPrecompiledContract = allowlist.CreateAllowListPrecompile(ContractAddress) + +// GetTxAllowListStatus returns the role of [address] for the tx allow list. +func GetTxAllowListStatus(stateDB contract.StateDB, address common.Address) allowlist.Role { + return allowlist.GetAllowListStatus(stateDB, ContractAddress, address) +} + +// SetTxAllowListStatus sets the permissions of [address] to [role] for the +// tx allow list. +// assumes [role] has already been verified as valid. +func SetTxAllowListStatus(stateDB contract.StateDB, address common.Address, role allowlist.Role) { + allowlist.SetAllowListRole(stateDB, ContractAddress, address, role) +} diff --git a/precompile/contracts/txallowlist/contract_test.go b/precompile/contracts/txallowlist/contract_test.go new file mode 100644 index 0000000000..4fb3dfab74 --- /dev/null +++ b/precompile/contracts/txallowlist/contract_test.go @@ -0,0 +1,214 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txallowlist + +import ( + "testing" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/subnet-evm/core/rawdb" + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +type precompileTest struct { + caller common.Address + input func() []byte + suppliedGas uint64 + readOnly bool + + config config.Config + + preCondition func(t *testing.T, state *state.StateDB) + assertState func(t *testing.T, state *state.StateDB) + + expectedRes []byte + expectedErr string +} + +func TestTxAllowListRun(t *testing.T) { + adminAddr := common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC") + noRoleAddr := common.HexToAddress("0xF60C45c607D0f41687c94C314d300f483661E13a") + + for name, test := range map[string]precompileTest{ + "set admin": { + caller: adminAddr, + input: func() []byte { + input, err := allowlist.PackModifyAllowList(noRoleAddr, allowlist.AdminRole) + require.NoError(t, err) + + return input + }, + suppliedGas: allowlist.ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetTxAllowListStatus(state, noRoleAddr) + require.Equal(t, allowlist.AdminRole, res) + }, + }, + "set allowed": { + caller: adminAddr, + input: func() []byte { + input, err := allowlist.PackModifyAllowList(noRoleAddr, allowlist.EnabledRole) + require.NoError(t, err) + + return input + }, + suppliedGas: allowlist.ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetTxAllowListStatus(state, noRoleAddr) + require.Equal(t, allowlist.EnabledRole, res) + }, + }, + "set no role": { + caller: adminAddr, + input: func() []byte { + input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.NoRole) + require.NoError(t, err) + + return input + }, + suppliedGas: allowlist.ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetTxAllowListStatus(state, adminAddr) + require.Equal(t, allowlist.NoRole, res) + }, + }, + "set no role from non-admin": { + caller: noRoleAddr, + input: func() []byte { + input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.NoRole) + require.NoError(t, err) + + return input + }, + suppliedGas: allowlist.ModifyAllowListGasCost, + readOnly: false, + expectedErr: allowlist.ErrCannotModifyAllowList.Error(), + }, + "set allowed from non-admin": { + caller: noRoleAddr, + input: func() []byte { + input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.EnabledRole) + require.NoError(t, err) + + return input + }, + suppliedGas: allowlist.ModifyAllowListGasCost, + readOnly: false, + expectedErr: allowlist.ErrCannotModifyAllowList.Error(), + }, + "set admin from non-admin": { + caller: noRoleAddr, + input: func() []byte { + input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.AdminRole) + require.NoError(t, err) + + return input + }, + suppliedGas: allowlist.ModifyAllowListGasCost, + readOnly: false, + expectedErr: allowlist.ErrCannotModifyAllowList.Error(), + }, + "set no role with readOnly enabled": { + caller: adminAddr, + input: func() []byte { + input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.EnabledRole) + require.NoError(t, err) + + return input + }, + suppliedGas: allowlist.ModifyAllowListGasCost, + readOnly: true, + expectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "set no role insufficient gas": { + caller: adminAddr, + input: func() []byte { + input, err := allowlist.PackModifyAllowList(adminAddr, allowlist.EnabledRole) + require.NoError(t, err) + + return input + }, + suppliedGas: allowlist.ModifyAllowListGasCost - 1, + readOnly: false, + expectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "read allow list with no role": { + caller: noRoleAddr, + input: func() []byte { + return allowlist.PackReadAllowList(noRoleAddr) + }, + suppliedGas: allowlist.ReadAllowListGasCost, + readOnly: false, + expectedRes: common.Hash(allowlist.NoRole).Bytes(), + assertState: nil, + }, + "read allow list with admin role": { + caller: adminAddr, + input: func() []byte { + return allowlist.PackReadAllowList(noRoleAddr) + }, + suppliedGas: allowlist.ReadAllowListGasCost, + readOnly: false, + expectedRes: common.Hash(allowlist.NoRole).Bytes(), + assertState: nil, + }, + "read allow list with readOnly enabled": { + caller: adminAddr, + input: func() []byte { + return allowlist.PackReadAllowList(noRoleAddr) + }, + suppliedGas: allowlist.ReadAllowListGasCost, + readOnly: true, + expectedRes: common.Hash(allowlist.NoRole).Bytes(), + assertState: nil, + }, + "read allow list out of gas": { + caller: adminAddr, + input: func() []byte { + return allowlist.PackReadAllowList(noRoleAddr) + }, + suppliedGas: allowlist.ReadAllowListGasCost - 1, + readOnly: true, + expectedErr: vmerrs.ErrOutOfGas.Error(), + }, + } { + t.Run(name, func(t *testing.T) { + db := rawdb.NewMemoryDatabase() + state, err := state.New(common.Hash{}, state.NewDatabase(db), nil) + require.NoError(t, err) + + // Set up the state so that each address has the expected permissions at the start. + SetTxAllowListStatus(state, adminAddr, allowlist.AdminRole) + require.Equal(t, allowlist.AdminRole, GetTxAllowListStatus(state, adminAddr)) + + blockContext := contract.NewMockBlockContext(common.Big0, 0) + accesibleState := contract.NewMockAccessibleState(state, blockContext, snow.DefaultContextTest()) + ret, remainingGas, err := TxAllowListPrecompile.Run(accesibleState, test.caller, ContractAddress, test.input(), test.suppliedGas, test.readOnly) + if len(test.expectedErr) != 0 { + require.ErrorContains(t, err, test.expectedErr) + } else { + require.NoError(t, err) + } + + require.Equal(t, uint64(0), remainingGas) + require.Equal(t, test.expectedRes, ret) + + if test.assertState != nil { + test.assertState(t, state) + } + }) + } +} diff --git a/precompile/contracts/txallowlist/module.go b/precompile/contracts/txallowlist/module.go new file mode 100644 index 0000000000..0da9721eb6 --- /dev/null +++ b/precompile/contracts/txallowlist/module.go @@ -0,0 +1,49 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txallowlist + +import ( + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/config" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ethereum/go-ethereum/common" +) + +var _ contract.Configurator = &configurator{} + +// ConfigKey is the key used in json config files to specify this precompile config. +// must be unique across all precompiles. +const ConfigKey = "txAllowListConfig" + +var ContractAddress = common.HexToAddress("0x0200000000000000000000000000000000000002") + +var Module = modules.Module{ + ConfigKey: ConfigKey, + Address: ContractAddress, + Contract: TxAllowListPrecompile, + Configurator: &configurator{}, +} + +type configurator struct{} + +func init() { + if err := modules.RegisterModule(Module); err != nil { + panic(err) + } +} + +func (*configurator) NewConfig() config.Config { + return &Config{} +} + +// Configure configures [state] with the initial state for the precompile. +func (*configurator) Configure(chainConfig contract.ChainConfig, cfg config.Config, state contract.StateDB, _ contract.BlockContext) error { + config, ok := cfg.(*Config) + if !ok { + return fmt.Errorf("incorrect config %T: %v", config, config) + } + return config.Config.Configure(state, ContractAddress) +} diff --git a/precompile/deployerallowlist/config.go b/precompile/deployerallowlist/config.go deleted file mode 100644 index 07b8e27dc2..0000000000 --- a/precompile/deployerallowlist/config.go +++ /dev/null @@ -1,76 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package deployerallowlist - -import ( - "encoding/json" - "math/big" - - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/precompile/allowlist" - "github.com/ethereum/go-ethereum/common" -) - -var _ precompile.StatefulPrecompileConfig = &ContractDeployerAllowListConfig{} - -// ContractDeployerAllowListConfig wraps [AllowListConfig] and uses it to implement the StatefulPrecompileConfig -// interface while adding in the contract deployer specific precompile address. -type ContractDeployerAllowListConfig struct { - allowlist.AllowListConfig - precompile.UpgradeableConfig -} - -// NewContractDeployerAllowListConfig returns a config for a network upgrade at [blockTimestamp] that enables -// ContractDeployerAllowList with [admins] and [enableds] as members of the allowlist. -func NewContractDeployerAllowListConfig(blockTimestamp *big.Int, admins []common.Address, enableds []common.Address) *ContractDeployerAllowListConfig { - return &ContractDeployerAllowListConfig{ - AllowListConfig: allowlist.AllowListConfig{ - AllowListAdmins: admins, - EnabledAddresses: enableds, - }, - UpgradeableConfig: precompile.UpgradeableConfig{BlockTimestamp: blockTimestamp}, - } -} - -// NewDisableContractDeployerAllowListConfig returns config for a network upgrade at [blockTimestamp] -// that disables ContractDeployerAllowList. -func NewDisableContractDeployerAllowListConfig(blockTimestamp *big.Int) *ContractDeployerAllowListConfig { - return &ContractDeployerAllowListConfig{ - UpgradeableConfig: precompile.UpgradeableConfig{ - BlockTimestamp: blockTimestamp, - Disable: true, - }, - } -} - -// Address returns the address of the contract deployer allow list. -func (c *ContractDeployerAllowListConfig) Address() common.Address { - return precompile.ContractDeployerAllowListAddress -} - -// Configure configures [state] with the desired admins based on [c]. -func (c *ContractDeployerAllowListConfig) Configure(_ precompile.ChainConfig, state precompile.StateDB, _ precompile.BlockContext) error { - return c.AllowListConfig.Configure(state, precompile.ContractDeployerAllowListAddress) -} - -// Contract returns the singleton stateful precompiled contract to be used for the allow list. -func (c *ContractDeployerAllowListConfig) Contract() precompile.StatefulPrecompiledContract { - return ContractDeployerAllowListPrecompile -} - -// Equal returns true if [s] is a [*ContractDeployerAllowListConfig] and it has been configured identical to [c]. -func (c *ContractDeployerAllowListConfig) Equal(s precompile.StatefulPrecompileConfig) bool { - // typecast before comparison - other, ok := (s).(*ContractDeployerAllowListConfig) - if !ok { - return false - } - return c.UpgradeableConfig.Equal(&other.UpgradeableConfig) && c.AllowListConfig.Equal(&other.AllowListConfig) -} - -// String returns a string representation of the ContractDeployerAllowListConfig. -func (c *ContractDeployerAllowListConfig) String() string { - bytes, _ := json.Marshal(c) - return string(bytes) -} diff --git a/precompile/deployerallowlist/contract.go b/precompile/deployerallowlist/contract.go deleted file mode 100644 index 4fb4ace7b9..0000000000 --- a/precompile/deployerallowlist/contract.go +++ /dev/null @@ -1,26 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package deployerallowlist - -import ( - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/precompile/allowlist" - "github.com/ethereum/go-ethereum/common" -) - -// Singleton StatefulPrecompiledContract for W/R access to the contract deployer allow list. -var ContractDeployerAllowListPrecompile precompile.StatefulPrecompiledContract = allowlist.CreateAllowListPrecompile(precompile.ContractDeployerAllowListAddress) - -// GetContractDeployerAllowListStatus returns the role of [address] for the contract deployer -// allow list. -func GetContractDeployerAllowListStatus(stateDB precompile.StateDB, address common.Address) allowlist.AllowListRole { - return allowlist.GetAllowListStatus(stateDB, precompile.ContractDeployerAllowListAddress, address) -} - -// SetContractDeployerAllowListStatus sets the permissions of [address] to [role] for the -// contract deployer allow list. -// assumes [role] has already been verified as valid. -func SetContractDeployerAllowListStatus(stateDB precompile.StateDB, address common.Address, role allowlist.AllowListRole) { - allowlist.SetAllowListRole(stateDB, precompile.ContractDeployerAllowListAddress, address, role) -} diff --git a/precompile/feemanager/config.go b/precompile/feemanager/config.go deleted file mode 100644 index b36a411342..0000000000 --- a/precompile/feemanager/config.go +++ /dev/null @@ -1,110 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package feemanager - -import ( - "encoding/json" - "fmt" - "math/big" - - "github.com/ava-labs/subnet-evm/commontype" - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/precompile/allowlist" - "github.com/ethereum/go-ethereum/common" -) - -// FeeManagerConfig wraps [AllowListConfig] and uses it to implement the StatefulPrecompileConfig -// interface while adding in the FeeManager specific precompile address. -type FeeManagerConfig struct { - allowlist.AllowListConfig - precompile.UpgradeableConfig - InitialFeeConfig *commontype.FeeConfig `json:"initialFeeConfig,omitempty"` // initial fee config to be immediately activated -} - -// NewFeeManagerConfig returns a config for a network upgrade at [blockTimestamp] that enables -// FeeManager with the given [admins] and [enableds] as members of the allowlist with [initialConfig] as initial fee config if specified. -func NewFeeManagerConfig(blockTimestamp *big.Int, admins []common.Address, enableds []common.Address, initialConfig *commontype.FeeConfig) *FeeManagerConfig { - return &FeeManagerConfig{ - AllowListConfig: allowlist.AllowListConfig{ - AllowListAdmins: admins, - EnabledAddresses: enableds, - }, - UpgradeableConfig: precompile.UpgradeableConfig{BlockTimestamp: blockTimestamp}, - InitialFeeConfig: initialConfig, - } -} - -// NewDisableFeeManagerConfig returns config for a network upgrade at [blockTimestamp] -// that disables FeeManager. -func NewDisableFeeManagerConfig(blockTimestamp *big.Int) *FeeManagerConfig { - return &FeeManagerConfig{ - UpgradeableConfig: precompile.UpgradeableConfig{ - BlockTimestamp: blockTimestamp, - Disable: true, - }, - } -} - -// Address returns the address of the FeeManager contract. -func (c *FeeManagerConfig) Address() common.Address { - return precompile.FeeManagerAddress -} - -// Equal returns true if [s] is a [*FeeManagerConfig] and it has been configured identical to [c]. -func (c *FeeManagerConfig) Equal(s precompile.StatefulPrecompileConfig) bool { - // typecast before comparison - other, ok := (s).(*FeeManagerConfig) - if !ok { - return false - } - eq := c.UpgradeableConfig.Equal(&other.UpgradeableConfig) && c.AllowListConfig.Equal(&other.AllowListConfig) - if !eq { - return false - } - - if c.InitialFeeConfig == nil { - return other.InitialFeeConfig == nil - } - - return c.InitialFeeConfig.Equal(other.InitialFeeConfig) -} - -// Configure configures [state] with the desired admins based on [c]. -func (c *FeeManagerConfig) Configure(chainConfig precompile.ChainConfig, state precompile.StateDB, blockContext precompile.BlockContext) error { - // Store the initial fee config into the state when the fee manager activates. - if c.InitialFeeConfig != nil { - if err := StoreFeeConfig(state, *c.InitialFeeConfig, blockContext); err != nil { - // This should not happen since we already checked this config with Verify() - return fmt.Errorf("cannot configure given initial fee config: %w", err) - } - } else { - if err := StoreFeeConfig(state, chainConfig.GetFeeConfig(), blockContext); err != nil { - // This should not happen since we already checked the chain config in the genesis creation. - return fmt.Errorf("cannot configure fee config in chain config: %w", err) - } - } - return c.AllowListConfig.Configure(state, precompile.FeeManagerAddress) -} - -// Contract returns the singleton stateful precompiled contract to be used for the fee manager. -func (c *FeeManagerConfig) Contract() precompile.StatefulPrecompiledContract { - return FeeManagerPrecompile -} - -func (c *FeeManagerConfig) Verify() error { - if err := c.AllowListConfig.Verify(); err != nil { - return err - } - if c.InitialFeeConfig == nil { - return nil - } - - return c.InitialFeeConfig.Verify() -} - -// String returns a string representation of the FeeManagerConfig. -func (c *FeeManagerConfig) String() string { - bytes, _ := json.Marshal(c) - return string(bytes) -} diff --git a/precompile/mock_interface.go b/precompile/mock_interface.go deleted file mode 100644 index 4d6ca13281..0000000000 --- a/precompile/mock_interface.go +++ /dev/null @@ -1,115 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package precompile - -import ( - "math/big" - - "github.com/ava-labs/avalanchego/snow" - "github.com/ava-labs/subnet-evm/commontype" - "github.com/ethereum/go-ethereum/common" -) - -// TODO: replace with gomock library - -var ( - _ BlockContext = &mockBlockContext{} - _ PrecompileAccessibleState = &mockAccessibleState{} - _ ChainConfig = &mockChainConfig{} - _ StatefulPrecompileConfig = &noopStatefulPrecompileConfig{} -) - -type mockBlockContext struct { - blockNumber *big.Int - timestamp uint64 -} - -func NewMockBlockContext(blockNumber *big.Int, timestamp uint64) *mockBlockContext { - return &mockBlockContext{ - blockNumber: blockNumber, - timestamp: timestamp, - } -} - -func (mb *mockBlockContext) Number() *big.Int { return mb.blockNumber } -func (mb *mockBlockContext) Timestamp() *big.Int { return new(big.Int).SetUint64(mb.timestamp) } - -type mockAccessibleState struct { - state StateDB - blockContext *mockBlockContext - snowContext *snow.Context -} - -func NewMockAccessibleState(state StateDB, blockContext *mockBlockContext, snowContext *snow.Context) *mockAccessibleState { - return &mockAccessibleState{ - state: state, - blockContext: blockContext, - snowContext: snowContext, - } -} - -func (m *mockAccessibleState) GetStateDB() StateDB { return m.state } - -func (m *mockAccessibleState) GetBlockContext() BlockContext { return m.blockContext } - -func (m *mockAccessibleState) GetSnowContext() *snow.Context { return m.snowContext } - -func (m *mockAccessibleState) CallFromPrecompile(caller common.Address, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) { - return nil, 0, nil -} - -type mockChainConfig struct { - feeConfig commontype.FeeConfig - allowedFeeRecipients bool -} - -func NewMockChainConfig(feeConfig commontype.FeeConfig, allowedFeeRecipients bool) *mockChainConfig { - return &mockChainConfig{ - feeConfig: feeConfig, - allowedFeeRecipients: allowedFeeRecipients, - } -} - -func (m *mockChainConfig) GetFeeConfig() commontype.FeeConfig { return m.feeConfig } - -func (m *mockChainConfig) AllowedFeeRecipients() bool { return m.allowedFeeRecipients } - -type noopStatefulPrecompileConfig struct { -} - -func NewNoopStatefulPrecompileConfig() *noopStatefulPrecompileConfig { - return &noopStatefulPrecompileConfig{} -} - -func (n *noopStatefulPrecompileConfig) Address() common.Address { - return common.Address{} -} - -func (n *noopStatefulPrecompileConfig) Timestamp() *big.Int { - return new(big.Int) -} - -func (n *noopStatefulPrecompileConfig) IsDisabled() bool { - return false -} - -func (n *noopStatefulPrecompileConfig) Equal(StatefulPrecompileConfig) bool { - return false -} - -func (n *noopStatefulPrecompileConfig) Verify() error { - return nil -} - -func (n *noopStatefulPrecompileConfig) Configure(ChainConfig, StateDB, BlockContext) error { - return nil -} - -func (n *noopStatefulPrecompileConfig) Contract() StatefulPrecompiledContract { - return nil -} - -func (n *noopStatefulPrecompileConfig) String() string { - return "" -} diff --git a/precompile/modules/module.go b/precompile/modules/module.go new file mode 100644 index 0000000000..d0a047c94d --- /dev/null +++ b/precompile/modules/module.go @@ -0,0 +1,37 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package modules + +import ( + "bytes" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" +) + +type Module struct { + // ConfigKey is the key used in json config files to specify this precompile config. + ConfigKey string + // Address returns the address where the stateful precompile is accessible. + Address common.Address + // Contract returns a thread-safe singleton that can be used as the StatefulPrecompiledContract when + // this config is enabled. + Contract contract.StatefulPrecompiledContract + // Configurator is used to configure the stateful precompile when the config is enabled. + contract.Configurator +} + +type moduleArray []Module + +func (u moduleArray) Len() int { + return len(u) +} + +func (u moduleArray) Swap(i, j int) { + u[i], u[j] = u[j], u[i] +} + +func (m moduleArray) Less(i, j int) bool { + return bytes.Compare(m[i].Address.Bytes(), m[j].Address.Bytes()) < 0 +} diff --git a/precompile/modules/registerer.go b/precompile/modules/registerer.go new file mode 100644 index 0000000000..5de73ec657 --- /dev/null +++ b/precompile/modules/registerer.go @@ -0,0 +1,93 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package modules + +import ( + "fmt" + "sort" + + "github.com/ava-labs/subnet-evm/utils" + "github.com/ethereum/go-ethereum/common" +) + +var ( + // registeredModules is a list of Module to preserve order + // for deterministic iteration + registeredModules = make([]Module, 0) + + reservedRanges = []utils.AddressRange{ + { + Start: common.HexToAddress("0x0100000000000000000000000000000000000000"), + End: common.HexToAddress("0x01000000000000000000000000000000000000ff"), + }, + { + Start: common.HexToAddress("0x0200000000000000000000000000000000000000"), + End: common.HexToAddress("0x02000000000000000000000000000000000000ff"), + }, + { + Start: common.HexToAddress("0x0300000000000000000000000000000000000000"), + End: common.HexToAddress("0x03000000000000000000000000000000000000ff"), + }, + } +) + +// ReservedAddress returns true if [addr] is in a reserved range for custom precompiles +func ReservedAddress(addr common.Address) bool { + for _, reservedRange := range reservedRanges { + if reservedRange.Contains(addr) { + return true + } + } + + return false +} + +// RegisterModule registers a stateful precompile module +func RegisterModule(stm Module) error { + address := stm.Address + key := stm.ConfigKey + if !ReservedAddress(address) { + return fmt.Errorf("address %s not in a reserved range", address) + } + + for _, registeredModule := range registeredModules { + if registeredModule.ConfigKey == key { + return fmt.Errorf("name %s already used by a stateful precompile", key) + } + if registeredModule.Address == address { + return fmt.Errorf("address %s already used by a stateful precompile", address) + } + } + // sort by address to ensure deterministic iteration + registeredModules = insertSortedByAddress(registeredModules, stm) + return nil +} + +func GetPrecompileModuleByAddress(address common.Address) (Module, bool) { + for _, stm := range registeredModules { + if stm.Address == address { + return stm, true + } + } + return Module{}, false +} + +func GetPrecompileModule(key string) (Module, bool) { + for _, stm := range registeredModules { + if stm.ConfigKey == key { + return stm, true + } + } + return Module{}, false +} + +func RegisteredModules() []Module { + return registeredModules +} + +func insertSortedByAddress(data []Module, stm Module) []Module { + data = append(data, stm) + sort.Sort(moduleArray(data)) + return data +} diff --git a/precompile/modules/registerer_test.go b/precompile/modules/registerer_test.go new file mode 100644 index 0000000000..b91e0b01ed --- /dev/null +++ b/precompile/modules/registerer_test.go @@ -0,0 +1,44 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package modules + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestRegisterModule(t *testing.T) { + data := make([]Module, 0) + // test that the module is registered in sorted order + module1 := Module{ + Address: common.BigToAddress(big.NewInt(1)), + } + data = insertSortedByAddress(data, module1) + + require.Equal(t, []Module{module1}, data) + + module0 := Module{ + Address: common.BigToAddress(big.NewInt(0)), + } + + data = insertSortedByAddress(data, module0) + require.Equal(t, []Module{module0, module1}, data) + + module3 := Module{ + Address: common.BigToAddress(big.NewInt(3)), + } + + data = insertSortedByAddress(data, module3) + require.Equal(t, []Module{module0, module1, module3}, data) + + module2 := Module{ + Address: common.BigToAddress(big.NewInt(2)), + } + + data = insertSortedByAddress(data, module2) + require.Equal(t, []Module{module0, module1, module2, module3}, data) +} diff --git a/precompile/nativeminter/config.go b/precompile/nativeminter/config.go deleted file mode 100644 index 51194dcc88..0000000000 --- a/precompile/nativeminter/config.go +++ /dev/null @@ -1,126 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package nativeminter - -import ( - "encoding/json" - "fmt" - "math/big" - - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/precompile/allowlist" - "github.com/ava-labs/subnet-evm/utils" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/math" -) - -var _ precompile.StatefulPrecompileConfig = &ContractNativeMinterConfig{} - -// ContractNativeMinterConfig wraps [AllowListConfig] and uses it to implement the StatefulPrecompileConfig -// interface while adding in the ContractNativeMinter specific precompile address. -type ContractNativeMinterConfig struct { - allowlist.AllowListConfig - precompile.UpgradeableConfig - InitialMint map[common.Address]*math.HexOrDecimal256 `json:"initialMint,omitempty"` // initial mint config to be immediately minted -} - -// NewContractNativeMinterConfig returns a config for a network upgrade at [blockTimestamp] that enables -// ContractNativeMinter with the given [admins] and [enableds] as members of the allowlist. Also mints balances according to [initialMint] when the upgrade activates. -func NewContractNativeMinterConfig(blockTimestamp *big.Int, admins []common.Address, enableds []common.Address, initialMint map[common.Address]*math.HexOrDecimal256) *ContractNativeMinterConfig { - return &ContractNativeMinterConfig{ - AllowListConfig: allowlist.AllowListConfig{ - AllowListAdmins: admins, - EnabledAddresses: enableds, - }, - UpgradeableConfig: precompile.UpgradeableConfig{BlockTimestamp: blockTimestamp}, - InitialMint: initialMint, - } -} - -// NewDisableContractNativeMinterConfig returns config for a network upgrade at [blockTimestamp] -// that disables ContractNativeMinter. -func NewDisableContractNativeMinterConfig(blockTimestamp *big.Int) *ContractNativeMinterConfig { - return &ContractNativeMinterConfig{ - UpgradeableConfig: precompile.UpgradeableConfig{ - BlockTimestamp: blockTimestamp, - Disable: true, - }, - } -} - -// Address returns the address of the native minter contract. -func (c *ContractNativeMinterConfig) Address() common.Address { - return precompile.ContractNativeMinterAddress -} - -// Configure configures [state] with the desired admins based on [c]. -func (c *ContractNativeMinterConfig) Configure(_ precompile.ChainConfig, state precompile.StateDB, _ precompile.BlockContext) error { - for to, amount := range c.InitialMint { - if amount != nil { - bigIntAmount := (*big.Int)(amount) - state.AddBalance(to, bigIntAmount) - } - } - - return c.AllowListConfig.Configure(state, precompile.ContractNativeMinterAddress) -} - -// Contract returns the singleton stateful precompiled contract to be used for the native minter. -func (c *ContractNativeMinterConfig) Contract() precompile.StatefulPrecompiledContract { - return ContractNativeMinterPrecompile -} - -func (c *ContractNativeMinterConfig) Verify() error { - if err := c.AllowListConfig.Verify(); err != nil { - return err - } - // ensure that all of the initial mint values in the map are non-nil positive values - for addr, amount := range c.InitialMint { - if amount == nil { - return fmt.Errorf("initial mint cannot contain nil amount for address %s", addr) - } - bigIntAmount := (*big.Int)(amount) - if bigIntAmount.Sign() < 1 { - return fmt.Errorf("initial mint cannot contain invalid amount %v for address %s", bigIntAmount, addr) - } - } - return nil -} - -// Equal returns true if [s] is a [*ContractNativeMinterConfig] and it has been configured identical to [c]. -func (c *ContractNativeMinterConfig) Equal(s precompile.StatefulPrecompileConfig) bool { - // typecast before comparison - other, ok := (s).(*ContractNativeMinterConfig) - if !ok { - return false - } - eq := c.UpgradeableConfig.Equal(&other.UpgradeableConfig) && c.AllowListConfig.Equal(&other.AllowListConfig) - if !eq { - return false - } - - if len(c.InitialMint) != len(other.InitialMint) { - return false - } - - for address, amount := range c.InitialMint { - val, ok := other.InitialMint[address] - if !ok { - return false - } - bigIntAmount := (*big.Int)(amount) - bigIntVal := (*big.Int)(val) - if !utils.BigNumEqual(bigIntAmount, bigIntVal) { - return false - } - } - - return true -} - -// String returns a string representation of the ContractNativeMinterConfig. -func (c *ContractNativeMinterConfig) String() string { - bytes, _ := json.Marshal(c) - return string(bytes) -} diff --git a/precompile/params.go b/precompile/params.go deleted file mode 100644 index bfa5e40a4f..0000000000 --- a/precompile/params.go +++ /dev/null @@ -1,82 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package precompile - -import ( - "fmt" - - "github.com/ethereum/go-ethereum/common" -) - -// Gas costs for stateful precompiles -const ( - WriteGasCostPerSlot = 20_000 - ReadGasCostPerSlot = 5_000 -) - -// Designated addresses of stateful precompiles -// Note: it is important that none of these addresses conflict with each other or any other precompiles -// in core/vm/contracts.go. -// The first stateful precompiles were added in coreth to support nativeAssetCall and nativeAssetBalance. New stateful precompiles -// originating in coreth will continue at this prefix, so we reserve this range in subnet-evm so that they can be migrated into -// subnet-evm without issue. -// These start at the address: 0x0100000000000000000000000000000000000000 and will increment by 1. -// Optional precompiles implemented in subnet-evm start at 0x0200000000000000000000000000000000000000 and will increment by 1 -// from here to reduce the risk of conflicts. -// For forks of subnet-evm, users should start at 0x0300000000000000000000000000000000000000 to ensure -// that their own modifications do not conflict with stateful precompiles that may be added to subnet-evm -// in the future. -var ( - ContractDeployerAllowListAddress = common.HexToAddress("0x0200000000000000000000000000000000000000") - ContractNativeMinterAddress = common.HexToAddress("0x0200000000000000000000000000000000000001") - TxAllowListAddress = common.HexToAddress("0x0200000000000000000000000000000000000002") - FeeManagerAddress = common.HexToAddress("0x0200000000000000000000000000000000000003") - RewardManagerAddress = common.HexToAddress("0x0200000000000000000000000000000000000004") - // ADD YOUR PRECOMPILE HERE - // {YourPrecompile}Address = common.HexToAddress("0x03000000000000000000000000000000000000??") - - UsedAddresses = []common.Address{ - ContractDeployerAllowListAddress, - ContractNativeMinterAddress, - TxAllowListAddress, - FeeManagerAddress, - RewardManagerAddress, - // ADD YOUR PRECOMPILE HERE - // YourPrecompileAddress - } - reservedRanges = []AddressRange{ - { - common.HexToAddress("0x0100000000000000000000000000000000000000"), - common.HexToAddress("0x01000000000000000000000000000000000000ff"), - }, - { - common.HexToAddress("0x0200000000000000000000000000000000000000"), - common.HexToAddress("0x02000000000000000000000000000000000000ff"), - }, - { - common.HexToAddress("0x0300000000000000000000000000000000000000"), - common.HexToAddress("0x03000000000000000000000000000000000000ff"), - }, - } -) - -// UsedAddress returns true if [addr] is in a reserved range for custom precompiles -func ReservedAddress(addr common.Address) bool { - for _, reservedRange := range reservedRanges { - if reservedRange.Contains(addr) { - return true - } - } - - return false -} - -func init() { - // Ensure that every address used by a precompile is in a reserved range. - for _, addr := range UsedAddresses { - if !ReservedAddress(addr) { - panic(fmt.Errorf("address %s used for stateful precompile but not specified in any reserved range", addr)) - } - } -} diff --git a/precompile/registry/registry.go b/precompile/registry/registry.go new file mode 100644 index 0000000000..273ebbcde3 --- /dev/null +++ b/precompile/registry/registry.go @@ -0,0 +1,41 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Module to facilitate the registration of precompiles and their configuration. +package registry + +// Force imports of each precompile to ensure each precompile's init function runs and registers itself +// with the registry. +import ( + _ "github.com/ava-labs/subnet-evm/precompile/contracts/deployerallowlist" + + _ "github.com/ava-labs/subnet-evm/precompile/contracts/nativeminter" + + _ "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" + + _ "github.com/ava-labs/subnet-evm/precompile/contracts/feemanager" + + _ "github.com/ava-labs/subnet-evm/precompile/contracts/rewardmanager" + // ADD YOUR PRECOMPILE HERE + // _ "github.com/ava-labs/subnet-evm/precompile/contracts/yourprecompile" +) + +// This list is kept just for reference. The actual addresses defined in respective packages of precompiles. +// Note: it is important that none of these addresses conflict with each other or any other precompiles +// in core/vm/contracts.go. +// The first stateful precompiles were added in coreth to support nativeAssetCall and nativeAssetBalance. New stateful precompiles +// originating in coreth will continue at this prefix, so we reserve this range in subnet-evm so that they can be migrated into +// subnet-evm without issue. +// These start at the address: 0x0100000000000000000000000000000000000000 and will increment by 1. +// Optional precompiles implemented in subnet-evm start at 0x0200000000000000000000000000000000000000 and will increment by 1 +// from here to reduce the risk of conflicts. +// For forks of subnet-evm, users should start at 0x0300000000000000000000000000000000000000 to ensure +// that their own modifications do not conflict with stateful precompiles that may be added to subnet-evm +// in the future. +// ContractDeployerAllowListAddress = common.HexToAddress("0x0200000000000000000000000000000000000000") +// ContractNativeMinterAddress = common.HexToAddress("0x0200000000000000000000000000000000000001") +// TxAllowListAddress = common.HexToAddress("0x0200000000000000000000000000000000000002") +// FeeManagerAddress = common.HexToAddress("0x0200000000000000000000000000000000000003") +// RewardManagerAddress = common.HexToAddress("0x0200000000000000000000000000000000000004") +// ADD YOUR PRECOMPILE HERE +// {YourPrecompile}Address = common.HexToAddress("0x03000000000000000000000000000000000000??") diff --git a/precompile/rewardmanager/config.go b/precompile/rewardmanager/config.go deleted file mode 100644 index 640198da30..0000000000 --- a/precompile/rewardmanager/config.go +++ /dev/null @@ -1,153 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Code generated -// This file is a generated precompile contract with stubbed abstract functions. - -package rewardmanager - -import ( - "encoding/json" - "math/big" - - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/precompile/allowlist" - "github.com/ethereum/go-ethereum/common" -) - -var _ precompile.StatefulPrecompileConfig = &RewardManagerConfig{} - -type InitialRewardConfig struct { - AllowFeeRecipients bool `json:"allowFeeRecipients"` - RewardAddress common.Address `json:"rewardAddress,omitempty"` -} - -func (i *InitialRewardConfig) Verify() error { - switch { - case i.AllowFeeRecipients && i.RewardAddress != (common.Address{}): - return ErrCannotEnableBothRewards - default: - return nil - } -} - -func (i *InitialRewardConfig) Equal(other *InitialRewardConfig) bool { - if other == nil { - return false - } - - return i.AllowFeeRecipients == other.AllowFeeRecipients && i.RewardAddress == other.RewardAddress -} - -func (i *InitialRewardConfig) Configure(state precompile.StateDB) error { - // enable allow fee recipients - if i.AllowFeeRecipients { - EnableAllowFeeRecipients(state) - } else if i.RewardAddress == (common.Address{}) { - // if reward address is empty and allow fee recipients is false - // then disable rewards - DisableFeeRewards(state) - } else { - // set reward address - return StoreRewardAddress(state, i.RewardAddress) - } - return nil -} - -// RewardManagerConfig implements the StatefulPrecompileConfig -// interface while adding in the RewardManager specific precompile config. -type RewardManagerConfig struct { - allowlist.AllowListConfig - precompile.UpgradeableConfig - InitialRewardConfig *InitialRewardConfig `json:"initialRewardConfig,omitempty"` -} - -// NewRewardManagerConfig returns a config for a network upgrade at [blockTimestamp] that enables -// RewardManager with the given [admins] and [enableds] as members of the allowlist with [initialConfig] as initial rewards config if specified. -func NewRewardManagerConfig(blockTimestamp *big.Int, admins []common.Address, enableds []common.Address, initialConfig *InitialRewardConfig) *RewardManagerConfig { - return &RewardManagerConfig{ - AllowListConfig: allowlist.AllowListConfig{ - AllowListAdmins: admins, - EnabledAddresses: enableds, - }, - UpgradeableConfig: precompile.UpgradeableConfig{BlockTimestamp: blockTimestamp}, - InitialRewardConfig: initialConfig, - } -} - -// NewDisableRewardManagerConfig returns config for a network upgrade at [blockTimestamp] -// that disables RewardManager. -func NewDisableRewardManagerConfig(blockTimestamp *big.Int) *RewardManagerConfig { - return &RewardManagerConfig{ - UpgradeableConfig: precompile.UpgradeableConfig{ - BlockTimestamp: blockTimestamp, - Disable: true, - }, - } -} - -// Equal returns true if [s] is a [*RewardManagerConfig] and it has been configured identical to [c]. -func (c *RewardManagerConfig) Equal(s precompile.StatefulPrecompileConfig) bool { - // typecast before comparison - other, ok := (s).(*RewardManagerConfig) - if !ok { - return false - } - // modify this boolean accordingly with your custom RewardManagerConfig, to check if [other] and the current [c] are equal - // if RewardManagerConfig contains only UpgradeableConfig and precompile.AllowListConfig you can skip modifying it. - equals := c.UpgradeableConfig.Equal(&other.UpgradeableConfig) && c.AllowListConfig.Equal(&other.AllowListConfig) - if !equals { - return false - } - - if c.InitialRewardConfig == nil { - return other.InitialRewardConfig == nil - } - - return c.InitialRewardConfig.Equal(other.InitialRewardConfig) -} - -// Address returns the address of the RewardManager. Addresses reside under the precompile/params.go -// Select a non-conflicting address and set it in the params.go. -func (c *RewardManagerConfig) Address() common.Address { - return precompile.RewardManagerAddress -} - -// Configure configures [state] with the initial configuration. -func (c *RewardManagerConfig) Configure(chainConfig precompile.ChainConfig, state precompile.StateDB, _ precompile.BlockContext) error { - c.AllowListConfig.Configure(state, precompile.RewardManagerAddress) - // configure the RewardManager with the given initial configuration - if c.InitialRewardConfig != nil { - return c.InitialRewardConfig.Configure(state) - } else if chainConfig.AllowedFeeRecipients() { - // configure the RewardManager according to chainConfig - EnableAllowFeeRecipients(state) - } else { - // chainConfig does not have any reward address - // if chainConfig does not enable fee recipients - // default to disabling rewards - DisableFeeRewards(state) - } - return nil -} - -// Contract returns the singleton stateful precompiled contract to be used for RewardManager. -func (c *RewardManagerConfig) Contract() precompile.StatefulPrecompiledContract { - return RewardManagerPrecompile -} - -func (c *RewardManagerConfig) Verify() error { - if err := c.AllowListConfig.Verify(); err != nil { - return err - } - if c.InitialRewardConfig != nil { - return c.InitialRewardConfig.Verify() - } - return nil -} - -// String returns a string representation of the RewardManagerConfig. -func (c *RewardManagerConfig) String() string { - bytes, _ := json.Marshal(c) - return string(bytes) -} diff --git a/precompile/stateful_precompile_config.go b/precompile/stateful_precompile_config.go deleted file mode 100644 index 3a293cbb5f..0000000000 --- a/precompile/stateful_precompile_config.go +++ /dev/null @@ -1,57 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package precompile - -import ( - "fmt" - "math/big" - - "github.com/ethereum/go-ethereum/common" -) - -// StatefulPrecompileConfig defines the interface for a stateful precompile to -type StatefulPrecompileConfig interface { - // Address returns the address where the stateful precompile is accessible. - Address() common.Address - // Timestamp returns the timestamp at which this stateful precompile should be enabled. - // 1) 0 indicates that the precompile should be enabled from genesis. - // 2) n indicates that the precompile should be enabled in the first block with timestamp >= [n]. - // 3) nil indicates that the precompile is never enabled. - Timestamp() *big.Int - // IsDisabled returns true if this network upgrade should disable the precompile. - IsDisabled() bool - // Equal returns true if the provided argument configures the same precompile with the same parameters. - Equal(StatefulPrecompileConfig) bool - // Verify is called on startup and an error is treated as fatal. Configure can assume the Config has passed verification. - Verify() error - // Configure is called on the first block where the stateful precompile should be enabled. - // This allows the stateful precompile to configure its own state via [StateDB] and [BlockContext] as necessary. - // This function must be deterministic since it will impact the EVM state. If a change to the - // config causes a change to the state modifications made in Configure, then it cannot be safely - // made to the config after the network upgrade has gone into effect. - // - // Configure is called on the first block where the stateful precompile should be enabled. This - // provides the config the ability to set its initial state and should only modify the state within - // its own address space. - Configure(ChainConfig, StateDB, BlockContext) error - // Contract returns a thread-safe singleton that can be used as the StatefulPrecompiledContract when - // this config is enabled. - Contract() StatefulPrecompiledContract - - fmt.Stringer -} - -// Configure sets the nonce and code to non-empty values then calls Configure on [precompileConfig] to make the necessary -// state update to enable the StatefulPrecompile. -// Assumes that [precompileConfig] is non-nil. -func Configure(chainConfig ChainConfig, blockContext BlockContext, precompileConfig StatefulPrecompileConfig, state StateDB) error { - // Set the nonce of the precompile's address (as is done when a contract is created) to ensure - // that it is marked as non-empty and will not be cleaned up when the statedb is finalized. - state.SetNonce(precompileConfig.Address(), 1) - // Set the code of the precompile's address to a non-zero length byte slice to ensure that the precompile - // can be called from within Solidity contracts. Solidity adds a check before invoking a contract to ensure - // that it does not attempt to invoke a non-existent contract. - state.SetCode(precompileConfig.Address(), []byte{0x1}) - return precompileConfig.Configure(chainConfig, state, blockContext) -} diff --git a/precompile/txallowlist/config.go b/precompile/txallowlist/config.go deleted file mode 100644 index 88435c977b..0000000000 --- a/precompile/txallowlist/config.go +++ /dev/null @@ -1,76 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package txallowlist - -import ( - "encoding/json" - "math/big" - - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/precompile/allowlist" - "github.com/ethereum/go-ethereum/common" -) - -var _ precompile.StatefulPrecompileConfig = &TxAllowListConfig{} - -// TxAllowListConfig wraps [AllowListConfig] and uses it to implement the StatefulPrecompileConfig -// interface while adding in the TxAllowList specific precompile address. -type TxAllowListConfig struct { - allowlist.AllowListConfig - precompile.UpgradeableConfig -} - -// NewTxAllowListConfig returns a config for a network upgrade at [blockTimestamp] that enables -// TxAllowList with the given [admins] and [enableds] as members of the allowlist. -func NewTxAllowListConfig(blockTimestamp *big.Int, admins []common.Address, enableds []common.Address) *TxAllowListConfig { - return &TxAllowListConfig{ - AllowListConfig: allowlist.AllowListConfig{ - AllowListAdmins: admins, - EnabledAddresses: enableds, - }, - UpgradeableConfig: precompile.UpgradeableConfig{BlockTimestamp: blockTimestamp}, - } -} - -// NewDisableTxAllowListConfig returns config for a network upgrade at [blockTimestamp] -// that disables TxAllowList. -func NewDisableTxAllowListConfig(blockTimestamp *big.Int) *TxAllowListConfig { - return &TxAllowListConfig{ - UpgradeableConfig: precompile.UpgradeableConfig{ - BlockTimestamp: blockTimestamp, - Disable: true, - }, - } -} - -// Address returns the address of the contract deployer allow list. -func (c *TxAllowListConfig) Address() common.Address { - return precompile.TxAllowListAddress -} - -// Configure configures [state] with the desired admins based on [c]. -func (c *TxAllowListConfig) Configure(_ precompile.ChainConfig, state precompile.StateDB, _ precompile.BlockContext) error { - return c.AllowListConfig.Configure(state, precompile.TxAllowListAddress) -} - -// Contract returns the singleton stateful precompiled contract to be used for the allow list. -func (c *TxAllowListConfig) Contract() precompile.StatefulPrecompiledContract { - return TxAllowListPrecompile -} - -// Equal returns true if [s] is a [*TxAllowListConfig] and it has been configured identical to [c]. -func (c *TxAllowListConfig) Equal(s precompile.StatefulPrecompileConfig) bool { - // typecast before comparison - other, ok := (s).(*TxAllowListConfig) - if !ok { - return false - } - return c.UpgradeableConfig.Equal(&other.UpgradeableConfig) && c.AllowListConfig.Equal(&other.AllowListConfig) -} - -// String returns a string representation of the TxAllowListConfig. -func (c *TxAllowListConfig) String() string { - bytes, _ := json.Marshal(c) - return string(bytes) -} diff --git a/precompile/txallowlist/contract.go b/precompile/txallowlist/contract.go deleted file mode 100644 index 2917e9b9c4..0000000000 --- a/precompile/txallowlist/contract.go +++ /dev/null @@ -1,33 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package txallowlist - -import ( - "errors" - - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/precompile/allowlist" - "github.com/ethereum/go-ethereum/common" -) - -var ( - _ precompile.StatefulPrecompileConfig = &TxAllowListConfig{} - - // Singleton StatefulPrecompiledContract for W/R access to the tx allow list. - TxAllowListPrecompile precompile.StatefulPrecompiledContract = allowlist.CreateAllowListPrecompile(precompile.TxAllowListAddress) - - ErrSenderAddressNotAllowListed = errors.New("cannot issue transaction from non-allow listed address") -) - -// GetTxAllowListStatus returns the role of [address] for the allow list. -func GetTxAllowListStatus(stateDB precompile.StateDB, address common.Address) allowlist.AllowListRole { - return allowlist.GetAllowListStatus(stateDB, precompile.TxAllowListAddress, address) -} - -// SetTxAllowListStatus sets the permissions of [address] to [role] for the -// tx allow list. -// assumes [role] has already been verified as valid. -func SetTxAllowListStatus(stateDB precompile.StateDB, address common.Address, role allowlist.AllowListRole) { - allowlist.SetAllowListRole(stateDB, precompile.TxAllowListAddress, address, role) -} diff --git a/tests/e2e/utils/evm_client.go b/tests/e2e/utils/evm_client.go index 6f8c0a3950..e1b9eecd12 100644 --- a/tests/e2e/utils/evm_client.go +++ b/tests/e2e/utils/evm_client.go @@ -15,7 +15,7 @@ import ( "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/ethclient" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile/txallowlist" + "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" ) @@ -167,7 +167,7 @@ func (ec *EvmClient) TransferTx( if err := ec.ethClient.SendTransaction(ctx, signedTx); err != nil { log.Printf("failed to send transaction: %v (key address %s)", err, sender) - if strings.Contains(err.Error(), txallowlist.ErrSenderAddressNotAllowListed.Error()) { + if strings.Contains(err.Error(), vmerrs.ErrSenderAddressNotAllowListed.Error()) { return nil, err } diff --git a/precompile/reserved_range.go b/utils/address_range.go similarity index 82% rename from precompile/reserved_range.go rename to utils/address_range.go index 4bbc953b54..ee40d2bcca 100644 --- a/precompile/reserved_range.go +++ b/utils/address_range.go @@ -1,7 +1,7 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// (c) 2021-2022, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package precompile +package utils import ( "bytes" @@ -16,6 +16,7 @@ type AddressRange struct { } // Contains returns true iff [addr] is contained within the (inclusive) +// range of addresses defined by [a]. func (a *AddressRange) Contains(addr common.Address) bool { addrBytes := addr.Bytes() return bytes.Compare(addrBytes, a.Start[:]) >= 0 && bytes.Compare(addrBytes, a.End[:]) <= 0 diff --git a/vmerrs/vmerrs.go b/vmerrs/vmerrs.go index 5ab30248ff..ea3871852c 100644 --- a/vmerrs/vmerrs.go +++ b/vmerrs/vmerrs.go @@ -32,19 +32,20 @@ import ( // List evm execution errors var ( - ErrOutOfGas = errors.New("out of gas") - ErrCodeStoreOutOfGas = errors.New("contract creation code storage out of gas") - ErrDepth = errors.New("max call depth exceeded") - ErrInsufficientBalance = errors.New("insufficient balance for transfer") - ErrContractAddressCollision = errors.New("contract address collision") - ErrExecutionReverted = errors.New("execution reverted") - ErrMaxCodeSizeExceeded = errors.New("max code size exceeded") - ErrInvalidJump = errors.New("invalid jump destination") - ErrWriteProtection = errors.New("write protection") - ErrReturnDataOutOfBounds = errors.New("return data out of bounds") - ErrGasUintOverflow = errors.New("gas uint64 overflow") - ErrInvalidCode = errors.New("invalid code: must not begin with 0xef") - ErrNonceUintOverflow = errors.New("nonce uint64 overflow") - ErrAddrProhibited = errors.New("prohibited address cannot be sender or created contract address") - ErrInvalidCoinbase = errors.New("invalid coinbase") + ErrOutOfGas = errors.New("out of gas") + ErrCodeStoreOutOfGas = errors.New("contract creation code storage out of gas") + ErrDepth = errors.New("max call depth exceeded") + ErrInsufficientBalance = errors.New("insufficient balance for transfer") + ErrContractAddressCollision = errors.New("contract address collision") + ErrExecutionReverted = errors.New("execution reverted") + ErrMaxCodeSizeExceeded = errors.New("max code size exceeded") + ErrInvalidJump = errors.New("invalid jump destination") + ErrWriteProtection = errors.New("write protection") + ErrReturnDataOutOfBounds = errors.New("return data out of bounds") + ErrGasUintOverflow = errors.New("gas uint64 overflow") + ErrInvalidCode = errors.New("invalid code: must not begin with 0xef") + ErrNonceUintOverflow = errors.New("nonce uint64 overflow") + ErrAddrProhibited = errors.New("prohibited address cannot be sender or created contract address") + ErrInvalidCoinbase = errors.New("invalid coinbase") + ErrSenderAddressNotAllowListed = errors.New("cannot issue transaction from non-allow listed address") )