diff --git a/accounts/abi/bind/bind.go b/accounts/abi/bind/bind.go index c2be8309e1..d15d452f89 100644 --- a/accounts/abi/bind/bind.go +++ b/accounts/abi/bind/bind.go @@ -51,7 +51,7 @@ const ( readAllowListFuncKey = "readAllowList" ) -type BindHook func(lang Lang, types []string, contracts map[string]*tmplContract, structs map[string]*tmplStruct) (data interface{}, templateSource string, err error) +type BindHook func(lang Lang, pkg string, types []string, contracts map[string]*tmplContract, structs map[string]*tmplStruct) (data interface{}, templateSource string, err error) // Lang is a target programming language selector to generate bindings for. type Lang int @@ -295,7 +295,7 @@ func bindHelper(types []string, abis []string, bytecodes []string, fsigs []map[s // Generate the contract template data according to hook if bindHook != nil { var err error - data, templateSource, err = bindHook(lang, types, contracts, structs) + data, templateSource, err = bindHook(lang, pkg, types, contracts, structs) if err != nil { return "", err } diff --git a/accounts/abi/bind/precompile_bind.go b/accounts/abi/bind/precompile_bind.go index 3b33478b43..d31d9d0fef 100644 --- a/accounts/abi/bind/precompile_bind.go +++ b/accounts/abi/bind/precompile_bind.go @@ -35,9 +35,23 @@ import ( "fmt" ) -func PrecompileBind(types []string, abis []string, bytecodes []string, fsigs []map[string]string, pkg string, lang Lang, libs map[string]string, aliases map[string]string, abifilename string) (string, error) { - // create hook - var createPrecompileFunc BindHook = func(lang Lang, types []string, contracts map[string]*tmplContract, structs map[string]*tmplStruct) (interface{}, string, error) { +// PrecompileBind generates a Go binding for a precompiled contract. It returns config binding and contract binding. +func PrecompileBind(types []string, abis []string, bytecodes []string, fsigs []map[string]string, pkg string, lang Lang, libs map[string]string, aliases map[string]string, abifilename string) (string, string, error) { + // create hooks + configHook := createPrecompileHook(abifilename, tmplSourcePrecompileConfigGo) + contractHook := createPrecompileHook(abifilename, tmplSourcePrecompileContractGo) + + configBind, err := bindHelper(types, abis, bytecodes, fsigs, pkg, lang, libs, aliases, configHook) + if err != nil { + return "", "", err + } + contractBind, err := bindHelper(types, abis, bytecodes, fsigs, pkg, lang, libs, aliases, contractHook) + + return configBind, contractBind, err +} + +func createPrecompileHook(abifilename string, template string) BindHook { + var bindHook BindHook = func(lang Lang, pkg string, types []string, contracts map[string]*tmplContract, structs map[string]*tmplStruct) (interface{}, string, error) { // verify first if lang != LangGo { return nil, "", errors.New("only GoLang binding for precompiled contracts is supported yet") @@ -82,11 +96,11 @@ func PrecompileBind(types []string, abis []string, bytecodes []string, fsigs []m data := &tmplPrecompileData{ Contract: precompileContract, Structs: structs, + Package: pkg, } - return data, tmplSourcePrecompileGo, nil + return data, template, nil } - - return bindHelper(types, abis, bytecodes, fsigs, pkg, lang, libs, aliases, createPrecompileFunc) + return bindHook } func allowListEnabled(funcs map[string]*tmplMethod) bool { diff --git a/accounts/abi/bind/precompile_bind_test.go b/accounts/abi/bind/precompile_bind_test.go index 8ac85fb349..23f1503142 100644 --- a/accounts/abi/bind/precompile_bind_test.go +++ b/accounts/abi/bind/precompile_bind_test.go @@ -96,7 +96,7 @@ func golangBindingsFailure(t *testing.T) { for i, tt := range bindFailedTests { t.Run(tt.name, func(t *testing.T) { // Generate the binding - _, err := PrecompileBind([]string{tt.name}, tt.abi, tt.bytecode, tt.fsigs, "bindtest", LangGo, tt.libs, tt.aliases, "") + _, _, err := PrecompileBind([]string{tt.name}, tt.abi, tt.bytecode, tt.fsigs, "bindtest", LangGo, tt.libs, tt.aliases, "") if err == nil { t.Fatalf("test %d: no error occurred but was expected", i) } diff --git a/accounts/abi/bind/precompile_config_template.go b/accounts/abi/bind/precompile_config_template.go new file mode 100644 index 0000000000..5b4e9f6fd7 --- /dev/null +++ b/accounts/abi/bind/precompile_config_template.go @@ -0,0 +1,158 @@ +// (c) 2019-2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package bind + +// tmplSourcePrecompileConfigGo is the Go precompiled config source template. +const tmplSourcePrecompileConfigGo = ` +// Code generated +// 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 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. +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 +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/ +10- Create e2e test for your solidity test in tests/e2e/solidity/suites.go +11- Run your e2e precompile Solidity tests with 'E2E=true ./scripts/run.sh' + +*/ + +package {{.Package}} + +import ( + "encoding/json" + "math/big" + + "github.com/ava-labs/subnet-evm/precompile" + + "github.com/ethereum/go-ethereum/common" +) + +// CUSTOM CODE STARTS HERE +// Reference imports to suppress errors from unused imports. This code and any unnecessary imports can be removed. +var ( + _ = big.NewInt + _ = json.Unmarshal +) + +{{$contract := .Contract}} +var ( + _ precompile.StatefulPrecompileConfig = &{{.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 + {{- end}} + precompile.UpgradeableConfig +} + +{{$structs := .Structs}} +{{range $structs}} + // {{.Name}} is an auto generated low-level Go binding around an user-defined struct. + type {{.Name}} struct { + {{range $field := .Fields}} + {{$field.Name}} {{$field.Type}}{{end}} + } +{{- end}} + +{{- range .Contract.Funcs}} +{{ if len .Normalized.Inputs | lt 1}} +type {{capitalise .Normalized.Name}}Input struct{ +{{range .Normalized.Inputs}} {{capitalise .Name}} {{bindtype .Type $structs}}; {{end}} +} +{{- end}} +{{ if len .Normalized.Outputs | lt 1}} +type {{capitalise .Normalized.Name}}Output struct{ +{{range .Normalized.Outputs}} {{capitalise .Name}} {{bindtype .Type $structs}}; {{end}} +} +{{- end}} +{{- end}} + +// New{{.Contract.Type}}Config returns a config for a network upgrade at [blockTimestamp] that enables +// {{.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}} + UpgradeableConfig: precompile.UpgradeableConfig{BlockTimestamp: blockTimestamp}, + } +} + +// NewDisable{{.Contract.Type}}Config returns config for a network upgrade at [blockTimestamp] +// that disables {{.Contract.Type}}. +func NewDisable{{.Contract.Type}}Config(blockTimestamp *big.Int) *{{.Contract.Type}}Config { + return &{{.Contract.Type}}Config{ + UpgradeableConfig: precompile.UpgradeableConfig{ + BlockTimestamp: blockTimestamp, + Disable: true, + }, + } +} + +// Verify tries to verify {{.Contract.Type}}Config and returns an error accordingly. +func (c *{{.Contract.Type}}Config) Verify() error { + {{if .Contract.AllowList}} + // Verify AllowList first + if err := c.AllowListConfig.Verify(); err != nil { + return err + } + {{end}} + // CUSTOM CODE STARTS HERE + // Add your own custom verify code for {{.Contract.Type}}Config here + // and return an error accordingly + return nil +} + +// Equal returns true if [s] is a [*{{.Contract.Type}}Config] and it has been configured identical to [c]. +func (c *{{.Contract.Type}}Config) Equal(s precompile.StatefulPrecompileConfig) bool { + // typecast before comparison + other, ok := (s).(*{{.Contract.Type}}Config) + if !ok { + return false + } + // CUSTOM CODE STARTS HERE + // modify this boolean accordingly with your custom {{.Contract.Type}}Config, to check if [other] and the current [c] are equal + // if {{.Contract.Type}}Config contains only UpgradeableConfig {{if .Contract.AllowList}} and AllowListConfig {{end}} you can skip modifying it. + equals := c.UpgradeableConfig.Equal(&other.UpgradeableConfig) {{if .Contract.AllowList}} && c.AllowListConfig.Equal(&other.AllowListConfig) {{end}} + 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}} + // CUSTOM CODE STARTS HERE + return nil +} + +// Contract returns the singleton stateful precompiled contract to be used for {{.Contract.Type}}. +func (c *{{.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) +} +` diff --git a/accounts/abi/bind/precompile_template.go b/accounts/abi/bind/precompile_contract_template.go similarity index 69% rename from accounts/abi/bind/precompile_template.go rename to accounts/abi/bind/precompile_contract_template.go index 596d96c351..6dd7967b3e 100644 --- a/accounts/abi/bind/precompile_template.go +++ b/accounts/abi/bind/precompile_contract_template.go @@ -4,6 +4,7 @@ package bind // tmplPrecompileData is the data structure required to fill the binding template. type tmplPrecompileData struct { + Package string Contract *tmplPrecompileContract // The contract to generate into this file Structs map[string]*tmplStruct // Contract struct type definitions } @@ -16,8 +17,8 @@ type tmplPrecompileContract struct { ABIFilename string // Path to the ABI file } -// tmplSourcePrecompileGo is the Go precompiled source template. -const tmplSourcePrecompileGo = ` +// tmplSourcePrecompileContractGo is the Go precompiled contract source template. +const tmplSourcePrecompileContractGo = ` // Code generated // This file is a generated precompile contract with stubbed abstract functions. // The file is generated by a template. Please inspect every code and comment in this file before use. @@ -30,20 +31,21 @@ const tmplSourcePrecompileGo = ` /* 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 here +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. 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 solidity interface and test contract to contract-examples/contracts -7- Write solidity tests for your precompile in contract-examples/test -8- Create your genesis with your precompile enabled in tests/e2e/genesis/ -9- Create e2e test for your solidity test in tests/e2e/solidity/suites.go -10- Run your e2e precompile Solidity tests with 'E2E=true ./scripts/run.sh' +6- Add your config unit test in {generatedpkg}/config_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/ +10- Create e2e test for your solidity test in tests/e2e/solidity/suites.go +11- Run your e2e precompile Solidity tests with 'E2E=true ./scripts/run.sh' */ -package precompile +package {{.Package}} import ( "encoding/json" @@ -53,6 +55,7 @@ import ( "strings" "github.com/ava-labs/subnet-evm/accounts/abi" + "github.com/ava-labs/subnet-evm/precompile" "github.com/ava-labs/subnet-evm/vmerrs" _ "embed" @@ -82,8 +85,6 @@ var ( {{$contract := .Contract}} // Singleton StatefulPrecompiledContract and signatures. var ( - _ StatefulPrecompileConfig = &{{.Contract.Type}}Config{} - {{- range .Contract.Funcs}} {{- if not .Original.IsConstant | and $contract.AllowList}} @@ -105,22 +106,13 @@ var ( {{- end}} {{.Contract.Type}}ABI abi.ABI // will be initialized by init function - {{.Contract.Type}}Precompile StatefulPrecompiledContract // 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") ) -// {{.Contract.Type}}Config implements the StatefulPrecompileConfig -// interface while adding in the {{.Contract.Type}} specific precompile address. -type {{.Contract.Type}}Config struct { - {{- if .Contract.AllowList}} - AllowListConfig - {{- end}} - UpgradeableConfig -} - {{$structs := .Structs}} {{range $structs}} // {{.Name}} is an auto generated low-level Go binding around an user-defined struct. @@ -156,82 +148,10 @@ func init() { } } -// New{{.Contract.Type}}Config returns a config for a network upgrade at [blockTimestamp] that enables -// {{.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: AllowListConfig{AllowListAdmins: admins},{{end}} - UpgradeableConfig: UpgradeableConfig{BlockTimestamp: blockTimestamp}, - } -} - -// NewDisable{{.Contract.Type}}Config returns config for a network upgrade at [blockTimestamp] -// that disables {{.Contract.Type}}. -func NewDisable{{.Contract.Type}}Config(blockTimestamp *big.Int) *{{.Contract.Type}}Config { - return &{{.Contract.Type}}Config{ - UpgradeableConfig: UpgradeableConfig{ - BlockTimestamp: blockTimestamp, - Disable: true, - }, - } -} - -// Equal returns true if [s] is a [*{{.Contract.Type}}Config] and it has been configured identical to [c]. -func (c *{{.Contract.Type}}Config) Equal(s StatefulPrecompileConfig) bool { - // typecast before comparison - other, ok := (s).(*{{.Contract.Type}}Config) - if !ok { - return false - } - // CUSTOM CODE STARTS HERE - // modify this boolean accordingly with your custom {{.Contract.Type}}Config, to check if [other] and the current [c] are equal - // if {{.Contract.Type}}Config contains only UpgradeableConfig {{if .Contract.AllowList}} and AllowListConfig {{end}} you can skip modifying it. - equals := c.UpgradeableConfig.Equal(&other.UpgradeableConfig) {{if .Contract.AllowList}} && c.AllowListConfig.Equal(&other.AllowListConfig) {{end}} - return equals -} - -// String returns a string representation of the {{.Contract.Type}}Config. -func (c *{{.Contract.Type}}Config) String() string { - bytes, _ := json.Marshal(c) - return string(bytes) -} - -// 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(_ ChainConfig, state StateDB, _ BlockContext) error { - {{if .Contract.AllowList}}c.AllowListConfig.Configure(state, {{.Contract.Type}}Address){{end}} - // CUSTOM CODE STARTS HERE - return nil -} - -// Contract returns the singleton stateful precompiled contract to be used for {{.Contract.Type}}. -func (c *{{.Contract.Type}}Config) Contract() StatefulPrecompiledContract { - return {{.Contract.Type}}Precompile -} - -// Verify tries to verify {{.Contract.Type}}Config and returns an error accordingly. -func (c *{{.Contract.Type}}Config) Verify() error { - {{if .Contract.AllowList}} - // Verify AllowList first - if err := c.AllowListConfig.Verify(); err != nil { - return err - } - {{end}} - // CUSTOM CODE STARTS HERE - // Add your own custom verify code for {{.Contract.Type}}Config here - // and return an error accordingly - return nil -} - {{if .Contract.AllowList}} // Get{{.Contract.Type}}AllowListStatus returns the role of [address] for the {{.Contract.Type}} list. -func Get{{.Contract.Type}}AllowListStatus(stateDB StateDB, address common.Address) AllowListRole { - return getAllowListStatus(stateDB, {{.Contract.Type}}Address, address) +func Get{{.Contract.Type}}AllowListStatus(stateDB precompile.StateDB, address common.Address) precompile.AllowListRole { + return precompile.GetAllowListStatus(stateDB, {{.Contract.Type}}Address, address) } // Set{{.Contract.Type}}AllowListStatus sets the permissions of [address] to [role] for the @@ -240,8 +160,8 @@ func Get{{.Contract.Type}}AllowListStatus(stateDB StateDB, address common.Addres // 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 StateDB, address common.Address, role AllowListRole) { - setAllowListRole(stateDB, {{.Contract.Type}}Address, address, role) +func Set{{.Contract.Type}}AllowListStatus(stateDB precompile.StateDB, address common.Address, role precompile.AllowListRole) { + precompile.SetAllowListRole(stateDB, {{.Contract.Type}}Address, address, role) } {{end}} @@ -309,8 +229,8 @@ func Pack{{$method.Normalized.Name}}Output ({{decapitalise $output.Name}} {{bind } {{end}} -func {{decapitalise .Normalized.Name}}(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = deductGas(suppliedGas, {{.Normalized.Name}}GasCost); err != nil { +func {{decapitalise .Normalized.Name}}(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, {{.Normalized.Name}}GasCost); err != nil { return nil, 0, err } @@ -338,7 +258,7 @@ func {{decapitalise .Normalized.Name}}(accessibleState PrecompileAccessibleState // 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 := getAllowListStatus(stateDB, {{$contract.Type}}Address, caller) + callerStatus := precompile.GetAllowListStatus(stateDB, {{$contract.Type}}Address, caller) if !callerStatus.IsEnabled() { return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannot{{.Normalized.Name}}, caller) } @@ -374,8 +294,8 @@ func {{decapitalise .Normalized.Name}}(accessibleState PrecompileAccessibleState {{- with .Contract.Fallback}} // {{decapitalise $contract.Type}}Fallback executed if a function identifier does not match any of the available functions in a smart contract. // This function cannot take an input or return an output. -func {{decapitalise $contract.Type}}Fallback (accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, _ []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = deductGas(suppliedGas, {{$contract.Type}}FallbackGasCost); err != nil { +func {{decapitalise $contract.Type}}Fallback (accessibleState precompile.PrecompileAccessibleState, caller common.Address, addr common.Address, _ []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = precompile.DeductGas(suppliedGas, {{$contract.Type}}FallbackGasCost); err != nil { return nil, 0, err } @@ -389,7 +309,7 @@ func {{decapitalise $contract.Type}}Fallback (accessibleState PrecompileAccessib // 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 := getAllowListStatus(stateDB, {{$contract.Type}}Address, caller) + callerStatus := precompile.GetAllowListStatus(stateDB, {{$contract.Type}}Address, caller) if !callerStatus.IsEnabled() { return nil, remainingGas, fmt.Errorf("%w: %s", Err{{$contract.Type}}CannotFallback, caller) } @@ -410,13 +330,13 @@ func {{decapitalise $contract.Type}}Fallback (accessibleState PrecompileAccessib // 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) (StatefulPrecompiledContract, error) { - var functions []*statefulPrecompileFunction +func create{{.Contract.Type}}Precompile(precompileAddr common.Address) (precompile.StatefulPrecompiledContract, error) { + var functions []*precompile.StatefulPrecompileFunction {{- if .Contract.AllowList}} - functions = append(functions, createAllowListFunctions(precompileAddr)...) + functions = append(functions, precompile.CreateAllowListFunctions(precompileAddr)...) {{- end}} - abiFunctionMap := map[string]RunStatefulPrecompileFunc{ + abiFunctionMap := map[string]precompile.RunStatefulPrecompileFunc{ {{- range .Contract.Funcs}} "{{.Original.Name}}": {{decapitalise .Normalized.Name}}, {{- end}} @@ -427,15 +347,15 @@ func create{{.Contract.Type}}Precompile(precompileAddr common.Address) (Stateful if !ok { return nil, fmt.Errorf("given method (%s) does not exist in the ABI", name) } - functions = append(functions, newStatefulPrecompileFunction(method.ID, function)) + functions = append(functions, precompile.NewStatefulPrecompileFunction(method.ID, function)) } {{- if .Contract.Fallback}} // Construct the contract with the fallback function. - return NewStatefulPrecompileContract({{decapitalise $contract.Type}}Fallback, functions) + return precompile.NewStatefulPrecompileContract({{decapitalise $contract.Type}}Fallback, functions) {{- else}} // Construct the contract with no fallback function. - return NewStatefulPrecompileContract(nil, functions) + return precompile.NewStatefulPrecompileContract(nil, functions) {{- end}} } ` diff --git a/cmd/precompilegen/main.go b/cmd/precompilegen/main.go index 454a560bc5..0a60ed187f 100644 --- a/cmd/precompilegen/main.go +++ b/cmd/precompilegen/main.go @@ -56,15 +56,15 @@ var ( } typeFlag = &cli.StringFlag{ Name: "type", - Usage: "Struct name for the precompile (default = ABI name)", + Usage: "Struct name for the precompile (default = {ABI name})", } pkgFlag = &cli.StringFlag{ Name: "pkg", - Usage: "Package name to generate the precompile into (default = precompile)", + Usage: "Package name to generate the precompile into (default = {type})", } outFlag = &cli.StringFlag{ Name: "out", - Usage: "Output file for the generated precompile (default = STDOUT)", + Usage: "Output folder for the generated precompile files, - for STDOUT (default = ./{pkg})", } ) @@ -81,13 +81,12 @@ func init() { } func precompilegen(c *cli.Context) error { - if !c.IsSet(outFlag.Name) && !c.IsSet(typeFlag.Name) { + outFlagStr := c.String(outFlag.Name) + isOutStdout := outFlagStr == "-" + + if isOutStdout && !c.IsSet(typeFlag.Name) { utils.Fatalf("type (--type) should be set explicitly for STDOUT ") } - pkg := pkgFlag.Name - if pkg == "" { - pkg = "precompile" - } lang := bind.LangGo // If the entire solidity code was specified, build and bind based on that var ( @@ -106,6 +105,7 @@ func precompilegen(c *cli.Context) error { abi []byte err error ) + input := c.String(abiFlag.Name) if input == "-" { abi, err = io.ReadAll(os.Stdin) @@ -116,7 +116,9 @@ func precompilegen(c *cli.Context) error { utils.Fatalf("Failed to read input ABI: %v", err) } abis = append(abis, string(abi)) + bins = append(bins, "") + kind := c.String(typeFlag.Name) if kind == "" { fn := filepath.Base(input) @@ -124,30 +126,51 @@ func precompilegen(c *cli.Context) error { kind = strings.TrimSpace(kind) } types = append(types, kind) - outFlagSet := c.IsSet(outFlag.Name) - outFlag := c.String(outFlag.Name) + + pkg := c.String(pkgFlag.Name) + if pkg == "" { + pkg = strings.ToLower(kind) + } + + if outFlagStr == "" { + outFlagStr = filepath.Join("./", pkg) + } + abifilename := "" abipath := "" // we should not generate the abi file if output is set to stdout - if outFlagSet { + if !isOutStdout { // get file name from the output path - pathNoExt := strings.TrimSuffix(outFlag, filepath.Ext(outFlag)) - abipath = pathNoExt + ".abi" - abifilename = filepath.Base(abipath) + abifilename = "contract.abi" + abipath = filepath.Join(outFlagStr, abifilename) } // Generate the contract precompile - code, err := bind.PrecompileBind(types, abis, bins, sigs, pkg, lang, libs, aliases, abifilename) + 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) } // Either flush it out to a file or display on the standard output - if !outFlagSet { - fmt.Printf("%s\n", code) + if isOutStdout { + fmt.Print("-----Config Code-----\n") + fmt.Printf("%s\n", configCode) + fmt.Print("-----Contract Code-----\n") + fmt.Printf("%s\n", contractCode) return nil } - if err := os.WriteFile(outFlag, []byte(code), 0o600); err != nil { + if _, err := os.Stat(outFlagStr); os.IsNotExist(err) { + os.MkdirAll(outFlagStr, 0700) // Create your file + } + 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) + } + + 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) } @@ -155,7 +178,7 @@ func precompilegen(c *cli.Context) error { utils.Fatalf("Failed to write ABI: %v", err) } - fmt.Println("Precompile Generation was a success!") + fmt.Println("Precompile files generated successfully at: ", outFlagStr) return nil } diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index f17527c82e..143f73f588 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -39,7 +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/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/event" ) @@ -348,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.FeeConfigManagerAddress, bigTime) { + if !config.IsPrecompileEnabled(feemanager.Key, bigTime) { return config.FeeConfig, common.Big0, nil } @@ -366,7 +367,7 @@ func (bc *BlockChain) GetFeeConfigAt(parent *types.Header) (commontype.FeeConfig return commontype.EmptyFeeConfig, nil, err } - storedFeeConfig := precompile.GetStoredFeeConfig(stateDB) + storedFeeConfig := feemanager.GetStoredFeeConfig(stateDB) // this should not return an invalid fee config since it's assumed that // StoreFeeConfig returns an error when an invalid fee config is attempted to be stored. // However an external stateDB call can modify the contract state. @@ -374,7 +375,7 @@ func (bc *BlockChain) GetFeeConfigAt(parent *types.Header) (commontype.FeeConfig if err := storedFeeConfig.Verify(); err != nil { return commontype.EmptyFeeConfig, nil, err } - lastChangedAt := precompile.GetFeeConfigLastChangedAt(stateDB) + lastChangedAt := feemanager.GetFeeConfigLastChangedAt(stateDB) cacheable := &cacheableFeeConfig{feeConfig: storedFeeConfig, lastChangedAt: lastChangedAt} // add it to the cache bc.feeConfigCache.Add(parent.Root, cacheable) @@ -392,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.Key, bigTime) { if bc.chainConfig.AllowFeeRecipients { return common.Address{}, true, nil } else { @@ -413,7 +414,7 @@ func (bc *BlockChain) GetCoinbaseAt(parent *types.Header) (common.Address, bool, if err != nil { return common.Address{}, false, err } - rewardAddress, feeRecipients := precompile.GetStoredRewardAddress(stateDB) + rewardAddress, feeRecipients := rewardmanager.GetStoredRewardAddress(stateDB) cacheable := &cacheableCoinbaseConfig{coinbaseAddress: rewardAddress, allowFeeRecipients: feeRecipients} bc.coinbaseConfigCache.Add(parent.Root, cacheable) diff --git a/core/genesis_test.go b/core/genesis_test.go index 920e5337b6..24b56e5ce0 100644 --- a/core/genesis_test.go +++ b/core/genesis_test.go @@ -39,6 +39,7 @@ import ( "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/deployerallowlist" "github.com/davecgh/go-spew/spew" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" @@ -190,12 +191,12 @@ func TestStatefulPrecompilesConfigure(t *testing.T) { "allow list enabled in genesis": { getConfig: func() *params.ChainConfig { config := *params.TestChainConfig - config.ContractDeployerAllowListConfig = precompile.NewContractDeployerAllowListConfig(big.NewInt(0), []common.Address{addr}, nil) + config.Precompiles[deployerallowlist.Key] = deployerallowlist.NewContractDeployerAllowListConfig(big.NewInt(0), []common.Address{addr}, nil) return &config }, assertState: func(t *testing.T, sdb *state.StateDB) { - assert.Equal(t, precompile.AllowListAdmin, precompile.GetContractDeployerAllowListStatus(sdb, addr), "unexpected allow list status for modified address") - assert.Equal(t, uint64(1), sdb.GetNonce(precompile.ContractDeployerAllowListAddress)) + assert.Equal(t, precompile.AllowListAdmin, deployerallowlist.GetContractDeployerAllowListStatus(sdb, addr), "unexpected allow list status for modified address") + assert.Equal(t, uint64(1), sdb.GetNonce(deployerallowlist.Address)) }, }, } { @@ -265,11 +266,10 @@ func TestPrecompileActivationAfterHeaderBlock(t *testing.T) { require.Greater(block.Time(), bc.lastAccepted.Time()) activatedGenesis := customg - contractDeployerConfig := precompile.NewContractDeployerAllowListConfig(big.NewInt(51), nil, nil) + contractDeployerConfig := deployerallowlist.NewContractDeployerAllowListConfig(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_test.go b/core/state_processor_test.go index 6bf05057d9..793c0f8eca 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -37,6 +37,7 @@ import ( "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/trie" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -315,8 +316,8 @@ func TestBadTxAllowListBlock(t *testing.T) { NetworkUpgrades: params.NetworkUpgrades{ SubnetEVMTimestamp: big.NewInt(0), }, - PrecompileUpgrade: params.PrecompileUpgrade{ - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(0), nil, nil), + Precompiles: map[string]precompile.StatefulPrecompileConfig{ + txallowlist.Key: txallowlist.NewTxAllowListConfig(big.NewInt(0), nil, nil), }, } signer = types.LatestSigner(config) diff --git a/core/state_transition.go b/core/state_transition.go index acd81569e2..d58d31a95b 100644 --- a/core/state_transition.go +++ b/core/state_transition.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" + "github.com/ava-labs/subnet-evm/precompile/txallowlist" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" ) @@ -249,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) { - txAllowListRole := precompile.GetTxAllowListStatus(st.state, st.msg.From()) + if st.evm.ChainConfig().IsPrecompileEnabled(txallowlist.Key, st.evm.Context.Time) { + txAllowListRole := txallowlist.GetTxAllowListStatus(st.state, st.msg.From()) if !txAllowListRole.IsEnabled() { - return fmt.Errorf("%w: %s", precompile.ErrSenderAddressNotAllowListed, st.msg.From()) + return fmt.Errorf("%w: %s", txallowlist.ErrSenderAddressNotAllowListed, st.msg.From()) } } } diff --git a/core/stateful_precompile_test.go b/core/stateful_precompile_test.go deleted file mode 100644 index 702c8715b4..0000000000 --- a/core/stateful_precompile_test.go +++ /dev/null @@ -1,1288 +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/vmerrs" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/math" - "github.com/stretchr/testify/require" -) - -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 := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListAdmin) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := precompile.GetContractDeployerAllowListStatus(state, noRoleAddr) - require.Equal(t, precompile.AllowListAdmin, res) - }, - }, - "set deployer": { - caller: adminAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := precompile.GetContractDeployerAllowListStatus(state, noRoleAddr) - require.Equal(t, precompile.AllowListEnabled, res) - }, - }, - "set no role": { - caller: adminAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListNoRole) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := precompile.GetContractDeployerAllowListStatus(state, adminAddr) - require.Equal(t, precompile.AllowListNoRole, res) - }, - }, - "set no role from non-admin": { - caller: noRoleAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListNoRole) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedErr: precompile.ErrCannotModifyAllowList.Error(), - }, - "set deployer from non-admin": { - caller: noRoleAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedErr: precompile.ErrCannotModifyAllowList.Error(), - }, - "set admin from non-admin": { - caller: noRoleAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListAdmin) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedErr: precompile.ErrCannotModifyAllowList.Error(), - }, - "set no role with readOnly enabled": { - caller: adminAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListNoRole) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "set no role insufficient gas": { - caller: adminAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListNoRole) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "read allow list no role": { - caller: noRoleAddr, - input: func() []byte { - return precompile.PackReadAllowList(noRoleAddr) - }, - suppliedGas: precompile.ReadAllowListGasCost, - readOnly: false, - expectedRes: common.Hash(precompile.AllowListNoRole).Bytes(), - assertState: nil, - }, - "read allow list admin role": { - caller: adminAddr, - input: func() []byte { - return precompile.PackReadAllowList(noRoleAddr) - }, - suppliedGas: precompile.ReadAllowListGasCost, - readOnly: false, - expectedRes: common.Hash(precompile.AllowListNoRole).Bytes(), - assertState: nil, - }, - "read allow list with readOnly enabled": { - caller: adminAddr, - input: func() []byte { - return precompile.PackReadAllowList(noRoleAddr) - }, - suppliedGas: precompile.ReadAllowListGasCost, - readOnly: true, - expectedRes: common.Hash(precompile.AllowListNoRole).Bytes(), - assertState: nil, - }, - "read allow list out of gas": { - caller: adminAddr, - input: func() []byte { - return precompile.PackReadAllowList(noRoleAddr) - }, - suppliedGas: precompile.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. - precompile.SetContractDeployerAllowListStatus(state, adminAddr, precompile.AllowListAdmin) - precompile.SetContractDeployerAllowListStatus(state, noRoleAddr, precompile.AllowListNoRole) - require.Equal(t, precompile.AllowListAdmin, precompile.GetContractDeployerAllowListStatus(state, adminAddr)) - require.Equal(t, precompile.AllowListNoRole, precompile.GetContractDeployerAllowListStatus(state, noRoleAddr)) - - blockContext := &mockBlockContext{blockNumber: common.Big0} - ret, remainingGas, err := precompile.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 := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListAdmin) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := precompile.GetTxAllowListStatus(state, noRoleAddr) - require.Equal(t, precompile.AllowListAdmin, res) - }, - }, - "set allowed": { - caller: adminAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := precompile.GetTxAllowListStatus(state, noRoleAddr) - require.Equal(t, precompile.AllowListEnabled, res) - }, - }, - "set no role": { - caller: adminAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListNoRole) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := precompile.GetTxAllowListStatus(state, adminAddr) - require.Equal(t, precompile.AllowListNoRole, res) - }, - }, - "set no role from non-admin": { - caller: noRoleAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListNoRole) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedErr: precompile.ErrCannotModifyAllowList.Error(), - }, - "set allowed from non-admin": { - caller: noRoleAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedErr: precompile.ErrCannotModifyAllowList.Error(), - }, - "set admin from non-admin": { - caller: noRoleAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListAdmin) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedErr: precompile.ErrCannotModifyAllowList.Error(), - }, - "set no role with readOnly enabled": { - caller: adminAddr, - precompileAddr: precompile.TxAllowListAddress, - input: func() []byte { - input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListNoRole) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "set no role insufficient gas": { - caller: adminAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListNoRole) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "read allow list no role": { - caller: noRoleAddr, - input: func() []byte { - return precompile.PackReadAllowList(noRoleAddr) - }, - suppliedGas: precompile.ReadAllowListGasCost, - readOnly: false, - expectedRes: common.Hash(precompile.AllowListNoRole).Bytes(), - assertState: nil, - }, - "read allow list admin role": { - caller: adminAddr, - input: func() []byte { - return precompile.PackReadAllowList(noRoleAddr) - }, - suppliedGas: precompile.ReadAllowListGasCost, - readOnly: false, - expectedRes: common.Hash(precompile.AllowListNoRole).Bytes(), - assertState: nil, - }, - "read allow list with readOnly enabled": { - caller: adminAddr, - input: func() []byte { - return precompile.PackReadAllowList(noRoleAddr) - }, - suppliedGas: precompile.ReadAllowListGasCost, - readOnly: true, - expectedRes: common.Hash(precompile.AllowListNoRole).Bytes(), - assertState: nil, - }, - "read allow list out of gas": { - caller: adminAddr, - input: func() []byte { - return precompile.PackReadAllowList(noRoleAddr) - }, - suppliedGas: precompile.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. - precompile.SetTxAllowListStatus(state, adminAddr, precompile.AllowListAdmin) - require.Equal(t, precompile.AllowListAdmin, precompile.GetTxAllowListStatus(state, adminAddr)) - - blockContext := &mockBlockContext{blockNumber: common.Big0} - ret, remainingGas, err := precompile.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 *precompile.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 := precompile.PackMintInput(noRoleAddr, common.Big1) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.MintGasCost, - readOnly: false, - expectedErr: precompile.ErrCannotMint.Error(), - }, - "mint funds from enabled address": { - caller: enabledAddr, - input: func() []byte { - input, err := precompile.PackMintInput(enabledAddr, common.Big1) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.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 precompile.PackReadAllowList(testAddr) - }, - suppliedGas: precompile.ReadAllowListGasCost, - readOnly: false, - expectedRes: common.Hash(precompile.AllowListEnabled).Bytes(), - assertState: func(t *testing.T, state *state.StateDB) { - require.Equal(t, precompile.AllowListEnabled, precompile.GetContractNativeMinterStatus(state, testAddr)) - }, - config: &precompile.ContractNativeMinterConfig{ - AllowListConfig: precompile.AllowListConfig{EnabledAddresses: []common.Address{testAddr}}, - }, - }, - "initial mint funds": { - caller: enabledAddr, - config: &precompile.ContractNativeMinterConfig{ - InitialMint: map[common.Address]*math.HexOrDecimal256{ - enabledAddr: math.NewHexOrDecimal256(2), - }, - }, - input: func() []byte { - return precompile.PackReadAllowList(noRoleAddr) - }, - suppliedGas: precompile.ReadAllowListGasCost, - readOnly: false, - expectedRes: common.Hash(precompile.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 := precompile.PackMintInput(adminAddr, common.Big1) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.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 := precompile.PackMintInput(adminAddr, math.MaxBig256) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.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 := precompile.PackMintInput(adminAddr, common.Big1) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.MintGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "readOnly mint with allow role fails": { - caller: enabledAddr, - input: func() []byte { - input, err := precompile.PackMintInput(enabledAddr, common.Big1) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.MintGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "readOnly mint with admin role fails": { - caller: adminAddr, - input: func() []byte { - input, err := precompile.PackMintInput(adminAddr, common.Big1) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.MintGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "insufficient gas mint from admin": { - caller: adminAddr, - input: func() []byte { - input, err := precompile.PackMintInput(enabledAddr, common.Big1) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.MintGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "read from noRole address": { - caller: noRoleAddr, - input: func() []byte { - return precompile.PackReadAllowList(noRoleAddr) - }, - suppliedGas: precompile.ReadAllowListGasCost, - readOnly: false, - expectedRes: common.Hash(precompile.AllowListNoRole).Bytes(), - assertState: func(t *testing.T, state *state.StateDB) {}, - }, - "read from noRole address readOnly enabled": { - caller: noRoleAddr, - input: func() []byte { - return precompile.PackReadAllowList(noRoleAddr) - }, - suppliedGas: precompile.ReadAllowListGasCost, - readOnly: true, - expectedRes: common.Hash(precompile.AllowListNoRole).Bytes(), - assertState: func(t *testing.T, state *state.StateDB) {}, - }, - "read from noRole address with insufficient gas": { - caller: noRoleAddr, - input: func() []byte { - return precompile.PackReadAllowList(noRoleAddr) - }, - suppliedGas: precompile.ReadAllowListGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "set allow role from admin": { - caller: adminAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := precompile.GetContractNativeMinterStatus(state, noRoleAddr) - require.Equal(t, precompile.AllowListEnabled, res) - }, - }, - "set allow role from non-admin fails": { - caller: enabledAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedErr: precompile.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. - precompile.SetContractNativeMinterStatus(state, adminAddr, precompile.AllowListAdmin) - precompile.SetContractNativeMinterStatus(state, enabledAddr, precompile.AllowListEnabled) - precompile.SetContractNativeMinterStatus(state, noRoleAddr, precompile.AllowListNoRole) - require.Equal(t, precompile.AllowListAdmin, precompile.GetContractNativeMinterStatus(state, adminAddr)) - require.Equal(t, precompile.AllowListEnabled, precompile.GetContractNativeMinterStatus(state, enabledAddr)) - require.Equal(t, precompile.AllowListNoRole, precompile.GetContractNativeMinterStatus(state, noRoleAddr)) - - blockContext := &mockBlockContext{blockNumber: common.Big0} - if test.config != nil { - test.config.Configure(params.TestChainConfig, state, blockContext) - } - ret, remainingGas, err := precompile.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 TestFeeConfigManagerRun(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 *precompile.FeeConfigManagerConfig - - 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 := precompile.PackSetFeeConfig(testFeeConfig) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.SetFeeConfigGasCost, - readOnly: false, - expectedErr: precompile.ErrCannotChangeFee.Error(), - }, - "set config from enabled address": { - caller: enabledAddr, - input: func() []byte { - input, err := precompile.PackSetFeeConfig(testFeeConfig) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.SetFeeConfigGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - feeConfig := precompile.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 := precompile.PackSetFeeConfig(feeConfig) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.SetFeeConfigGasCost, - readOnly: false, - expectedRes: nil, - config: &precompile.FeeConfigManagerConfig{ - InitialFeeConfig: &testFeeConfig, - }, - expectedErr: "cannot be greater than maxBlockGasCost", - assertState: func(t *testing.T, state *state.StateDB) { - feeConfig := precompile.GetStoredFeeConfig(state) - require.Equal(t, testFeeConfig, feeConfig) - }, - }, - "set config from admin address": { - caller: adminAddr, - input: func() []byte { - input, err := precompile.PackSetFeeConfig(testFeeConfig) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.SetFeeConfigGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - feeConfig := precompile.GetStoredFeeConfig(state) - require.Equal(t, testFeeConfig, feeConfig) - lastChangedAt := precompile.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 := precompile.StoreFeeConfig(state, testFeeConfig, &mockBlockContext{blockNumber: big.NewInt(6)}) - require.NoError(t, err) - }, - input: func() []byte { - return precompile.PackGetFeeConfigInput() - }, - suppliedGas: precompile.GetFeeConfigGasCost, - readOnly: true, - expectedRes: func() []byte { - res, err := precompile.PackFeeConfig(testFeeConfig) - require.NoError(t, err) - return res - }(), - assertState: func(t *testing.T, state *state.StateDB) { - feeConfig := precompile.GetStoredFeeConfig(state) - lastChangedAt := precompile.GetFeeConfigLastChangedAt(state) - require.Equal(t, testFeeConfig, feeConfig) - require.EqualValues(t, big.NewInt(6), lastChangedAt) - }, - }, - "get initial fee config": { - caller: noRoleAddr, - input: func() []byte { - return precompile.PackGetFeeConfigInput() - }, - suppliedGas: precompile.GetFeeConfigGasCost, - config: &precompile.FeeConfigManagerConfig{ - InitialFeeConfig: &testFeeConfig, - }, - readOnly: true, - expectedRes: func() []byte { - res, err := precompile.PackFeeConfig(testFeeConfig) - require.NoError(t, err) - return res - }(), - assertState: func(t *testing.T, state *state.StateDB) { - feeConfig := precompile.GetStoredFeeConfig(state) - lastChangedAt := precompile.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 := precompile.StoreFeeConfig(state, testFeeConfig, &mockBlockContext{blockNumber: testBlockNumber}) - require.NoError(t, err) - }, - input: func() []byte { - return precompile.PackGetLastChangedAtInput() - }, - suppliedGas: precompile.GetLastChangedAtGasCost, - readOnly: true, - expectedRes: common.BigToHash(testBlockNumber).Bytes(), - assertState: func(t *testing.T, state *state.StateDB) { - feeConfig := precompile.GetStoredFeeConfig(state) - lastChangedAt := precompile.GetFeeConfigLastChangedAt(state) - require.Equal(t, testFeeConfig, feeConfig) - require.Equal(t, testBlockNumber, lastChangedAt) - }, - }, - "readOnly setFeeConfig with noRole fails": { - caller: noRoleAddr, - input: func() []byte { - input, err := precompile.PackSetFeeConfig(testFeeConfig) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.SetFeeConfigGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "readOnly setFeeConfig with allow role fails": { - caller: enabledAddr, - input: func() []byte { - input, err := precompile.PackSetFeeConfig(testFeeConfig) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.SetFeeConfigGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "readOnly setFeeConfig with admin role fails": { - caller: adminAddr, - input: func() []byte { - input, err := precompile.PackSetFeeConfig(testFeeConfig) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.SetFeeConfigGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "insufficient gas setFeeConfig from admin": { - caller: adminAddr, - input: func() []byte { - input, err := precompile.PackSetFeeConfig(testFeeConfig) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.SetFeeConfigGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "set allow role from admin": { - caller: adminAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := precompile.GetFeeConfigManagerStatus(state, noRoleAddr) - require.Equal(t, precompile.AllowListEnabled, res) - }, - }, - "set allow role from non-admin fails": { - caller: enabledAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedErr: precompile.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. - precompile.SetFeeConfigManagerStatus(state, adminAddr, precompile.AllowListAdmin) - precompile.SetFeeConfigManagerStatus(state, enabledAddr, precompile.AllowListEnabled) - precompile.SetFeeConfigManagerStatus(state, noRoleAddr, precompile.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 := precompile.FeeConfigManagerPrecompile.Run(&mockAccessibleState{state: state, blockContext: blockContext, snowContext: snow.DefaultContextTest()}, test.caller, precompile.FeeConfigManagerAddress, 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 *precompile.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 := precompile.PackAllowFeeRecipients() - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.AllowFeeRecipientsGasCost, - readOnly: false, - expectedErr: precompile.ErrCannotAllowFeeRecipients.Error(), - }, - "set reward address from no role fails": { - caller: noRoleAddr, - input: func() []byte { - input, err := precompile.PackSetRewardAddress(testAddr) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.SetRewardAddressGasCost, - readOnly: false, - expectedErr: precompile.ErrCannotSetRewardAddress.Error(), - }, - "disable rewards from no role fails": { - caller: noRoleAddr, - input: func() []byte { - input, err := precompile.PackDisableRewards() - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.DisableRewardsGasCost, - readOnly: false, - expectedErr: precompile.ErrCannotDisableRewards.Error(), - }, - "set allow fee recipients from enabled succeeds": { - caller: enabledAddr, - input: func() []byte { - input, err := precompile.PackAllowFeeRecipients() - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.AllowFeeRecipientsGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - _, isFeeRecipients := precompile.GetStoredRewardAddress(state) - require.True(t, isFeeRecipients) - }, - }, - "set reward address from enabled succeeds": { - caller: enabledAddr, - input: func() []byte { - input, err := precompile.PackSetRewardAddress(testAddr) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.SetRewardAddressGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - address, isFeeRecipients := precompile.GetStoredRewardAddress(state) - require.Equal(t, testAddr, address) - require.False(t, isFeeRecipients) - }, - }, - "disable rewards from enabled succeeds": { - caller: enabledAddr, - input: func() []byte { - input, err := precompile.PackDisableRewards() - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.DisableRewardsGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - address, isFeeRecipients := precompile.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) { - precompile.StoreRewardAddress(state, testAddr) - }, - input: func() []byte { - input, err := precompile.PackCurrentRewardAddress() - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.CurrentRewardAddressGasCost, - readOnly: false, - expectedRes: func() []byte { - res, err := precompile.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) { - precompile.EnableAllowFeeRecipients(state) - }, - input: func() []byte { - input, err := precompile.PackAreFeeRecipientsAllowed() - require.NoError(t, err) - return input - }, - suppliedGas: precompile.AreFeeRecipientsAllowedGasCost, - readOnly: false, - expectedRes: func() []byte { - res, err := precompile.PackAreFeeRecipientsAllowedOutput(true) - require.NoError(t, err) - return res - }(), - }, - "get initial config with address": { - caller: noRoleAddr, - input: func() []byte { - input, err := precompile.PackCurrentRewardAddress() - require.NoError(t, err) - return input - }, - suppliedGas: precompile.CurrentRewardAddressGasCost, - config: &precompile.RewardManagerConfig{ - InitialRewardConfig: &precompile.InitialRewardConfig{ - RewardAddress: testAddr, - }, - }, - readOnly: false, - expectedRes: func() []byte { - res, err := precompile.PackCurrentRewardAddressOutput(testAddr) - require.NoError(t, err) - return res - }(), - }, - "get initial config with allow fee recipients enabled": { - caller: noRoleAddr, - input: func() []byte { - input, err := precompile.PackAreFeeRecipientsAllowed() - require.NoError(t, err) - return input - }, - suppliedGas: precompile.AreFeeRecipientsAllowedGasCost, - config: &precompile.RewardManagerConfig{ - InitialRewardConfig: &precompile.InitialRewardConfig{ - AllowFeeRecipients: true, - }, - }, - readOnly: false, - expectedRes: func() []byte { - res, err := precompile.PackAreFeeRecipientsAllowedOutput(true) - require.NoError(t, err) - return res - }(), - }, - "readOnly allow fee recipients with allowed role fails": { - caller: enabledAddr, - input: func() []byte { - input, err := precompile.PackAllowFeeRecipients() - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.AllowFeeRecipientsGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "readOnly set reward addresss with allowed role fails": { - caller: enabledAddr, - input: func() []byte { - input, err := precompile.PackSetRewardAddress(testAddr) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.SetRewardAddressGasCost, - readOnly: true, - expectedErr: vmerrs.ErrWriteProtection.Error(), - }, - "insufficient gas set reward address from allowed role": { - caller: enabledAddr, - input: func() []byte { - input, err := precompile.PackSetRewardAddress(testAddr) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.SetRewardAddressGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "insufficient gas allow fee recipients from allowed role": { - caller: enabledAddr, - input: func() []byte { - input, err := precompile.PackAllowFeeRecipients() - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.AllowFeeRecipientsGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "insufficient gas read current reward address from allowed role": { - caller: enabledAddr, - input: func() []byte { - input, err := precompile.PackCurrentRewardAddress() - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.CurrentRewardAddressGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "insufficient gas are fee recipients allowed from allowed role": { - caller: enabledAddr, - input: func() []byte { - input, err := precompile.PackAreFeeRecipientsAllowed() - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.AreFeeRecipientsAllowedGasCost - 1, - readOnly: false, - expectedErr: vmerrs.ErrOutOfGas.Error(), - }, - "set allow role from admin": { - caller: adminAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedRes: []byte{}, - assertState: func(t *testing.T, state *state.StateDB) { - res := precompile.GetRewardManagerAllowListStatus(state, noRoleAddr) - require.Equal(t, precompile.AllowListEnabled, res) - }, - }, - "set allow role from non-admin fails": { - caller: enabledAddr, - input: func() []byte { - input, err := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListEnabled) - require.NoError(t, err) - - return input - }, - suppliedGas: precompile.ModifyAllowListGasCost, - readOnly: false, - expectedErr: precompile.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. - precompile.SetRewardManagerAllowListStatus(state, adminAddr, precompile.AllowListAdmin) - precompile.SetRewardManagerAllowListStatus(state, enabledAddr, precompile.AllowListEnabled) - precompile.SetRewardManagerAllowListStatus(state, noRoleAddr, precompile.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 := precompile.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 29b465a22f..a74566e108 100644 --- a/core/test_blockchain.go +++ b/core/test_blockchain.go @@ -17,6 +17,8 @@ import ( "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/deployerallowlist" + "github.com/ava-labs/subnet-evm/precompile/feemanager" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/assert" @@ -1546,8 +1548,8 @@ 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 = precompile.NewContractDeployerAllowListConfig(big.NewInt(0), []common.Address{addr1}, nil) - config.FeeManagerConfig = precompile.NewFeeManagerConfig(big.NewInt(0), []common.Address{addr1}, nil, nil) + config.Precompiles[deployerallowlist.Key] = deployerallowlist.NewContractDeployerAllowListConfig(big.NewInt(0), []common.Address{addr1}, nil) + config.Precompiles[feemanager.Key] = feemanager.NewFeeManagerConfig(big.NewInt(0), []common.Address{addr1}, nil, nil) gspec := &Genesis{ Config: &config, Alloc: GenesisAlloc{addr1: {Balance: genesisBalance}}, @@ -1594,7 +1596,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.ContractDeployerAllowListAddress, + To: &deployerallowlist.Address, Gas: 3_000_000, Value: common.Big0, GasFeeCap: feeCap, @@ -1609,22 +1611,22 @@ func TestStatefulPrecompiles(t *testing.T, create func(db ethdb.Database, chainC gen.AddTx(signedTx) }, verifyState: func(sdb *state.StateDB) error { - res := precompile.GetContractDeployerAllowListStatus(sdb, addr1) + res := deployerallowlist.GetContractDeployerAllowListStatus(sdb, addr1) if precompile.AllowListAdmin != res { return fmt.Errorf("unexpected allow list status for addr1 %s, expected %s", res, precompile.AllowListAdmin) } - res = precompile.GetContractDeployerAllowListStatus(sdb, addr2) + res = deployerallowlist.GetContractDeployerAllowListStatus(sdb, addr2) if precompile.AllowListAdmin != res { return fmt.Errorf("unexpected allow list status for addr2 %s, expected %s", res, precompile.AllowListAdmin) } return nil }, verifyGenesis: func(sdb *state.StateDB) { - res := precompile.GetContractDeployerAllowListStatus(sdb, addr1) + res := deployerallowlist.GetContractDeployerAllowListStatus(sdb, addr1) if precompile.AllowListAdmin != res { t.Fatalf("unexpected allow list status for addr1 %s, expected %s", res, precompile.AllowListAdmin) } - res = precompile.GetContractDeployerAllowListStatus(sdb, addr2) + res = deployerallowlist.GetContractDeployerAllowListStatus(sdb, addr2) if precompile.AllowListNoRole != res { t.Fatalf("unexpected allow list status for addr2 %s, expected %s", res, precompile.AllowListNoRole) } @@ -1633,14 +1635,14 @@ func TestStatefulPrecompiles(t *testing.T, create func(db ethdb.Database, chainC "fee manager set config": { addTx: func(gen *BlockGen) { feeCap := new(big.Int).Add(gen.BaseFee(), tip) - input, err := precompile.PackSetFeeConfig(testFeeConfig) + input, err := feemanager.PackSetFeeConfig(testFeeConfig) if err != nil { t.Fatal(err) } tx := types.NewTx(&types.DynamicFeeTx{ ChainID: params.TestChainConfig.ChainID, Nonce: gen.TxNonce(addr1), - To: &precompile.FeeConfigManagerAddress, + To: &feemanager.Address, Gas: 3_000_000, Value: common.Big0, GasFeeCap: feeCap, @@ -1655,10 +1657,10 @@ func TestStatefulPrecompiles(t *testing.T, create func(db ethdb.Database, chainC gen.AddTx(signedTx) }, verifyState: func(sdb *state.StateDB) error { - res := precompile.GetFeeConfigManagerStatus(sdb, addr1) + res := feemanager.GetFeeConfigManagerStatus(sdb, addr1) assert.Equal(precompile.AllowListAdmin, res) - storedConfig := precompile.GetStoredFeeConfig(sdb) + storedConfig := feemanager.GetStoredFeeConfig(sdb) assert.EqualValues(testFeeConfig, storedConfig) feeConfig, _, err := blockchain.GetFeeConfigAt(blockchain.CurrentHeader()) @@ -1667,7 +1669,7 @@ func TestStatefulPrecompiles(t *testing.T, create func(db ethdb.Database, chainC return nil }, verifyGenesis: func(sdb *state.StateDB) { - res := precompile.GetFeeConfigManagerStatus(sdb, addr1) + res := feemanager.GetFeeConfigManagerStatus(sdb, addr1) assert.Equal(precompile.AllowListAdmin, res) feeConfig, _, err := blockchain.GetFeeConfigAt(blockchain.Genesis().Header()) diff --git a/core/tx_pool.go b/core/tx_pool.go index e60e6a22ec..5b6db49534 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -42,7 +42,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" + "github.com/ava-labs/subnet-evm/precompile/feemanager" + "github.com/ava-labs/subnet-evm/precompile/txallowlist" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/prque" "github.com/ethereum/go-ethereum/event" @@ -693,10 +694,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) { - txAllowListRole := precompile.GetTxAllowListStatus(pool.currentState, from) + if pool.chainconfig.IsPrecompileEnabled(txallowlist.Key, headTimestamp) { + txAllowListRole := txallowlist.GetTxAllowListStatus(pool.currentState, from) if !txAllowListRole.IsEnabled() { - return fmt.Errorf("%w: %s", precompile.ErrSenderAddressNotAllowListed, from) + return fmt.Errorf("%w: %s", txallowlist.ErrSenderAddressNotAllowListed, from) } } return nil @@ -1445,7 +1446,7 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) { // without requiring FeeConfigManager 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.FeeConfigManagerAddress, new(big.Int).SetUint64(newHead.Time)) { + if pool.chainconfig.IsPrecompileEnabled(feemanager.Key, 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..ab52c59cf3 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -158,12 +158,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 precompile.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/evm.go b/core/vm/evm.go index bb4b14b3e8..baff568146 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -36,6 +36,7 @@ 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/deployerallowlist" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -507,8 +508,8 @@ 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) { - allowListRole := precompile.GetContractDeployerAllowListStatus(evm.StateDB, evm.TxContext.Origin) + if evm.chainRules.IsPrecompileEnabled(deployerallowlist.Address) { + 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 48e3b938ea..ea432bdfab 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/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.FeeConfigManagerAddress, new(big.Int).SetUint64(head.Time)) { + if oracle.backend.ChainConfig().IsPrecompileEnabled(feemanager.Key, 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 62e770564f..e5606f7084 100644 --- a/eth/gasprice/gasprice_test.go +++ b/eth/gasprice/gasprice_test.go @@ -40,7 +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/rpc" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -434,13 +434,13 @@ 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 = precompile.NewFeeManagerConfig(big.NewInt(0), []common.Address{addr}, nil, nil) + chainConfig.Precompiles[feemanager.Key] = feemanager.NewFeeManagerConfig(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) highFeeConfig := chainConfig.FeeConfig highFeeConfig.MinBaseFee = big.NewInt(28_000_000_000) - data, err := precompile.PackSetFeeConfig(highFeeConfig) + data, err := feemanager.PackSetFeeConfig(highFeeConfig) require.NoError(err) // before issuing the block changing the fee into the chain, the fee estimation should @@ -462,7 +462,7 @@ func TestSuggestGasPriceAfterFeeConfigUpdate(t *testing.T) { tx := types.NewTx(&types.DynamicFeeTx{ ChainID: chainConfig.ChainID, Nonce: b.TxNonce(addr), - To: &precompile.FeeConfigManagerAddress, + To: &feemanager.Address, 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..7e766b0e52 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -44,6 +44,7 @@ import ( "github.com/ava-labs/subnet-evm/core/vm" "github.com/ava-labs/subnet-evm/eth/tracers/logger" "github.com/ava-labs/subnet-evm/params" + "github.com/ava-labs/subnet-evm/precompile" "github.com/ava-labs/subnet-evm/rpc" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/davecgh/go-spew/spew" @@ -626,12 +627,20 @@ 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) map[string]precompile.StatefulPrecompileConfig { if blockTimestamp == nil { blockTimestampInt := s.b.CurrentHeader().Time blockTimestamp = new(big.Int).SetUint64(blockTimestampInt) } - return s.b.ChainConfig().GetActivePrecompileUpgrade(blockTimestamp) + res := make(map[string]precompile.StatefulPrecompileConfig) + for _, module := range precompile.RegisteredModules() { + key := module.Key() + if config := s.b.ChainConfig().GetPrecompileConfig(key, blockTimestamp); config != nil && !config.IsDisabled() { + res[key] = config + } + } + + return res } type FeeConfigResult struct { diff --git a/params/config.go b/params/config.go index 1407ce1bb8..3e31a094d3 100644 --- a/params/config.go +++ b/params/config.go @@ -83,7 +83,7 @@ var ( PetersburgBlock: big.NewInt(0), IstanbulBlock: big.NewInt(0), MuirGlacierBlock: big.NewInt(0), - + Precompiles: map[string]precompile.StatefulPrecompileConfig{}, NetworkUpgrades: NetworkUpgrades{ SubnetEVMTimestamp: big.NewInt(0), }, @@ -105,7 +105,7 @@ var ( IstanbulBlock: big.NewInt(0), MuirGlacierBlock: big.NewInt(0), NetworkUpgrades: NetworkUpgrades{big.NewInt(0)}, - PrecompileUpgrade: PrecompileUpgrade{}, + Precompiles: map[string]precompile.StatefulPrecompileConfig{}, UpgradeConfig: UpgradeConfig{}, } @@ -125,7 +125,7 @@ var ( IstanbulBlock: big.NewInt(0), MuirGlacierBlock: big.NewInt(0), NetworkUpgrades: NetworkUpgrades{}, - PrecompileUpgrade: PrecompileUpgrade{}, + Precompiles: map[string]precompile.StatefulPrecompileConfig{}, UpgradeConfig: UpgradeConfig{}, } ) @@ -157,9 +157,40 @@ 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 + Precompiles map[string]precompile.StatefulPrecompileConfig // 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. +} + +func (c *ChainConfig) UnmarshalJSON(data []byte) error { + // Alias ChainConfig to avoid recursion + type _ChainConfig ChainConfig + dec := _ChainConfig{} + err := json.Unmarshal(data, &dec) + if err != nil { + return err + } + + // At this point we have populated all fields except PrecompileUpgrade + *c = ChainConfig(dec) + // Unmarshal PrecompileUpgrade + raw := make(map[string]json.RawMessage) + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + c.Precompiles = make(map[string]precompile.StatefulPrecompileConfig) + for _, module := range precompile.RegisteredModules() { + key := module.Key() + if value, ok := raw[key]; ok { + conf := module.New() + err := conf.UnmarshalJSON(value) + if err != nil { + return err + } + c.Precompiles[key] = conf + } + } + return nil } // UpgradeConfig includes the following configs that may be specified in upgradeBytes: @@ -191,7 +222,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.Precompiles) if err != nil { precompileUpgradeBytes = []byte("cannot marshal PrecompileUpgrade") } @@ -273,8 +304,8 @@ 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) +func (c *ChainConfig) IsPrecompileEnabled(name string, blockTimestamp *big.Int) bool { + config := c.GetPrecompileConfig(name, blockTimestamp) return config != nil && !config.IsDisabled() } diff --git a/params/precompile_config.go b/params/precompile_config.go index cd629ced55..e1913fcfa4 100644 --- a/params/precompile_config.go +++ b/params/precompile_config.go @@ -4,12 +4,12 @@ package params import ( + "encoding/json" "fmt" "math/big" "github.com/ava-labs/subnet-evm/precompile" "github.com/ava-labs/subnet-evm/utils" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" ) @@ -17,36 +17,30 @@ import ( // each of the possible stateful precompile types that can be activated // as a network upgrade. type PrecompileUpgrade struct { - ContractDeployerAllowListConfig *precompile.ContractDeployerAllowListConfig `json:"contractDeployerAllowListConfig,omitempty"` // Config for the contract deployer allow list precompile - ContractNativeMinterConfig *precompile.ContractNativeMinterConfig `json:"contractNativeMinterConfig,omitempty"` // Config for the native minter precompile - TxAllowListConfig *precompile.TxAllowListConfig `json:"txAllowListConfig,omitempty"` // Config for the tx allow list precompile - FeeManagerConfig *precompile.FeeConfigManagerConfig `json:"feeManagerConfig,omitempty"` // Config for the fee manager precompile - RewardManagerConfig *precompile.RewardManagerConfig `json:"rewardManagerConfig,omitempty"` // Config for the reward manager precompile - // ADD YOUR PRECOMPILE HERE - // {YourPrecompile}Config *precompile.{YourPrecompile}Config `json:"{yourPrecompile}Config,omitempty"` + Config precompile.StatefulPrecompileConfig } -// 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.FeeConfigManagerAddress: - 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)) +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) != 1 { + return fmt.Errorf("PrecompileUpgrade must have exactly one key") + } + for key, value := range raw { + module, ok := precompile.GetPrecompileModule(key) + if !ok { + return fmt.Errorf("unknown precompile module: %s", key) + } + conf := module.New() + err := conf.UnmarshalJSON(value) + if err != nil { + return err + } + u.Config = conf + } + return nil } // verifyPrecompileUpgrades checks [c.PrecompileUpgrades] is well formed: @@ -58,69 +52,72 @@ func (p *PrecompileUpgrade) getByAddress(address common.Address) (precompile.Sta 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 + config := upgrade.Config + 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 + } - 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 + // Store this struct to keep track of the last upgrade for each precompile key. + // Required for timestamp and disabled checks. + type lastUpgradeData struct { + lastUpgraded *big.Int + disabled bool + } + + lastUpgradeMap := make(map[string]lastUpgradeData) + + // verify genesis precompiles + for key, config := range c.Precompiles { + if err := config.Verify(); err != nil { + return err } - if !hasKey { - return fmt.Errorf("empty precompile upgrade at index %d", i) + // check the genesis chain config for any enabled upgrade + lastUpgradeMap[key] = lastUpgradeData{ + disabled: false, + lastUpgraded: config.Timestamp(), } } - for _, address := range precompile.UsedAddresses { + // next range over upgrades to verify correct use of disabled and blockTimestamps. + for i, upgrade := range c.PrecompileUpgrades { + config := upgrade.Config + key := upgrade.Config.Key() + + lastUpgrade, ok := lastUpgradeMap[key] var ( - lastUpgraded *big.Int disabled bool + lastUpgraded *big.Int ) - // 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 - } + if !ok { disabled = false - lastUpgraded = config.Timestamp() + lastUpgraded = nil } else { - disabled = true + disabled = lastUpgrade.disabled + lastUpgraded = lastUpgrade.lastUpgraded } - // 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 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 - } + if err := config.Verify(); err != nil { + return err + } - disabled = config.IsDisabled() - lastUpgraded = config.Timestamp() + lastUpgradeMap[key] = lastUpgradeData{ + disabled: config.IsDisabled(), + lastUpgraded: config.Timestamp(), } } @@ -129,8 +126,8 @@ func (c *ChainConfig) verifyPrecompileUpgrades() error { // 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) +func (c *ChainConfig) getActivePrecompileConfig(key string, blockTimestamp *big.Int) precompile.StatefulPrecompileConfig { + configs := c.getActivatingPrecompileConfigs(key, nil, blockTimestamp, c.PrecompileUpgrades) if len(configs) == 0 { return nil } @@ -139,18 +136,19 @@ func (c *ChainConfig) getActivePrecompileConfig(blockTimestamp *big.Int, address // 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 { +func (c *ChainConfig) getActivatingPrecompileConfigs(key string, from *big.Int, to *big.Int, 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 config, ok := c.Precompiles[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 config, ok := upgrade.getByAddress(address); ok { + if upgrade.Config.Key() == key { + config := upgrade.Config // Check if the precompile activates in the specified range. if utils.IsForkTransition(config.Timestamp(), from, to) { configs = append(configs, config) @@ -160,40 +158,13 @@ func (c *ChainConfig) getActivatingPrecompileConfigs(from *big.Int, to *big.Int, return configs } -func (c *ChainConfig) GetPrecompileConfig(address common.Address, blockTimestamp *big.Int) precompile.StatefulPrecompileConfig { - if val := c.getActivePrecompileConfig(blockTimestamp, address, c.PrecompileUpgrades); val != nil { +func (c *ChainConfig) GetPrecompileConfig(key string, blockTimestamp *big.Int) precompile.StatefulPrecompileConfig { + if val := c.getActivePrecompileConfig(key, blockTimestamp); 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.(*precompile.ContractDeployerAllowListConfig) - } - if config := c.GetPrecompileConfig(precompile.ContractNativeMinterAddress, blockTimestamp); config != nil && !config.IsDisabled() { - pu.ContractNativeMinterConfig = config.(*precompile.ContractNativeMinterConfig) - } - if config := c.GetPrecompileConfig(precompile.TxAllowListAddress, blockTimestamp); config != nil && !config.IsDisabled() { - pu.TxAllowListConfig = config.(*precompile.TxAllowListConfig) - } - if config := c.GetPrecompileConfig(precompile.FeeConfigManagerAddress, blockTimestamp); config != nil && !config.IsDisabled() { - pu.FeeManagerConfig = config.(*precompile.FeeConfigManagerConfig) - } - if config := c.GetPrecompileConfig(precompile.RewardManagerAddress, blockTimestamp); config != nil && !config.IsDisabled() { - pu.RewardManagerConfig = config.(*precompile.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]. @@ -201,8 +172,8 @@ func (c *ChainConfig) GetActivePrecompileUpgrade(blockTimestamp *big.Int) Precom // 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 { + for _, module := range precompile.RegisteredModules() { + if err := c.checkPrecompileCompatible(module.Key(), precompileUpgrades, lastTimestamp); err != nil { return err } } @@ -213,10 +184,10 @@ func (c *ChainConfig) CheckPrecompilesCompatible(precompileUpgrades []Precompile // 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 { +func (c *ChainConfig) checkPrecompileCompatible(key string, 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) + activeUpgrades := c.getActivatingPrecompileConfigs(key, nil, lastTimestamp, c.PrecompileUpgrades) + newUpgrades := c.getActivatingPrecompileConfigs(key, nil, lastTimestamp, precompileUpgrades) // first, check existing upgrades are there for i, upgrade := range activeUpgrades { @@ -254,8 +225,8 @@ func (c *ChainConfig) checkPrecompileCompatible(address common.Address, precompi // 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 { + for _, module := range precompile.RegisteredModules() { + if config := c.getActivePrecompileConfig(module.Key(), blockTimestamp); config != nil { statefulPrecompileConfigs = append(statefulPrecompileConfigs, config) } } @@ -271,12 +242,13 @@ func (c *ChainConfig) EnabledStatefulPrecompiles(blockTimestamp *big.Int) []prec // - 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) { + for _, module := range precompile.RegisteredModules() { // Note: configure precompiles in a deterministic order. + key := module.Key() + for _, config := range c.getActivatingPrecompileConfigs(key, parentTimestamp, blockTimestamp, 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 + log.Info("Disabling precompile", "name", key) 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. @@ -284,9 +256,9 @@ func (c *ChainConfig) ConfigurePrecompiles(parentTimestamp *big.Int, blockContex // since Suicide will be committed after the reconfiguration. statedb.Finalise(true) } else { - log.Info("Activating new precompile", "precompileAddress", address, "config", config) + log.Info("Activating new precompile", "name", key, "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 fmt.Errorf("could not configure precompile, name: %s, reason: %w", key, err) } } } diff --git a/params/precompile_config_test.go b/params/precompile_config_test.go index 3ac584aec4..bfa9fb44f8 100644 --- a/params/precompile_config_test.go +++ b/params/precompile_config_test.go @@ -9,6 +9,9 @@ import ( "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/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -18,17 +21,17 @@ func TestVerifyWithChainConfig(t *testing.T) { admins := []common.Address{{1}} baseConfig := *SubnetEVMDefaultChainConfig config := &baseConfig - config.PrecompileUpgrade = PrecompileUpgrade{ - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(2), nil, nil), + config.Precompiles = map[string]precompile.StatefulPrecompileConfig{ + txallowlist.Key: txallowlist.NewTxAllowListConfig(big.NewInt(2), nil, nil), } config.PrecompileUpgrades = []PrecompileUpgrade{ { // disable TxAllowList at timestamp 4 - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(4)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(4)), }, { // re-enable TxAllowList at timestamp 5 - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(5), admins, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(5), admins, nil), }, } @@ -41,7 +44,7 @@ func TestVerifyWithChainConfig(t *testing.T) { badConfig.PrecompileUpgrades = append( badConfig.PrecompileUpgrades, PrecompileUpgrade{ - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(5)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(5)), }, ) err = badConfig.Verify() @@ -52,7 +55,7 @@ func TestVerifyWithChainConfig(t *testing.T) { badConfig.PrecompileUpgrades = append( badConfig.PrecompileUpgrades, PrecompileUpgrade{ - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(5), admins, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(5), admins, nil), }, ) err = badConfig.Verify() @@ -70,10 +73,10 @@ func TestVerifyPrecompileUpgrades(t *testing.T) { name: "enable and disable tx allow list", upgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(1), admins, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(1), admins, nil), }, { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(2)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(2)), }, }, expectedError: "", @@ -82,13 +85,13 @@ func TestVerifyPrecompileUpgrades(t *testing.T) { name: "invalid allow list config in tx allowlist", upgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(1), admins, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(1), admins, nil), }, { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(2)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(2)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(3), admins, admins), + txallowlist.NewTxAllowListConfig(big.NewInt(3), admins, admins), }, }, expectedError: "cannot set address", @@ -97,7 +100,7 @@ func TestVerifyPrecompileUpgrades(t *testing.T) { name: "invalid initial fee manager config", upgrades: []PrecompileUpgrade{ { - FeeManagerConfig: precompile.NewFeeManagerConfig(big.NewInt(3), admins, nil, + feemanager.NewFeeManagerConfig(big.NewInt(3), admins, nil, &commontype.FeeConfig{ GasLimit: big.NewInt(-1), }), @@ -109,7 +112,7 @@ func TestVerifyPrecompileUpgrades(t *testing.T) { name: "invalid initial fee manager config gas limit 0", upgrades: []PrecompileUpgrade{ { - FeeManagerConfig: precompile.NewFeeManagerConfig(big.NewInt(3), admins, nil, + feemanager.NewFeeManagerConfig(big.NewInt(3), admins, nil, &commontype.FeeConfig{ GasLimit: big.NewInt(0), }), @@ -139,20 +142,21 @@ func TestVerifyPrecompiles(t *testing.T) { admins := []common.Address{{1}} tests := []struct { name string - upgrade PrecompileUpgrade + upgrade map[string]precompile.StatefulPrecompileConfig expectedError string }{ { name: "invalid allow list config in tx allowlist", - upgrade: PrecompileUpgrade{ - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(3), admins, admins), + upgrade: map[string]precompile.StatefulPrecompileConfig{ + + txallowlist.Key: txallowlist.NewTxAllowListConfig(big.NewInt(3), admins, admins), }, expectedError: "cannot set address", }, { name: "invalid initial fee manager config", - upgrade: PrecompileUpgrade{ - FeeManagerConfig: precompile.NewFeeManagerConfig(big.NewInt(3), admins, nil, + upgrade: map[string]precompile.StatefulPrecompileConfig{ + feemanager.Key: feemanager.NewFeeManagerConfig(big.NewInt(3), admins, nil, &commontype.FeeConfig{ GasLimit: big.NewInt(-1), }), @@ -165,7 +169,7 @@ func TestVerifyPrecompiles(t *testing.T) { require := require.New(t) baseConfig := *SubnetEVMDefaultChainConfig config := &baseConfig - config.PrecompileUpgrade = tt.upgrade + config.Precompiles = tt.upgrade err := config.Verify() if tt.expectedError == "" { @@ -183,10 +187,10 @@ func TestVerifyRequiresSortedTimestamps(t *testing.T) { config := &baseConfig config.PrecompileUpgrades = []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(2), admins, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(2), admins, nil), }, { - ContractDeployerAllowListConfig: precompile.NewContractDeployerAllowListConfig(big.NewInt(1), admins, nil), + deployerallowlist.NewContractDeployerAllowListConfig(big.NewInt(1), admins, nil), }, } @@ -199,19 +203,19 @@ func TestGetPrecompileConfig(t *testing.T) { assert := assert.New(t) baseConfig := *SubnetEVMDefaultChainConfig config := &baseConfig - config.PrecompileUpgrade = PrecompileUpgrade{ - ContractDeployerAllowListConfig: precompile.NewContractDeployerAllowListConfig(big.NewInt(10), nil, nil), + config.Precompiles = map[string]precompile.StatefulPrecompileConfig{ + deployerallowlist.Key: deployerallowlist.NewContractDeployerAllowListConfig(big.NewInt(10), nil, nil), } - deployerConfig := config.GetPrecompileConfig(precompile.ContractDeployerAllowListAddress, big.NewInt(0)) + deployerConfig := config.GetPrecompileConfig(deployerallowlist.Key, big.NewInt(0)) assert.Nil(deployerConfig) - deployerConfig = config.GetPrecompileConfig(precompile.ContractDeployerAllowListAddress, big.NewInt(10)) + deployerConfig = config.GetPrecompileConfig(deployerallowlist.Key, big.NewInt(10)) assert.NotNil(deployerConfig) - deployerConfig = config.GetPrecompileConfig(precompile.ContractDeployerAllowListAddress, big.NewInt(11)) + deployerConfig = config.GetPrecompileConfig(deployerallowlist.Key, big.NewInt(11)) assert.NotNil(deployerConfig) - txAllowListConfig := config.GetPrecompileConfig(precompile.TxAllowListAddress, big.NewInt(0)) + txAllowListConfig := config.GetPrecompileConfig(txallowlist.Key, big.NewInt(0)) assert.Nil(txAllowListConfig) } diff --git a/params/upgrade_config_test.go b/params/upgrade_config_test.go index 38c93f5386..4338ae8340 100644 --- a/params/upgrade_config_test.go +++ b/params/upgrade_config_test.go @@ -7,7 +7,8 @@ import ( "math/big" "testing" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/deployerallowlist" + "github.com/ava-labs/subnet-evm/precompile/txallowlist" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" ) @@ -15,7 +16,7 @@ import ( func TestVerifyUpgradeConfig(t *testing.T) { admins := []common.Address{{1}} chainConfig := *TestChainConfig - chainConfig.TxAllowListConfig = precompile.NewTxAllowListConfig(big.NewInt(1), admins, nil) + chainConfig.Precompiles[txallowlist.Key] = txallowlist.NewTxAllowListConfig(big.NewInt(1), admins, nil) type test struct { upgrades []PrecompileUpgrade @@ -27,7 +28,7 @@ func TestVerifyUpgradeConfig(t *testing.T) { expectedErrorString: "disable should be [true]", upgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(2), admins, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(2), admins, nil), }, }, }, @@ -35,7 +36,7 @@ func TestVerifyUpgradeConfig(t *testing.T) { expectedErrorString: "config timestamp (0) <= previous timestamp (1)", upgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(0)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(0)), }, }, }, @@ -43,7 +44,7 @@ func TestVerifyUpgradeConfig(t *testing.T) { expectedErrorString: "config timestamp (1) <= previous timestamp (1)", upgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(1)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(1)), }, }, }, @@ -70,8 +71,8 @@ func TestVerifyUpgradeConfig(t *testing.T) { func TestCheckCompatibleUpgradeConfigs(t *testing.T) { admins := []common.Address{{1}} chainConfig := *TestChainConfig - chainConfig.TxAllowListConfig = precompile.NewTxAllowListConfig(big.NewInt(1), admins, nil) - chainConfig.ContractDeployerAllowListConfig = precompile.NewContractDeployerAllowListConfig(big.NewInt(10), admins, nil) + chainConfig.Precompiles[txallowlist.Key] = txallowlist.NewTxAllowListConfig(big.NewInt(1), admins, nil) + chainConfig.Precompiles[deployerallowlist.Key] = deployerallowlist.NewContractDeployerAllowListConfig(big.NewInt(10), admins, nil) type test struct { configs []*UpgradeConfig @@ -86,10 +87,10 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), admins, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(7), admins, nil), }, }, }, @@ -101,20 +102,20 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), admins, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(8), admins, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(8), admins, nil), }, }, }, @@ -127,20 +128,20 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), admins, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(8), admins, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(8), admins, nil), }, }, }, @@ -152,17 +153,17 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), admins, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), }, }, }, @@ -175,17 +176,17 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), admins, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), }, }, }, @@ -198,21 +199,21 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), admins, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), }, { // uses a different (empty) admin list, not allowed - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), []common.Address{}, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(7), []common.Address{}, nil), }, }, }, @@ -224,20 +225,20 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), admins, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + txallowlist.NewDisableTxAllowListConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), admins, nil), + txallowlist.NewTxAllowListConfig(big.NewInt(7), admins, nil), }, }, }, diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go index baf1a1c322..827436e198 100644 --- a/plugin/evm/vm_test.go +++ b/plugin/evm/vm_test.go @@ -20,12 +20,17 @@ 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/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/trie" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/api/keystore" @@ -2116,7 +2121,7 @@ func TestBuildAllowListActivationBlock(t *testing.T) { if err := genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM)); err != nil { t.Fatal(err) } - genesis.Config.ContractDeployerAllowListConfig = precompile.NewContractDeployerAllowListConfig(big.NewInt(time.Now().Unix()), testEthAddrs, nil) + genesis.Config.Precompiles[deployerallowlist.Key] = deployerallowlist.NewContractDeployerAllowListConfig(big.NewInt(time.Now().Unix()), testEthAddrs, nil) genesisJSON, err := genesis.MarshalJSON() if err != nil { @@ -2137,7 +2142,7 @@ func TestBuildAllowListActivationBlock(t *testing.T) { if err != nil { t.Fatal(err) } - role := precompile.GetContractDeployerAllowListStatus(genesisState, testEthAddrs[0]) + role := deployerallowlist.GetContractDeployerAllowListStatus(genesisState, testEthAddrs[0]) if role != precompile.AllowListNoRole { t.Fatalf("Expected allow list status to be set to no role: %s, but found: %s", precompile.AllowListNoRole, role) } @@ -2167,7 +2172,7 @@ func TestBuildAllowListActivationBlock(t *testing.T) { if err != nil { t.Fatal(err) } - role = precompile.GetContractDeployerAllowListStatus(blkState, testEthAddrs[0]) + role = deployerallowlist.GetContractDeployerAllowListStatus(blkState, testEthAddrs[0]) if role != precompile.AllowListAdmin { t.Fatalf("Expected allow list status to be set to Admin: %s, but found: %s", precompile.AllowListAdmin, role) } @@ -2180,7 +2185,7 @@ func TestTxAllowListSuccessfulTx(t *testing.T) { if err := genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM)); err != nil { t.Fatal(err) } - genesis.Config.TxAllowListConfig = precompile.NewTxAllowListConfig(big.NewInt(0), testEthAddrs[0:1], nil) + genesis.Config.Precompiles[txallowlist.Key] = txallowlist.NewTxAllowListConfig(big.NewInt(0), testEthAddrs[0:1], nil) genesisJSON, err := genesis.MarshalJSON() if err != nil { t.Fatal(err) @@ -2202,11 +2207,11 @@ func TestTxAllowListSuccessfulTx(t *testing.T) { } // Check that address 0 is whitelisted and address 1 is not - role := precompile.GetTxAllowListStatus(genesisState, testEthAddrs[0]) + role := txallowlist.GetTxAllowListStatus(genesisState, testEthAddrs[0]) if role != precompile.AllowListAdmin { t.Fatalf("Expected allow list status to be set to admin: %s, but found: %s", precompile.AllowListAdmin, role) } - role = precompile.GetTxAllowListStatus(genesisState, testEthAddrs[1]) + role = txallowlist.GetTxAllowListStatus(genesisState, testEthAddrs[1]) if role != precompile.AllowListNoRole { t.Fatalf("Expected allow list status to be set to no role: %s, but found: %s", precompile.AllowListNoRole, role) } @@ -2229,7 +2234,7 @@ func TestTxAllowListSuccessfulTx(t *testing.T) { } errs = vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1}) - if err := errs[0]; !errors.Is(err, precompile.ErrSenderAddressNotAllowListed) { + if err := errs[0]; !errors.Is(err, txallowlist.ErrSenderAddressNotAllowListed) { t.Fatalf("expected ErrSenderAddressNotAllowListed, got: %s", err) } @@ -2256,7 +2261,9 @@ func TestTxAllowListDisablePrecompile(t *testing.T) { t.Fatal(err) } enableAllowListTimestamp := time.Unix(0, 0) // enable at genesis - genesis.Config.TxAllowListConfig = precompile.NewTxAllowListConfig(big.NewInt(enableAllowListTimestamp.Unix()), testEthAddrs[0:1], nil) + genesis.Config.Precompiles = map[string]precompile.StatefulPrecompileConfig{ + txallowlist.Key: txallowlist.NewTxAllowListConfig(big.NewInt(enableAllowListTimestamp.Unix()), testEthAddrs[0:1], nil), + } genesisJSON, err := genesis.MarshalJSON() if err != nil { t.Fatal(err) @@ -2297,11 +2304,11 @@ func TestTxAllowListDisablePrecompile(t *testing.T) { } // Check that address 0 is whitelisted and address 1 is not - role := precompile.GetTxAllowListStatus(genesisState, testEthAddrs[0]) + role := txallowlist.GetTxAllowListStatus(genesisState, testEthAddrs[0]) if role != precompile.AllowListAdmin { t.Fatalf("Expected allow list status to be set to admin: %s, but found: %s", precompile.AllowListAdmin, role) } - role = precompile.GetTxAllowListStatus(genesisState, testEthAddrs[1]) + role = txallowlist.GetTxAllowListStatus(genesisState, testEthAddrs[1]) if role != precompile.AllowListNoRole { t.Fatalf("Expected allow list status to be set to no role: %s, but found: %s", precompile.AllowListNoRole, role) } @@ -2324,7 +2331,7 @@ func TestTxAllowListDisablePrecompile(t *testing.T) { } errs = vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1}) - if err := errs[0]; !errors.Is(err, precompile.ErrSenderAddressNotAllowListed) { + if err := errs[0]; !errors.Is(err, txallowlist.ErrSenderAddressNotAllowListed) { t.Fatalf("expected ErrSenderAddressNotAllowListed, got: %s", err) } @@ -2368,7 +2375,9 @@ func TestFeeManagerChangeFee(t *testing.T) { if err := genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM)); err != nil { t.Fatal(err) } - genesis.Config.FeeManagerConfig = precompile.NewFeeManagerConfig(big.NewInt(0), testEthAddrs[0:1], nil, nil) + genesis.Config.Precompiles = map[string]precompile.StatefulPrecompileConfig{ + feemanager.Key: feemanager.NewFeeManagerConfig(big.NewInt(0), testEthAddrs[0:1], nil, nil), + } // set a lower fee config now testLowFeeConfig := commontype.FeeConfig{ @@ -2406,13 +2415,13 @@ func TestFeeManagerChangeFee(t *testing.T) { } // Check that address 0 is whitelisted and address 1 is not - role := precompile.GetFeeConfigManagerStatus(genesisState, testEthAddrs[0]) + role := feemanager.GetFeeConfigManagerStatus(genesisState, testEthAddrs[0]) if role != precompile.AllowListAdmin { - t.Fatalf("Expected fee manager list status to be set to admin: %s, but found: %s", precompile.FeeConfigManagerAddress, role) + t.Fatalf("Expected fee manager list status to be set to admin: %s, but found: %s", feemanager.Address, role) } - role = precompile.GetFeeConfigManagerStatus(genesisState, testEthAddrs[1]) + role = feemanager.GetFeeConfigManagerStatus(genesisState, testEthAddrs[1]) if role != precompile.AllowListNoRole { - t.Fatalf("Expected fee manager list status to be set to no role: %s, but found: %s", precompile.FeeConfigManagerAddress, role) + t.Fatalf("Expected fee manager list status to be set to no role: %s, but found: %s", feemanager.Address, 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()) @@ -2424,13 +2433,13 @@ func TestFeeManagerChangeFee(t *testing.T) { testHighFeeConfig := testLowFeeConfig testHighFeeConfig.MinBaseFee = big.NewInt(28_000_000_000) - data, err := precompile.PackSetFeeConfig(testHighFeeConfig) - require.NoError(t, err) + data, err := feemanager.PackSetFeeConfig(testHighFeeConfig) + assert.NoError(t, err) tx := types.NewTx(&types.DynamicFeeTx{ ChainID: genesis.Config.ChainID, Nonce: uint64(0), - To: &precompile.FeeConfigManagerAddress, + To: &feemanager.Address, Gas: testLowFeeConfig.GasLimit.Uint64(), Value: common.Big0, GasFeeCap: testLowFeeConfig.MinBaseFee, // give low fee, it should work since we still haven't applied high fees @@ -2466,7 +2475,7 @@ func TestFeeManagerChangeFee(t *testing.T) { tx2 := types.NewTx(&types.DynamicFeeTx{ ChainID: genesis.Config.ChainID, Nonce: uint64(1), - To: &precompile.FeeConfigManagerAddress, + To: &feemanager.Address, Gas: genesis.Config.FeeConfig.GasLimit.Uint64(), Value: common.Big0, GasFeeCap: testLowFeeConfig.MinBaseFee, // this is too low for applied config, should fail @@ -2608,7 +2617,9 @@ func TestRewardManagerPrecompileSetRewardAddress(t *testing.T) { genesis := &core.Genesis{} require.NoError(t, genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM))) - genesis.Config.RewardManagerConfig = precompile.NewRewardManagerConfig(common.Big0, testEthAddrs[0:1], nil, nil) + genesis.Config.Precompiles = map[string]precompile.StatefulPrecompileConfig{ + rewardmanager.Key: rewardmanager.NewRewardManagerConfig(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) @@ -2649,12 +2660,12 @@ func TestRewardManagerPrecompileSetRewardAddress(t *testing.T) { vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan) testAddr := common.HexToAddress("0x9999991111") - data, err := precompile.PackSetRewardAddress(testAddr) + data, err := rewardmanager.PackSetRewardAddress(testAddr) require.NoError(t, err) - gas := 21000 + 240 + precompile.SetRewardAddressGasCost // 21000 for tx, 240 for tx data + 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.Address, big.NewInt(1), gas, big.NewInt(testMinGasPrice), data) signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm.chainConfig.ChainID), testKeys[0]) require.NoError(t, err) @@ -2748,7 +2759,9 @@ func TestRewardManagerPrecompileAllowFeeRecipients(t *testing.T) { genesis := &core.Genesis{} require.NoError(t, genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM))) - genesis.Config.RewardManagerConfig = precompile.NewRewardManagerConfig(common.Big0, testEthAddrs[0:1], nil, nil) + genesis.Config.Precompiles = map[string]precompile.StatefulPrecompileConfig{ + rewardmanager.Key: rewardmanager.NewRewardManagerConfig(common.Big0, testEthAddrs[0:1], nil, nil), + } genesis.Config.AllowFeeRecipients = false // disable this in genesis genesisJSON, err := genesis.MarshalJSON() require.NoError(t, err) @@ -2785,12 +2798,12 @@ func TestRewardManagerPrecompileAllowFeeRecipients(t *testing.T) { newTxPoolHeadChan := make(chan core.NewTxPoolReorgEvent, 1) vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan) - data, err := precompile.PackAllowFeeRecipients() + data, err := rewardmanager.PackAllowFeeRecipients() require.NoError(t, err) - gas := 21000 + 240 + precompile.AllowFeeRecipientsGasCost // 21000 for tx, 240 for tx data + 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.Address, 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 22c78b5800..3a3ad1218d 100644 --- a/plugin/evm/vm_upgrade_bytes_test.go +++ b/plugin/evm/vm_upgrade_bytes_test.go @@ -18,7 +18,7 @@ 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/stretchr/testify/assert" ) @@ -28,7 +28,7 @@ func TestVMUpgradeBytesPrecompile(t *testing.T) { upgradeConfig := ¶ms.UpgradeConfig{ PrecompileUpgrades: []params.PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(enableAllowListTimestamp.Unix()), testEthAddrs[0:1], nil), + Config: txallowlist.NewTxAllowListConfig(big.NewInt(enableAllowListTimestamp.Unix()), testEthAddrs[0:1], nil), }, }, } @@ -57,7 +57,7 @@ func TestVMUpgradeBytesPrecompile(t *testing.T) { t.Fatal(err) } errs = vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1}) - if err := errs[0]; !errors.Is(err, precompile.ErrSenderAddressNotAllowListed) { + if err := errs[0]; !errors.Is(err, txallowlist.ErrSenderAddressNotAllowListed) { t.Fatalf("expected ErrSenderAddressNotAllowListed, got: %s", err) } @@ -71,7 +71,7 @@ func TestVMUpgradeBytesPrecompile(t *testing.T) { upgradeConfig.PrecompileUpgrades = append( upgradeConfig.PrecompileUpgrades, params.PrecompileUpgrade{ - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(disableAllowListTimestamp.Unix())), + Config: txallowlist.NewDisableTxAllowListConfig(big.NewInt(disableAllowListTimestamp.Unix())), }, ) upgradeBytesJSON, err = json.Marshal(upgradeConfig) @@ -108,7 +108,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, precompile.ErrSenderAddressNotAllowListed) { + if err := errs[0]; !errors.Is(err, txallowlist.ErrSenderAddressNotAllowListed) { t.Fatalf("expected ErrSenderAddressNotAllowListed, got: %s", err) } diff --git a/precompile/allow_list.go b/precompile/allow_list.go index b4f75becdb..9e1f867a51 100644 --- a/precompile/allow_list.go +++ b/precompile/allow_list.go @@ -18,8 +18,8 @@ const ( SetNoneFuncKey = "setNone" ReadAllowListFuncKey = "readAllowList" - ModifyAllowListGasCost = writeGasCostPerSlot - ReadAllowListGasCost = readGasCostPerSlot + ModifyAllowListGasCost = WriteGasCostPerSlot + ReadAllowListGasCost = ReadGasCostPerSlot ) var ( @@ -48,10 +48,10 @@ type AllowListConfig struct { // the addresses in [AllowListAdmins]. func (c *AllowListConfig) Configure(state StateDB, precompileAddr common.Address) error { for _, enabledAddr := range c.EnabledAddresses { - setAllowListRole(state, precompileAddr, enabledAddr, AllowListEnabled) + SetAllowListRole(state, precompileAddr, enabledAddr, AllowListEnabled) } for _, adminAddr := range c.AllowListAdmins { - setAllowListRole(state, precompileAddr, adminAddr, AllowListAdmin) + SetAllowListRole(state, precompileAddr, adminAddr, AllowListAdmin) } return nil } @@ -112,18 +112,18 @@ func (c *AllowListConfig) Verify() error { return nil } -// getAllowListStatus returns the allow list role of [address] for the precompile +// GetAllowListStatus returns the allow list role of [address] for the precompile // at [precompileAddr] -func getAllowListStatus(state StateDB, precompileAddr common.Address, address common.Address) AllowListRole { +func GetAllowListStatus(state 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 +// SetAllowListRole sets the permissions of [address] to [role] for the precompile // at [precompileAddr]. // assumes [role] has already been verified as valid. -func setAllowListRole(stateDB StateDB, precompileAddr, address common.Address, role AllowListRole) { +func SetAllowListRole(stateDB StateDB, precompileAddr, address common.Address, role AllowListRole) { // Generate the state key for [address] addressKey := address.Hash() // Assign [role] to the address @@ -139,7 +139,7 @@ func setAllowListRole(stateDB StateDB, precompileAddr, address common.Address, r // 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, selectorLen+common.HashLength) + input := make([]byte, 0, SelectorLen+common.HashLength) switch role { case AllowListAdmin: @@ -158,7 +158,7 @@ func PackModifyAllowList(address common.Address, role AllowListRole) ([]byte, er // PackReadAllowList packs [address] into the input data to the read allow list function func PackReadAllowList(address common.Address) []byte { - input := make([]byte, 0, selectorLen+common.HashLength) + input := make([]byte, 0, SelectorLen+common.HashLength) input = append(input, readAllowListSignature...) input = append(input, address.Hash().Bytes()...) return input @@ -168,7 +168,7 @@ func PackReadAllowList(address common.Address) []byte { // This execution function is speciifc to [precompileAddr]. func createAllowListRoleSetter(precompileAddr common.Address, role AllowListRole) RunStatefulPrecompileFunc { return func(evm PrecompileAccessibleState, callerAddr, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = deductGas(suppliedGas, ModifyAllowListGasCost); err != nil { + if remainingGas, err = DeductGas(suppliedGas, ModifyAllowListGasCost); err != nil { return nil, 0, err } @@ -185,12 +185,12 @@ func createAllowListRoleSetter(precompileAddr common.Address, role AllowListRole 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) + callerStatus := GetAllowListStatus(stateDB, precompileAddr, callerAddr) if !callerStatus.IsAdmin() { return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotModifyAllowList, callerAddr) } - setAllowListRole(stateDB, precompileAddr, modifyAddress, role) + SetAllowListRole(stateDB, precompileAddr, modifyAddress, role) // Return an empty output and the remaining gas return []byte{}, remainingGas, nil } @@ -201,7 +201,7 @@ func createAllowListRoleSetter(precompileAddr common.Address, role AllowListRole // designated role of that address func createReadAllowList(precompileAddr common.Address) RunStatefulPrecompileFunc { return func(evm PrecompileAccessibleState, callerAddr common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = deductGas(suppliedGas, ReadAllowListGasCost); err != nil { + if remainingGas, err = DeductGas(suppliedGas, ReadAllowListGasCost); err != nil { return nil, 0, err } @@ -210,16 +210,16 @@ func createReadAllowList(precompileAddr common.Address) RunStatefulPrecompileFun } readAddress := common.BytesToAddress(input) - role := getAllowListStatus(evm.GetStateDB(), precompileAddr, readAddress) + 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) StatefulPrecompiledContract { +func CreateAllowListPrecompile(precompileAddr common.Address) StatefulPrecompiledContract { // Construct the contract with no fallback function. - allowListFuncs := createAllowListFunctions(precompileAddr) + allowListFuncs := CreateAllowListFunctions(precompileAddr) contract, err := NewStatefulPrecompileContract(nil, allowListFuncs) // TODO Change this to be returned as an error after refactoring this precompile // to use the new precompile template. @@ -229,11 +229,11 @@ func createAllowListPrecompile(precompileAddr common.Address) StatefulPrecompile return contract } -func createAllowListFunctions(precompileAddr common.Address) []*statefulPrecompileFunction { - setAdmin := newStatefulPrecompileFunction(setAdminSignature, createAllowListRoleSetter(precompileAddr, AllowListAdmin)) - setEnabled := newStatefulPrecompileFunction(setEnabledSignature, createAllowListRoleSetter(precompileAddr, AllowListEnabled)) - setNone := newStatefulPrecompileFunction(setNoneSignature, createAllowListRoleSetter(precompileAddr, AllowListNoRole)) - read := newStatefulPrecompileFunction(readAllowListSignature, createReadAllowList(precompileAddr)) +func CreateAllowListFunctions(precompileAddr common.Address) []*StatefulPrecompileFunction { + setAdmin := NewStatefulPrecompileFunction(setAdminSignature, createAllowListRoleSetter(precompileAddr, AllowListAdmin)) + setEnabled := NewStatefulPrecompileFunction(setEnabledSignature, createAllowListRoleSetter(precompileAddr, AllowListEnabled)) + setNone := NewStatefulPrecompileFunction(setNoneSignature, createAllowListRoleSetter(precompileAddr, AllowListNoRole)) + read := NewStatefulPrecompileFunction(readAllowListSignature, createReadAllowList(precompileAddr)) - return []*statefulPrecompileFunction{setAdmin, setEnabled, setNone, read} + return []*StatefulPrecompileFunction{setAdmin, setEnabled, setNone, read} } diff --git a/precompile/config_test.go b/precompile/config_test.go deleted file mode 100644 index a396cec543..0000000000 --- a/precompile/config_test.go +++ /dev/null @@ -1,462 +0,0 @@ -// (c) 2022 Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package precompile - -import ( - "math/big" - "testing" - - "github.com/ava-labs/subnet-evm/commontype" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/math" - "github.com/stretchr/testify/require" -) - -var validFeeConfig = 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 TestVerifyPrecompileUpgrades(t *testing.T) { - admins := []common.Address{{1}} - enableds := []common.Address{{2}} - tests := []struct { - name string - config StatefulPrecompileConfig - expectedError string - }{ - { - name: "invalid allow list config in tx allowlist", - config: NewTxAllowListConfig(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), - expectedError: "", - }, - { - name: "empty member allow list config in tx allowlist", - config: NewTxAllowListConfig(big.NewInt(3), []common.Address{}, []common.Address{}), - expectedError: "", - }, - { - name: "valid allow list config in tx allowlist", - config: NewTxAllowListConfig(big.NewInt(3), admins, enableds), - expectedError: "", - }, - { - name: "invalid allow list config in deployer allowlist", - config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, admins), - expectedError: "cannot set address", - }, - { - name: "invalid allow list config in native minter allowlist", - config: NewContractNativeMinterConfig(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), - expectedError: "duplicate address", - }, - { - name: "duplicate enableds in config in native minter allowlist", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, append(enableds, enableds[0]), nil), - expectedError: "duplicate address", - }, - { - name: "invalid allow list config in fee manager allowlist", - config: NewFeeManagerConfig(big.NewInt(3), admins, admins, nil), - expectedError: "cannot set address", - }, - { - name: "invalid initial fee manager config", - config: NewFeeManagerConfig(big.NewInt(3), admins, nil, - &commontype.FeeConfig{ - GasLimit: big.NewInt(0), - }), - expectedError: "gasLimit = 0 cannot be less than or equal to 0", - }, - { - name: "nil amount in native minter config", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, - map[common.Address]*math.HexOrDecimal256{ - common.HexToAddress("0x01"): math.NewHexOrDecimal256(123), - common.HexToAddress("0x02"): nil, - }), - expectedError: "initial mint cannot contain nil", - }, - { - name: "negative amount in native minter config", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, - map[common.Address]*math.HexOrDecimal256{ - common.HexToAddress("0x01"): math.NewHexOrDecimal256(123), - common.HexToAddress("0x02"): math.NewHexOrDecimal256(-1), - }), - expectedError: "initial mint cannot contain invalid amount", - }, - { - name: "duplicate enableds in config in reward manager allowlist", - config: NewRewardManagerConfig(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{ - AllowFeeRecipients: true, - RewardAddress: common.HexToAddress("0x01"), - }), - expectedError: ErrCannotEnableBothRewards.Error(), - }, - } - 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 TestEqualTxAllowListConfig(t *testing.T) { - admins := []common.Address{{1}} - enableds := []common.Address{{2}} - tests := []struct { - name string - config StatefulPrecompileConfig - other StatefulPrecompileConfig - expected bool - }{ - { - name: "non-nil config and nil other", - config: NewTxAllowListConfig(big.NewInt(3), admins, enableds), - other: nil, - expected: false, - }, - { - name: "different type", - config: NewTxAllowListConfig(big.NewInt(3), admins, enableds), - other: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), - expected: false, - }, - { - name: "different admin", - config: NewTxAllowListConfig(big.NewInt(3), admins, enableds), - other: NewTxAllowListConfig(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}}), - expected: false, - }, - { - name: "different timestamp", - config: NewTxAllowListConfig(big.NewInt(3), admins, enableds), - other: NewTxAllowListConfig(big.NewInt(4), admins, enableds), - expected: false, - }, - { - name: "same config", - config: NewTxAllowListConfig(big.NewInt(3), admins, enableds), - other: NewTxAllowListConfig(big.NewInt(3), 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)) - }) - } -} - -func TestEqualContractDeployerAllowListConfig(t *testing.T) { - admins := []common.Address{{1}} - enableds := []common.Address{{2}} - tests := []struct { - name string - config StatefulPrecompileConfig - other StatefulPrecompileConfig - expected bool - }{ - { - name: "non-nil config and nil other", - config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), - other: nil, - expected: false, - }, - { - name: "different type", - config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), - other: NewTxAllowListConfig(big.NewInt(3), admins, enableds), - expected: false, - }, - { - name: "different admin", - config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), - other: NewContractDeployerAllowListConfig(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}}), - expected: false, - }, - { - name: "different timestamp", - config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), - other: NewContractDeployerAllowListConfig(big.NewInt(4), admins, enableds), - expected: false, - }, - { - name: "same config", - config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), - other: NewContractDeployerAllowListConfig(big.NewInt(3), 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)) - }) - } -} - -func TestEqualContractNativeMinterConfig(t *testing.T) { - admins := []common.Address{{1}} - enableds := []common.Address{{2}} - tests := []struct { - name string - config StatefulPrecompileConfig - other StatefulPrecompileConfig - expected bool - }{ - { - name: "non-nil config and nil other", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, enableds, nil), - other: nil, - expected: false, - }, - { - name: "different type", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, enableds, nil), - other: NewTxAllowListConfig(big.NewInt(3), []common.Address{{1}}, []common.Address{{2}}), - expected: false, - }, - { - name: "different timestamps", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, nil), - other: NewContractNativeMinterConfig(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), - expected: false, - }, - { - name: "different initial mint amounts", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, - map[common.Address]*math.HexOrDecimal256{ - common.HexToAddress("0x01"): math.NewHexOrDecimal256(1), - }), - other: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, - map[common.Address]*math.HexOrDecimal256{ - common.HexToAddress("0x01"): math.NewHexOrDecimal256(2), - }), - expected: false, - }, - { - name: "different initial mint addresses", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, - map[common.Address]*math.HexOrDecimal256{ - common.HexToAddress("0x01"): math.NewHexOrDecimal256(1), - }), - other: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, - map[common.Address]*math.HexOrDecimal256{ - common.HexToAddress("0x02"): math.NewHexOrDecimal256(1), - }), - expected: false, - }, - - { - name: "same config", - config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, - map[common.Address]*math.HexOrDecimal256{ - common.HexToAddress("0x01"): math.NewHexOrDecimal256(1), - }), - other: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, - map[common.Address]*math.HexOrDecimal256{ - common.HexToAddress("0x01"): math.NewHexOrDecimal256(1), - }), - 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)) - }) - } -} - -func TestEqualFeeConfigManagerConfig(t *testing.T) { - admins := []common.Address{{1}} - enableds := []common.Address{{2}} - tests := []struct { - name string - config StatefulPrecompileConfig - other StatefulPrecompileConfig - expected bool - }{ - { - name: "non-nil config and nil other", - config: NewFeeManagerConfig(big.NewInt(3), admins, enableds, nil), - other: nil, - expected: false, - }, - { - name: "different type", - config: NewFeeManagerConfig(big.NewInt(3), admins, enableds, nil), - other: NewTxAllowListConfig(big.NewInt(3), []common.Address{{1}}, []common.Address{{2}}), - expected: false, - }, - { - name: "different timestamp", - config: NewFeeManagerConfig(big.NewInt(3), admins, nil, nil), - other: NewFeeManagerConfig(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), - 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), - expected: false, - }, - { - name: "different initial config", - config: NewFeeManagerConfig(big.NewInt(3), admins, nil, &validFeeConfig), - other: NewFeeManagerConfig(big.NewInt(3), admins, nil, - func() *commontype.FeeConfig { - c := validFeeConfig - c.GasLimit = big.NewInt(123) - return &c - }()), - expected: false, - }, - { - name: "same config", - config: NewFeeManagerConfig(big.NewInt(3), admins, nil, &validFeeConfig), - other: NewFeeManagerConfig(big.NewInt(3), admins, nil, &validFeeConfig), - 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)) - }) - } -} - -func TestEqualRewardManagerConfig(t *testing.T) { - admins := []common.Address{{1}} - enableds := []common.Address{{2}} - tests := []struct { - name string - config StatefulPrecompileConfig - other StatefulPrecompileConfig - expected bool - }{ - { - name: "non-nil config and nil other", - config: NewRewardManagerConfig(big.NewInt(3), admins, enableds, nil), - other: nil, - expected: false, - }, - { - name: "different type", - config: NewRewardManagerConfig(big.NewInt(3), admins, enableds, nil), - other: NewTxAllowListConfig(big.NewInt(3), []common.Address{{1}}, []common.Address{{2}}), - expected: false, - }, - { - name: "different timestamp", - config: NewRewardManagerConfig(big.NewInt(3), admins, nil, nil), - other: NewRewardManagerConfig(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), - expected: false, - }, - { - name: "non-nil initial config and nil initial config", - config: NewRewardManagerConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ - AllowFeeRecipients: true, - }), - other: NewRewardManagerConfig(big.NewInt(3), admins, nil, nil), - expected: false, - }, - { - name: "different initial config", - config: NewRewardManagerConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ - RewardAddress: common.HexToAddress("0x01"), - }), - other: NewRewardManagerConfig(big.NewInt(3), admins, nil, - &InitialRewardConfig{ - RewardAddress: common.HexToAddress("0x02"), - }), - expected: false, - }, - { - name: "same config", - config: NewRewardManagerConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ - RewardAddress: common.HexToAddress("0x01"), - }), - other: NewRewardManagerConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ - RewardAddress: common.HexToAddress("0x01"), - }), - 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/contract.go b/precompile/contract.go index 69ed968641..596ea6dc00 100644 --- a/precompile/contract.go +++ b/precompile/contract.go @@ -5,76 +5,24 @@ package precompile import ( "fmt" - "math/big" - "github.com/ava-labs/avalanchego/snow" - "github.com/ava-labs/subnet-evm/commontype" "github.com/ethereum/go-ethereum/common" ) const ( - selectorLen = 4 + 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) -// 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 -} - -// ChainContext defines an interface that provides information to a stateful precompile -// about the chain configuration. The precompile can access this information to initialize -// its state. -type ChainConfig interface { - // GetFeeConfig returns the original FeeConfig that was set in the genesis. - GetFeeConfig() commontype.FeeConfig - // AllowedFeeRecipients returns true if fee recipients are allowed in the genesis. - AllowedFeeRecipients() bool -} - -// StateDB is the interface for accessing EVM state -type StateDB interface { - GetState(common.Address, common.Hash) common.Hash - SetState(common.Address, common.Hash, common.Hash) - - SetCode(common.Address, []byte) - - SetNonce(common.Address, uint64) - GetNonce(common.Address) uint64 - - GetBalance(common.Address) *big.Int - AddBalance(common.Address, *big.Int) - SubBalance(common.Address, *big.Int) - - CreateAccount(common.Address) - Exist(common.Address) bool - - AddLog(addr common.Address, topics []common.Hash, data []byte, blockNumber uint64) - - Suicide(common.Address) bool - Finalise(deleteEmptyObjects bool) -} - // 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) } -// statefulPrecompileFunction defines a function implemented by a stateful precompile -type statefulPrecompileFunction struct { +// StatefulPrecompileFunction defines a function implemented by a stateful precompile +type StatefulPrecompileFunction struct { // selector is the 4 byte function selector for this function // This should be calculated from the function signature using CalculateFunctionSelector selector []byte @@ -82,9 +30,9 @@ type statefulPrecompileFunction struct { execute RunStatefulPrecompileFunc } -// newStatefulPrecompileFunction creates a stateful precompile function with the given arguments -func newStatefulPrecompileFunction(selector []byte, execute RunStatefulPrecompileFunc) *statefulPrecompileFunction { - return &statefulPrecompileFunction{ +// NewStatefulPrecompileFunction creates a stateful precompile function with the given arguments +func NewStatefulPrecompileFunction(selector []byte, execute RunStatefulPrecompileFunc) *StatefulPrecompileFunction { + return &StatefulPrecompileFunction{ selector: selector, execute: execute, } @@ -95,16 +43,16 @@ func newStatefulPrecompileFunction(selector []byte, execute RunStatefulPrecompil // Note: because we only ever read from [functions] there no lock is required to make it thread-safe. type statefulPrecompileWithFunctionSelectors struct { fallback RunStatefulPrecompileFunc - functions map[string]*statefulPrecompileFunction + functions map[string]*StatefulPrecompileFunction } // NewStatefulPrecompileContract generates new StatefulPrecompile using [functions] as the available functions and [fallback] // as an optional fallback if there is no input data. Note: the selector of [fallback] will be ignored, so it is required to be left empty. -func NewStatefulPrecompileContract(fallback RunStatefulPrecompileFunc, functions []*statefulPrecompileFunction) (StatefulPrecompiledContract, error) { +func NewStatefulPrecompileContract(fallback RunStatefulPrecompileFunc, functions []*StatefulPrecompileFunction) (StatefulPrecompiledContract, error) { // Construct the contract and populate [functions]. contract := &statefulPrecompileWithFunctionSelectors{ fallback: fallback, - functions: make(map[string]*statefulPrecompileFunction), + functions: make(map[string]*StatefulPrecompileFunction), } for _, function := range functions { _, exists := contract.functions[string(function.selector)] @@ -126,13 +74,13 @@ func (s *statefulPrecompileWithFunctionSelectors) Run(accessibleState Precompile } // Otherwise, an unexpected input size will result in an error. - if len(input) < selectorLen { + if len(input) < SelectorLen { return nil, suppliedGas, fmt.Errorf("missing function selector to precompile - input length (%d)", len(input)) } // Use the function selector to grab the correct function - selector := input[:selectorLen] - functionInput := input[selectorLen:] + selector := input[:SelectorLen] + functionInput := input[SelectorLen:] function, ok := s.functions[string(selector)] if !ok { return nil, suppliedGas, fmt.Errorf("invalid function selector %#x", selector) diff --git a/precompile/contract_native_minter.go b/precompile/contract_native_minter.go deleted file mode 100644 index 26ccaf8e68..0000000000 --- a/precompile/contract_native_minter.go +++ /dev/null @@ -1,227 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package precompile - -import ( - "encoding/json" - "errors" - "fmt" - "math/big" - - "github.com/ava-labs/subnet-evm/utils" - "github.com/ava-labs/subnet-evm/vmerrs" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/math" -) - -const ( - mintInputAddressSlot = iota - mintInputAmountSlot - - mintInputLen = common.HashLength + common.HashLength - - MintGasCost = 30_000 -) - -var ( - _ StatefulPrecompileConfig = &ContractNativeMinterConfig{} - // Singleton StatefulPrecompiledContract for minting native assets by permissioned callers. - ContractNativeMinterPrecompile StatefulPrecompiledContract = createNativeMinterPrecompile(ContractNativeMinterAddress) - - mintSignature = CalculateFunctionSelector("mintNativeCoin(address,uint256)") // address, amount - ErrCannotMint = errors.New("non-enabled cannot mint") -) - -// ContractNativeMinterConfig wraps [AllowListConfig] and uses it to implement the StatefulPrecompileConfig -// interface while adding in the ContractNativeMinter specific precompile address. -type ContractNativeMinterConfig struct { - AllowListConfig - 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: AllowListConfig{ - AllowListAdmins: admins, - EnabledAddresses: enableds, - }, - UpgradeableConfig: 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: UpgradeableConfig{ - BlockTimestamp: blockTimestamp, - Disable: true, - }, - } -} - -// Address returns the address of the native minter contract. -func (c *ContractNativeMinterConfig) Address() common.Address { - return ContractNativeMinterAddress -} - -// Configure configures [state] with the desired admins based on [c]. -func (c *ContractNativeMinterConfig) Configure(_ ChainConfig, state StateDB, _ BlockContext) error { - for to, amount := range c.InitialMint { - if amount != nil { - bigIntAmount := (*big.Int)(amount) - state.AddBalance(to, bigIntAmount) - } - } - - return c.AllowListConfig.Configure(state, ContractNativeMinterAddress) -} - -// Contract returns the singleton stateful precompiled contract to be used for the native minter. -func (c *ContractNativeMinterConfig) Contract() 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 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) -} - -// GetContractNativeMinterStatus returns the role of [address] for the minter list. -func GetContractNativeMinterStatus(stateDB StateDB, address common.Address) AllowListRole { - return getAllowListStatus(stateDB, ContractNativeMinterAddress, address) -} - -// SetContractNativeMinterStatus sets the permissions of [address] to [role] for the -// minter list. assumes [role] has already been verified as valid. -func SetContractNativeMinterStatus(stateDB StateDB, address common.Address, role AllowListRole) { - setAllowListRole(stateDB, ContractNativeMinterAddress, 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, selectorLen+mintInputLen) - err := packOrderedHashesWithSelector(res, mintSignature, []common.Hash{ - address.Hash(), - common.BigToHash(amount), - }) - - return res, err -} - -// UnpackMintInput attempts to unpack [input] into the arguments to the mint precompile -// assumes that [input] does not include selector (omits first 4 bytes in PackMintInput) -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(returnPackedHash(input, mintInputAddressSlot)) - assetAmount := new(big.Int).SetBytes(returnPackedHash(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 PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = deductGas(suppliedGas, MintGasCost); err != nil { - return nil, 0, err - } - - if readOnly { - return nil, remainingGas, vmerrs.ErrWriteProtection - } - - to, amount, err := UnpackMintInput(input) - if err != nil { - return nil, remainingGas, err - } - - stateDB := accessibleState.GetStateDB() - // Verify that the caller is in the allow list and therefore has the right to modify it - callerStatus := getAllowListStatus(stateDB, ContractNativeMinterAddress, caller) - if !callerStatus.IsEnabled() { - return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotMint, caller) - } - - // if there is no address in the state, create one. - if !stateDB.Exist(to) { - stateDB.CreateAccount(to) - } - - stateDB.AddBalance(to, amount) - // Return an empty output and the remaining gas - return []byte{}, remainingGas, nil -} - -// createNativeMinterPrecompile returns a StatefulPrecompiledContract with R/W control of an allow list at [precompileAddr] and a native coin minter. -func createNativeMinterPrecompile(precompileAddr common.Address) StatefulPrecompiledContract { - enabledFuncs := createAllowListFunctions(precompileAddr) - - mintFunc := newStatefulPrecompileFunction(mintSignature, mintNativeCoin) - - enabledFuncs = append(enabledFuncs, mintFunc) - // Construct the contract with no fallback function. - contract, err := NewStatefulPrecompileContract(nil, enabledFuncs) - // 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 -} diff --git a/precompile/contract_deployer_allow_list.go b/precompile/deployerallowlist/config.go similarity index 56% rename from precompile/contract_deployer_allow_list.go rename to precompile/deployerallowlist/config.go index 11df6ff3fe..4876a7540e 100644 --- a/precompile/contract_deployer_allow_list.go +++ b/precompile/deployerallowlist/config.go @@ -1,37 +1,46 @@ // (c) 2019-2020, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package precompile +package deployerallowlist import ( "encoding/json" "math/big" + "github.com/ava-labs/subnet-evm/precompile" "github.com/ethereum/go-ethereum/common" ) var ( - _ StatefulPrecompileConfig = &ContractDeployerAllowListConfig{} - // Singleton StatefulPrecompiledContract for W/R access to the contract deployer allow list. - ContractDeployerAllowListPrecompile StatefulPrecompiledContract = createAllowListPrecompile(ContractDeployerAllowListAddress) + _ precompile.StatefulPrecompileConfig = &ContractDeployerAllowListConfig{} + + Address = common.HexToAddress("0x0200000000000000000000000000000000000000") + Key = "contractDeployerAllowListConfig" ) // ContractDeployerAllowListConfig wraps [AllowListConfig] and uses it to implement the StatefulPrecompileConfig // interface while adding in the contract deployer specific precompile address. type ContractDeployerAllowListConfig struct { - AllowListConfig - UpgradeableConfig + precompile.AllowListConfig + precompile.UpgradeableConfig +} + +func init() { + err := precompile.RegisterModule(ContractDeployerAllowListConfig{}) + if err != nil { + panic(err) + } } // 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: AllowListConfig{ + AllowListConfig: precompile.AllowListConfig{ AllowListAdmins: admins, EnabledAddresses: enableds, }, - UpgradeableConfig: UpgradeableConfig{BlockTimestamp: blockTimestamp}, + UpgradeableConfig: precompile.UpgradeableConfig{BlockTimestamp: blockTimestamp}, } } @@ -39,7 +48,7 @@ func NewContractDeployerAllowListConfig(blockTimestamp *big.Int, admins []common // that disables ContractDeployerAllowList. func NewDisableContractDeployerAllowListConfig(blockTimestamp *big.Int) *ContractDeployerAllowListConfig { return &ContractDeployerAllowListConfig{ - UpgradeableConfig: UpgradeableConfig{ + UpgradeableConfig: precompile.UpgradeableConfig{ BlockTimestamp: blockTimestamp, Disable: true, }, @@ -47,22 +56,22 @@ func NewDisableContractDeployerAllowListConfig(blockTimestamp *big.Int) *Contrac } // Address returns the address of the contract deployer allow list. -func (c *ContractDeployerAllowListConfig) Address() common.Address { - return ContractDeployerAllowListAddress +func (ContractDeployerAllowListConfig) Address() common.Address { + return Address } // Configure configures [state] with the desired admins based on [c]. -func (c *ContractDeployerAllowListConfig) Configure(_ ChainConfig, state StateDB, _ BlockContext) error { - return c.AllowListConfig.Configure(state, ContractDeployerAllowListAddress) +func (c *ContractDeployerAllowListConfig) Configure(_ precompile.ChainConfig, state precompile.StateDB, _ precompile.BlockContext) error { + return c.AllowListConfig.Configure(state, Address) } // Contract returns the singleton stateful precompiled contract to be used for the allow list. -func (c *ContractDeployerAllowListConfig) Contract() StatefulPrecompiledContract { +func (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 StatefulPrecompileConfig) bool { +func (c *ContractDeployerAllowListConfig) Equal(s precompile.StatefulPrecompileConfig) bool { // typecast before comparison other, ok := (s).(*ContractDeployerAllowListConfig) if !ok { @@ -77,15 +86,19 @@ func (c *ContractDeployerAllowListConfig) String() string { return string(bytes) } -// GetContractDeployerAllowListStatus returns the role of [address] for the contract deployer -// allow list. -func GetContractDeployerAllowListStatus(stateDB StateDB, address common.Address) AllowListRole { - return getAllowListStatus(stateDB, ContractDeployerAllowListAddress, address) +func (c ContractDeployerAllowListConfig) Key() string { + return Key } -// 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 StateDB, address common.Address, role AllowListRole) { - setAllowListRole(stateDB, ContractDeployerAllowListAddress, address, role) +func (ContractDeployerAllowListConfig) New() precompile.StatefulPrecompileConfig { + return new(ContractDeployerAllowListConfig) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (c *ContractDeployerAllowListConfig) UnmarshalJSON(b []byte) error { + type Alias ContractDeployerAllowListConfig + if err := json.Unmarshal(b, (*Alias)(c)); err != nil { + return err + } + return nil } diff --git a/precompile/deployerallowlist/config_test.go b/precompile/deployerallowlist/config_test.go new file mode 100644 index 0000000000..2ffb0be77c --- /dev/null +++ b/precompile/deployerallowlist/config_test.go @@ -0,0 +1,95 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package deployerallowlist + +import ( + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/precompile" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestVerifyContractDeployerConfig(t *testing.T) { + admins := []common.Address{{1}} + tests := []struct { + name string + config precompile.StatefulPrecompileConfig + expectedError string + }{ + { + name: "invalid allow list config in deployer allowlist", + config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, admins), + expectedError: "cannot set address", + }, + } + 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 TestEqualContractDeployerAllowListConfig(t *testing.T) { + admins := []common.Address{{1}} + enableds := []common.Address{{2}} + tests := []struct { + name string + config precompile.StatefulPrecompileConfig + other precompile.StatefulPrecompileConfig + expected bool + }{ + { + name: "non-nil config and nil other", + config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), + other: nil, + expected: false, + }, + { + name: "different type", + config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), + other: precompile.NewNoopStatefulPrecompileConfig(), + expected: false, + }, + { + name: "different admin", + config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), + other: NewContractDeployerAllowListConfig(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}}), + expected: false, + }, + { + name: "different timestamp", + config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), + other: NewContractDeployerAllowListConfig(big.NewInt(4), admins, enableds), + expected: false, + }, + { + name: "same config", + config: NewContractDeployerAllowListConfig(big.NewInt(3), admins, enableds), + other: NewContractDeployerAllowListConfig(big.NewInt(3), 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/deployerallowlist/contract.go b/precompile/deployerallowlist/contract.go new file mode 100644 index 0000000000..0c5591a752 --- /dev/null +++ b/precompile/deployerallowlist/contract.go @@ -0,0 +1,27 @@ +// (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/ethereum/go-ethereum/common" +) + +var ( + // Singleton StatefulPrecompiledContract for W/R access to the contract deployer allow list. + ContractDeployerAllowListPrecompile precompile.StatefulPrecompiledContract = precompile.CreateAllowListPrecompile(Address) +) + +// GetContractDeployerAllowListStatus returns the role of [address] for the contract deployer +// allow list. +func GetContractDeployerAllowListStatus(stateDB precompile.StateDB, address common.Address) precompile.AllowListRole { + return precompile.GetAllowListStatus(stateDB, Address, 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 precompile.AllowListRole) { + precompile.SetAllowListRole(stateDB, Address, address, role) +} diff --git a/precompile/deployerallowlist/contract_test.go b/precompile/deployerallowlist/contract_test.go new file mode 100644 index 0000000000..5c86787306 --- /dev/null +++ b/precompile/deployerallowlist/contract_test.go @@ -0,0 +1,211 @@ +// (c) 2019-2020, 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" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +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 := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListAdmin) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetContractDeployerAllowListStatus(state, noRoleAddr) + require.Equal(t, precompile.AllowListAdmin, res) + }, + }, + "set deployer": { + caller: adminAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListEnabled) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetContractDeployerAllowListStatus(state, noRoleAddr) + require.Equal(t, precompile.AllowListEnabled, res) + }, + }, + "set no role": { + caller: adminAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListNoRole) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetContractDeployerAllowListStatus(state, adminAddr) + require.Equal(t, precompile.AllowListNoRole, res) + }, + }, + "set no role from non-admin": { + caller: noRoleAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListNoRole) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedErr: precompile.ErrCannotModifyAllowList.Error(), + }, + "set deployer from non-admin": { + caller: noRoleAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListEnabled) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedErr: precompile.ErrCannotModifyAllowList.Error(), + }, + "set admin from non-admin": { + caller: noRoleAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListAdmin) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedErr: precompile.ErrCannotModifyAllowList.Error(), + }, + "set no role with readOnly enabled": { + caller: adminAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListNoRole) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: true, + expectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "set no role insufficient gas": { + caller: adminAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListNoRole) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost - 1, + readOnly: false, + expectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "read allow list no role": { + caller: noRoleAddr, + input: func() []byte { + return precompile.PackReadAllowList(noRoleAddr) + }, + suppliedGas: precompile.ReadAllowListGasCost, + readOnly: false, + expectedRes: common.Hash(precompile.AllowListNoRole).Bytes(), + assertState: nil, + }, + "read allow list admin role": { + caller: adminAddr, + input: func() []byte { + return precompile.PackReadAllowList(noRoleAddr) + }, + suppliedGas: precompile.ReadAllowListGasCost, + readOnly: false, + expectedRes: common.Hash(precompile.AllowListNoRole).Bytes(), + assertState: nil, + }, + "read allow list with readOnly enabled": { + caller: adminAddr, + input: func() []byte { + return precompile.PackReadAllowList(noRoleAddr) + }, + suppliedGas: precompile.ReadAllowListGasCost, + readOnly: true, + expectedRes: common.Hash(precompile.AllowListNoRole).Bytes(), + assertState: nil, + }, + "read allow list out of gas": { + caller: adminAddr, + input: func() []byte { + return precompile.PackReadAllowList(noRoleAddr) + }, + suppliedGas: precompile.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, precompile.AllowListAdmin) + SetContractDeployerAllowListStatus(state, noRoleAddr, precompile.AllowListNoRole) + require.Equal(t, precompile.AllowListAdmin, GetContractDeployerAllowListStatus(state, adminAddr)) + require.Equal(t, precompile.AllowListNoRole, GetContractDeployerAllowListStatus(state, noRoleAddr)) + + blockContext := precompile.NewMockBlockContext(common.Big0, 0) + accesibleState := precompile.NewMockAccessibleState(state, blockContext, snow.DefaultContextTest()) + ret, remainingGas, err := ContractDeployerAllowListPrecompile.Run(accesibleState, test.caller, Address, 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/feemanager/config.go b/precompile/feemanager/config.go new file mode 100644 index 0000000000..2f7d4eff9d --- /dev/null +++ b/precompile/feemanager/config.go @@ -0,0 +1,140 @@ +// (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/ethereum/go-ethereum/common" +) + +var ( + _ precompile.StatefulPrecompileConfig = &FeeConfigManagerConfig{} + + Address = common.HexToAddress("0x0200000000000000000000000000000000000003") + Key = "feeManagerConfig" +) + +// FeeConfigManagerConfig wraps [AllowListConfig] and uses it to implement the StatefulPrecompileConfig +// interface while adding in the FeeConfigManager specific precompile address. +type FeeConfigManagerConfig struct { + precompile.AllowListConfig // Config for the fee config manager allow list + precompile.UpgradeableConfig + InitialFeeConfig *commontype.FeeConfig `json:"initialFeeConfig,omitempty"` // initial fee config to be immediately activated +} + +func init() { + err := precompile.RegisterModule(FeeConfigManagerConfig{}) + if err != nil { + panic(err) + } +} + +// NewFeeManagerConfig returns a config for a network upgrade at [blockTimestamp] that enables +// FeeConfigManager 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) *FeeConfigManagerConfig { + return &FeeConfigManagerConfig{ + AllowListConfig: precompile.AllowListConfig{ + AllowListAdmins: admins, + EnabledAddresses: enableds, + }, + UpgradeableConfig: precompile.UpgradeableConfig{BlockTimestamp: blockTimestamp}, + InitialFeeConfig: initialConfig, + } +} + +// NewDisableFeeManagerConfig returns config for a network upgrade at [blockTimestamp] +// that disables FeeConfigManager. +func NewDisableFeeManagerConfig(blockTimestamp *big.Int) *FeeConfigManagerConfig { + return &FeeConfigManagerConfig{ + UpgradeableConfig: precompile.UpgradeableConfig{ + BlockTimestamp: blockTimestamp, + Disable: true, + }, + } +} + +// Address returns the address of the fee config manager contract. +func (c FeeConfigManagerConfig) Address() common.Address { + return Address +} + +// Equal returns true if [s] is a [*FeeConfigManagerConfig] and it has been configured identical to [c]. +func (c *FeeConfigManagerConfig) Equal(s precompile.StatefulPrecompileConfig) bool { + // typecast before comparison + other, ok := (s).(*FeeConfigManagerConfig) + 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 *FeeConfigManagerConfig) Configure(chainConfig precompile.ChainConfig, state precompile.StateDB, blockContext precompile.BlockContext) error { + // Store the initial fee config into the state when the fee config 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, Address) +} + +// Contract returns the singleton stateful precompiled contract to be used for the fee manager. +func (c FeeConfigManagerConfig) Contract() precompile.StatefulPrecompiledContract { + return FeeConfigManagerPrecompile +} + +func (c *FeeConfigManagerConfig) 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 FeeConfigManagerConfig. +func (c *FeeConfigManagerConfig) String() string { + bytes, _ := json.Marshal(c) + return string(bytes) +} + +func (c FeeConfigManagerConfig) Key() string { + return Key +} + +func (FeeConfigManagerConfig) New() precompile.StatefulPrecompileConfig { + return new(FeeConfigManagerConfig) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (c *FeeConfigManagerConfig) UnmarshalJSON(b []byte) error { + type Alias FeeConfigManagerConfig + if err := json.Unmarshal(b, (*Alias)(c)); err != nil { + return err + } + return nil +} diff --git a/precompile/feemanager/config_test.go b/precompile/feemanager/config_test.go new file mode 100644 index 0000000000..ed45821d6a --- /dev/null +++ b/precompile/feemanager/config_test.go @@ -0,0 +1,128 @@ +// (c) 2022 Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package feemanager + +import ( + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/commontype" + "github.com/ava-labs/subnet-evm/precompile" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +var validFeeConfig = 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 TestVerifyFeeManagerConfig(t *testing.T) { + admins := []common.Address{{1}} + tests := []struct { + name string + config precompile.StatefulPrecompileConfig + expectedError string + }{ + { + name: "invalid allow list config in fee manager allowlist", + config: NewFeeManagerConfig(big.NewInt(3), admins, admins, nil), + expectedError: "cannot set address", + }, + { + name: "invalid initial fee manager config", + config: NewFeeManagerConfig(big.NewInt(3), admins, nil, + &commontype.FeeConfig{ + GasLimit: big.NewInt(0), + }), + expectedError: "gasLimit = 0 cannot be less than or equal to 0", + }, + } + 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 TestEqualFeeConfigManagerConfig(t *testing.T) { + admins := []common.Address{{1}} + enableds := []common.Address{{2}} + tests := []struct { + name string + config precompile.StatefulPrecompileConfig + other precompile.StatefulPrecompileConfig + expected bool + }{ + { + name: "non-nil config and nil other", + config: NewFeeManagerConfig(big.NewInt(3), admins, enableds, nil), + other: nil, + expected: false, + }, + { + name: "different type", + config: NewFeeManagerConfig(big.NewInt(3), admins, enableds, nil), + other: precompile.NewNoopStatefulPrecompileConfig(), + expected: false, + }, + { + name: "different timestamp", + config: NewFeeManagerConfig(big.NewInt(3), admins, nil, nil), + other: NewFeeManagerConfig(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), + 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), + expected: false, + }, + { + name: "different initial config", + config: NewFeeManagerConfig(big.NewInt(3), admins, nil, &validFeeConfig), + other: NewFeeManagerConfig(big.NewInt(3), admins, nil, + func() *commontype.FeeConfig { + c := validFeeConfig + c.GasLimit = big.NewInt(123) + return &c + }()), + expected: false, + }, + { + name: "same config", + config: NewFeeManagerConfig(big.NewInt(3), admins, nil, &validFeeConfig), + other: NewFeeManagerConfig(big.NewInt(3), admins, nil, &validFeeConfig), + 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/fee_config_manager.go b/precompile/feemanager/contract.go similarity index 56% rename from precompile/fee_config_manager.go rename to precompile/feemanager/contract.go index 56567d840a..8c20bee6fe 100644 --- a/precompile/fee_config_manager.go +++ b/precompile/feemanager/contract.go @@ -1,15 +1,15 @@ // (c) 2019-2020, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package precompile +package feemanager import ( - "encoding/json" "errors" "fmt" "math/big" "github.com/ava-labs/subnet-evm/commontype" + "github.com/ava-labs/subnet-evm/precompile" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" ) @@ -32,130 +32,35 @@ const ( // [numFeeConfigField] fields in FeeConfig struct feeConfigInputLen = common.HashLength * numFeeConfigField - SetFeeConfigGasCost = writeGasCostPerSlot * (numFeeConfigField + 1) // plus one for setting last changed at - GetFeeConfigGasCost = readGasCostPerSlot * numFeeConfigField - GetLastChangedAtGasCost = readGasCostPerSlot + SetFeeConfigGasCost = precompile.WriteGasCostPerSlot * (numFeeConfigField + 1) // plus one for setting last changed at + GetFeeConfigGasCost = precompile.ReadGasCostPerSlot * numFeeConfigField + GetLastChangedAtGasCost = precompile.ReadGasCostPerSlot ) var ( - _ StatefulPrecompileConfig = &FeeConfigManagerConfig{} + _ precompile.StatefulPrecompileConfig = &FeeConfigManagerConfig{} // Singleton StatefulPrecompiledContract for setting fee configs by permissioned callers. - FeeConfigManagerPrecompile StatefulPrecompiledContract = createFeeConfigManagerPrecompile(FeeConfigManagerAddress) + FeeConfigManagerPrecompile precompile.StatefulPrecompiledContract = createFeeConfigManagerPrecompile(Address) - setFeeConfigSignature = CalculateFunctionSelector("setFeeConfig(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)") - getFeeConfigSignature = CalculateFunctionSelector("getFeeConfig()") - getFeeConfigLastChangedAtSignature = CalculateFunctionSelector("getFeeConfigLastChangedAt()") + setFeeConfigSignature = precompile.CalculateFunctionSelector("setFeeConfig(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)") + getFeeConfigSignature = precompile.CalculateFunctionSelector("getFeeConfig()") + getFeeConfigLastChangedAtSignature = precompile.CalculateFunctionSelector("getFeeConfigLastChangedAt()") feeConfigLastChangedAtKey = common.Hash{'l', 'c', 'a'} ErrCannotChangeFee = errors.New("non-enabled cannot change fee config") ) -// FeeConfigManagerConfig wraps [AllowListConfig] and uses it to implement the StatefulPrecompileConfig -// interface while adding in the FeeConfigManager specific precompile address. -type FeeConfigManagerConfig struct { - AllowListConfig // Config for the fee config manager allow list - 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 -// FeeConfigManager 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) *FeeConfigManagerConfig { - return &FeeConfigManagerConfig{ - AllowListConfig: AllowListConfig{ - AllowListAdmins: admins, - EnabledAddresses: enableds, - }, - UpgradeableConfig: UpgradeableConfig{BlockTimestamp: blockTimestamp}, - InitialFeeConfig: initialConfig, - } -} - -// NewDisableFeeManagerConfig returns config for a network upgrade at [blockTimestamp] -// that disables FeeConfigManager. -func NewDisableFeeManagerConfig(blockTimestamp *big.Int) *FeeConfigManagerConfig { - return &FeeConfigManagerConfig{ - UpgradeableConfig: UpgradeableConfig{ - BlockTimestamp: blockTimestamp, - Disable: true, - }, - } -} - -// Address returns the address of the fee config manager contract. -func (c *FeeConfigManagerConfig) Address() common.Address { - return FeeConfigManagerAddress -} - -// Equal returns true if [s] is a [*FeeConfigManagerConfig] and it has been configured identical to [c]. -func (c *FeeConfigManagerConfig) Equal(s StatefulPrecompileConfig) bool { - // typecast before comparison - other, ok := (s).(*FeeConfigManagerConfig) - 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 *FeeConfigManagerConfig) Configure(chainConfig ChainConfig, state StateDB, blockContext BlockContext) error { - // Store the initial fee config into the state when the fee config 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, FeeConfigManagerAddress) -} - -// Contract returns the singleton stateful precompiled contract to be used for the fee manager. -func (c *FeeConfigManagerConfig) Contract() StatefulPrecompiledContract { - return FeeConfigManagerPrecompile -} - -func (c *FeeConfigManagerConfig) 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 FeeConfigManagerConfig. -func (c *FeeConfigManagerConfig) String() string { - bytes, _ := json.Marshal(c) - return string(bytes) -} - // GetFeeConfigManagerStatus returns the role of [address] for the fee config manager list. -func GetFeeConfigManagerStatus(stateDB StateDB, address common.Address) AllowListRole { - return getAllowListStatus(stateDB, FeeConfigManagerAddress, address) +func GetFeeConfigManagerStatus(stateDB precompile.StateDB, address common.Address) precompile.AllowListRole { + return precompile.GetAllowListStatus(stateDB, Address, address) } // SetFeeConfigManagerStatus sets the permissions of [address] to [role] for the // fee config manager list. assumes [role] has already been verified as valid. -func SetFeeConfigManagerStatus(stateDB StateDB, address common.Address, role AllowListRole) { - setAllowListRole(stateDB, FeeConfigManagerAddress, address, role) +func SetFeeConfigManagerStatus(stateDB precompile.StateDB, address common.Address, role precompile.AllowListRole) { + precompile.SetAllowListRole(stateDB, Address, address, role) } // PackGetFeeConfigInput packs the getFeeConfig signature @@ -194,12 +99,12 @@ func packFeeConfigHelper(feeConfig commontype.FeeConfig, useSelector bool) ([]by if useSelector { res := make([]byte, len(setFeeConfigSignature)+feeConfigInputLen) - err := packOrderedHashesWithSelector(res, setFeeConfigSignature, hashes) + err := precompile.PackOrderedHashesWithSelector(res, setFeeConfigSignature, hashes) return res, err } res := make([]byte, len(hashes)*common.HashLength) - err := packOrderedHashes(res, hashes) + err := precompile.PackOrderedHashes(res, hashes) return res, err } @@ -212,7 +117,7 @@ func UnpackFeeConfigInput(input []byte) (commontype.FeeConfig, error) { feeConfig := commontype.FeeConfig{} for i := minFeeConfigFieldKey; i <= numFeeConfigField; i++ { listIndex := i - 1 - packedElement := returnPackedHash(input, listIndex) + packedElement := precompile.PackedHash(input, listIndex) switch i { case gasLimitKey: feeConfig.GasLimit = new(big.Int).SetBytes(packedElement) @@ -239,10 +144,10 @@ func UnpackFeeConfigInput(input []byte) (commontype.FeeConfig, error) { } // GetStoredFeeConfig returns fee config from contract storage in given state -func GetStoredFeeConfig(stateDB StateDB) commontype.FeeConfig { +func GetStoredFeeConfig(stateDB precompile.StateDB) commontype.FeeConfig { feeConfig := commontype.FeeConfig{} for i := minFeeConfigFieldKey; i <= numFeeConfigField; i++ { - val := stateDB.GetState(FeeConfigManagerAddress, common.Hash{byte(i)}) + val := stateDB.GetState(Address, common.Hash{byte(i)}) switch i { case gasLimitKey: feeConfig.GasLimit = new(big.Int).Set(val.Big()) @@ -268,14 +173,14 @@ func GetStoredFeeConfig(stateDB StateDB) commontype.FeeConfig { return feeConfig } -func GetFeeConfigLastChangedAt(stateDB StateDB) *big.Int { - val := stateDB.GetState(FeeConfigManagerAddress, feeConfigLastChangedAtKey) +func GetFeeConfigLastChangedAt(stateDB precompile.StateDB) *big.Int { + val := stateDB.GetState(Address, 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 StateDB, feeConfig commontype.FeeConfig, blockContext BlockContext) error { +func StoreFeeConfig(stateDB precompile.StateDB, feeConfig commontype.FeeConfig, blockContext precompile.BlockContext) error { if err := feeConfig.Verify(); err != nil { return fmt.Errorf("cannot verify fee config: %w", err) } @@ -303,22 +208,22 @@ func StoreFeeConfig(stateDB StateDB, feeConfig commontype.FeeConfig, blockContex // This should never encounter an unknown fee config key panic(fmt.Sprintf("unknown fee config key: %d", i)) } - stateDB.SetState(FeeConfigManagerAddress, common.Hash{byte(i)}, input) + stateDB.SetState(Address, common.Hash{byte(i)}, input) } blockNumber := blockContext.Number() if blockNumber == nil { return fmt.Errorf("blockNumber cannot be nil") } - stateDB.SetState(FeeConfigManagerAddress, feeConfigLastChangedAtKey, common.BigToHash(blockNumber)) + stateDB.SetState(Address, 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 PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = deductGas(suppliedGas, SetFeeConfigGasCost); err != nil { +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 { return nil, 0, err } @@ -333,7 +238,7 @@ func setFeeConfig(accessibleState PrecompileAccessibleState, caller common.Addre stateDB := accessibleState.GetStateDB() // Verify that the caller is in the allow list and therefore has the right to modify it - callerStatus := getAllowListStatus(stateDB, FeeConfigManagerAddress, caller) + callerStatus := precompile.GetAllowListStatus(stateDB, Address, caller) if !callerStatus.IsEnabled() { return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotChangeFee, caller) } @@ -348,8 +253,8 @@ func setFeeConfig(accessibleState PrecompileAccessibleState, caller common.Addre // 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 PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = deductGas(suppliedGas, GetFeeConfigGasCost); err != nil { +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 { return nil, 0, err } @@ -366,8 +271,8 @@ func getFeeConfig(accessibleState PrecompileAccessibleState, caller common.Addre // 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 PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = deductGas(suppliedGas, GetLastChangedAtGasCost); err != nil { +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 { return nil, 0, err } @@ -380,16 +285,16 @@ func getFeeConfigLastChangedAt(accessibleState PrecompileAccessibleState, caller // createFeeConfigManagerPrecompile 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 createFeeConfigManagerPrecompile(precompileAddr common.Address) StatefulPrecompiledContract { - feeConfigManagerFunctions := createAllowListFunctions(precompileAddr) +func createFeeConfigManagerPrecompile(precompileAddr common.Address) precompile.StatefulPrecompiledContract { + feeConfigManagerFunctions := precompile.CreateAllowListFunctions(precompileAddr) - setFeeConfigFunc := newStatefulPrecompileFunction(setFeeConfigSignature, setFeeConfig) - getFeeConfigFunc := newStatefulPrecompileFunction(getFeeConfigSignature, getFeeConfig) - getFeeConfigLastChangedAtFunc := newStatefulPrecompileFunction(getFeeConfigLastChangedAtSignature, getFeeConfigLastChangedAt) + setFeeConfigFunc := precompile.NewStatefulPrecompileFunction(setFeeConfigSignature, setFeeConfig) + getFeeConfigFunc := precompile.NewStatefulPrecompileFunction(getFeeConfigSignature, getFeeConfig) + getFeeConfigLastChangedAtFunc := precompile.NewStatefulPrecompileFunction(getFeeConfigLastChangedAtSignature, getFeeConfigLastChangedAt) feeConfigManagerFunctions = append(feeConfigManagerFunctions, setFeeConfigFunc, getFeeConfigFunc, getFeeConfigLastChangedAtFunc) // Construct the contract with no fallback function. - contract, err := NewStatefulPrecompileContract(nil, feeConfigManagerFunctions) + contract, err := precompile.NewStatefulPrecompileContract(nil, feeConfigManagerFunctions) // 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/feemanager/contract_test.go b/precompile/feemanager/contract_test.go new file mode 100644 index 0000000000..89ef0eb194 --- /dev/null +++ b/precompile/feemanager/contract_test.go @@ -0,0 +1,300 @@ +// (c) 2019-2020, 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/params" + "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +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), + } + + testBlockNumber = big.NewInt(7) +) + +func TestFeeConfigManagerRun(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 *FeeConfigManagerConfig + + 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 := 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: &FeeConfigManagerConfig{ + 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, precompile.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: &FeeConfigManagerConfig{ + 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, precompile.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(), + }, + "set allow role from admin": { + caller: adminAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListEnabled) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetFeeConfigManagerStatus(state, noRoleAddr) + require.Equal(t, precompile.AllowListEnabled, res) + }, + }, + "set allow role from non-admin fails": { + caller: enabledAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListEnabled) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedErr: precompile.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. + SetFeeConfigManagerStatus(state, adminAddr, precompile.AllowListAdmin) + SetFeeConfigManagerStatus(state, enabledAddr, precompile.AllowListEnabled) + SetFeeConfigManagerStatus(state, noRoleAddr, precompile.AllowListNoRole) + + if test.preCondition != nil { + test.preCondition(t, state) + } + blockContext := precompile.NewMockBlockContext(testBlockNumber, 0) + accesibleState := precompile.NewMockAccessibleState(state, blockContext, snow.DefaultContextTest()) + if test.config != nil { + test.config.Configure(params.TestChainConfig, state, blockContext) + } + ret, remainingGas, err := FeeConfigManagerPrecompile.Run(accesibleState, test.caller, Address, 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/interface.go b/precompile/interface.go new file mode 100644 index 0000000000..82be0f749a --- /dev/null +++ b/precompile/interface.go @@ -0,0 +1,61 @@ +// (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" +) + +// 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 +} + +// ChainContext defines an interface that provides information to a stateful precompile +// about the chain configuration. The precompile can access this information to initialize +// its state. +type ChainConfig interface { + // GetFeeConfig returns the original FeeConfig that was set in the genesis. + GetFeeConfig() commontype.FeeConfig + // AllowedFeeRecipients returns true if fee recipients are allowed in the genesis. + AllowedFeeRecipients() bool +} + +// StateDB is the interface for accessing EVM state +type StateDB interface { + GetState(common.Address, common.Hash) common.Hash + SetState(common.Address, common.Hash, common.Hash) + + SetCode(common.Address, []byte) + + SetNonce(common.Address, uint64) + GetNonce(common.Address) uint64 + + GetBalance(common.Address) *big.Int + AddBalance(common.Address, *big.Int) + SubBalance(common.Address, *big.Int) + + CreateAccount(common.Address) + Exist(common.Address) bool + + AddLog(addr common.Address, topics []common.Hash, data []byte, blockNumber uint64) + + Suicide(common.Address) bool + Finalise(deleteEmptyObjects bool) +} diff --git a/precompile/mock_interface.go b/precompile/mock_interface.go new file mode 100644 index 0000000000..2ed7bf20af --- /dev/null +++ b/precompile/mock_interface.go @@ -0,0 +1,128 @@ +// (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 "" +} + +func (n *noopStatefulPrecompileConfig) Key() string { + return "" +} + +func (noopStatefulPrecompileConfig) New() StatefulPrecompileConfig { + return new(noopStatefulPrecompileConfig) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (c *noopStatefulPrecompileConfig) UnmarshalJSON(b []byte) error { + return nil +} diff --git a/precompile/module.go b/precompile/module.go new file mode 100644 index 0000000000..c79f3f51ab --- /dev/null +++ b/precompile/module.go @@ -0,0 +1,113 @@ +// (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 ( + // This list is kept just for reference. The actual addresses defined in respective packages of precompiles. + // 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??") + + registeredModules = make(map[string]StatefulPrecompileModule, 0) + // Sorted with the addresses in ascending order + sortedModules = make([]StatefulPrecompileModule, 0) + + 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 RegisterModule(stm StatefulPrecompileModule) error { + address := stm.Address() + key := stm.Key() + if !ReservedAddress(address) { + return fmt.Errorf("address %s not in a reserved range", address) + } + _, ok := registeredModules[key] + if ok { + return fmt.Errorf("name %s already used by a stateful precompile", key) + } + + for _, precompile := range registeredModules { + if precompile.Address() == address { + return fmt.Errorf("address %s already used by a stateful precompile", address) + } + } + registeredModules[key] = stm + // keep the list sorted + insertSortedModules(stm) + return nil +} + +// TODO: if not used remove this function +func GetPrecompileModule(name string) (StatefulPrecompileModule, bool) { + stm, ok := registeredModules[name] + return stm, ok +} + +// insertSortedModules inserts the module into the sorted list of modules +func insertSortedModules(stm StatefulPrecompileModule) { + for i := 0; i < len(sortedModules); i++ { + if stm.Address().Hash().Big().Cmp(sortedModules[i].Address().Hash().Big()) < 0 { + sortedModules = append(sortedModules[:i], append([]StatefulPrecompileModule{stm}, sortedModules[i:]...)...) + return + } + } + // if we get here, the module should be appended to the end of the list + sortedModules = append(sortedModules, stm) +} + +func RegisteredModules() []StatefulPrecompileModule { + return sortedModules +} diff --git a/precompile/nativeminter/config.go b/precompile/nativeminter/config.go new file mode 100644 index 0000000000..ff7a7d4e7a --- /dev/null +++ b/precompile/nativeminter/config.go @@ -0,0 +1,154 @@ +// (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/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" +) + +var ( + _ precompile.StatefulPrecompileConfig = &ContractNativeMinterConfig{} + + Address = common.HexToAddress("0x0200000000000000000000000000000000000001") + Key = "contractNativeMinterConfig" +) + +// ContractNativeMinterConfig wraps [AllowListConfig] and uses it to implement the StatefulPrecompileConfig +// interface while adding in the ContractNativeMinter specific precompile address. +type ContractNativeMinterConfig struct { + precompile.AllowListConfig + precompile.UpgradeableConfig + InitialMint map[common.Address]*math.HexOrDecimal256 `json:"initialMint,omitempty"` // initial mint config to be immediately minted +} + +func init() { + err := precompile.RegisterModule(ContractNativeMinterConfig{}) + if err != nil { + panic(err) + } +} + +// 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: precompile.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 Address +} + +// 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, Address) +} + +// 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) +} + +func (c ContractNativeMinterConfig) Key() string { + return Key +} + +func (ContractNativeMinterConfig) New() precompile.StatefulPrecompileConfig { + return new(ContractNativeMinterConfig) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (c *ContractNativeMinterConfig) UnmarshalJSON(b []byte) error { + type Alias ContractNativeMinterConfig + if err := json.Unmarshal(b, (*Alias)(c)); err != nil { + return err + } + return nil +} diff --git a/precompile/nativeminter/config_test.go b/precompile/nativeminter/config_test.go new file mode 100644 index 0000000000..99c87f4aad --- /dev/null +++ b/precompile/nativeminter/config_test.go @@ -0,0 +1,150 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package nativeminter + +import ( + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/precompile" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/stretchr/testify/require" +) + +func TestVerifyContractNativeMinterConfig(t *testing.T) { + admins := []common.Address{{1}} + enableds := []common.Address{{2}} + tests := []struct { + name string + config precompile.StatefulPrecompileConfig + expectedError string + }{ + { + name: "invalid allow list config in native minter allowlist", + config: NewContractNativeMinterConfig(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), + expectedError: "duplicate address", + }, + { + name: "duplicate enableds in config in native minter allowlist", + config: NewContractNativeMinterConfig(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, + map[common.Address]*math.HexOrDecimal256{ + common.HexToAddress("0x01"): math.NewHexOrDecimal256(123), + common.HexToAddress("0x02"): nil, + }), + expectedError: "initial mint cannot contain nil", + }, + { + name: "negative amount in native minter config", + config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, + map[common.Address]*math.HexOrDecimal256{ + common.HexToAddress("0x01"): math.NewHexOrDecimal256(123), + common.HexToAddress("0x02"): math.NewHexOrDecimal256(-1), + }), + expectedError: "initial mint cannot contain invalid amount", + }, + } + 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 TestEqualContractNativeMinterConfig(t *testing.T) { + admins := []common.Address{{1}} + enableds := []common.Address{{2}} + tests := []struct { + name string + config precompile.StatefulPrecompileConfig + other precompile.StatefulPrecompileConfig + expected bool + }{ + { + name: "non-nil config and nil other", + config: NewContractNativeMinterConfig(big.NewInt(3), admins, enableds, nil), + other: nil, + expected: false, + }, + { + name: "different type", + config: NewContractNativeMinterConfig(big.NewInt(3), admins, enableds, nil), + other: precompile.NewNoopStatefulPrecompileConfig(), + expected: false, + }, + { + name: "different timestamps", + config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, nil), + other: NewContractNativeMinterConfig(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), + expected: false, + }, + { + name: "different initial mint amounts", + config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, + map[common.Address]*math.HexOrDecimal256{ + common.HexToAddress("0x01"): math.NewHexOrDecimal256(1), + }), + other: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, + map[common.Address]*math.HexOrDecimal256{ + common.HexToAddress("0x01"): math.NewHexOrDecimal256(2), + }), + expected: false, + }, + { + name: "different initial mint addresses", + config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, + map[common.Address]*math.HexOrDecimal256{ + common.HexToAddress("0x01"): math.NewHexOrDecimal256(1), + }), + other: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, + map[common.Address]*math.HexOrDecimal256{ + common.HexToAddress("0x02"): math.NewHexOrDecimal256(1), + }), + expected: false, + }, + + { + name: "same config", + config: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, + map[common.Address]*math.HexOrDecimal256{ + common.HexToAddress("0x01"): math.NewHexOrDecimal256(1), + }), + other: NewContractNativeMinterConfig(big.NewInt(3), admins, nil, + map[common.Address]*math.HexOrDecimal256{ + common.HexToAddress("0x01"): math.NewHexOrDecimal256(1), + }), + 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/nativeminter/contract_native_minter.go b/precompile/nativeminter/contract_native_minter.go new file mode 100644 index 0000000000..066f58fba7 --- /dev/null +++ b/precompile/nativeminter/contract_native_minter.go @@ -0,0 +1,116 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package nativeminter + +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 ( + mintInputAddressSlot = iota + mintInputAmountSlot + + mintInputLen = common.HashLength + common.HashLength + + MintGasCost = 30_000 +) + +var ( + // Singleton StatefulPrecompiledContract for minting native assets by permissioned callers. + ContractNativeMinterPrecompile precompile.StatefulPrecompiledContract = createNativeMinterPrecompile(Address) + + mintSignature = precompile.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) precompile.AllowListRole { + return precompile.GetAllowListStatus(stateDB, Address, 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 precompile.AllowListRole) { + precompile.SetAllowListRole(stateDB, Address, 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{ + address.Hash(), + common.BigToHash(amount), + }) + + return res, err +} + +// UnpackMintInput attempts to unpack [input] into the arguments to the mint precompile +// assumes that [input] does not include selector (omits first 4 bytes in PackMintInput) +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)) + 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 { + return nil, 0, err + } + + if readOnly { + return nil, remainingGas, vmerrs.ErrWriteProtection + } + + to, amount, err := UnpackMintInput(input) + if err != nil { + return nil, remainingGas, err + } + + stateDB := accessibleState.GetStateDB() + // Verify that the caller is in the allow list and therefore has the right to modify it + callerStatus := precompile.GetAllowListStatus(stateDB, Address, caller) + if !callerStatus.IsEnabled() { + return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotMint, caller) + } + + // if there is no address in the state, create one. + if !stateDB.Exist(to) { + stateDB.CreateAccount(to) + } + + stateDB.AddBalance(to, amount) + // Return an empty output and the remaining gas + return []byte{}, remainingGas, nil +} + +// createNativeMinterPrecompile returns a StatefulPrecompiledContract with R/W control of an allow list at [precompileAddr] and a native coin minter. +func createNativeMinterPrecompile(precompileAddr common.Address) precompile.StatefulPrecompiledContract { + enabledFuncs := precompile.CreateAllowListFunctions(precompileAddr) + + mintFunc := precompile.NewStatefulPrecompileFunction(mintSignature, mintNativeCoin) + + enabledFuncs = append(enabledFuncs, mintFunc) + // Construct the contract with no fallback function. + contract, err := precompile.NewStatefulPrecompileContract(nil, enabledFuncs) + // 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 +} diff --git a/precompile/nativeminter/contract_test.go b/precompile/nativeminter/contract_test.go new file mode 100644 index 0000000000..904a0c6116 --- /dev/null +++ b/precompile/nativeminter/contract_test.go @@ -0,0 +1,268 @@ +// (c) 2019-2020, 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" + "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" +) + +func TestContractNativeMinterRun(t *testing.T) { + type test struct { + caller common.Address + input func() []byte + suppliedGas uint64 + readOnly bool + config *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 := 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") + }, + }, + "enabled role by config": { + caller: noRoleAddr, + input: func() []byte { + return precompile.PackReadAllowList(testAddr) + }, + suppliedGas: precompile.ReadAllowListGasCost, + readOnly: false, + expectedRes: common.Hash(precompile.AllowListEnabled).Bytes(), + assertState: func(t *testing.T, state *state.StateDB) { + require.Equal(t, precompile.AllowListEnabled, GetContractNativeMinterStatus(state, testAddr)) + }, + config: &ContractNativeMinterConfig{ + AllowListConfig: precompile.AllowListConfig{EnabledAddresses: []common.Address{testAddr}}, + }, + }, + "initial mint funds": { + caller: enabledAddr, + config: &ContractNativeMinterConfig{ + InitialMint: map[common.Address]*math.HexOrDecimal256{ + enabledAddr: math.NewHexOrDecimal256(2), + }, + }, + input: func() []byte { + return precompile.PackReadAllowList(noRoleAddr) + }, + suppliedGas: precompile.ReadAllowListGasCost, + readOnly: false, + expectedRes: common.Hash(precompile.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 := 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(), + }, + "read from noRole address": { + caller: noRoleAddr, + input: func() []byte { + return precompile.PackReadAllowList(noRoleAddr) + }, + suppliedGas: precompile.ReadAllowListGasCost, + readOnly: false, + expectedRes: common.Hash(precompile.AllowListNoRole).Bytes(), + assertState: func(t *testing.T, state *state.StateDB) {}, + }, + "read from noRole address readOnly enabled": { + caller: noRoleAddr, + input: func() []byte { + return precompile.PackReadAllowList(noRoleAddr) + }, + suppliedGas: precompile.ReadAllowListGasCost, + readOnly: true, + expectedRes: common.Hash(precompile.AllowListNoRole).Bytes(), + assertState: func(t *testing.T, state *state.StateDB) {}, + }, + "read from noRole address with insufficient gas": { + caller: noRoleAddr, + input: func() []byte { + return precompile.PackReadAllowList(noRoleAddr) + }, + suppliedGas: precompile.ReadAllowListGasCost - 1, + readOnly: false, + expectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "set allow role from admin": { + caller: adminAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListEnabled) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetContractNativeMinterStatus(state, noRoleAddr) + require.Equal(t, precompile.AllowListEnabled, res) + }, + }, + "set allow role from non-admin fails": { + caller: enabledAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListEnabled) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedErr: precompile.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. + SetContractNativeMinterStatus(state, adminAddr, precompile.AllowListAdmin) + SetContractNativeMinterStatus(state, enabledAddr, precompile.AllowListEnabled) + SetContractNativeMinterStatus(state, noRoleAddr, precompile.AllowListNoRole) + require.Equal(t, precompile.AllowListAdmin, GetContractNativeMinterStatus(state, adminAddr)) + require.Equal(t, precompile.AllowListEnabled, GetContractNativeMinterStatus(state, enabledAddr)) + require.Equal(t, precompile.AllowListNoRole, GetContractNativeMinterStatus(state, noRoleAddr)) + + blockContext := precompile.NewMockBlockContext(common.Big0, 0) + accesibleState := precompile.NewMockAccessibleState(state, blockContext, snow.DefaultContextTest()) + if test.config != nil { + test.config.Configure(params.TestChainConfig, state, blockContext) + } + ret, remainingGas, err := ContractNativeMinterPrecompile.Run(accesibleState, test.caller, Address, 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/params.go b/precompile/params.go deleted file mode 100644 index 965ab1df20..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") - FeeConfigManagerAddress = common.HexToAddress("0x0200000000000000000000000000000000000003") - RewardManagerAddress = common.HexToAddress("0x0200000000000000000000000000000000000004") - // ADD YOUR PRECOMPILE HERE - // {YourPrecompile}Address = common.HexToAddress("0x03000000000000000000000000000000000000??") - - UsedAddresses = []common.Address{ - ContractDeployerAllowListAddress, - ContractNativeMinterAddress, - TxAllowListAddress, - FeeConfigManagerAddress, - 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/rewardmanager/config.go b/precompile/rewardmanager/config.go new file mode 100644 index 0000000000..bae092e761 --- /dev/null +++ b/precompile/rewardmanager/config.go @@ -0,0 +1,181 @@ +// (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/ethereum/go-ethereum/common" +) + +var ( + _ precompile.StatefulPrecompileConfig = &RewardManagerConfig{} + + Address = common.HexToAddress("0x0200000000000000000000000000000000000004") + Key = "rewardManagerConfig" +) + +func init() { + err := precompile.RegisterModule(RewardManagerConfig{}) + if err != nil { + panic(err) + } +} + +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 (c *InitialRewardConfig) Equal(other *InitialRewardConfig) bool { + if other == nil { + return false + } + + return c.AllowFeeRecipients == other.AllowFeeRecipients && c.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 { + precompile.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: precompile.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 Address +} + +// Configure configures [state] with the initial configuration. +func (c *RewardManagerConfig) Configure(chainConfig precompile.ChainConfig, state precompile.StateDB, _ precompile.BlockContext) error { + c.AllowListConfig.Configure(state, Address) + // 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) +} + +func (c RewardManagerConfig) Key() string { + return Key +} + +func (RewardManagerConfig) New() precompile.StatefulPrecompileConfig { + return new(RewardManagerConfig) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (c *RewardManagerConfig) UnmarshalJSON(b []byte) error { + type Alias RewardManagerConfig + if err := json.Unmarshal(b, (*Alias)(c)); err != nil { + return err + } + return nil +} diff --git a/precompile/rewardmanager/config_test.go b/precompile/rewardmanager/config_test.go new file mode 100644 index 0000000000..7e5fb5c19f --- /dev/null +++ b/precompile/rewardmanager/config_test.go @@ -0,0 +1,121 @@ +// (c) 2022 Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package rewardmanager + +import ( + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/precompile" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestVerifyRewardManagerConfig(t *testing.T) { + admins := []common.Address{{1}} + enableds := []common.Address{{2}} + tests := []struct { + name string + config precompile.StatefulPrecompileConfig + expectedError string + }{ + { + name: "duplicate enableds in config in reward manager allowlist", + config: NewRewardManagerConfig(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{ + AllowFeeRecipients: true, + RewardAddress: common.HexToAddress("0x01"), + }), + expectedError: ErrCannotEnableBothRewards.Error(), + }, + } + 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 TestEqualRewardManagerConfig(t *testing.T) { + admins := []common.Address{{1}} + enableds := []common.Address{{2}} + tests := []struct { + name string + config precompile.StatefulPrecompileConfig + other precompile.StatefulPrecompileConfig + expected bool + }{ + { + name: "non-nil config and nil other", + config: NewRewardManagerConfig(big.NewInt(3), admins, enableds, nil), + other: nil, + expected: false, + }, + { + name: "different type", + config: NewRewardManagerConfig(big.NewInt(3), admins, enableds, nil), + other: precompile.NewNoopStatefulPrecompileConfig(), + expected: false, + }, + { + name: "different timestamp", + config: NewRewardManagerConfig(big.NewInt(3), admins, nil, nil), + other: NewRewardManagerConfig(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), + expected: false, + }, + { + name: "non-nil initial config and nil initial config", + config: NewRewardManagerConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ + AllowFeeRecipients: true, + }), + other: NewRewardManagerConfig(big.NewInt(3), admins, nil, nil), + expected: false, + }, + { + name: "different initial config", + config: NewRewardManagerConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ + RewardAddress: common.HexToAddress("0x01"), + }), + other: NewRewardManagerConfig(big.NewInt(3), admins, nil, + &InitialRewardConfig{ + RewardAddress: common.HexToAddress("0x02"), + }), + expected: false, + }, + { + name: "same config", + config: NewRewardManagerConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ + RewardAddress: common.HexToAddress("0x01"), + }), + other: NewRewardManagerConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ + RewardAddress: common.HexToAddress("0x01"), + }), + 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/reward_manager.abi b/precompile/rewardmanager/contract.abi similarity index 100% rename from precompile/reward_manager.abi rename to precompile/rewardmanager/contract.abi diff --git a/precompile/reward_manager.go b/precompile/rewardmanager/contract.go similarity index 52% rename from precompile/reward_manager.go rename to precompile/rewardmanager/contract.go index 7262efa0b8..4c01b43ced 100644 --- a/precompile/reward_manager.go +++ b/precompile/rewardmanager/contract.go @@ -4,17 +4,16 @@ // Code generated // This file is a generated precompile contract with stubbed abstract functions. -package precompile +package rewardmanager import ( - "encoding/json" "errors" "fmt" - "math/big" "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/vmerrs" _ "embed" @@ -23,17 +22,15 @@ import ( ) const ( - AllowFeeRecipientsGasCost uint64 = (writeGasCostPerSlot) + ReadAllowListGasCost // write 1 slot + read allow list - AreFeeRecipientsAllowedGasCost uint64 = readGasCostPerSlot - CurrentRewardAddressGasCost uint64 = readGasCostPerSlot - DisableRewardsGasCost uint64 = (writeGasCostPerSlot) + ReadAllowListGasCost // write 1 slot + read allow list - SetRewardAddressGasCost uint64 = (writeGasCostPerSlot) + ReadAllowListGasCost // write 1 slot + read allow list + AllowFeeRecipientsGasCost uint64 = (precompile.WriteGasCostPerSlot) + precompile.ReadAllowListGasCost // write 1 slot + read allow list + AreFeeRecipientsAllowedGasCost uint64 = precompile.ReadAllowListGasCost + CurrentRewardAddressGasCost uint64 = precompile.ReadAllowListGasCost + DisableRewardsGasCost uint64 = (precompile.WriteGasCostPerSlot) + precompile.ReadAllowListGasCost // write 1 slot + read allow list + SetRewardAddressGasCost uint64 = (precompile.WriteGasCostPerSlot) + precompile.ReadAllowListGasCost // write 1 slot + read allow list ) // Singleton StatefulPrecompiledContract and signatures. var ( - _ StatefulPrecompileConfig = &RewardManagerConfig{} - ErrCannotAllowFeeRecipients = errors.New("non-enabled cannot call allowFeeRecipients") ErrCannotAreFeeRecipientsAllowed = errors.New("non-enabled cannot call areFeeRecipientsAllowed") ErrCannotCurrentRewardAddress = errors.New("non-enabled cannot call currentRewardAddress") @@ -44,172 +41,37 @@ var ( ErrEmptyRewardAddress = errors.New("reward address cannot be empty") // RewardManagerRawABI contains the raw ABI of RewardManager contract. - //go:embed reward_manager.abi + //go:embed contract.abi RewardManagerRawABI string - RewardManagerABI abi.ABI // will be initialized by init function - RewardManagerPrecompile StatefulPrecompiledContract // will be initialized by init function + RewardManagerABI abi.ABI // will be initialized by init function + RewardManagerPrecompile precompile.StatefulPrecompiledContract // will be initialized by init function rewardAddressStorageKey = common.Hash{'r', 'a', 's', 'k'} allowFeeRecipientsAddressValue = common.Hash{'a', 'f', 'r', 'a', 'v'} ) -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 (c *InitialRewardConfig) Equal(other *InitialRewardConfig) bool { - if other == nil { - return false - } - - return c.AllowFeeRecipients == other.AllowFeeRecipients && c.RewardAddress == other.RewardAddress -} - -func (i *InitialRewardConfig) Configure(state 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 { - AllowListConfig - UpgradeableConfig - InitialRewardConfig *InitialRewardConfig `json:"initialRewardConfig,omitempty"` -} - func init() { parsed, err := abi.JSON(strings.NewReader(RewardManagerRawABI)) if err != nil { panic(err) } RewardManagerABI = parsed - RewardManagerPrecompile, err = createRewardManagerPrecompile(RewardManagerAddress) + RewardManagerPrecompile, err = createRewardManagerPrecompile(Address) if err != nil { panic(err) } } -// 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: AllowListConfig{ - AllowListAdmins: admins, - EnabledAddresses: enableds, - }, - UpgradeableConfig: 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: 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 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 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 RewardManagerAddress -} - -// Configure configures [state] with the initial configuration. -func (c *RewardManagerConfig) Configure(chainConfig ChainConfig, state StateDB, _ BlockContext) error { - c.AllowListConfig.Configure(state, 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() 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) -} - // GetRewardManagerAllowListStatus returns the role of [address] for the RewardManager list. -func GetRewardManagerAllowListStatus(stateDB StateDB, address common.Address) AllowListRole { - return getAllowListStatus(stateDB, RewardManagerAddress, address) +func GetRewardManagerAllowListStatus(stateDB precompile.StateDB, address common.Address) precompile.AllowListRole { + return precompile.GetAllowListStatus(stateDB, Address, address) } // SetRewardManagerAllowListStatus sets the permissions of [address] to [role] for the // RewardManager list. Assumes [role] has already been verified as valid. -func SetRewardManagerAllowListStatus(stateDB StateDB, address common.Address, role AllowListRole) { - setAllowListRole(stateDB, RewardManagerAddress, address, role) +func SetRewardManagerAllowListStatus(stateDB precompile.StateDB, address common.Address, role precompile.AllowListRole) { + precompile.SetAllowListRole(stateDB, Address, address, role) } // PackAllowFeeRecipients packs the function selector (first 4 func signature bytes). @@ -219,17 +81,17 @@ func PackAllowFeeRecipients() ([]byte, error) { } // EnableAllowFeeRecipients enables fee recipients. -func EnableAllowFeeRecipients(stateDB StateDB) { - stateDB.SetState(RewardManagerAddress, rewardAddressStorageKey, allowFeeRecipientsAddressValue) +func EnableAllowFeeRecipients(stateDB precompile.StateDB) { + stateDB.SetState(Address, rewardAddressStorageKey, allowFeeRecipientsAddressValue) } // DisableRewardAddress disables rewards and burns them by sending to Blackhole Address. -func DisableFeeRewards(stateDB StateDB) { - stateDB.SetState(RewardManagerAddress, rewardAddressStorageKey, constants.BlackholeAddr.Hash()) +func DisableFeeRewards(stateDB precompile.StateDB) { + stateDB.SetState(Address, rewardAddressStorageKey, constants.BlackholeAddr.Hash()) } -func allowFeeRecipients(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = deductGas(suppliedGas, AllowFeeRecipientsGasCost); err != nil { +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 { return nil, 0, err } if readOnly { @@ -242,7 +104,7 @@ func allowFeeRecipients(accessibleState PrecompileAccessibleState, caller common // 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 := getAllowListStatus(stateDB, RewardManagerAddress, caller) + callerStatus := precompile.GetAllowListStatus(stateDB, Address, caller) if !callerStatus.IsEnabled() { return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotAllowFeeRecipients, caller) } @@ -268,8 +130,8 @@ func PackAreFeeRecipientsAllowedOutput(isAllowed bool) ([]byte, error) { return RewardManagerABI.PackOutput("areFeeRecipientsAllowed", isAllowed) } -func areFeeRecipientsAllowed(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = deductGas(suppliedGas, AreFeeRecipientsAllowedGasCost); err != nil { +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 { return nil, 0, err } // no input provided for this function @@ -301,18 +163,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 StateDB) (common.Address, bool) { - val := stateDB.GetState(RewardManagerAddress, rewardAddressStorageKey) +func GetStoredRewardAddress(stateDB precompile.StateDB) (common.Address, bool) { + val := stateDB.GetState(Address, rewardAddressStorageKey) return common.BytesToAddress(val.Bytes()), val == allowFeeRecipientsAddressValue } // StoredRewardAddress stores the given [val] under rewardAddressStorageKey. -func StoreRewardAddress(stateDB StateDB, val common.Address) error { +func StoreRewardAddress(stateDB precompile.StateDB, val common.Address) error { // if input is empty, return an error if val == (common.Address{}) { return ErrEmptyRewardAddress } - stateDB.SetState(RewardManagerAddress, rewardAddressStorageKey, val.Hash()) + stateDB.SetState(Address, rewardAddressStorageKey, val.Hash()) return nil } @@ -334,8 +196,8 @@ func UnpackSetRewardAddressInput(input []byte) (common.Address, error) { return unpacked, nil } -func setRewardAddress(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = deductGas(suppliedGas, SetRewardAddressGasCost); err != 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 { return nil, 0, err } if readOnly { @@ -354,7 +216,7 @@ func setRewardAddress(accessibleState PrecompileAccessibleState, caller common.A // 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 := getAllowListStatus(stateDB, RewardManagerAddress, caller) + callerStatus := precompile.GetAllowListStatus(stateDB, Address, caller) if !callerStatus.IsEnabled() { return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotSetRewardAddress, caller) } @@ -370,8 +232,8 @@ func setRewardAddress(accessibleState PrecompileAccessibleState, caller common.A return packedOutput, remainingGas, nil } -func currentRewardAddress(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = deductGas(suppliedGas, CurrentRewardAddressGasCost); err != 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 { return nil, 0, err } @@ -393,8 +255,8 @@ func PackDisableRewards() ([]byte, error) { return RewardManagerABI.Pack("disableRewards") } -func disableRewards(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - if remainingGas, err = deductGas(suppliedGas, DisableRewardsGasCost); err != nil { +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 { return nil, 0, err } if readOnly { @@ -407,7 +269,7 @@ func disableRewards(accessibleState PrecompileAccessibleState, caller common.Add // 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 := getAllowListStatus(stateDB, RewardManagerAddress, caller) + callerStatus := precompile.GetAllowListStatus(stateDB, Address, caller) if !callerStatus.IsEnabled() { return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotDisableRewards, caller) } @@ -422,10 +284,10 @@ func disableRewards(accessibleState PrecompileAccessibleState, caller common.Add // 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) (StatefulPrecompiledContract, error) { - var functions []*statefulPrecompileFunction - functions = append(functions, createAllowListFunctions(precompileAddr)...) - abiFunctionMap := map[string]RunStatefulPrecompileFunc{ +func createRewardManagerPrecompile(precompileAddr common.Address) (precompile.StatefulPrecompiledContract, error) { + var functions []*precompile.StatefulPrecompileFunction + functions = append(functions, precompile.CreateAllowListFunctions(precompileAddr)...) + abiFunctionMap := map[string]precompile.RunStatefulPrecompileFunc{ "allowFeeRecipients": allowFeeRecipients, "areFeeRecipientsAllowed": areFeeRecipientsAllowed, "currentRewardAddress": currentRewardAddress, @@ -438,9 +300,9 @@ func createRewardManagerPrecompile(precompileAddr common.Address) (StatefulPreco if !ok { return nil, fmt.Errorf("given method (%s) does not exist in the ABI", name) } - functions = append(functions, newStatefulPrecompileFunction(method.ID, function)) + functions = append(functions, precompile.NewStatefulPrecompileFunction(method.ID, function)) } // Construct the contract with no fallback function. - return NewStatefulPrecompileContract(nil, functions) + return precompile.NewStatefulPrecompileContract(nil, functions) } diff --git a/precompile/rewardmanager/contract_test.go b/precompile/rewardmanager/contract_test.go new file mode 100644 index 0000000000..8a528b3948 --- /dev/null +++ b/precompile/rewardmanager/contract_test.go @@ -0,0 +1,343 @@ +// (c) 2019-2020, 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/params" + "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +var testBlockNumber = big.NewInt(7) + +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 *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 := 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: &RewardManagerConfig{ + 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: &RewardManagerConfig{ + 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(), + }, + "set allow role from admin": { + caller: adminAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListEnabled) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetRewardManagerAllowListStatus(state, noRoleAddr) + require.Equal(t, precompile.AllowListEnabled, res) + }, + }, + "set allow role from non-admin fails": { + caller: enabledAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListEnabled) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedErr: precompile.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. + SetRewardManagerAllowListStatus(state, adminAddr, precompile.AllowListAdmin) + SetRewardManagerAllowListStatus(state, enabledAddr, precompile.AllowListEnabled) + SetRewardManagerAllowListStatus(state, noRoleAddr, precompile.AllowListNoRole) + + if test.preCondition != nil { + test.preCondition(t, state) + } + + blockContext := precompile.NewMockBlockContext(testBlockNumber, 0) + accesibleState := precompile.NewMockAccessibleState(state, blockContext, snow.DefaultContextTest()) + + if test.config != nil { + test.config.Configure(params.TestChainConfig, state, blockContext) + } + ret, remainingGas, err := RewardManagerPrecompile.Run(accesibleState, test.caller, Address, 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/stateful_precompile_config.go b/precompile/stateful_precompile_config.go index 6955b8d448..2e0ba28eef 100644 --- a/precompile/stateful_precompile_config.go +++ b/precompile/stateful_precompile_config.go @@ -4,16 +4,28 @@ package precompile import ( + "encoding/json" "fmt" "math/big" "github.com/ethereum/go-ethereum/common" ) -// StatefulPrecompileConfig defines the interface for a stateful precompile to -type StatefulPrecompileConfig interface { +type StatefulPrecompileModule interface { // 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() StatefulPrecompiledContract + // Key returns the unique key for the stateful precompile. + Key() string + // New returns a new instance of the stateful precompile config. + New() StatefulPrecompileConfig +} + +// StatefulPrecompileConfig defines the interface for a stateful precompile to +type StatefulPrecompileConfig interface { + StatefulPrecompileModule // 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]. @@ -23,6 +35,8 @@ type StatefulPrecompileConfig interface { 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 @@ -33,12 +47,8 @@ type StatefulPrecompileConfig interface { // 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 - // Verify is called on startup and an error is treated as fatal. Configure can assume the Config has passed verification. - Verify() error + json.Unmarshaler // TODO: this might not be necessary fmt.Stringer } diff --git a/precompile/tx_allow_list.go b/precompile/txallowlist/config.go similarity index 55% rename from precompile/tx_allow_list.go rename to precompile/txallowlist/config.go index 4ab39a8f04..2deb580be2 100644 --- a/precompile/tx_allow_list.go +++ b/precompile/txallowlist/config.go @@ -1,40 +1,46 @@ // (c) 2019-2020, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package precompile +package txallowlist import ( "encoding/json" - "errors" "math/big" + "github.com/ava-labs/subnet-evm/precompile" "github.com/ethereum/go-ethereum/common" ) var ( - _ StatefulPrecompileConfig = &TxAllowListConfig{} - // Singleton StatefulPrecompiledContract for W/R access to the contract deployer allow list. - TxAllowListPrecompile StatefulPrecompiledContract = createAllowListPrecompile(TxAllowListAddress) + _ precompile.StatefulPrecompileConfig = &TxAllowListConfig{} - ErrSenderAddressNotAllowListed = errors.New("cannot issue transaction from non-allow listed address") + Address = common.HexToAddress("0x0200000000000000000000000000000000000002") + Key = "txAllowListConfig" ) // TxAllowListConfig wraps [AllowListConfig] and uses it to implement the StatefulPrecompileConfig // interface while adding in the TxAllowList specific precompile address. type TxAllowListConfig struct { - AllowListConfig - UpgradeableConfig + precompile.AllowListConfig + precompile.UpgradeableConfig +} + +func init() { + err := precompile.RegisterModule(TxAllowListConfig{}) + if err != nil { + panic(err) + } } // 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: AllowListConfig{ + AllowListConfig: precompile.AllowListConfig{ AllowListAdmins: admins, EnabledAddresses: enableds, }, - UpgradeableConfig: UpgradeableConfig{BlockTimestamp: blockTimestamp}, + UpgradeableConfig: precompile.UpgradeableConfig{BlockTimestamp: blockTimestamp}, } } @@ -42,7 +48,7 @@ func NewTxAllowListConfig(blockTimestamp *big.Int, admins []common.Address, enab // that disables TxAllowList. func NewDisableTxAllowListConfig(blockTimestamp *big.Int) *TxAllowListConfig { return &TxAllowListConfig{ - UpgradeableConfig: UpgradeableConfig{ + UpgradeableConfig: precompile.UpgradeableConfig{ BlockTimestamp: blockTimestamp, Disable: true, }, @@ -50,22 +56,22 @@ func NewDisableTxAllowListConfig(blockTimestamp *big.Int) *TxAllowListConfig { } // Address returns the address of the contract deployer allow list. -func (c *TxAllowListConfig) Address() common.Address { - return TxAllowListAddress +func (c TxAllowListConfig) Address() common.Address { + return Address } // Configure configures [state] with the desired admins based on [c]. -func (c *TxAllowListConfig) Configure(_ ChainConfig, state StateDB, _ BlockContext) error { - return c.AllowListConfig.Configure(state, TxAllowListAddress) +func (c *TxAllowListConfig) Configure(_ precompile.ChainConfig, state precompile.StateDB, _ precompile.BlockContext) error { + return c.AllowListConfig.Configure(state, Address) } // Contract returns the singleton stateful precompiled contract to be used for the allow list. -func (c *TxAllowListConfig) Contract() StatefulPrecompiledContract { +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 StatefulPrecompileConfig) bool { +func (c *TxAllowListConfig) Equal(s precompile.StatefulPrecompileConfig) bool { // typecast before comparison other, ok := (s).(*TxAllowListConfig) if !ok { @@ -80,15 +86,19 @@ func (c *TxAllowListConfig) String() string { return string(bytes) } -// GetTxAllowListStatus returns the role of [address] for the contract deployer -// allow list. -func GetTxAllowListStatus(stateDB StateDB, address common.Address) AllowListRole { - return getAllowListStatus(stateDB, TxAllowListAddress, address) +func (c TxAllowListConfig) Key() string { + return Key } -// SetTxAllowListStatus sets the permissions of [address] to [role] for the -// tx allow list. -// assumes [role] has already been verified as valid. -func SetTxAllowListStatus(stateDB StateDB, address common.Address, role AllowListRole) { - setAllowListRole(stateDB, TxAllowListAddress, address, role) +func (TxAllowListConfig) New() precompile.StatefulPrecompileConfig { + return new(TxAllowListConfig) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (c *TxAllowListConfig) UnmarshalJSON(b []byte) error { + type Alias TxAllowListConfig + if err := json.Unmarshal(b, (*Alias)(c)); err != nil { + return err + } + return nil } diff --git a/precompile/txallowlist/config_test.go b/precompile/txallowlist/config_test.go new file mode 100644 index 0000000000..651bfa9ba2 --- /dev/null +++ b/precompile/txallowlist/config_test.go @@ -0,0 +1,105 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txallowlist + +import ( + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/precompile" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestVerifyTxAllowlistConfig(t *testing.T) { + admins := []common.Address{{1}} + enableds := []common.Address{{2}} + tests := []struct { + name string + config precompile.StatefulPrecompileConfig + expectedError string + }{ + { + name: "invalid allow list config in tx allowlist", + config: NewTxAllowListConfig(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), + expectedError: "", + }, + { + name: "empty member allow list config in tx allowlist", + config: NewTxAllowListConfig(big.NewInt(3), []common.Address{}, []common.Address{}), + expectedError: "", + }, + { + name: "valid allow list config in tx allowlist", + config: NewTxAllowListConfig(big.NewInt(3), 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 TestEqualTxAllowListConfig(t *testing.T) { + admins := []common.Address{{1}} + enableds := []common.Address{{2}} + tests := []struct { + name string + config precompile.StatefulPrecompileConfig + other precompile.StatefulPrecompileConfig + expected bool + }{ + { + name: "non-nil config and nil other", + config: NewTxAllowListConfig(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), + expected: false, + }, + { + name: "different enabled", + config: NewTxAllowListConfig(big.NewInt(3), admins, enableds), + other: NewTxAllowListConfig(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), + expected: false, + }, + { + name: "same config", + config: NewTxAllowListConfig(big.NewInt(3), admins, enableds), + other: NewTxAllowListConfig(big.NewInt(3), 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/txallowlist/contract.go b/precompile/txallowlist/contract.go new file mode 100644 index 0000000000..d2c8dfef05 --- /dev/null +++ b/precompile/txallowlist/contract.go @@ -0,0 +1,32 @@ +// (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/ethereum/go-ethereum/common" +) + +var ( + _ precompile.StatefulPrecompileConfig = &TxAllowListConfig{} + // Singleton StatefulPrecompiledContract for W/R access to the contract deployer allow list. + TxAllowListPrecompile precompile.StatefulPrecompiledContract = precompile.CreateAllowListPrecompile(Address) + + ErrSenderAddressNotAllowListed = errors.New("cannot issue transaction from non-allow listed address") +) + +// GetTxAllowListStatus returns the role of [address] for the contract deployer +// allow list. +func GetTxAllowListStatus(stateDB precompile.StateDB, address common.Address) precompile.AllowListRole { + return precompile.GetAllowListStatus(stateDB, Address, 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 precompile.AllowListRole) { + precompile.SetAllowListRole(stateDB, Address, address, role) +} diff --git a/precompile/txallowlist/contract_test.go b/precompile/txallowlist/contract_test.go new file mode 100644 index 0000000000..b1ab07b962 --- /dev/null +++ b/precompile/txallowlist/contract_test.go @@ -0,0 +1,211 @@ +// (c) 2019-2020, 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" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +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 := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListAdmin) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetTxAllowListStatus(state, noRoleAddr) + require.Equal(t, precompile.AllowListAdmin, res) + }, + }, + "set allowed": { + caller: adminAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(noRoleAddr, precompile.AllowListEnabled) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetTxAllowListStatus(state, noRoleAddr) + require.Equal(t, precompile.AllowListEnabled, res) + }, + }, + "set no role": { + caller: adminAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListNoRole) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedRes: []byte{}, + assertState: func(t *testing.T, state *state.StateDB) { + res := GetTxAllowListStatus(state, adminAddr) + require.Equal(t, precompile.AllowListNoRole, res) + }, + }, + "set no role from non-admin": { + caller: noRoleAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListNoRole) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedErr: precompile.ErrCannotModifyAllowList.Error(), + }, + "set allowed from non-admin": { + caller: noRoleAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListEnabled) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedErr: precompile.ErrCannotModifyAllowList.Error(), + }, + "set admin from non-admin": { + caller: noRoleAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListAdmin) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: false, + expectedErr: precompile.ErrCannotModifyAllowList.Error(), + }, + "set no role with readOnly enabled": { + caller: adminAddr, + precompileAddr: Address, + input: func() []byte { + input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListNoRole) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost, + readOnly: true, + expectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "set no role insufficient gas": { + caller: adminAddr, + input: func() []byte { + input, err := precompile.PackModifyAllowList(adminAddr, precompile.AllowListNoRole) + require.NoError(t, err) + + return input + }, + suppliedGas: precompile.ModifyAllowListGasCost - 1, + readOnly: false, + expectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "read allow list no role": { + caller: noRoleAddr, + input: func() []byte { + return precompile.PackReadAllowList(noRoleAddr) + }, + suppliedGas: precompile.ReadAllowListGasCost, + readOnly: false, + expectedRes: common.Hash(precompile.AllowListNoRole).Bytes(), + assertState: nil, + }, + "read allow list admin role": { + caller: adminAddr, + input: func() []byte { + return precompile.PackReadAllowList(noRoleAddr) + }, + suppliedGas: precompile.ReadAllowListGasCost, + readOnly: false, + expectedRes: common.Hash(precompile.AllowListNoRole).Bytes(), + assertState: nil, + }, + "read allow list with readOnly enabled": { + caller: adminAddr, + input: func() []byte { + return precompile.PackReadAllowList(noRoleAddr) + }, + suppliedGas: precompile.ReadAllowListGasCost, + readOnly: true, + expectedRes: common.Hash(precompile.AllowListNoRole).Bytes(), + assertState: nil, + }, + "read allow list out of gas": { + caller: adminAddr, + input: func() []byte { + return precompile.PackReadAllowList(noRoleAddr) + }, + suppliedGas: precompile.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, precompile.AllowListAdmin) + require.Equal(t, precompile.AllowListAdmin, GetTxAllowListStatus(state, adminAddr)) + + blockContext := precompile.NewMockBlockContext(common.Big0, 0) + accesibleState := precompile.NewMockAccessibleState(state, blockContext, snow.DefaultContextTest()) + ret, remainingGas, err := TxAllowListPrecompile.Run(accesibleState, test.caller, Address, 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/utils.go b/precompile/utils.go index 6da328f9c8..dfbe4affd6 100644 --- a/precompile/utils.go +++ b/precompile/utils.go @@ -17,6 +17,7 @@ var functionSignatureRegex = regexp.MustCompile(`[\w]+\(((([\w]+)?)|((([\w]+),)+ // CalculateFunctionSelector returns the 4 byte function selector that results from [functionSignature] // Ex. the function setBalance(addr address, balance uint256) should be passed in as the string: // "setBalance(address,uint256)" +// TODO: remove this after moving to ABI based function selectors. func CalculateFunctionSelector(functionSignature string) []byte { if !functionSignatureRegex.MatchString(functionSignature) { panic(fmt.Errorf("invalid function signature: %q", functionSignature)) @@ -25,8 +26,8 @@ func CalculateFunctionSelector(functionSignature string) []byte { return hash[:4] } -// deductGas checks if [suppliedGas] is sufficient against [requiredGas] and deducts [requiredGas] from [suppliedGas]. -func deductGas(suppliedGas uint64, requiredGas uint64) (uint64, error) { +// DeductGas checks if [suppliedGas] is sufficient against [requiredGas] and deducts [requiredGas] from [suppliedGas]. +func DeductGas(suppliedGas uint64, requiredGas uint64) (uint64, error) { if suppliedGas < requiredGas { return 0, vmerrs.ErrOutOfGas } @@ -36,14 +37,14 @@ func deductGas(suppliedGas uint64, requiredGas uint64) (uint64, error) { // packOrderedHashesWithSelector packs the function selector and ordered list of hashes into [dst] // byte slice. // assumes that [dst] has sufficient room for [functionSelector] and [hashes]. -func packOrderedHashesWithSelector(dst []byte, functionSelector []byte, hashes []common.Hash) error { +func PackOrderedHashesWithSelector(dst []byte, functionSelector []byte, hashes []common.Hash) error { copy(dst[:len(functionSelector)], functionSelector) - return packOrderedHashes(dst[len(functionSelector):], hashes) + return PackOrderedHashes(dst[len(functionSelector):], hashes) } // packOrderedHashes packs the ordered list of [hashes] into the [dst] byte buffer. // assumes that [dst] has sufficient space to pack [hashes] or else this function will panic. -func packOrderedHashes(dst []byte, hashes []common.Hash) error { +func PackOrderedHashes(dst []byte, hashes []common.Hash) error { if len(dst) != len(hashes)*common.HashLength { return fmt.Errorf("destination byte buffer has insufficient length (%d) for %d hashes", len(dst), len(hashes)) } @@ -60,10 +61,10 @@ func packOrderedHashes(dst []byte, hashes []common.Hash) error { return nil } -// returnPackedHash returns packed the byte slice with common.HashLength from [packed] +// PackedHash returns packed the byte slice with common.HashLength from [packed] // at the given [index]. // Assumes that [packed] is composed entirely of packed 32 byte segments. -func returnPackedHash(packed []byte, index int) []byte { +func PackedHash(packed []byte, index int) []byte { start := common.HashLength * index end := start + common.HashLength return packed[start:end] diff --git a/tests/e2e/utils/evm_client.go b/tests/e2e/utils/evm_client.go index 65dbe1785f..6f8c0a3950 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" + "github.com/ava-labs/subnet-evm/precompile/txallowlist" "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(), precompile.ErrSenderAddressNotAllowListed.Error()) { + if strings.Contains(err.Error(), txallowlist.ErrSenderAddressNotAllowListed.Error()) { return nil, err }