diff --git a/accounts/abi/bind/bind.go b/accounts/abi/bind/bind.go index 82d5be5488..37fd93a690 100644 --- a/accounts/abi/bind/bind.go +++ b/accounts/abi/bind/bind.go @@ -44,12 +44,8 @@ import ( "github.com/ethereum/go-ethereum/log" ) -const ( - setAdminFuncKey = "setAdmin" - setEnabledFuncKey = "setEnabled" - setNoneFuncKey = "setNone" - readAllowListFuncKey = "readAllowList" -) +// BindHook is a callback function that can be used to customize the binding. +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 @@ -101,13 +97,17 @@ func isKeyWord(arg string) bool { // to be used as is in client code, but rather as an intermediate struct which // enforces compile time type safety and naming convention opposed to having to // manually maintain hard coded strings that break on runtime. -func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string]string, pkg string, lang Lang, libs map[string]string, aliases map[string]string, isPrecompile bool) (string, error) { +func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string]string, pkg string, lang Lang, libs map[string]string, aliases map[string]string) (string, error) { + return BindHelper(types, abis, bytecodes, fsigs, pkg, lang, libs, aliases, nil) +} + +func BindHelper(types []string, abis []string, bytecodes []string, fsigs []map[string]string, pkg string, lang Lang, libs map[string]string, aliases map[string]string, bindHook BindHook) (string, error) { var ( // contracts is the map of each individual contract requested binding - contracts = make(map[string]*tmplContract) + contracts = make(map[string]*TmplContract) // structs is the map of all redeclared structs shared by passed contracts. - structs = make(map[string]*tmplStruct) + structs = make(map[string]*TmplStruct) // isLib is the map used to flag each encountered library as such isLib = make(map[string]struct{}) @@ -128,11 +128,11 @@ func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string] // Extract the call and transact methods; events, struct definitions; and sort them alphabetically var ( - calls = make(map[string]*tmplMethod) - transacts = make(map[string]*tmplMethod) + calls = make(map[string]*TmplMethod) + transacts = make(map[string]*TmplMethod) events = make(map[string]*tmplEvent) - fallback *tmplMethod - receive *tmplMethod + fallback *TmplMethod + receive *TmplMethod // identifiers are used to detect duplicated identifiers of functions // and events. For all calls, transacts and events, abigen will generate @@ -155,7 +155,7 @@ func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string] normalizedName := methodNormalizer[lang](alias(aliases, original.Name)) // Ensure there is no duplicated identifier - var identifiers = callIdentifiers + identifiers := callIdentifiers if !original.IsConstant() { identifiers = transactIdentifiers } @@ -178,11 +178,6 @@ func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string] normalized.Outputs = make([]abi.Argument, len(original.Outputs)) copy(normalized.Outputs, original.Outputs) for j, output := range normalized.Outputs { - if isPrecompile { - if output.Name == "" { - return "", fmt.Errorf("ABI outputs for %s require a name to generate the precompile binding, re-generate the ABI from a Solidity source file with all named outputs", normalized.Name) - } - } if output.Name != "" { normalized.Outputs[j].Name = capitalise(output.Name) } @@ -192,9 +187,9 @@ func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string] } // Append the methods to the call or transact lists if original.IsConstant() { - calls[original.Name] = &tmplMethod{Original: original, Normalized: normalized, Structured: structured(original.Outputs)} + calls[original.Name] = &TmplMethod{Original: original, Normalized: normalized, Structured: structured(original.Outputs)} } else { - transacts[original.Name] = &tmplMethod{Original: original, Normalized: normalized, Structured: structured(original.Outputs)} + transacts[original.Name] = &TmplMethod{Original: original, Normalized: normalized, Structured: structured(original.Outputs)} } } for _, original := range evmABI.Events { @@ -238,17 +233,17 @@ func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string] } // Add two special fallback functions if they exist if evmABI.HasFallback() { - fallback = &tmplMethod{Original: evmABI.Fallback} + fallback = &TmplMethod{Original: evmABI.Fallback} } if evmABI.HasReceive() { - receive = &tmplMethod{Original: evmABI.Receive} + receive = &TmplMethod{Original: evmABI.Receive} } // There is no easy way to pass arbitrary java objects to the Go side. if len(structs) > 0 && lang == LangJava { return "", errors.New("java binding for tuple arguments is not supported yet") } - contracts[types[i]] = &tmplContract{ + contracts[types[i]] = &TmplContract{ Type: capitalise(types[i]), InputABI: strings.ReplaceAll(strippedABI, "\"", "\\\""), InputBin: strings.TrimPrefix(strings.TrimSpace(bytecodes[i]), "0x"), @@ -291,19 +286,14 @@ func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string] templateSource string ) - // Generate the contract template data according to contract type (precompile/non) - if isPrecompile { - if lang != LangGo { - return "", errors.New("only GoLang binding for precompiled contracts is supported yet") - } - - if len(contracts) != 1 { - return "", errors.New("cannot generate more than 1 contract") + // Generate the contract template data according to hook + if bindHook != nil { + var err error + data, templateSource, err = bindHook(lang, pkg, types, contracts, structs) + if err != nil { + return "", err } - precompileType := types[0] - firstContract := contracts[precompileType] - data, templateSource = createPrecompileDataAndTemplate(firstContract, structs) - } else { + } else { // default to generate contract binding templateSource = tmplSource[lang] data = &tmplData{ Package: pkg, @@ -342,7 +332,7 @@ func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string] // bindType is a set of type binders that convert Solidity types to some supported // programming language types. -var bindType = map[Lang]func(kind abi.Type, structs map[string]*tmplStruct) string{ +var bindType = map[Lang]func(kind abi.Type, structs map[string]*TmplStruct) string{ LangGo: bindTypeGo, LangJava: bindTypeJava, } @@ -374,7 +364,7 @@ func bindBasicTypeGo(kind abi.Type) string { // bindTypeGo converts solidity types to Go ones. Since there is no clear mapping // from all Solidity types to Go ones (e.g. uint17), those that cannot be exactly // mapped will use an upscaled type (e.g. BigDecimal). -func bindTypeGo(kind abi.Type, structs map[string]*tmplStruct) string { +func bindTypeGo(kind abi.Type, structs map[string]*TmplStruct) string { switch kind.T { case abi.TupleTy: return structs[kind.TupleRawName+kind.String()].Name @@ -451,7 +441,7 @@ func pluralizeJavaType(typ string) string { // bindTypeJava converts a Solidity type to a Java one. Since there is no clear mapping // from all Solidity types to Java ones (e.g. uint17), those that cannot be exactly // mapped will use an upscaled type (e.g. BigDecimal). -func bindTypeJava(kind abi.Type, structs map[string]*tmplStruct) string { +func bindTypeJava(kind abi.Type, structs map[string]*TmplStruct) string { switch kind.T { case abi.TupleTy: return structs[kind.TupleRawName+kind.String()].Name @@ -464,14 +454,14 @@ func bindTypeJava(kind abi.Type, structs map[string]*tmplStruct) string { // bindTopicType is a set of type binders that convert Solidity types to some // supported programming language topic types. -var bindTopicType = map[Lang]func(kind abi.Type, structs map[string]*tmplStruct) string{ +var bindTopicType = map[Lang]func(kind abi.Type, structs map[string]*TmplStruct) string{ LangGo: bindTopicTypeGo, LangJava: bindTopicTypeJava, } // bindTopicTypeGo converts a Solidity topic type to a Go one. It is almost the same // functionality as for simple types, but dynamic types get converted to hashes. -func bindTopicTypeGo(kind abi.Type, structs map[string]*tmplStruct) string { +func bindTopicTypeGo(kind abi.Type, structs map[string]*TmplStruct) string { bound := bindTypeGo(kind, structs) // todo(rjl493456442) according solidity documentation, indexed event @@ -488,7 +478,7 @@ func bindTopicTypeGo(kind abi.Type, structs map[string]*tmplStruct) string { // bindTopicTypeJava converts a Solidity topic type to a Java one. It is almost the same // functionality as for simple types, but dynamic types get converted to hashes. -func bindTopicTypeJava(kind abi.Type, structs map[string]*tmplStruct) string { +func bindTopicTypeJava(kind abi.Type, structs map[string]*TmplStruct) string { bound := bindTypeJava(kind, structs) // todo(rjl493456442) according solidity documentation, indexed event @@ -505,7 +495,7 @@ func bindTopicTypeJava(kind abi.Type, structs map[string]*tmplStruct) string { // bindStructType is a set of type binders that convert Solidity tuple types to some supported // programming language struct definition. -var bindStructType = map[Lang]func(kind abi.Type, structs map[string]*tmplStruct) string{ +var bindStructType = map[Lang]func(kind abi.Type, structs map[string]*TmplStruct) string{ LangGo: bindStructTypeGo, LangJava: bindStructTypeJava, } @@ -513,7 +503,7 @@ var bindStructType = map[Lang]func(kind abi.Type, structs map[string]*tmplStruct // bindStructTypeGo converts a Solidity tuple type to a Go one and records the mapping // in the given map. // Notably, this function will resolve and record nested struct recursively. -func bindStructTypeGo(kind abi.Type, structs map[string]*tmplStruct) string { +func bindStructTypeGo(kind abi.Type, structs map[string]*TmplStruct) string { switch kind.T { case abi.TupleTy: // We compose a raw struct name and a canonical parameter expression @@ -542,7 +532,7 @@ func bindStructTypeGo(kind abi.Type, structs map[string]*tmplStruct) string { } name = capitalise(name) - structs[id] = &tmplStruct{ + structs[id] = &TmplStruct{ Name: name, Fields: fields, } @@ -559,7 +549,7 @@ func bindStructTypeGo(kind abi.Type, structs map[string]*tmplStruct) string { // bindStructTypeJava converts a Solidity tuple type to a Java one and records the mapping // in the given map. // Notably, this function will resolve and record nested struct recursively. -func bindStructTypeJava(kind abi.Type, structs map[string]*tmplStruct) string { +func bindStructTypeJava(kind abi.Type, structs map[string]*TmplStruct) string { switch kind.T { case abi.TupleTy: // We compose a raw struct name and a canonical parameter expression @@ -581,7 +571,7 @@ func bindStructTypeJava(kind abi.Type, structs map[string]*tmplStruct) string { if name == "" { name = fmt.Sprintf("Class%d", len(structs)) } - structs[id] = &tmplStruct{ + structs[id] = &TmplStruct{ Name: name, Fields: fields, } @@ -710,45 +700,3 @@ func hasStruct(t abi.Type) bool { return false } } - -func createPrecompileDataAndTemplate(contract *tmplContract, structs map[string]*tmplStruct) (interface{}, string) { - funcs := make(map[string]*tmplMethod) - - for k, v := range contract.Transacts { - funcs[k] = v - } - - for k, v := range contract.Calls { - funcs[k] = v - } - isAllowList := allowListEnabled(funcs) - if isAllowList { - // remove these functions as we will directly inherit AllowList - delete(funcs, readAllowListFuncKey) - delete(funcs, setAdminFuncKey) - delete(funcs, setEnabledFuncKey) - delete(funcs, setNoneFuncKey) - } - - precompileContract := &tmplPrecompileContract{ - tmplContract: contract, - AllowList: isAllowList, - Funcs: funcs, - } - - data := &tmplPrecompileData{ - Contract: precompileContract, - Structs: structs, - } - return data, tmplSourcePrecompileGo -} - -func allowListEnabled(funcs map[string]*tmplMethod) bool { - keys := []string{readAllowListFuncKey, setAdminFuncKey, setEnabledFuncKey, setNoneFuncKey} - for _, key := range keys { - if _, ok := funcs[key]; !ok { - return false - } - } - return true -} diff --git a/accounts/abi/bind/bind_test.go b/accounts/abi/bind/bind_test.go index 1e1a7c538b..78a6714e3d 100644 --- a/accounts/abi/bind/bind_test.go +++ b/accounts/abi/bind/bind_test.go @@ -2099,7 +2099,7 @@ func golangBindings(t *testing.T, overload bool) { types = []string{tt.name} } // Generate the binding and create a Go source file in the workspace - bind, err := Bind(types, tt.abi, tt.bytecode, tt.fsigs, "bindtest", LangGo, tt.libs, tt.aliases, false) + bind, err := Bind(types, tt.abi, tt.bytecode, tt.fsigs, "bindtest", LangGo, tt.libs, tt.aliases) if err != nil { t.Fatalf("test %d: failed to generate binding: %v", i, err) } @@ -2529,7 +2529,7 @@ public class Test { }, } for i, c := range cases { - binding, err := Bind([]string{c.name}, []string{c.abi}, []string{c.bytecode}, nil, "bindtest", LangJava, nil, nil, false) + binding, err := Bind([]string{c.name}, []string{c.abi}, []string{c.bytecode}, nil, "bindtest", LangJava, nil, nil) if err != nil { t.Fatalf("test %d: failed to generate binding: %v", i, err) } diff --git a/accounts/abi/bind/precompilebind/precompile_bind.go b/accounts/abi/bind/precompilebind/precompile_bind.go new file mode 100644 index 0000000000..ae4b40b604 --- /dev/null +++ b/accounts/abi/bind/precompilebind/precompile_bind.go @@ -0,0 +1,141 @@ +// (c) 2019-2020, Ava Labs, Inc. +// +// This file is a derived work, based on the go-ethereum library whose original +// notices appear below. +// +// It is distributed under a license compatible with the licensing terms of the +// original code from which it is derived. +// +// Much love to the original authors for their work. +// ********** +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Package bind generates Ethereum contract Go bindings. +// +// Detailed usage document and tutorial available on the go-ethereum Wiki page: +// https://github.com/ethereum/go-ethereum/wiki/Native-DApps:-Go-bindings-to-Ethereum-contracts +package precompilebind + +import ( + "errors" + "fmt" + + "github.com/ava-labs/subnet-evm/accounts/abi/bind" +) + +const ( + setAdminFuncKey = "setAdmin" + setEnabledFuncKey = "setEnabled" + setNoneFuncKey = "setNone" + readAllowListFuncKey = "readAllowList" +) + +// 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 bind.Lang, libs map[string]string, aliases map[string]string, abifilename string) (string, string, string, error) { + // create hooks + configHook := createPrecompileHook(abifilename, tmplSourcePrecompileConfigGo) + contractHook := createPrecompileHook(abifilename, tmplSourcePrecompileContractGo) + moduleHook := createPrecompileHook(abifilename, tmplSourcePrecompileModuleGo) + + configBind, err := bind.BindHelper(types, abis, bytecodes, fsigs, pkg, lang, libs, aliases, configHook) + if err != nil { + return "", "", "", fmt.Errorf("failed to generate config binding: %w", err) + } + contractBind, err := bind.BindHelper(types, abis, bytecodes, fsigs, pkg, lang, libs, aliases, contractHook) + if err != nil { + return "", "", "", fmt.Errorf("failed to generate contract binding: %w", err) + } + moduleBind, err := bind.BindHelper(types, abis, bytecodes, fsigs, pkg, lang, libs, aliases, moduleHook) + if err != nil { + return "", "", "", fmt.Errorf("failed to generate module binding: %w", err) + } + return configBind, contractBind, moduleBind, nil +} + +// createPrecompileHook creates a bind hook for precompiled contracts. +func createPrecompileHook(abifilename string, template string) bind.BindHook { + return func(lang bind.Lang, pkg string, types []string, contracts map[string]*bind.TmplContract, structs map[string]*bind.TmplStruct) (interface{}, string, error) { + // verify first + if lang != bind.LangGo { + return nil, "", errors.New("only GoLang binding for precompiled contracts is supported yet") + } + + if len(types) != 1 { + return nil, "", errors.New("cannot generate more than 1 contract") + } + funcs := make(map[string]*bind.TmplMethod) + + contract := contracts[types[0]] + + for k, v := range contract.Transacts { + if err := checkOutputName(*v); err != nil { + return nil, "", err + } + funcs[k] = v + } + + for k, v := range contract.Calls { + if err := checkOutputName(*v); err != nil { + return nil, "", err + } + funcs[k] = v + } + isAllowList := allowListEnabled(funcs) + if isAllowList { + // these functions are not needed for binded contract. + // AllowList struct can provide the same functionality, + // so we don't need to generate them. + delete(funcs, readAllowListFuncKey) + delete(funcs, setAdminFuncKey) + delete(funcs, setEnabledFuncKey) + delete(funcs, setNoneFuncKey) + } + + precompileContract := &tmplPrecompileContract{ + TmplContract: contract, + AllowList: isAllowList, + Funcs: funcs, + ABIFilename: abifilename, + } + + data := &tmplPrecompileData{ + Contract: precompileContract, + Structs: structs, + Package: pkg, + } + return data, template, nil + } +} + +func allowListEnabled(funcs map[string]*bind.TmplMethod) bool { + keys := []string{readAllowListFuncKey, setAdminFuncKey, setEnabledFuncKey, setNoneFuncKey} + for _, key := range keys { + if _, ok := funcs[key]; !ok { + return false + } + } + return true +} + +func checkOutputName(method bind.TmplMethod) error { + for _, output := range method.Original.Outputs { + if output.Name == "" { + return fmt.Errorf("ABI outputs for %s require a name to generate the precompile binding, re-generate the ABI from a Solidity source file with all named outputs", method.Original.Name) + } + } + return nil +} diff --git a/accounts/abi/bind/bind_precompile_test.go b/accounts/abi/bind/precompilebind/precompile_bind_test.go similarity index 84% rename from accounts/abi/bind/bind_precompile_test.go rename to accounts/abi/bind/precompilebind/precompile_bind_test.go index 03cbcebc09..f37d54e1ba 100644 --- a/accounts/abi/bind/bind_precompile_test.go +++ b/accounts/abi/bind/precompilebind/precompile_bind_test.go @@ -24,10 +24,13 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package bind +package precompilebind import ( "testing" + + "github.com/ava-labs/subnet-evm/accounts/abi/bind" + "github.com/stretchr/testify/require" ) var bindFailedTests = []struct { @@ -49,7 +52,7 @@ var bindFailedTests = []struct { {"type":"function","name":"anonOutput","constant":true,"inputs":[],"outputs":[{"name":"","type":"string"}]} ] `}, - "ABI outputs for AnonOutput require a name to generate the precompile binding, re-generate the ABI from a Solidity source file with all named outputs", + "ABI outputs for anonOutput require a name to generate the precompile binding, re-generate the ABI from a Solidity source file with all named outputs", nil, nil, nil, @@ -64,7 +67,7 @@ var bindFailedTests = []struct { {"type":"function","name":"anonOutputs","constant":true,"inputs":[],"outputs":[{"name":"","type":"string"},{"name":"","type":"string"}]} ] `}, - "ABI outputs for AnonOutputs require a name to generate the precompile binding, re-generate the ABI from a Solidity source file with all named outputs", + "ABI outputs for anonOutputs require a name to generate the precompile binding, re-generate the ABI from a Solidity source file with all named outputs", nil, nil, nil, @@ -79,7 +82,7 @@ var bindFailedTests = []struct { {"type":"function","name":"mixedOutputs","constant":true,"inputs":[],"outputs":[{"name":"","type":"string"},{"name":"str","type":"string"}]} ] `}, - "ABI outputs for MixedOutputs require a name to generate the precompile binding, re-generate the ABI from a Solidity source file with all named outputs", + "ABI outputs for mixedOutputs require a name to generate the precompile binding, re-generate the ABI from a Solidity source file with all named outputs", nil, nil, nil, @@ -96,14 +99,11 @@ func golangBindingsFailure(t *testing.T) { for i, tt := range bindFailedTests { t.Run(tt.name, func(t *testing.T) { // Generate the binding - _, err := Bind([]string{tt.name}, tt.abi, tt.bytecode, tt.fsigs, "bindtest", LangGo, tt.libs, tt.aliases, true) + _, _, _, err := PrecompileBind([]string{tt.name}, tt.abi, tt.bytecode, tt.fsigs, "bindtest", bind.LangGo, tt.libs, tt.aliases, "") if err == nil { t.Fatalf("test %d: no error occurred but was expected", i) } - - if tt.errorMsg != err.Error() { - t.Fatalf("test %d: expected Err %s but got actual Err: %s", i, tt.errorMsg, err.Error()) - } + require.ErrorContains(t, err, tt.errorMsg) }) } } diff --git a/accounts/abi/bind/precompilebind/precompile_config_template.go b/accounts/abi/bind/precompilebind/precompile_config_template.go new file mode 100644 index 0000000000..4d8f9252de --- /dev/null +++ b/accounts/abi/bind/precompilebind/precompile_config_template.go @@ -0,0 +1,117 @@ +// (c) 2019-2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package precompilebind + +// 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. + +package {{.Package}} + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + {{- if .Contract.AllowList}} + "github.com/ava-labs/subnet-evm/precompile/allowlist" + + "github.com/ethereum/go-ethereum/common" + {{- end}} + +) + +var _ precompileconfig.Config = &Config{} + +// Config implements the StatefulPrecompileConfig +// interface while adding in the {{.Contract.Type}} specific precompile address. +type Config struct { + {{- if .Contract.AllowList}} + allowlist.AllowListConfig + {{- end}} + precompileconfig.Upgrade + // CUSTOM CODE STARTS HERE + // Add your own custom fields for Config here +} + +{{$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}} + +// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables +// {{.Contract.Type}} with the given [admins] and [enableds] as members of the allowlist. +// {{.Contract.Type}} {{if .Contract.AllowList}} with the given [admins] as members of the allowlist {{end}}. +func NewConfig(blockTimestamp *big.Int{{if .Contract.AllowList}}, admins []common.Address, enableds []common.Address,{{end}}) *Config { + return &Config{ + {{- if .Contract.AllowList}} + AllowListConfig: allowlist.AllowListConfig{ + AdminAddresses: admins, + EnabledAddresses: enableds, + }, + {{- end}} + Upgrade: precompileconfig.Upgrade{BlockTimestamp: blockTimestamp}, + } +} + +// NewDisableConfig returns config for a network upgrade at [blockTimestamp] +// that disables {{.Contract.Type}}. +func NewDisableConfig(blockTimestamp *big.Int) *Config { + return &Config{ + Upgrade: precompileconfig.Upgrade{ + BlockTimestamp: blockTimestamp, + Disable: true, + }, + } +} + +// Key returns the key for the {{.Contract.Type}} precompileconfig. +// This should be the same key as used in the precompile module. +func (*Config) Key() string { return ConfigKey } + +// Verify tries to verify Config and returns an error accordingly. +func (c *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 Config here + // and return an error accordingly + return nil +} + +// Equal returns true if [s] is a [*Config] and it has been configured identical to [c]. +func (c *Config) Equal(s precompileconfig.Config) bool { + // typecast before comparison + other, ok := (s).(*Config) + if !ok { + return false + } + // CUSTOM CODE STARTS HERE + // modify this boolean accordingly with your custom Config, to check if [other] and the current [c] are equal + // if Config contains only Upgrade {{if .Contract.AllowList}} and AllowListConfig {{end}} you can skip modifying it. + equals := c.Upgrade.Equal(&other.Upgrade) {{if .Contract.AllowList}} && c.AllowListConfig.Equal(&other.AllowListConfig) {{end}} + return equals +} +` diff --git a/accounts/abi/bind/precompile_template.go b/accounts/abi/bind/precompilebind/precompile_contract_template.go similarity index 55% rename from accounts/abi/bind/precompile_template.go rename to accounts/abi/bind/precompilebind/precompile_contract_template.go index 20bad6bdf5..1c302d4b49 100644 --- a/accounts/abi/bind/precompile_template.go +++ b/accounts/abi/bind/precompilebind/precompile_contract_template.go @@ -1,72 +1,64 @@ // (c) 2019-2022, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package bind +package precompilebind + +import "github.com/ava-labs/subnet-evm/accounts/abi/bind" // tmplPrecompileData is the data structure required to fill the binding template. type tmplPrecompileData struct { - Contract *tmplPrecompileContract // The contract to generate into this file - Structs map[string]*tmplStruct // Contract struct type definitions + Package string + Contract *tmplPrecompileContract // The contract to generate into this file + Structs map[string]*bind.TmplStruct // Contract struct type definitions } // tmplPrecompileContract contains the data needed to generate an individual contract binding. type tmplPrecompileContract struct { - *tmplContract - AllowList bool // Indicator whether the contract uses AllowList precompile - Funcs map[string]*tmplMethod // Contract functions that include both Calls + Transacts in tmplContract + *bind.TmplContract + AllowList bool // Indicator whether the contract uses AllowList precompile + Funcs map[string]*bind.TmplMethod // Contract functions that include both Calls + Transacts in tmplContract + 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. +// 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 - -/* 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 -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 './scripts/run_ginkgo.sh' - -*/ - -package precompile +package {{.Package}} import ( - "encoding/json" "errors" "fmt" "math/big" - "strings" "github.com/ava-labs/subnet-evm/accounts/abi" + {{- if .Contract.AllowList}} + "github.com/ava-labs/subnet-evm/precompile/allowlist" + {{- end}} + "github.com/ava-labs/subnet-evm/precompile/contract" "github.com/ava-labs/subnet-evm/vmerrs" + _ "embed" + "github.com/ethereum/go-ethereum/common" ) - +{{$contract := .Contract}} const ( + // Gas costs for each function. These are set to 0 by default. + // You should set a gas cost for each function in your contract. + // Generally, you should not set gas costs very low as this may cause your network to be vulnerable to DoS attacks. + // There are some predefined gas costs in contract/utils.go that you can use. + {{- if .Contract.AllowList}} + // This contract also uses AllowList precompile. + // You should also increase gas costs of functions that read from AllowList storage. + {{- end}}} {{- range .Contract.Funcs}} - {{.Normalized.Name}}GasCost uint64 = 0 // SET A GAS COST HERE + {{.Normalized.Name}}GasCost uint64 = 0 {{if not .Original.IsConstant | and $contract.AllowList}} + allowlist.ReadAllowListGasCost {{end}} // SET A GAS COST HERE {{- end}} {{- if .Contract.Fallback}} {{.Contract.Type}}FallbackGasCost uint64 = 0 // SET A GAS COST LESS THAN 2300 HERE {{- end}} - - // {{.Contract.Type}}RawABI contains the raw ABI of {{.Contract.Type}} contract. - {{.Contract.Type}}RawABI = "{{.Contract.InputABI}}" ) // CUSTOM CODE STARTS HERE @@ -74,15 +66,10 @@ const ( var ( _ = errors.New _ = big.NewInt - _ = strings.NewReader - _ = fmt.Printf ) -{{$contract := .Contract}} // Singleton StatefulPrecompiledContract and signatures. var ( - _ StatefulPrecompileConfig = &{{.Contract.Type}}Config{} - {{- range .Contract.Funcs}} {{- if not .Original.IsConstant | and $contract.AllowList}} @@ -95,24 +82,19 @@ var ( Err{{.Contract.Type}}CannotFallback = errors.New("non-enabled cannot call fallback function") {{- end}} - {{.Contract.Type}}ABI abi.ABI // will be initialized by init function + // {{.Contract.Type}}RawABI contains the raw ABI of {{.Contract.Type}} contract. + {{- if .Contract.ABIFilename | eq ""}} + {{.Contract.Type}}RawABI = "{{.Contract.InputABI}}" + {{- else}} + //go:embed {{.Contract.ABIFilename}} + {{.Contract.Type}}RawABI string + {{- end}} - {{.Contract.Type}}Precompile StatefulPrecompiledContract // will be initialized by init function + {{.Contract.Type}}ABI = contract.ParseABI({{.Contract.Type}}RawABI) - // 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}}Precompile = create{{.Contract.Type}}Precompile() ) -// {{.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. @@ -135,101 +117,20 @@ type {{capitalise .Normalized.Name}}Output struct{ {{- end}} {{- end}} -func init() { - parsed, err := abi.JSON(strings.NewReader({{.Contract.Type}}RawABI)) - if err != nil { - panic(err) - } - {{.Contract.Type}}ABI = parsed - - {{.Contract.Type}}Precompile = create{{.Contract.Type}}Precompile({{.Contract.Type}}Address) -} - -// 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) { - {{if .Contract.AllowList}}c.AllowListConfig.Configure(state, {{.Contract.Type}}Address){{end}} - // CUSTOM CODE STARTS HERE -} - -// 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 contract.StateDB, address common.Address) allowlist.Role { + return allowlist.GetAllowListStatus(stateDB, ContractAddress, address) } // Set{{.Contract.Type}}AllowListStatus sets the permissions of [address] to [role] for the // {{.Contract.Type}} list. Assumes [role] has already been verified as valid. -// This stores the [role] in the contract storage with address [{{.Contract.Type}}Address] +// This stores the [role] in the contract storage with address [ContractAddress] // and [address] hash. It means that any reusage of the [address] key for different value // conflicts with the same slot [role] is stored. // Precompile implementations must use a different key than [address] for their storage. -func Set{{.Contract.Type}}AllowListStatus(stateDB StateDB, address common.Address, role AllowListRole) { - setAllowListRole(stateDB, {{.Contract.Type}}Address, address, role) +func Set{{.Contract.Type}}AllowListStatus(stateDB contract.StateDB, address common.Address, role allowlist.Role) { + allowlist.SetAllowListRole(stateDB, ContractAddress, address, role) } {{end}} @@ -297,8 +198,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 contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, {{.Normalized.Name}}GasCost); err != nil { return nil, 0, err } @@ -325,8 +226,8 @@ func {{decapitalise .Normalized.Name}}(accessibleState PrecompileAccessibleState // This part of the code restricts the function to be called only by enabled/admin addresses in the allow list. // You can modify/delete this code if you don't want this function to be restricted by the allow list. stateDB := accessibleState.GetStateDB() - // Verify that the caller is in the allow list and therefore has the right to modify it - callerStatus := getAllowListStatus(stateDB, {{$contract.Type}}Address, caller) + // Verify that the caller is in the allow list and therefore has the right to call this function. + callerStatus := allowlist.GetAllowListStatus(stateDB, ContractAddress, caller) if !callerStatus.IsEnabled() { return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannot{{.Normalized.Name}}, caller) } @@ -362,8 +263,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 contract.AccessibleState, caller common.Address, addr common.Address, _ []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, {{$contract.Type}}FallbackGasCost); err != nil { return nil, 0, err } @@ -376,8 +277,8 @@ func {{decapitalise $contract.Type}}Fallback (accessibleState PrecompileAccessib // This part of the code restricts the function to be called only by enabled/admin addresses in the allow list. // You can modify/delete this code if you don't want this function to be restricted by the allow list. stateDB := accessibleState.GetStateDB() - // Verify that the caller is in the allow list and therefore has the right to modify it - callerStatus := getAllowListStatus(stateDB, {{$contract.Type}}Address, caller) + // Verify that the caller is in the allow list and therefore has the right to call this function. + callerStatus := allowlist.GetAllowListStatus(stateDB, ContractAddress, caller) if !callerStatus.IsEnabled() { return nil, remainingGas, fmt.Errorf("%w: %s", Err{{$contract.Type}}CannotFallback, caller) } @@ -397,28 +298,41 @@ func {{decapitalise $contract.Type}}Fallback (accessibleState PrecompileAccessib {{- end}} // create{{.Contract.Type}}Precompile returns a StatefulPrecompiledContract with getters and setters for the precompile. -{{if .Contract.AllowList}} // Access to the getters/setters is controlled by an allow list for [precompileAddr].{{end}} -func create{{.Contract.Type}}Precompile(precompileAddr common.Address) StatefulPrecompiledContract { - var functions []*statefulPrecompileFunction +{{if .Contract.AllowList}} // Access to the getters/setters is controlled by an allow list for ContractAddress.{{end}} +func create{{.Contract.Type}}Precompile() contract.StatefulPrecompiledContract { + var functions []*contract.StatefulPrecompileFunction {{- if .Contract.AllowList}} - functions = append(functions, createAllowListFunctions(precompileAddr)...) + functions = append(functions, allowlist.CreateAllowListFunctions(ContractAddress)...) {{- end}} - {{range .Contract.Funcs}} - method{{.Normalized.Name}}, ok := {{$contract.Type}}ABI.Methods["{{.Original.Name}}"] - if !ok{ - panic("given method does not exist in the ABI") + abiFunctionMap := map[string]contract.RunStatefulPrecompileFunc{ + {{- range .Contract.Funcs}} + "{{.Original.Name}}": {{decapitalise .Normalized.Name}}, + {{- end}} + } + + for name, function := range abiFunctionMap { + method, ok := {{$contract.Type}}ABI.Methods[name] + if !ok { + panic(fmt.Errorf("given method (%s) does not exist in the ABI", name)) + } + functions = append(functions, contract.NewStatefulPrecompileFunction(method.ID, function)) } - functions = append(functions, newStatefulPrecompileFunction(method{{.Normalized.Name}}.ID, {{decapitalise .Normalized.Name}})) - {{end}} {{- if .Contract.Fallback}} // Construct the contract with the fallback function. - contract := newStatefulPrecompileWithFunctionSelectors({{decapitalise $contract.Type}}Fallback, functions) + statefulContract, err := contract.NewStatefulPrecompileContract({{decapitalise $contract.Type}}Fallback, functions) + if err != nil { + panic(err) + } + return statefulContract {{- else}} // Construct the contract with no fallback function. - contract := newStatefulPrecompileWithFunctionSelectors(nil, functions) + statefulContract, err := contract.NewStatefulPrecompileContract(nil, functions) + if err != nil { + panic(err) + } + return statefulContract {{- end}} - return contract } ` diff --git a/accounts/abi/bind/precompilebind/precompile_module_template.go b/accounts/abi/bind/precompilebind/precompile_module_template.go new file mode 100644 index 0000000000..db3950e3f5 --- /dev/null +++ b/accounts/abi/bind/precompilebind/precompile_module_template.go @@ -0,0 +1,75 @@ +// (c) 2019-2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package precompilebind + +// tmplSourcePrecompileModuleGo is the Go precompiled module source template. +const tmplSourcePrecompileModuleGo = ` +// 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. + +package {{.Package}} + +import ( + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + + "github.com/ethereum/go-ethereum/common" +) + +var _ contract.Configurator = &configurator{} + +// ConfigKey is the key used in json config files to specify this precompile precompileconfig. +// must be unique across all precompiles. +const ConfigKey = "{{decapitalise .Contract.Type}}Config" + +// ContractAddress is the defined address of the precompile contract. +// This should be unique across all precompile contracts. +// See params/precompile_modules.go for registered precompile contracts and more information. +var ContractAddress = common.HexToAddress("{ASUITABLEHEXADDRESS}") // SET A SUITABLE HEX ADDRESS HERE + +// Module is the precompile module. It is used to register the precompile contract. +var Module = modules.Module{ + ConfigKey: ConfigKey, + Address: ContractAddress, + Contract: {{.Contract.Type}}Precompile, + Configurator: &configurator{}, +} + +type configurator struct{} + +func init() { + // Register the precompile module. + // Each precompile contract registers itself through [RegisterModule] function. + if err := modules.RegisterModule(Module); err != nil { + panic(err) + } +} + +// MakeConfig returns a new precompile config instance. +// This is required for Marshal/Unmarshal the precompile config. +func (*configurator) MakeConfig() precompileconfig.Config { + return new(Config) +} + +// Configure configures [state] with the given [cfg] precompileconfig. +// This function is called by the EVM once per precompile contract activation. +// You can use this function to set up your precompile contract's initial state, +// by using the [cfg] config and [state] stateDB. +func (*configurator) Configure(chainConfig contract.ChainConfig, cfg precompileconfig.Config, state contract.StateDB, _ contract.BlockContext) error { + config, ok := cfg.(*Config) + if !ok { + return fmt.Errorf("incorrect config %T: %v", config, config) + } + // CUSTOM CODE STARTS HERE + {{if .Contract.AllowList}} + // AllowList is activated for this precompile. Configuring allowlist addresses here. + return config.AllowListConfig.Configure(state, ContractAddress) + {{else}} + return nil + {{end}} +} +` diff --git a/accounts/abi/bind/template.go b/accounts/abi/bind/template.go index 7ea1635671..35e6e481f8 100644 --- a/accounts/abi/bind/template.go +++ b/accounts/abi/bind/template.go @@ -31,30 +31,30 @@ import "github.com/ava-labs/subnet-evm/accounts/abi" // tmplData is the data structure required to fill the binding template. type tmplData struct { Package string // Name of the package to place the generated file in - Contracts map[string]*tmplContract // List of contracts to generate into this file + Contracts map[string]*TmplContract // List of contracts to generate into this file Libraries map[string]string // Map the bytecode's link pattern to the library name - Structs map[string]*tmplStruct // Contract struct type definitions + Structs map[string]*TmplStruct // Contract struct type definitions } -// tmplContract contains the data needed to generate an individual contract binding. -type tmplContract struct { +// TmplContract contains the data needed to generate an individual contract binding. +type TmplContract struct { Type string // Type name of the main contract binding InputABI string // JSON ABI used as the input to generate the binding from InputBin string // Optional EVM bytecode used to generate deploy code from FuncSigs map[string]string // Optional map: string signature -> 4-byte signature Constructor abi.Method // Contract constructor for deploy parametrization - Calls map[string]*tmplMethod // Contract calls that only read state data - Transacts map[string]*tmplMethod // Contract calls that write state data - Fallback *tmplMethod // Additional special fallback function - Receive *tmplMethod // Additional special receive function + Calls map[string]*TmplMethod // Contract calls that only read state data + Transacts map[string]*TmplMethod // Contract calls that write state data + Fallback *TmplMethod // Additional special fallback function + Receive *TmplMethod // Additional special receive function Events map[string]*tmplEvent // Contract events accessors Libraries map[string]string // Same as tmplData, but filtered to only keep what the contract needs Library bool // Indicator whether the contract is a library } -// tmplMethod is a wrapper around an abi.Method that contains a few preprocessed +// TmplMethod is a wrapper around an abi.Method that contains a few preprocessed // and cached data fields. -type tmplMethod struct { +type TmplMethod struct { Original abi.Method // Original method as parsed by the abi package Normalized abi.Method // Normalized version of the parsed method (capitalized names, non-anonymous args/returns) Structured bool // Whether the returns should be accumulated into a struct @@ -75,9 +75,9 @@ type tmplField struct { SolKind abi.Type // Raw abi type information } -// tmplStruct is a wrapper around an abi.tuple and contains an auto-generated +// TmplStruct is a wrapper around an abi.tuple and contains an auto-generated // struct name. -type tmplStruct struct { +type TmplStruct struct { Name string // Auto-generated struct name(before solidity v0.5.11) or raw name. Fields []*tmplField // Struct fields definition depends on the binding language. } @@ -335,7 +335,7 @@ var ( if err != nil { return *outstruct, err } - {{range $i, $t := .Normalized.Outputs}} + {{range $i, $t := .Normalized.Outputs}} outstruct.{{.Name}} = *abi.ConvertType(out[{{$i}}], new({{bindtype .Type $structs}})).(*{{bindtype .Type $structs}}){{end}} return *outstruct, err @@ -345,7 +345,7 @@ var ( } {{range $i, $t := .Normalized.Outputs}} out{{$i}} := *abi.ConvertType(out[{{$i}}], new({{bindtype .Type $structs}})).(*{{bindtype .Type $structs}}){{end}} - + return {{range $i, $t := .Normalized.Outputs}}out{{$i}}, {{end}} err {{end}} } @@ -388,7 +388,7 @@ var ( } {{end}} - {{if .Fallback}} + {{if .Fallback}} // Fallback is a paid mutator transaction binding the contract fallback function. // // Solidity: {{.Fallback.Original.String}} @@ -402,16 +402,16 @@ var ( func (_{{$contract.Type}} *{{$contract.Type}}Session) Fallback(calldata []byte) (*types.Transaction, error) { return _{{$contract.Type}}.Contract.Fallback(&_{{$contract.Type}}.TransactOpts, calldata) } - + // Fallback is a paid mutator transaction binding the contract fallback function. - // + // // Solidity: {{.Fallback.Original.String}} func (_{{$contract.Type}} *{{$contract.Type}}TransactorSession) Fallback(calldata []byte) (*types.Transaction, error) { return _{{$contract.Type}}.Contract.Fallback(&_{{$contract.Type}}.TransactOpts, calldata) } {{end}} - {{if .Receive}} + {{if .Receive}} // Receive is a paid mutator transaction binding the contract receive function. // // Solidity: {{.Receive.Original.String}} @@ -425,9 +425,9 @@ var ( func (_{{$contract.Type}} *{{$contract.Type}}Session) Receive() (*types.Transaction, error) { return _{{$contract.Type}}.Contract.Receive(&_{{$contract.Type}}.TransactOpts) } - + // Receive is a paid mutator transaction binding the contract receive function. - // + // // Solidity: {{.Receive.Original.String}} func (_{{$contract.Type}} *{{$contract.Type}}TransactorSession) Receive() (*types.Transaction, error) { return _{{$contract.Type}}.Contract.Receive(&_{{$contract.Type}}.TransactOpts) @@ -700,7 +700,7 @@ import java.util.*; // Fallback is a paid mutator transaction binding the contract fallback function. // // Solidity: {{.Fallback.Original.String}} - public Transaction Fallback(TransactOpts opts, byte[] calldata) throws Exception { + public Transaction Fallback(TransactOpts opts, byte[] calldata) throws Exception { return this.Contract.rawTransact(opts, calldata); } {{end}} @@ -709,7 +709,7 @@ import java.util.*; // Receive is a paid mutator transaction binding the contract receive function. // // Solidity: {{.Receive.Original.String}} - public Transaction Receive(TransactOpts opts) throws Exception { + public Transaction Receive(TransactOpts opts) throws Exception { return this.Contract.rawTransact(opts, null); } {{end}} diff --git a/cmd/abigen/main.go b/cmd/abigen/main.go index 5efc6c95f2..08900279d0 100644 --- a/cmd/abigen/main.go +++ b/cmd/abigen/main.go @@ -232,7 +232,7 @@ func abigen(c *cli.Context) error { } } // Generate the contract binding - code, err := bind.Bind(types, abis, bins, sigs, c.String(pkgFlag.Name), lang, libs, aliases, false) + code, err := bind.Bind(types, abis, bins, sigs, c.String(pkgFlag.Name), lang, libs, aliases) if err != nil { utils.Fatalf("Failed to generate ABI binding: %v", err) } diff --git a/cmd/precompilegen/main.go b/cmd/precompilegen/main.go index 99aed93c52..a2545e3572 100644 --- a/cmd/precompilegen/main.go +++ b/cmd/precompilegen/main.go @@ -33,7 +33,10 @@ import ( "path/filepath" "strings" + _ "embed" + "github.com/ava-labs/subnet-evm/accounts/abi/bind" + "github.com/ava-labs/subnet-evm/accounts/abi/bind/precompilebind" "github.com/ava-labs/subnet-evm/internal/flags" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/log" @@ -46,25 +49,28 @@ var ( gitDate = "" app *cli.App + + //go:embed template-readme.md + readme string ) var ( // Flags needed by abigen abiFlag = &cli.StringFlag{ Name: "abi", - Usage: "Path to the Ethereum contract ABI json to bind, - for STDIN", + Usage: "Path to the contract ABI json to generate, - for STDIN", } typeFlag = &cli.StringFlag{ Name: "type", - Usage: "Struct name for the precompile (default = ABI name)", + Usage: "Struct name for the precompile (default = {abi file name})", } pkgFlag = &cli.StringFlag{ Name: "pkg", - Usage: "Package name to generate the precompile into (default = precompile)", + Usage: "Go 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 = ./precompile/contracts/{pkg})", } ) @@ -81,13 +87,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 +111,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 +122,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) @@ -125,23 +133,72 @@ func precompilegen(c *cli.Context) error { } types = append(types, kind) + pkg := c.String(pkgFlag.Name) + if pkg == "" { + pkg = strings.ToLower(kind) + } + + if outFlagStr == "" { + outFlagStr = filepath.Join("./precompile/contracts", pkg) + } + + abifilename := "" + abipath := "" + // we should not generate the abi file if output is set to stdout + if !isOutStdout { + // get file name from the output path + abifilename = "contract.abi" + abipath = filepath.Join(outFlagStr, abifilename) + } // Generate the contract precompile - code, err := bind.Bind(types, abis, bins, sigs, pkg, lang, libs, aliases, true) + configCode, contractCode, moduleCode, err := precompilebind.PrecompileBind(types, abis, bins, sigs, pkg, lang, libs, aliases, abifilename) if err != nil { - utils.Fatalf("Failed to generate ABI precompile: %v", err) + utils.Fatalf("Failed to generate precompile: %v", err) } // Either flush it out to a file or display on the standard output - if !c.IsSet(outFlag.Name) { - 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) + fmt.Print("-----Module Code-----\n") + fmt.Printf("%s\n", moduleCode) return nil } - if err := os.WriteFile(c.String(outFlag.Name), []byte(code), 0o600); err != nil { - utils.Fatalf("Failed to write ABI precompile: %v", err) + if _, err := os.Stat(outFlagStr); os.IsNotExist(err) { + os.MkdirAll(outFlagStr, 0o700) // 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 config code: %v", err) + } + + contractCodeOut := filepath.Join(outFlagStr, "contract.go") + + if err := os.WriteFile(contractCodeOut, []byte(contractCode), 0o600); err != nil { + utils.Fatalf("Failed to write generated contract code: %v", err) + } + + moduleCodeOut := filepath.Join(outFlagStr, "module.go") + + if err := os.WriteFile(moduleCodeOut, []byte(moduleCode), 0o600); err != nil { + utils.Fatalf("Failed to write generated module code: %v", err) + } + + if err := os.WriteFile(abipath, []byte(abis[0]), 0o600); err != nil { + utils.Fatalf("Failed to write ABI: %v", err) + } + + readmeOut := filepath.Join(outFlagStr, "README.md") + + if err := os.WriteFile(readmeOut, []byte(readme), 0o600); err != nil { + utils.Fatalf("Failed to write README: %v", err) } - fmt.Println("Precompile Generation was a success!") + fmt.Println("Precompile files generated successfully at: ", outFlagStr) return nil } diff --git a/cmd/precompilegen/template-readme.md b/cmd/precompilegen/template-readme.md new file mode 100644 index 0000000000..883f41f105 --- /dev/null +++ b/cmd/precompilegen/template-readme.md @@ -0,0 +1,22 @@ +There are some must-be-done changes waiting in the generated 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 contract_test.go and config_test.go in other precompile folders. +See the tutorial in for more information about precompile development. + +General guidelines for precompile development: +1- Set a suitable config key in generated module.go. E.g: "yourPrecompileConfig" +2- Read the comment and set a suitable contract address in generated module.go. E.g: +ContractAddress = common.HexToAddress("ASUITABLEHEXADDRESS") +3- It is recommended to only modify code in the highlighted areas marked with "CUSTOM CODE STARTS HERE". Typically, custom codes are required in only those areas. +Modifying code outside of these areas should be done with caution and with a deep understanding of how these changes may impact the EVM. +4- Set gas costs in generated contract.go +5- Force import your precompile package in precompile/registry/registry.go +6- Add your config unit tests under generated package config_test.go +7- Add your contract unit tests under generated package contract_test.go +8- Additionally you can add a full-fledged VM test for your precompile under plugin/vm/vm_test.go. See existing precompile tests for examples. +9- Add your solidity interface and test contract to contract-examples/contracts +10- Write solidity tests for your precompile in contract-examples/test +11- Create your genesis with your precompile enabled in tests/e2e/genesis/ +12- Create e2e test for your solidity test in tests/e2e/solidity/suites.go +13- Run your e2e precompile Solidity tests with 'E2E=true ./scripts/run.sh diff --git a/commontype/fee_config.go b/commontype/fee_config.go index e1dee3182a..3089df5d9c 100644 --- a/commontype/fee_config.go +++ b/commontype/fee_config.go @@ -16,8 +16,8 @@ import ( // // The dynamic fee algorithm simply increases fees when the network is operating at a utilization level above the target and decreases fees // when the network is operating at a utilization level below the target. -// This struct is used by params.Config and precompile.FeeConfigManager -// any modification of this struct has direct affect on the precompiled contract +// This struct is used by Genesis and Fee Manager precompile. +// Any modification of this struct has direct affect on the precompiled contract // and changes should be carefully handled in the precompiled contract code. type FeeConfig struct { // GasLimit sets the max amount of gas consumed per block. diff --git a/commontype/fee_config_test.go b/commontype/fee_config_test.go index 997cb708e3..c0e26c4ced 100644 --- a/commontype/fee_config_test.go +++ b/commontype/fee_config_test.go @@ -10,19 +10,6 @@ import ( "github.com/stretchr/testify/require" ) -var validFeeConfig = 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 TestVerify(t *testing.T) { tests := []struct { name string @@ -47,43 +34,43 @@ func TestVerify(t *testing.T) { }, { name: "invalid GasLimit in FeeConfig", - config: func() *FeeConfig { c := validFeeConfig; c.GasLimit = big.NewInt(0); return &c }(), + config: func() *FeeConfig { c := ValidTestFeeConfig; c.GasLimit = big.NewInt(0); return &c }(), expectedError: "gasLimit = 0 cannot be less than or equal to 0", }, { name: "invalid TargetBlockRate in FeeConfig", - config: func() *FeeConfig { c := validFeeConfig; c.TargetBlockRate = 0; return &c }(), + config: func() *FeeConfig { c := ValidTestFeeConfig; c.TargetBlockRate = 0; return &c }(), expectedError: "targetBlockRate = 0 cannot be less than or equal to 0", }, { name: "invalid MinBaseFee in FeeConfig", - config: func() *FeeConfig { c := validFeeConfig; c.MinBaseFee = big.NewInt(-1); return &c }(), + config: func() *FeeConfig { c := ValidTestFeeConfig; c.MinBaseFee = big.NewInt(-1); return &c }(), expectedError: "minBaseFee = -1 cannot be less than 0", }, { name: "invalid TargetGas in FeeConfig", - config: func() *FeeConfig { c := validFeeConfig; c.TargetGas = big.NewInt(0); return &c }(), + config: func() *FeeConfig { c := ValidTestFeeConfig; c.TargetGas = big.NewInt(0); return &c }(), expectedError: "targetGas = 0 cannot be less than or equal to 0", }, { name: "invalid BaseFeeChangeDenominator in FeeConfig", - config: func() *FeeConfig { c := validFeeConfig; c.BaseFeeChangeDenominator = big.NewInt(0); return &c }(), + config: func() *FeeConfig { c := ValidTestFeeConfig; c.BaseFeeChangeDenominator = big.NewInt(0); return &c }(), expectedError: "baseFeeChangeDenominator = 0 cannot be less than or equal to 0", }, { name: "invalid MinBlockGasCost in FeeConfig", - config: func() *FeeConfig { c := validFeeConfig; c.MinBlockGasCost = big.NewInt(-1); return &c }(), + config: func() *FeeConfig { c := ValidTestFeeConfig; c.MinBlockGasCost = big.NewInt(-1); return &c }(), expectedError: "minBlockGasCost = -1 cannot be less than 0", }, { name: "valid FeeConfig", - config: &validFeeConfig, + config: &ValidTestFeeConfig, expectedError: "", }, { name: "MinBlockGasCost bigger than MaxBlockGasCost in FeeConfig", config: func() *FeeConfig { - c := validFeeConfig + c := ValidTestFeeConfig c.MinBlockGasCost = big.NewInt(2) c.MaxBlockGasCost = big.NewInt(1) return &c @@ -92,7 +79,7 @@ func TestVerify(t *testing.T) { }, { name: "invalid BlockGasCostStep in FeeConfig", - config: func() *FeeConfig { c := validFeeConfig; c.BlockGasCostStep = big.NewInt(-1); return &c }(), + config: func() *FeeConfig { c := ValidTestFeeConfig; c.BlockGasCostStep = big.NewInt(-1); return &c }(), expectedError: "blockGasCostStep = -1 cannot be less than 0", }, } @@ -119,7 +106,7 @@ func TestEqual(t *testing.T) { }{ { name: "equal", - a: &validFeeConfig, + a: &ValidTestFeeConfig, b: &FeeConfig{ GasLimit: big.NewInt(8_000_000), TargetBlockRate: 2, // in seconds @@ -136,13 +123,13 @@ func TestEqual(t *testing.T) { }, { name: "not equal", - a: &validFeeConfig, - b: func() *FeeConfig { c := validFeeConfig; c.GasLimit = big.NewInt(1); return &c }(), + a: &ValidTestFeeConfig, + b: func() *FeeConfig { c := ValidTestFeeConfig; c.GasLimit = big.NewInt(1); return &c }(), expected: false, }, { name: "not equal nil", - a: &validFeeConfig, + a: &ValidTestFeeConfig, b: nil, expected: false, }, diff --git a/commontype/test_fee_config.go b/commontype/test_fee_config.go new file mode 100644 index 0000000000..646f21d1cb --- /dev/null +++ b/commontype/test_fee_config.go @@ -0,0 +1,19 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package commontype + +import "math/big" + +var ValidTestFeeConfig = 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), +} diff --git a/contract-examples/README.md b/contract-examples/README.md index 6fbbc5f513..1e4b2c7cb0 100644 --- a/contract-examples/README.md +++ b/contract-examples/README.md @@ -44,7 +44,7 @@ $ yarn `ExampleDeployerList` shows how `ContractDeployerAllowList` precompile can be used in a smart contract. It uses `IAllowList` to interact with `ContractDeployerAllowList` precompile. When the precompile is activated only those allowed can deploy contracts. -`ExampleFeeManager` shows how a contract can change fee configuration with the `FeeConfigManager` precompile. +`ExampleFeeManager` shows how a contract can change fee configuration with the `FeeManager` precompile. All of these `NativeMinter`, `FeeManager` and `AllowList` contracts should be enabled by a chain config in genesis or as an upgrade. See the example genesis under [Tests](#tests) section. diff --git a/contract-examples/contracts/ExampleFeeManager.sol b/contract-examples/contracts/ExampleFeeManager.sol index dcc5828689..cd22b04f24 100644 --- a/contract-examples/contracts/ExampleFeeManager.sol +++ b/contract-examples/contracts/ExampleFeeManager.sol @@ -6,7 +6,7 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "./AllowList.sol"; import "./IFeeManager.sol"; -// ExampleFeeManager shows how FeeConfigManager precompile can be used in a smart contract +// ExampleFeeManager shows how FeeManager precompile can be used in a smart contract // All methods of [allowList] can be directly called. There are example calls as tasks in hardhat.config.ts file. contract ExampleFeeManager is AllowList { // Precompiled Fee Manager Contract Address diff --git a/contract-examples/contracts/ExampleTxAllowList.sol b/contract-examples/contracts/ExampleTxAllowList.sol index 5c17fe1169..9d5c80095f 100644 --- a/contract-examples/contracts/ExampleTxAllowList.sol +++ b/contract-examples/contracts/ExampleTxAllowList.sol @@ -4,11 +4,11 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/access/Ownable.sol"; import "./AllowList.sol"; -// ExampleDeployerList shows how ContractDeployerAllowList precompile can be used in a smart contract +// ExampleTxAllowList shows how TxAllowList precompile can be used in a smart contract // All methods of [allowList] can be directly called. There are example calls as tasks in hardhat.config.ts file. contract ExampleTxAllowList is AllowList { // Precompiled Allow List Contract Address - address constant DEPLOYER_LIST = 0x0200000000000000000000000000000000000002; + address constant TX_ALLOW_LIST = 0x0200000000000000000000000000000000000002; - constructor() AllowList(DEPLOYER_LIST) {} + constructor() AllowList(TX_ALLOW_LIST) {} } diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index f30a7fe553..6d1b56f77a 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/contracts/feemanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/rewardmanager" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/event" ) @@ -342,13 +343,13 @@ func (bc *BlockChain) SubscribeAcceptedTransactionEvent(ch chan<- NewTxsEvent) e } // GetFeeConfigAt returns the fee configuration and the last changed block number at [parent]. -// If FeeConfigManager is activated at [parent], returns the fee config in the precompile contract state. +// If FeeManager is activated at [parent], returns the fee config in the precompile contract state. // Otherwise returns the fee config in the chain config. // Assumes that a valid configuration is stored when the precompile is activated. func (bc *BlockChain) GetFeeConfigAt(parent *types.Header) (commontype.FeeConfig, *big.Int, error) { config := bc.Config() bigTime := new(big.Int).SetUint64(parent.Time) - if !config.IsFeeConfigManager(bigTime) { + if !config.IsPrecompileEnabled(feemanager.ContractAddress, 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.IsRewardManager(bigTime) { + if !config.IsPrecompileEnabled(rewardmanager.ContractAddress, 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/blockchain_test.go b/core/blockchain_test.go index e4eb86c3d0..678c0dbe91 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -747,6 +747,7 @@ func TestCanonicalHashMarker(t *testing.T) { } func TestTransactionIndices(t *testing.T) { + t.Skip("FLAKY") // Configure and generate a sample block chain require := require.New(t) var ( diff --git a/core/genesis.go b/core/genesis.go index eb88bc82f4..07a0ac1bc0 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -307,7 +307,10 @@ func (g *Genesis) ToBlock(db ethdb.Database) *types.Block { } // Configure any stateful precompiles that should be enabled in the genesis. - g.Config.CheckConfigurePrecompiles(nil, types.NewBlockWithHeader(head), statedb) + err = ApplyPrecompileActivations(g.Config, nil, types.NewBlockWithHeader(head), statedb) + if err != nil { + panic(fmt.Sprintf("unable to configure precompiles in genesis block: %v", err)) + } // Do custom allocation after airdrop in case an address shows up in standard // allocation diff --git a/core/genesis_test.go b/core/genesis_test.go index 920e5337b6..90ecc54984 100644 --- a/core/genesis_test.go +++ b/core/genesis_test.go @@ -38,7 +38,8 @@ import ( "github.com/ava-labs/subnet-evm/core/vm" "github.com/ava-labs/subnet-evm/ethdb" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/deployerallowlist" "github.com/davecgh/go-spew/spew" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" @@ -190,12 +191,14 @@ 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.GenesisPrecompiles = params.Precompiles{ + deployerallowlist.ConfigKey: deployerallowlist.NewConfig(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, allowlist.AdminRole, deployerallowlist.GetContractDeployerAllowListStatus(sdb, addr), "unexpected allow list status for modified address") + assert.Equal(t, uint64(1), sdb.GetNonce(deployerallowlist.ContractAddress)) }, }, } { @@ -265,11 +268,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.NewConfig(big.NewInt(51), nil, nil) activatedGenesis.Config.UpgradeConfig.PrecompileUpgrades = []params.PrecompileUpgrade{ { - // Enable ContractDeployerAllowList at timestamp 50 - ContractDeployerAllowListConfig: contractDeployerConfig, + Config: contractDeployerConfig, }, } diff --git a/core/state/test_statedb.go b/core/state/test_statedb.go new file mode 100644 index 0000000000..6dc1aa1065 --- /dev/null +++ b/core/state/test_statedb.go @@ -0,0 +1,20 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "testing" + + "github.com/ava-labs/subnet-evm/ethdb/memorydb" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func NewTestStateDB(t *testing.T) contract.StateDB { + db := memorydb.New() + stateDB, err := New(common.Hash{}, NewDatabase(db), nil) + require.NoError(t, err) + return stateDB +} diff --git a/core/state_processor.go b/core/state_processor.go index 2b28f8b082..2634f83640 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -35,8 +35,11 @@ import ( "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/core/vm" "github.com/ava-labs/subnet-evm/params" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" ) // StateProcessor is a basic Processor, which takes care of transitioning @@ -78,7 +81,11 @@ func (p *StateProcessor) Process(block *types.Block, parent *types.Header, state ) // Configure any stateful precompiles that should go into effect during this block. - p.config.CheckConfigurePrecompiles(new(big.Int).SetUint64(parent.Time), block, statedb) + err := ApplyPrecompileActivations(p.config, new(big.Int).SetUint64(parent.Time), block, statedb) + if err != nil { + log.Error("failed to configure precompiles processing block", "hash", block.Hash(), "number", block.NumberU64(), "timestamp", block.Time(), "err", err) + return nil, nil, 0, err + } blockContext := NewEVMBlockContext(header, p.bc, nil) vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg) @@ -163,3 +170,51 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, config, cfg) return applyTransaction(msg, config, author, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv) } + +// ApplyPrecompileActivations checks if any of the precompiles specified by the chain config are enabled or disabled by the block +// transition from [parentTimestamp] to the timestamp set in [blockContext]. If this is the case, it calls [Configure] +// to apply the necessary state transitions for the upgrade. +// This function is called: +// - within genesis setup to configure the starting state for precompiles enabled at genesis, +// - during block processing to update the state before processing the given block. +// - during block producing to apply the precompile upgrades before producing the block. +func ApplyPrecompileActivations(c *params.ChainConfig, parentTimestamp *big.Int, blockContext contract.BlockContext, statedb *state.StateDB) error { + blockTimestamp := blockContext.Timestamp() + // Note: RegisteredModules returns precompiles sorted by module addresses. + // This ensures that the order we call Configure for each precompile is consistent. + // This ensures even if precompiles read/write state other than their own they will observe + // an identical global state in a deterministic order when they are configured. + for _, module := range modules.RegisteredModules() { + key := module.ConfigKey + for _, activatingConfig := range c.GetActivatingPrecompileConfigs(module.Address, parentTimestamp, blockTimestamp, c.PrecompileUpgrades) { + // If this transition activates the upgrade, configure the stateful precompile. + // (or deconfigure it if it is being disabled.) + if activatingConfig.IsDisabled() { + log.Info("Disabling precompile", "name", key) + statedb.Suicide(module.Address) + // Calling Finalise here effectively commits Suicide call and wipes the contract state. + // This enables re-configuration of the same contract state in the same block. + // Without an immediate Finalise call after the Suicide, a reconfigured precompiled state can be wiped out + // since Suicide will be committed after the reconfiguration. + statedb.Finalise(true) + } else { + module, ok := modules.GetPrecompileModule(key) + if !ok { + return fmt.Errorf("could not find module for activating precompile, name: %s", key) + } + log.Info("Activating new precompile", "name", key, "config", activatingConfig) + // Set the nonce of the precompile's address (as is done when a contract is created) to ensure + // that it is marked as non-empty and will not be cleaned up when the statedb is finalized. + statedb.SetNonce(module.Address, 1) + // Set the code of the precompile's address to a non-zero length byte slice to ensure that the precompile + // can be called from within Solidity contracts. Solidity adds a check before invoking a contract to ensure + // that it does not attempt to invoke a non-existent contract. + statedb.SetCode(module.Address, []byte{0x1}) + if err := module.Configure(c, activatingConfig, statedb, blockContext); err != nil { + return fmt.Errorf("could not configure precompile, name: %s, reason: %w", key, err) + } + } + } + } + return nil +} diff --git a/core/state_processor_test.go b/core/state_processor_test.go index 6bf05057d9..b48f612333 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -36,7 +36,7 @@ import ( "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/core/vm" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" "github.com/ava-labs/subnet-evm/trie" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -315,8 +315,8 @@ func TestBadTxAllowListBlock(t *testing.T) { NetworkUpgrades: params.NetworkUpgrades{ SubnetEVMTimestamp: big.NewInt(0), }, - PrecompileUpgrade: params.PrecompileUpgrade{ - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(0), nil, nil), + GenesisPrecompiles: params.Precompiles{ + txallowlist.ConfigKey: txallowlist.NewConfig(big.NewInt(0), nil, nil), }, } signer = types.LatestSigner(config) diff --git a/core/state_transition.go b/core/state_transition.go index d039d0694c..ccb72beeaa 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/contracts/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().IsTxAllowList(st.evm.Context.Time) { - txAllowListRole := precompile.GetTxAllowListStatus(st.state, st.msg.From()) + if st.evm.ChainConfig().IsPrecompileEnabled(txallowlist.ContractAddress, st.evm.Context.Time) { + txAllowListRole := txallowlist.GetTxAllowListStatus(st.state, st.msg.From()) if !txAllowListRole.IsEnabled() { - return fmt.Errorf("%w: %s", precompile.ErrSenderAddressNotAllowListed, st.msg.From()) + return fmt.Errorf("%w: %s", vmerrs.ErrSenderAddressNotAllowListed, st.msg.From()) } } } diff --git a/core/stateful_precompile_test.go b/core/stateful_precompile_test.go deleted file mode 100644 index 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..de4650106b 100644 --- a/core/test_blockchain.go +++ b/core/test_blockchain.go @@ -16,7 +16,9 @@ import ( "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/ethdb" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/deployerallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/feemanager" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/assert" @@ -1546,8 +1548,10 @@ func TestStatefulPrecompiles(t *testing.T, create func(db ethdb.Database, chainC genesisBalance := new(big.Int).Mul(big.NewInt(1000000), big.NewInt(params.Ether)) config := *params.TestChainConfig // Set all of the required config parameters - config.ContractDeployerAllowListConfig = precompile.NewContractDeployerAllowListConfig(big.NewInt(0), []common.Address{addr1}, nil) - config.FeeManagerConfig = precompile.NewFeeManagerConfig(big.NewInt(0), []common.Address{addr1}, nil, nil) + config.GenesisPrecompiles = params.Precompiles{ + deployerallowlist.ConfigKey: deployerallowlist.NewConfig(big.NewInt(0), []common.Address{addr1}, nil), + feemanager.ConfigKey: feemanager.NewConfig(big.NewInt(0), []common.Address{addr1}, nil, nil), + } gspec := &Genesis{ Config: &config, Alloc: GenesisAlloc{addr1: {Balance: genesisBalance}}, @@ -1587,14 +1591,14 @@ func TestStatefulPrecompiles(t *testing.T, create func(db ethdb.Database, chainC "allow list": { addTx: func(gen *BlockGen) { feeCap := new(big.Int).Add(gen.BaseFee(), tip) - input, err := precompile.PackModifyAllowList(addr2, precompile.AllowListAdmin) + input, err := allowlist.PackModifyAllowList(addr2, allowlist.AdminRole) if err != nil { t.Fatal(err) } tx := types.NewTx(&types.DynamicFeeTx{ ChainID: params.TestChainConfig.ChainID, Nonce: gen.TxNonce(addr1), - To: &precompile.ContractDeployerAllowListAddress, + To: &deployerallowlist.ContractAddress, Gas: 3_000_000, Value: common.Big0, GasFeeCap: feeCap, @@ -1609,38 +1613,38 @@ 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) - if precompile.AllowListAdmin != res { - return fmt.Errorf("unexpected allow list status for addr1 %s, expected %s", res, precompile.AllowListAdmin) + res := deployerallowlist.GetContractDeployerAllowListStatus(sdb, addr1) + if allowlist.AdminRole != res { + return fmt.Errorf("unexpected allow list status for addr1 %s, expected %s", res, allowlist.AdminRole) } - res = precompile.GetContractDeployerAllowListStatus(sdb, addr2) - if precompile.AllowListAdmin != res { - return fmt.Errorf("unexpected allow list status for addr2 %s, expected %s", res, precompile.AllowListAdmin) + res = deployerallowlist.GetContractDeployerAllowListStatus(sdb, addr2) + if allowlist.AdminRole != res { + return fmt.Errorf("unexpected allow list status for addr2 %s, expected %s", res, allowlist.AdminRole) } return nil }, verifyGenesis: func(sdb *state.StateDB) { - res := precompile.GetContractDeployerAllowListStatus(sdb, addr1) - if precompile.AllowListAdmin != res { - t.Fatalf("unexpected allow list status for addr1 %s, expected %s", res, precompile.AllowListAdmin) + res := deployerallowlist.GetContractDeployerAllowListStatus(sdb, addr1) + if allowlist.AdminRole != res { + t.Fatalf("unexpected allow list status for addr1 %s, expected %s", res, allowlist.AdminRole) } - res = precompile.GetContractDeployerAllowListStatus(sdb, addr2) - if precompile.AllowListNoRole != res { - t.Fatalf("unexpected allow list status for addr2 %s, expected %s", res, precompile.AllowListNoRole) + res = deployerallowlist.GetContractDeployerAllowListStatus(sdb, addr2) + if allowlist.NoRole != res { + t.Fatalf("unexpected allow list status for addr2 %s, expected %s", res, allowlist.NoRole) } }, }, "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.ContractAddress, Gas: 3_000_000, Value: common.Big0, GasFeeCap: feeCap, @@ -1655,10 +1659,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) - assert.Equal(precompile.AllowListAdmin, res) + res := feemanager.GetFeeManagerStatus(sdb, addr1) + assert.Equal(allowlist.AdminRole, res) - storedConfig := precompile.GetStoredFeeConfig(sdb) + storedConfig := feemanager.GetStoredFeeConfig(sdb) assert.EqualValues(testFeeConfig, storedConfig) feeConfig, _, err := blockchain.GetFeeConfigAt(blockchain.CurrentHeader()) @@ -1667,8 +1671,8 @@ func TestStatefulPrecompiles(t *testing.T, create func(db ethdb.Database, chainC return nil }, verifyGenesis: func(sdb *state.StateDB) { - res := precompile.GetFeeConfigManagerStatus(sdb, addr1) - assert.Equal(precompile.AllowListAdmin, res) + res := feemanager.GetFeeManagerStatus(sdb, addr1) + assert.Equal(allowlist.AdminRole, res) feeConfig, _, err := blockchain.GetFeeConfigAt(blockchain.Genesis().Header()) assert.NoError(err) diff --git a/core/tx_pool.go b/core/tx_pool.go index ed5502d94b..dd3e4a4f39 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -42,7 +42,10 @@ import ( "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/metrics" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/contracts/feemanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/prque" "github.com/ethereum/go-ethereum/event" @@ -693,10 +696,10 @@ func (pool *TxPool) checkTxState(from common.Address, tx *types.Transaction) err // If the tx allow list is enabled, return an error if the from address is not allow listed. headTimestamp := big.NewInt(int64(pool.currentHead.Time)) - if pool.chainconfig.IsTxAllowList(headTimestamp) { - txAllowListRole := precompile.GetTxAllowListStatus(pool.currentState, from) + if pool.chainconfig.IsPrecompileEnabled(txallowlist.ContractAddress, headTimestamp) { + txAllowListRole := txallowlist.GetTxAllowListStatus(pool.currentState, from) if !txAllowListRole.IsEnabled() { - return fmt.Errorf("%w: %s", precompile.ErrSenderAddressNotAllowListed, from) + return fmt.Errorf("%w: %s", vmerrs.ErrSenderAddressNotAllowListed, from) } } return nil @@ -1441,7 +1444,7 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) { // when we reset txPool we should explicitly check if fee struct for min base fee has changed // so that we can correctly drop txs with < minBaseFee from tx pool. - if pool.chainconfig.IsFeeConfigManager(new(big.Int).SetUint64(newHead.Time)) { + if pool.chainconfig.IsPrecompileEnabled(feemanager.ContractAddress, new(big.Int).SetUint64(newHead.Time)) { feeConfig, _, err := pool.chain.GetFeeConfigAt(newHead) if err != nil { log.Error("Failed to get fee config state", "err", err, "root", newHead.Root) diff --git a/core/tx_pool_test.go b/core/tx_pool_test.go index de7bea7273..2bca2c372d 100644 --- a/core/tx_pool_test.go +++ b/core/tx_pool_test.go @@ -56,6 +56,19 @@ var ( // eip1559Config is a chain config with EIP-1559 enabled at block 0. eip1559Config *params.ChainConfig + + testFeeConfig = commontype.FeeConfig{ + GasLimit: big.NewInt(8_000_000), + TargetBlockRate: 2, // in seconds + + MinBaseFee: big.NewInt(25_000_000_000), + TargetGas: big.NewInt(15_000_000), + BaseFeeChangeDenominator: big.NewInt(36), + + MinBlockGasCost: big.NewInt(0), + MaxBlockGasCost: big.NewInt(1_000_000), + BlockGasCostStep: big.NewInt(200_000), + } ) func init() { diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 056f7af1ad..2d8ec6b79d 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -33,9 +33,9 @@ import ( "fmt" "math/big" - "github.com/ava-labs/subnet-evm/constants" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" @@ -57,7 +57,7 @@ type PrecompiledContract interface { // PrecompiledContractsHomestead contains the default set of pre-compiled Ethereum // contracts used in the Frontier and Homestead releases. -var PrecompiledContractsHomestead = map[common.Address]precompile.StatefulPrecompiledContract{ +var PrecompiledContractsHomestead = map[common.Address]contract.StatefulPrecompiledContract{ common.BytesToAddress([]byte{1}): newWrappedPrecompiledContract(&ecrecover{}), common.BytesToAddress([]byte{2}): newWrappedPrecompiledContract(&sha256hash{}), common.BytesToAddress([]byte{3}): newWrappedPrecompiledContract(&ripemd160hash{}), @@ -66,7 +66,7 @@ var PrecompiledContractsHomestead = map[common.Address]precompile.StatefulPrecom // PrecompiledContractsByzantium contains the default set of pre-compiled Ethereum // contracts used in the Byzantium release. -var PrecompiledContractsByzantium = map[common.Address]precompile.StatefulPrecompiledContract{ +var PrecompiledContractsByzantium = map[common.Address]contract.StatefulPrecompiledContract{ common.BytesToAddress([]byte{1}): newWrappedPrecompiledContract(&ecrecover{}), common.BytesToAddress([]byte{2}): newWrappedPrecompiledContract(&sha256hash{}), common.BytesToAddress([]byte{3}): newWrappedPrecompiledContract(&ripemd160hash{}), @@ -79,7 +79,7 @@ var PrecompiledContractsByzantium = map[common.Address]precompile.StatefulPrecom // PrecompiledContractsIstanbul contains the default set of pre-compiled Ethereum // contracts used in the Istanbul release. -var PrecompiledContractsIstanbul = map[common.Address]precompile.StatefulPrecompiledContract{ +var PrecompiledContractsIstanbul = map[common.Address]contract.StatefulPrecompiledContract{ common.BytesToAddress([]byte{1}): newWrappedPrecompiledContract(&ecrecover{}), common.BytesToAddress([]byte{2}): newWrappedPrecompiledContract(&sha256hash{}), common.BytesToAddress([]byte{3}): newWrappedPrecompiledContract(&ripemd160hash{}), @@ -93,7 +93,7 @@ var PrecompiledContractsIstanbul = map[common.Address]precompile.StatefulPrecomp // PrecompiledContractsBerlin contains the default set of pre-compiled Ethereum // contracts used in the Berlin release. -var PrecompiledContractsBerlin = map[common.Address]precompile.StatefulPrecompiledContract{ +var PrecompiledContractsBerlin = map[common.Address]contract.StatefulPrecompiledContract{ common.BytesToAddress([]byte{1}): newWrappedPrecompiledContract(&ecrecover{}), common.BytesToAddress([]byte{2}): newWrappedPrecompiledContract(&sha256hash{}), common.BytesToAddress([]byte{3}): newWrappedPrecompiledContract(&ripemd160hash{}), @@ -107,7 +107,7 @@ var PrecompiledContractsBerlin = map[common.Address]precompile.StatefulPrecompil // PrecompiledContractsBLS contains the set of pre-compiled Ethereum // contracts specified in EIP-2537. These are exported for testing purposes. -var PrecompiledContractsBLS = map[common.Address]precompile.StatefulPrecompiledContract{ +var PrecompiledContractsBLS = map[common.Address]contract.StatefulPrecompiledContract{ common.BytesToAddress([]byte{10}): newWrappedPrecompiledContract(&bls12381G1Add{}), common.BytesToAddress([]byte{11}): newWrappedPrecompiledContract(&bls12381G1Mul{}), common.BytesToAddress([]byte{12}): newWrappedPrecompiledContract(&bls12381G1MultiExp{}), @@ -158,12 +158,10 @@ 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)) - } - if k == constants.BlackholeAddr { - panic(fmt.Errorf("cannot use address %s for stateful precompile - overlaps with blackhole address", k)) + for _, module := range modules.RegisteredModules() { + address := module.Address + if _, ok := PrecompileAllNativeAddresses[address]; ok { + panic(fmt.Errorf("precompile address collides with existing native address: %s", address)) } } } diff --git a/core/vm/contracts_stateful.go b/core/vm/contracts_stateful.go index 49c1aef48d..dc04120979 100644 --- a/core/vm/contracts_stateful.go +++ b/core/vm/contracts_stateful.go @@ -4,7 +4,7 @@ package vm import ( - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/contract" "github.com/ethereum/go-ethereum/common" ) @@ -16,16 +16,16 @@ type wrappedPrecompiledContract struct { // newWrappedPrecompiledContract returns a wrapped version of [PrecompiledContract] to be executed according to the StatefulPrecompiledContract // interface. -func newWrappedPrecompiledContract(p PrecompiledContract) precompile.StatefulPrecompiledContract { +func newWrappedPrecompiledContract(p PrecompiledContract) contract.StatefulPrecompiledContract { return &wrappedPrecompiledContract{p: p} } // Run implements the StatefulPrecompiledContract interface -func (w *wrappedPrecompiledContract) Run(accessibleState precompile.PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { +func (w *wrappedPrecompiledContract) Run(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { return RunPrecompiledContract(w.p, input, suppliedGas) } // RunStatefulPrecompiledContract confirms runs [precompile] with the specified parameters. -func RunStatefulPrecompiledContract(precompile precompile.StatefulPrecompiledContract, accessibleState precompile.PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { +func RunStatefulPrecompiledContract(precompile contract.StatefulPrecompiledContract, accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { return precompile.Run(accessibleState, caller, addr, input, suppliedGas, readOnly) } diff --git a/core/vm/evm.go b/core/vm/evm.go index c13db0def9..764bce4e65 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -35,7 +35,9 @@ import ( "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/subnet-evm/constants" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/contracts/deployerallowlist" + "github.com/ava-labs/subnet-evm/precompile/modules" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -43,8 +45,8 @@ import ( ) var ( - _ precompile.PrecompileAccessibleState = &EVM{} - _ precompile.BlockContext = &BlockContext{} + _ contract.AccessibleState = &EVM{} + _ contract.BlockContext = &BlockContext{} ) // IsProhibited returns true if [addr] is in the prohibited list of addresses which should @@ -54,7 +56,7 @@ func IsProhibited(addr common.Address) bool { return true } - return precompile.ReservedAddress(addr) + return modules.ReservedAddress(addr) } // emptyCodeHash is used by create to ensure deployment is disallowed to already @@ -71,8 +73,8 @@ type ( GetHashFunc func(uint64) common.Hash ) -func (evm *EVM) precompile(addr common.Address) (precompile.StatefulPrecompiledContract, bool) { - var precompiles map[common.Address]precompile.StatefulPrecompiledContract +func (evm *EVM) precompile(addr common.Address) (contract.StatefulPrecompiledContract, bool) { + var precompiles map[common.Address]contract.StatefulPrecompiledContract switch { case evm.chainRules.IsSubnetEVM: precompiles = PrecompiledContractsBerlin @@ -91,8 +93,12 @@ func (evm *EVM) precompile(addr common.Address) (precompile.StatefulPrecompiledC } // Otherwise, check the chain rules for the additionally configured precompiles. - p, ok = evm.chainRules.Precompiles[addr] - return p, ok + if _, ok = evm.chainRules.ActivePrecompiles[addr]; ok { + module, ok := modules.GetPrecompileModuleByAddress(addr) + return module.Contract, ok + } + + return nil, false } // BlockContext provides the EVM with auxiliary information. Once provided @@ -207,12 +213,12 @@ func (evm *EVM) GetSnowContext() *snow.Context { } // GetStateDB returns the evm's StateDB -func (evm *EVM) GetStateDB() precompile.StateDB { +func (evm *EVM) GetStateDB() contract.StateDB { return evm.StateDB } // GetBlockContext returns the evm's BlockContext -func (evm *EVM) GetBlockContext() precompile.BlockContext { +func (evm *EVM) GetBlockContext() contract.BlockContext { return &evm.Context } @@ -507,8 +513,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.IsContractDeployerAllowListEnabled { - allowListRole := precompile.GetContractDeployerAllowListStatus(evm.StateDB, evm.TxContext.Origin) + if evm.chainRules.IsPrecompileEnabled(deployerallowlist.ContractAddress) { + allowListRole := deployerallowlist.GetContractDeployerAllowListStatus(evm.StateDB, evm.TxContext.Origin) if !allowListRole.IsEnabled() { return nil, common.Address{}, 0, fmt.Errorf("tx.origin %s is not authorized to deploy a contract", evm.TxContext.Origin) } diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index 1554073693..253778c6af 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -39,6 +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/contracts/feemanager" "github.com/ava-labs/subnet-evm/rpc" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" @@ -317,7 +318,7 @@ func (oracle *Oracle) suggestDynamicFees(ctx context.Context) (*big.Int, *big.In feeLastChangedAt *big.Int feeConfig commontype.FeeConfig ) - if oracle.backend.ChainConfig().IsFeeConfigManager(new(big.Int).SetUint64(head.Time)) { + if oracle.backend.ChainConfig().IsPrecompileEnabled(feemanager.ContractAddress, new(big.Int).SetUint64(head.Time)) { feeConfig, feeLastChangedAt, err = oracle.backend.GetFeeConfigAt(head) if err != nil { return nil, nil, err diff --git a/eth/gasprice/gasprice_test.go b/eth/gasprice/gasprice_test.go index 62e770564f..45672a53f5 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/contracts/feemanager" "github.com/ava-labs/subnet-evm/rpc" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -434,13 +434,15 @@ 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.GenesisPrecompiles = params.Precompiles{ + feemanager.ConfigKey: feemanager.NewConfig(big.NewInt(0), []common.Address{addr}, nil, nil), + } // create a fee config with higher MinBaseFee and prepare it for inclusion in a tx signer := types.LatestSigner(params.TestChainConfig) 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 +464,7 @@ func TestSuggestGasPriceAfterFeeConfigUpdate(t *testing.T) { tx := types.NewTx(&types.DynamicFeeTx{ ChainID: chainConfig.ChainID, Nonce: b.TxNonce(addr), - To: &precompile.FeeConfigManagerAddress, + To: &feemanager.ContractAddress, Gas: chainConfig.FeeConfig.GasLimit.Uint64(), Value: common.Big0, GasFeeCap: chainConfig.FeeConfig.MinBaseFee, // give low fee, it should work since we still haven't applied high fees diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index ebf872015d..8ace36596f 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -625,12 +625,13 @@ func (api *BlockChainAPI) ChainId() *hexutil.Big { return (*hexutil.Big)(api.b.ChainConfig().ChainID) } -func (s *BlockChainAPI) GetActivePrecompilesAt(ctx context.Context, blockTimestamp *big.Int) params.PrecompileUpgrade { +// GetActivePrecompilesAt returns the active precompile configs at the given block timestamp. +func (s *BlockChainAPI) GetActivePrecompilesAt(ctx context.Context, blockTimestamp *big.Int) params.Precompiles { if blockTimestamp == nil { - blockTimestampInt := s.b.CurrentHeader().Time - blockTimestamp = new(big.Int).SetUint64(blockTimestampInt) + blockTimestamp = new(big.Int).SetUint64(s.b.CurrentHeader().Time) } - return s.b.ChainConfig().GetActivePrecompiles(blockTimestamp) + + return s.b.ChainConfig().EnabledStatefulPrecompiles(blockTimestamp) } type FeeConfigResult struct { diff --git a/miner/worker.go b/miner/worker.go index d56c06cafd..26a9c31168 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -128,7 +128,7 @@ func (w *worker) commitNewWork() (*types.Block, error) { bigTimestamp := new(big.Int).SetUint64(timestamp) var gasLimit uint64 - // The fee config manager relies on the state of the parent block to set the fee config + // The fee manager relies on the state of the parent block to set the fee config // because the fee config may be changed by the current block. feeConfig, _, err := w.chain.GetFeeConfigAt(parent.Header()) if err != nil { @@ -186,7 +186,11 @@ func (w *worker) commitNewWork() (*types.Block, error) { return nil, fmt.Errorf("failed to create new current environment: %w", err) } // Configure any stateful precompiles that should go into effect during this block. - w.chainConfig.CheckConfigurePrecompiles(new(big.Int).SetUint64(parent.Time()), types.NewBlockWithHeader(header), env.state) + err = core.ApplyPrecompileActivations(w.chainConfig, new(big.Int).SetUint64(parent.Time()), types.NewBlockWithHeader(header), env.state) + if err != nil { + log.Error("failed to configure precompiles mining new block", "parent", parent.Hash(), "number", header.Number, "timestamp", header.Time, "err", err) + return nil, err + } // Fill the block with all available pending transactions. pending := w.eth.TxPool().Pending(true) diff --git a/params/config.go b/params/config.go index 7a3f013dcd..1f2289f2a9 100644 --- a/params/config.go +++ b/params/config.go @@ -34,7 +34,8 @@ import ( "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/subnet-evm/commontype" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" "github.com/ava-labs/subnet-evm/utils" "github.com/ethereum/go-ethereum/common" ) @@ -83,7 +84,7 @@ var ( PetersburgBlock: big.NewInt(0), IstanbulBlock: big.NewInt(0), MuirGlacierBlock: big.NewInt(0), - + GenesisPrecompiles: Precompiles{}, NetworkUpgrades: NetworkUpgrades{ SubnetEVMTimestamp: big.NewInt(0), }, @@ -105,7 +106,7 @@ var ( IstanbulBlock: big.NewInt(0), MuirGlacierBlock: big.NewInt(0), NetworkUpgrades: NetworkUpgrades{big.NewInt(0)}, - PrecompileUpgrade: PrecompileUpgrade{}, + GenesisPrecompiles: Precompiles{}, UpgradeConfig: UpgradeConfig{}, } @@ -125,7 +126,7 @@ var ( IstanbulBlock: big.NewInt(0), MuirGlacierBlock: big.NewInt(0), NetworkUpgrades: NetworkUpgrades{}, - PrecompileUpgrade: PrecompileUpgrade{}, + GenesisPrecompiles: Precompiles{}, UpgradeConfig: UpgradeConfig{}, } ) @@ -157,9 +158,57 @@ type ChainConfig struct { IstanbulBlock *big.Int `json:"istanbulBlock,omitempty"` // Istanbul switch block (nil = no fork, 0 = already on istanbul) MuirGlacierBlock *big.Int `json:"muirGlacierBlock,omitempty"` // Eip-2384 (bomb delay) switch block (nil = no fork, 0 = already activated) - NetworkUpgrades // Config for timestamps that enable avalanche network upgrades - PrecompileUpgrade // Config for enabling precompiles from genesis - UpgradeConfig `json:"-"` // Config specified in upgradeBytes (avalanche network upgrades or enable/disabling precompiles). Skip encoding/decoding directly into ChainConfig. + NetworkUpgrades // Config for timestamps that enable avalanche network upgrades + GenesisPrecompiles Precompiles `json:"-"` // Config for enabling precompiles from genesis. JSON encode/decode will be handled by the custom marshaler/unmarshaler. + UpgradeConfig `json:"-"` // Config specified in upgradeBytes (avalanche network upgrades or enable/disabling precompiles). Skip encoding/decoding directly into ChainConfig. +} + +// UnmarshalJSON parses the JSON-encoded data and stores the result in the +// object pointed to by c. +// This is a custom unmarshaler to handle the Precompiles field. +// Precompiles was presented as an inline object in the JSON. +// This custom unmarshaler ensures backwards compatibility with the old format. +func (c *ChainConfig) UnmarshalJSON(data []byte) error { + // Alias ChainConfig to avoid recursion + type _ChainConfig ChainConfig + tmp := _ChainConfig{} + if err := json.Unmarshal(data, &tmp); err != nil { + return err + } + + // At this point we have populated all fields except PrecompileUpgrade + *c = ChainConfig(tmp) + + // Unmarshal inlined PrecompileUpgrade + return json.Unmarshal(data, &c.GenesisPrecompiles) +} + +// MarshalJSON returns the JSON encoding of c. +// This is a custom marshaler to handle the Precompiles field. +func (c ChainConfig) MarshalJSON() ([]byte, error) { + // Alias ChainConfig to avoid recursion + type _ChainConfig ChainConfig + tmp, err := json.Marshal(_ChainConfig(c)) + if err != nil { + return nil, err + } + + // To include PrecompileUpgrades, we unmarshal the json representing c + // then directly add the corresponding keys to the json. + raw := make(map[string]json.RawMessage) + if err := json.Unmarshal(tmp, &raw); err != nil { + return nil, err + } + + for key, value := range c.GenesisPrecompiles { + conf, err := json.Marshal(value) + if err != nil { + return nil, err + } + raw[key] = conf + } + + return json.Marshal(raw) } // UpgradeConfig includes the following configs that may be specified in upgradeBytes: @@ -191,7 +240,7 @@ func (c *ChainConfig) String() string { if err != nil { networkUpgradesBytes = []byte("cannot marshal NetworkUpgrades") } - precompileUpgradeBytes, err := json.Marshal(c.PrecompileUpgrade) + precompileUpgradeBytes, err := json.Marshal(c.GenesisPrecompiles) if err != nil { precompileUpgradeBytes = []byte("cannot marshal PrecompileUpgrade") } @@ -272,46 +321,12 @@ func (c *ChainConfig) IsSubnetEVM(blockTimestamp *big.Int) bool { return utils.IsForked(c.getNetworkUpgrades().SubnetEVMTimestamp, blockTimestamp) } -// PRECOMPILE UPGRADES START HERE - -// IsContractDeployerAllowList returns whether [blockTimestamp] is either equal to the ContractDeployerAllowList fork block timestamp or greater. -func (c *ChainConfig) IsContractDeployerAllowList(blockTimestamp *big.Int) bool { - config := c.GetContractDeployerAllowListConfig(blockTimestamp) - return config != nil && !config.Disable -} - -// IsContractNativeMinter returns whether [blockTimestamp] is either equal to the NativeMinter fork block timestamp or greater. -func (c *ChainConfig) IsContractNativeMinter(blockTimestamp *big.Int) bool { - config := c.GetContractNativeMinterConfig(blockTimestamp) - return config != nil && !config.Disable -} - -// IsTxAllowList returns whether [blockTimestamp] is either equal to the TxAllowList fork block timestamp or greater. -func (c *ChainConfig) IsTxAllowList(blockTimestamp *big.Int) bool { - config := c.GetTxAllowListConfig(blockTimestamp) - return config != nil && !config.Disable -} - -// IsFeeConfigManager returns whether [blockTimestamp] is either equal to the FeeConfigManager fork block timestamp or greater. -func (c *ChainConfig) IsFeeConfigManager(blockTimestamp *big.Int) bool { - config := c.GetFeeConfigManagerConfig(blockTimestamp) - return config != nil && !config.Disable +// IsPrecompileEnabled returns whether precompile with [address] is enabled at [blockTimestamp]. +func (c *ChainConfig) IsPrecompileEnabled(address common.Address, blockTimestamp *big.Int) bool { + config := c.GetActivePrecompileConfig(address, blockTimestamp) + return config != nil && !config.IsDisabled() } -// IsRewardManager returns whether [blockTimestamp] is either equal to the RewardManager fork block timestamp or greater. -func (c *ChainConfig) IsRewardManager(blockTimestamp *big.Int) bool { - config := c.GetRewardManagerConfig(blockTimestamp) - return config != nil && !config.Disable -} - -// ADD YOUR PRECOMPILE HERE -/* -func (c *ChainConfig) Is{YourPrecompile}(blockTimestamp *big.Int) bool { - config := c.Get{YourPrecompile}Config(blockTimestamp) - return config != nil && !config.Disable -} -*/ - // CheckCompatible checks whether scheduled fork transitions have been imported // with a mismatching chain configuration. func (c *ChainConfig) CheckCompatible(newcfg *ChainConfig, height uint64, timestamp uint64) *ConfigCompatError { @@ -339,7 +354,7 @@ func (c *ChainConfig) Verify() error { // Verify the precompile upgrades are internally consistent given the existing chainConfig. if err := c.verifyPrecompileUpgrades(); err != nil { - return err + return fmt.Errorf("invalid precompile upgrades: %w", err) } return nil @@ -539,20 +554,17 @@ type Rules struct { // Rules for Avalanche releases IsSubnetEVM bool - // Optional stateful precompile rules - IsContractDeployerAllowListEnabled bool - IsContractNativeMinterEnabled bool - IsTxAllowListEnabled bool - IsFeeConfigManagerEnabled bool - IsRewardManagerEnabled bool - // ADD YOUR PRECOMPILE HERE - // Is{YourPrecompile}Enabled bool - - // Precompiles maps addresses to stateful precompiled contracts that are enabled + // ActivePrecompiles maps addresses to stateful precompiled contracts that are enabled // for this rule set. // Note: none of these addresses should conflict with the address space used by // any existing precompiles. - Precompiles map[common.Address]precompile.StatefulPrecompiledContract + ActivePrecompiles map[common.Address]precompileconfig.Config +} + +// IsPrecompileEnabled returns true if the precompile at [addr] is enabled for this rule set. +func (r *Rules) IsPrecompileEnabled(addr common.Address) bool { + _, ok := r.ActivePrecompiles[addr] + return ok } // Rules ensures c's ChainID is not nil. @@ -580,21 +592,13 @@ func (c *ChainConfig) AvalancheRules(blockNum, blockTimestamp *big.Int) Rules { rules := c.rules(blockNum) rules.IsSubnetEVM = c.IsSubnetEVM(blockTimestamp) - rules.IsContractDeployerAllowListEnabled = c.IsContractDeployerAllowList(blockTimestamp) - rules.IsContractNativeMinterEnabled = c.IsContractNativeMinter(blockTimestamp) - rules.IsTxAllowListEnabled = c.IsTxAllowList(blockTimestamp) - rules.IsFeeConfigManagerEnabled = c.IsFeeConfigManager(blockTimestamp) - rules.IsRewardManagerEnabled = c.IsRewardManager(blockTimestamp) - // ADD YOUR PRECOMPILE HERE - // rules.Is{YourPrecompile}Enabled = c.{IsYourPrecompile}(blockTimestamp) // Initialize the stateful precompiles that should be enabled at [blockTimestamp]. - rules.Precompiles = make(map[common.Address]precompile.StatefulPrecompiledContract) - for _, config := range c.EnabledStatefulPrecompiles(blockTimestamp) { - if config.IsDisabled() { - continue + rules.ActivePrecompiles = make(map[common.Address]precompileconfig.Config) + for _, module := range modules.RegisteredModules() { + if config := c.GetActivePrecompileConfig(module.Address, blockTimestamp); config != nil && !config.IsDisabled() { + rules.ActivePrecompiles[module.Address] = config } - rules.Precompiles[config.Address()] = config.Contract() } return rules diff --git a/params/config_test.go b/params/config_test.go index 50487f159a..a407c81bae 100644 --- a/params/config_test.go +++ b/params/config_test.go @@ -27,9 +27,15 @@ package params import ( + "encoding/json" "math/big" "reflect" "testing" + + "github.com/ava-labs/subnet-evm/precompile/contracts/nativeminter" + "github.com/ava-labs/subnet-evm/precompile/contracts/rewardmanager" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" ) func TestCheckCompatible(t *testing.T) { @@ -136,3 +142,88 @@ func TestCheckCompatible(t *testing.T) { } } } + +func TestConfigUnmarshalJSON(t *testing.T) { + require := require.New(t) + + testRewardManagerConfig := rewardmanager.NewConfig( + big.NewInt(1671542573), + []common.Address{common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC")}, + nil, + &rewardmanager.InitialRewardConfig{ + AllowFeeRecipients: true, + }) + + testContractNativeMinterConfig := nativeminter.NewConfig( + big.NewInt(0), + []common.Address{common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC")}, + nil, + nil, + ) + + config := []byte(` + { + "chainId": 43214, + "allowFeeRecipients": true, + "rewardManagerConfig": { + "blockTimestamp": 1671542573, + "adminAddresses": [ + "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" + ], + "initialRewardConfig": { + "allowFeeRecipients": true + } + }, + "contractNativeMinterConfig": { + "blockTimestamp": 0, + "adminAddresses": [ + "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" + ] + } + } + `) + c := ChainConfig{} + err := json.Unmarshal(config, &c) + require.NoError(err) + + require.Equal(c.ChainID, big.NewInt(43214)) + require.Equal(c.AllowFeeRecipients, true) + + rewardManagerConfig, ok := c.GenesisPrecompiles[rewardmanager.ConfigKey] + require.True(ok) + require.Equal(rewardManagerConfig.Key(), rewardmanager.ConfigKey) + require.True(rewardManagerConfig.Equal(testRewardManagerConfig)) + + nativeMinterConfig := c.GenesisPrecompiles[nativeminter.ConfigKey] + require.Equal(nativeMinterConfig.Key(), nativeminter.ConfigKey) + require.True(nativeMinterConfig.Equal(testContractNativeMinterConfig)) + + // Marshal and unmarshal again and check that the result is the same + marshaled, err := json.Marshal(c) + require.NoError(err) + c2 := ChainConfig{} + err = json.Unmarshal(marshaled, &c2) + require.NoError(err) + require.Equal(c, c2) +} + +func TestActivePrecompiles(t *testing.T) { + config := ChainConfig{ + UpgradeConfig: UpgradeConfig{ + PrecompileUpgrades: []PrecompileUpgrade{ + { + nativeminter.NewConfig(common.Big0, nil, nil, nil), // enable at genesis + }, + { + nativeminter.NewDisableConfig(common.Big1), // disable at timestamp 1 + }, + }, + }, + } + + rules0 := config.AvalancheRules(common.Big0, common.Big0) + require.True(t, rules0.IsPrecompileEnabled(nativeminter.Module.Address)) + + rules1 := config.AvalancheRules(common.Big0, common.Big1) + require.False(t, rules1.IsPrecompileEnabled(nativeminter.Module.Address)) +} diff --git a/params/precompile_config.go b/params/precompile_config.go deleted file mode 100644 index 71ea6ee2dc..0000000000 --- a/params/precompile_config.go +++ /dev/null @@ -1,375 +0,0 @@ -// (c) 2022 Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package params - -import ( - "fmt" - "math/big" - - "github.com/ava-labs/subnet-evm/precompile" - "github.com/ava-labs/subnet-evm/utils" - "github.com/ethereum/go-ethereum/log" -) - -// precompileKey is a helper type used to reference each of the -// possible stateful precompile types that can be activated -// as a network upgrade. -type precompileKey int - -const ( - contractDeployerAllowListKey precompileKey = iota + 1 - contractNativeMinterKey - txAllowListKey - feeManagerKey - rewardManagerKey - // ADD YOUR PRECOMPILE HERE - // {yourPrecompile}Key -) - -// TODO: Move this to the interface or PrecompileConfig struct -func (k precompileKey) String() string { - switch k { - case contractDeployerAllowListKey: - return "contractDeployerAllowList" - case contractNativeMinterKey: - return "contractNativeMinter" - case txAllowListKey: - return "txAllowList" - case feeManagerKey: - return "feeManager" - case rewardManagerKey: - return "rewardManager" - // ADD YOUR PRECOMPILE HERE - /* - case {yourPrecompile}Key: - return "{yourPrecompile}" - */ - } - return "unknown" -} - -// ADD YOUR PRECOMPILE HERE -var precompileKeys = []precompileKey{contractDeployerAllowListKey, contractNativeMinterKey, txAllowListKey, feeManagerKey, rewardManagerKey /* {yourPrecompile}Key */} - -// PrecompileUpgrade is a helper struct embedded in UpgradeConfig, representing -// each of the possible stateful precompile types that can be activated -// as a network upgrade. -type PrecompileUpgrade struct { - ContractDeployerAllowListConfig *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"` -} - -func (p *PrecompileUpgrade) getByKey(key precompileKey) (precompile.StatefulPrecompileConfig, bool) { - switch key { - case contractDeployerAllowListKey: - return p.ContractDeployerAllowListConfig, p.ContractDeployerAllowListConfig != nil - case contractNativeMinterKey: - return p.ContractNativeMinterConfig, p.ContractNativeMinterConfig != nil - case txAllowListKey: - return p.TxAllowListConfig, p.TxAllowListConfig != nil - case feeManagerKey: - return p.FeeManagerConfig, p.FeeManagerConfig != nil - case rewardManagerKey: - return p.RewardManagerConfig, p.RewardManagerConfig != nil - // ADD YOUR PRECOMPILE HERE - /* - case {yourPrecompile}Key: - return p.{YourPrecompile}Config , p.{YourPrecompile}Config != nil - */ - default: - panic(fmt.Sprintf("unknown upgrade key: %v", key)) - } -} - -// verifyPrecompileUpgrades checks [c.PrecompileUpgrades] is well formed: -// - [upgrades] must specify exactly one key per PrecompileUpgrade -// - the specified blockTimestamps must monotonically increase -// - the specified blockTimestamps must be compatible with those -// specified in the chainConfig by genesis. -// - check a precompile is disabled before it is re-enabled -func (c *ChainConfig) verifyPrecompileUpgrades() error { - var lastBlockTimestamp *big.Int - for i, upgrade := range c.PrecompileUpgrades { - hasKey := false // used to verify if there is only one key per Upgrade - - for _, key := range precompileKeys { - config, ok := upgrade.getByKey(key) - if !ok { - continue - } - if hasKey { - return fmt.Errorf("PrecompileUpgrades[%d] has more than one key set", i) - } - configTimestamp := config.Timestamp() - if configTimestamp == nil { - return fmt.Errorf("PrecompileUpgrades[%d] cannot have a nil timestamp", i) - } - // Verify specified timestamps are monotonically increasing across all precompile keys. - // Note: It is OK for multiple configs of different keys to specify the same timestamp. - if lastBlockTimestamp != nil && configTimestamp.Cmp(lastBlockTimestamp) < 0 { - return fmt.Errorf("PrecompileUpgrades[%d] config timestamp (%v) < previous timestamp (%v)", i, configTimestamp, lastBlockTimestamp) - } - lastBlockTimestamp = configTimestamp - hasKey = true - } - if !hasKey { - return fmt.Errorf("empty precompile upgrade at index %d", i) - } - } - - for _, key := range precompileKeys { - var ( - lastUpgraded *big.Int - disabled bool - ) - // check the genesis chain config for any enabled upgrade - if config, ok := c.PrecompileUpgrade.getByKey(key); ok && config.Timestamp() != nil { - if err := config.Verify(); err != nil { - return err - } - disabled = false - lastUpgraded = config.Timestamp() - } else { - disabled = true - } - // next range over upgrades to verify correct use of disabled and blockTimestamps. - for i, upgrade := range c.PrecompileUpgrades { - config, ok := upgrade.getByKey(key) - // Skip the upgrade if it's not relevant to [key]. - if !ok { - continue - } - - if disabled == config.IsDisabled() { - return fmt.Errorf("PrecompileUpgrades[%d] disable should be [%v]", i, !disabled) - } - if lastUpgraded != nil && (config.Timestamp().Cmp(lastUpgraded) <= 0) { - return fmt.Errorf("PrecompileUpgrades[%d] config timestamp (%v) <= previous timestamp (%v)", i, config.Timestamp(), lastUpgraded) - } - - if err := config.Verify(); err != nil { - return err - } - - disabled = config.IsDisabled() - lastUpgraded = config.Timestamp() - } - } - - return nil -} - -// getActivePrecompileConfig returns the most recent precompile config corresponding to [key]. -// If none have occurred, returns nil. -func (c *ChainConfig) getActivePrecompileConfig(blockTimestamp *big.Int, key precompileKey, upgrades []PrecompileUpgrade) precompile.StatefulPrecompileConfig { - configs := c.getActivatingPrecompileConfigs(nil, blockTimestamp, key, upgrades) - if len(configs) == 0 { - return nil - } - return configs[len(configs)-1] // return the most recent config -} - -// getActivatingPrecompileConfigs returns all forks configured to activate during the state transition from a block with timestamp [from] -// to a block with timestamp [to]. -func (c *ChainConfig) getActivatingPrecompileConfigs(from *big.Int, to *big.Int, key precompileKey, 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.getByKey(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.getByKey(key); ok { - // Check if the precompile activates in the specified range. - if utils.IsForkTransition(config.Timestamp(), from, to) { - configs = append(configs, config) - } - } - } - return configs -} - -// GetContractDeployerAllowListConfig returns the latest forked ContractDeployerAllowListConfig -// specified by [c] or nil if it was never enabled. -func (c *ChainConfig) GetContractDeployerAllowListConfig(blockTimestamp *big.Int) *precompile.ContractDeployerAllowListConfig { - if val := c.getActivePrecompileConfig(blockTimestamp, contractDeployerAllowListKey, c.PrecompileUpgrades); val != nil { - return val.(*precompile.ContractDeployerAllowListConfig) - } - return nil -} - -// GetContractNativeMinterConfig returns the latest forked ContractNativeMinterConfig -// specified by [c] or nil if it was never enabled. -func (c *ChainConfig) GetContractNativeMinterConfig(blockTimestamp *big.Int) *precompile.ContractNativeMinterConfig { - if val := c.getActivePrecompileConfig(blockTimestamp, contractNativeMinterKey, c.PrecompileUpgrades); val != nil { - return val.(*precompile.ContractNativeMinterConfig) - } - return nil -} - -// GetTxAllowListConfig returns the latest forked TxAllowListConfig -// specified by [c] or nil if it was never enabled. -func (c *ChainConfig) GetTxAllowListConfig(blockTimestamp *big.Int) *precompile.TxAllowListConfig { - if val := c.getActivePrecompileConfig(blockTimestamp, txAllowListKey, c.PrecompileUpgrades); val != nil { - return val.(*precompile.TxAllowListConfig) - } - return nil -} - -// GetFeeConfigManagerConfig returns the latest forked FeeManagerConfig -// specified by [c] or nil if it was never enabled. -func (c *ChainConfig) GetFeeConfigManagerConfig(blockTimestamp *big.Int) *precompile.FeeConfigManagerConfig { - if val := c.getActivePrecompileConfig(blockTimestamp, feeManagerKey, c.PrecompileUpgrades); val != nil { - return val.(*precompile.FeeConfigManagerConfig) - } - return nil -} - -// GetRewardManagerConfig returns the latest forked RewardManagerConfig -// specified by [c] or nil if it was never enabled. -func (c *ChainConfig) GetRewardManagerConfig(blockTimestamp *big.Int) *precompile.RewardManagerConfig { - if val := c.getActivePrecompileConfig(blockTimestamp, rewardManagerKey, c.PrecompileUpgrades); val != nil { - return val.(*precompile.RewardManagerConfig) - } - return nil -} - -/* ADD YOUR PRECOMPILE HERE -func (c *ChainConfig) Get{YourPrecompile}Config(blockTimestamp *big.Int) *precompile.{YourPrecompile}Config { - if val := c.getActivePrecompileConfig(blockTimestamp, {yourPrecompile}Key, c.PrecompileUpgrades); val != nil { - return val.(*precompile.{YourPrecompile}Config) - } - return nil -} -*/ - -func (c *ChainConfig) GetActivePrecompiles(blockTimestamp *big.Int) PrecompileUpgrade { - pu := PrecompileUpgrade{} - if config := c.GetContractDeployerAllowListConfig(blockTimestamp); config != nil && !config.Disable { - pu.ContractDeployerAllowListConfig = config - } - if config := c.GetContractNativeMinterConfig(blockTimestamp); config != nil && !config.Disable { - pu.ContractNativeMinterConfig = config - } - if config := c.GetTxAllowListConfig(blockTimestamp); config != nil && !config.Disable { - pu.TxAllowListConfig = config - } - if config := c.GetFeeConfigManagerConfig(blockTimestamp); config != nil && !config.Disable { - pu.FeeManagerConfig = config - } - if config := c.GetRewardManagerConfig(blockTimestamp); config != nil && !config.Disable { - pu.RewardManagerConfig = config - } - // ADD YOUR PRECOMPILE HERE - // if config := c.{YourPrecompile}Config(blockTimestamp); config != nil && !config.Disable { - // pu.{YourPrecompile}Config = config - // } - - return pu -} - -// CheckPrecompilesCompatible checks if [precompileUpgrades] are compatible with [c] at [headTimestamp]. -// Returns a ConfigCompatError if upgrades already forked at [headTimestamp] are missing from -// [precompileUpgrades]. Upgrades not already forked may be modified or absent from [precompileUpgrades]. -// Returns nil if [precompileUpgrades] is compatible with [c]. -// Assumes given timestamp is the last accepted block timestamp. -// This ensures that as long as the node has not accepted a block with a different rule set it will allow a new upgrade to be applied as long as it activates after the last accepted block. -func (c *ChainConfig) CheckPrecompilesCompatible(precompileUpgrades []PrecompileUpgrade, lastTimestamp *big.Int) *ConfigCompatError { - for _, key := range precompileKeys { - if err := c.checkPrecompileCompatible(key, precompileUpgrades, lastTimestamp); err != nil { - return err - } - } - - return nil -} - -// checkPrecompileCompatible verifies that the precompile specified by [key] 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(key precompileKey, precompileUpgrades []PrecompileUpgrade, lastTimestamp *big.Int) *ConfigCompatError { - // all active upgrades must match - activeUpgrades := c.getActivatingPrecompileConfigs(nil, lastTimestamp, key, c.PrecompileUpgrades) - newUpgrades := c.getActivatingPrecompileConfigs(nil, lastTimestamp, key, precompileUpgrades) - - // first, check existing upgrades are there - for i, upgrade := range activeUpgrades { - if len(newUpgrades) <= i { - // missing upgrade - return newCompatError( - fmt.Sprintf("missing PrecompileUpgrade[%d]", i), - upgrade.Timestamp(), - nil, - ) - } - // All upgrades that have forked must be identical. - if !upgrade.Equal(newUpgrades[i]) { - return newCompatError( - fmt.Sprintf("PrecompileUpgrade[%d]", i), - upgrade.Timestamp(), - newUpgrades[i].Timestamp(), - ) - } - } - // then, make sure newUpgrades does not have additional upgrades - // that are already activated. (cannot perform retroactive upgrade) - if len(newUpgrades) > len(activeUpgrades) { - return newCompatError( - fmt.Sprintf("cannot retroactively enable PrecompileUpgrade[%d]", len(activeUpgrades)), - nil, - newUpgrades[len(activeUpgrades)].Timestamp(), // this indexes to the first element in newUpgrades after the end of activeUpgrades - ) - } - - return nil -} - -// EnabledStatefulPrecompiles returns a slice of stateful precompile configs that -// have been activated through an upgrade. -func (c *ChainConfig) EnabledStatefulPrecompiles(blockTimestamp *big.Int) []precompile.StatefulPrecompileConfig { - statefulPrecompileConfigs := make([]precompile.StatefulPrecompileConfig, 0) - for _, key := range precompileKeys { - if config := c.getActivePrecompileConfig(blockTimestamp, key, c.PrecompileUpgrades); config != nil { - statefulPrecompileConfigs = append(statefulPrecompileConfigs, config) - } - } - - return statefulPrecompileConfigs -} - -// CheckConfigurePrecompiles checks if any of the precompiles specified by the chain config are enabled or disabled by the block -// transition from [parentTimestamp] to the timestamp set in [blockContext]. If this is the case, it calls [Configure] -// or [Deconfigure] to apply the necessary state transitions for the upgrade. -// This function is called: -// - within genesis setup to configure the starting state for precompiles enabled at genesis, -// - during block processing to update the state before processing the given block. -func (c *ChainConfig) CheckConfigurePrecompiles(parentTimestamp *big.Int, blockContext precompile.BlockContext, statedb precompile.StateDB) { - blockTimestamp := blockContext.Timestamp() - for _, key := range precompileKeys { // Note: configure precompiles in a deterministic order. - for _, config := range c.getActivatingPrecompileConfigs(parentTimestamp, blockTimestamp, key, 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", "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. - // Without an immediate Finalise call after the Suicide, a reconfigured precompiled state can be wiped out - // since Suicide will be committed after the reconfiguration. - statedb.Finalise(true) - } else { - log.Info("Activating new precompile", "name", key, "config", config) - precompile.Configure(c, blockContext, config, statedb) - } - } - } -} diff --git a/params/precompile_config_test.go b/params/precompile_config_test.go index 42611899cc..ec81ecaf36 100644 --- a/params/precompile_config_test.go +++ b/params/precompile_config_test.go @@ -4,13 +4,17 @@ package params import ( + "encoding/json" "math/big" "testing" "github.com/ava-labs/subnet-evm/commontype" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/contracts/deployerallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/feemanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/nativeminter" + "github.com/ava-labs/subnet-evm/precompile/contracts/rewardmanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -18,60 +22,60 @@ 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.GenesisPrecompiles = Precompiles{ + txallowlist.ConfigKey: txallowlist.NewConfig(big.NewInt(2), nil, nil), } config.PrecompileUpgrades = []PrecompileUpgrade{ { // disable TxAllowList at timestamp 4 - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(4)), + txallowlist.NewDisableConfig(big.NewInt(4)), }, { // re-enable TxAllowList at timestamp 5 - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(5), admins, nil), + txallowlist.NewConfig(big.NewInt(5), admins, nil), }, } // check this config is valid err := config.Verify() - assert.NoError(t, err) + require.NoError(t, err) // same precompile cannot be configured twice for the same timestamp badConfig := *config badConfig.PrecompileUpgrades = append( badConfig.PrecompileUpgrades, PrecompileUpgrade{ - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(5)), + Config: txallowlist.NewDisableConfig(big.NewInt(5)), }, ) err = badConfig.Verify() - assert.ErrorContains(t, err, "config timestamp (5) <= previous timestamp (5)") + require.ErrorContains(t, err, "config block timestamp (5) <= previous timestamp (5) of same key") // cannot enable a precompile without disabling it first. badConfig = *config badConfig.PrecompileUpgrades = append( badConfig.PrecompileUpgrades, PrecompileUpgrade{ - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(5), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(5), admins, nil), }, ) err = badConfig.Verify() - assert.ErrorContains(t, err, "disable should be [true]") + require.ErrorContains(t, err, "disable should be [true]") } func TestVerifyWithChainConfigAtNilTimestamp(t *testing.T) { admins := []common.Address{{1}} baseConfig := *SubnetEVMDefaultChainConfig config := &baseConfig - config.PrecompileUpgrade = PrecompileUpgrade{ + config.PrecompileUpgrades = []PrecompileUpgrade{ // this does NOT enable the precompile, so it should be upgradeable. - TxAllowListConfig: precompile.NewTxAllowListConfig(nil, nil, nil), + {Config: txallowlist.NewConfig(nil, nil, nil)}, } - require.False(t, config.IsTxAllowList(common.Big0)) // check the precompile is not enabled. + require.False(t, config.IsPrecompileEnabled(txallowlist.ContractAddress, common.Big0)) // check the precompile is not enabled. config.PrecompileUpgrades = []PrecompileUpgrade{ { // enable TxAllowList at timestamp 5 - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(5), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(5), admins, nil), }, } @@ -90,10 +94,10 @@ func TestVerifyPrecompileUpgrades(t *testing.T) { name: "enable and disable tx allow list", upgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(1), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(1), admins, nil), }, { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(2)), + Config: txallowlist.NewDisableConfig(big.NewInt(2)), }, }, expectedError: "", @@ -102,13 +106,13 @@ func TestVerifyPrecompileUpgrades(t *testing.T) { name: "invalid allow list config in tx allowlist", upgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(1), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(1), admins, nil), }, { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(2)), + Config: txallowlist.NewDisableConfig(big.NewInt(2)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(3), admins, admins), + Config: txallowlist.NewConfig(big.NewInt(3), admins, admins), }, }, expectedError: "cannot set address", @@ -117,7 +121,7 @@ func TestVerifyPrecompileUpgrades(t *testing.T) { name: "invalid initial fee manager config", upgrades: []PrecompileUpgrade{ { - FeeManagerConfig: precompile.NewFeeManagerConfig(big.NewInt(3), admins, nil, + Config: feemanager.NewConfig(big.NewInt(3), admins, nil, func() *commontype.FeeConfig { feeConfig := DefaultFeeConfig feeConfig.GasLimit = big.NewInt(-1) @@ -131,7 +135,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, + Config: feemanager.NewConfig(big.NewInt(3), admins, nil, func() *commontype.FeeConfig { feeConfig := DefaultFeeConfig feeConfig.GasLimit = common.Big0 @@ -141,6 +145,42 @@ func TestVerifyPrecompileUpgrades(t *testing.T) { }, expectedError: "gasLimit = 0 cannot be less than or equal to 0", }, + { + name: "different upgrades are allowed to configure same timestamp for different precompiles", + upgrades: []PrecompileUpgrade{ + { + Config: txallowlist.NewConfig(big.NewInt(1), admins, nil), + }, + { + Config: feemanager.NewConfig(big.NewInt(1), admins, nil, nil), + }, + }, + expectedError: "", + }, + { + name: "different upgrades must be monotonically increasing", + upgrades: []PrecompileUpgrade{ + { + Config: txallowlist.NewConfig(big.NewInt(2), admins, nil), + }, + { + Config: feemanager.NewConfig(big.NewInt(1), admins, nil, nil), + }, + }, + expectedError: "config block timestamp (1) < previous timestamp (2)", + }, + { + name: "upgrades with same keys are not allowed to configure same timestamp for same precompiles", + upgrades: []PrecompileUpgrade{ + { + Config: txallowlist.NewConfig(big.NewInt(1), admins, nil), + }, + { + Config: txallowlist.NewDisableConfig(big.NewInt(1)), + }, + }, + expectedError: " config block timestamp (1) <= previous timestamp (1) of same key", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -163,20 +203,20 @@ func TestVerifyPrecompiles(t *testing.T) { admins := []common.Address{{1}} tests := []struct { name string - upgrade PrecompileUpgrade + precompiles Precompiles expectedError string }{ { name: "invalid allow list config in tx allowlist", - upgrade: PrecompileUpgrade{ - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(3), admins, admins), + precompiles: Precompiles{ + txallowlist.ConfigKey: txallowlist.NewConfig(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, + precompiles: Precompiles{ + feemanager.ConfigKey: feemanager.NewConfig(big.NewInt(3), admins, nil, func() *commontype.FeeConfig { feeConfig := DefaultFeeConfig feeConfig.GasLimit = big.NewInt(-1) @@ -191,7 +231,7 @@ func TestVerifyPrecompiles(t *testing.T) { require := require.New(t) baseConfig := *SubnetEVMDefaultChainConfig config := &baseConfig - config.PrecompileUpgrade = tt.upgrade + config.GenesisPrecompiles = tt.precompiles err := config.Verify() if tt.expectedError == "" { @@ -209,35 +249,93 @@ func TestVerifyRequiresSortedTimestamps(t *testing.T) { config := &baseConfig config.PrecompileUpgrades = []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(2), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(2), admins, nil), }, { - ContractDeployerAllowListConfig: precompile.NewContractDeployerAllowListConfig(big.NewInt(1), admins, nil), + Config: deployerallowlist.NewConfig(big.NewInt(1), admins, nil), }, } // block timestamps must be monotonically increasing, so this config is invalid err := config.Verify() - assert.ErrorContains(t, err, "config timestamp (1) < previous timestamp (2)") + require.ErrorContains(t, err, "config block timestamp (1) < previous timestamp (2)") } func TestGetPrecompileConfig(t *testing.T) { - assert := assert.New(t) + require := require.New(t) baseConfig := *SubnetEVMDefaultChainConfig config := &baseConfig - config.PrecompileUpgrade = PrecompileUpgrade{ - ContractDeployerAllowListConfig: precompile.NewContractDeployerAllowListConfig(big.NewInt(10), nil, nil), + config.GenesisPrecompiles = Precompiles{ + deployerallowlist.ConfigKey: deployerallowlist.NewConfig(big.NewInt(10), nil, nil), } - deployerConfig := config.GetContractDeployerAllowListConfig(big.NewInt(0)) - assert.Nil(deployerConfig) + deployerConfig := config.GetActivePrecompileConfig(deployerallowlist.ContractAddress, big.NewInt(0)) + require.Nil(deployerConfig) + + deployerConfig = config.GetActivePrecompileConfig(deployerallowlist.ContractAddress, big.NewInt(10)) + require.NotNil(deployerConfig) - deployerConfig = config.GetContractDeployerAllowListConfig(big.NewInt(10)) - assert.NotNil(deployerConfig) + deployerConfig = config.GetActivePrecompileConfig(deployerallowlist.ContractAddress, big.NewInt(11)) + require.NotNil(deployerConfig) + + txAllowListConfig := config.GetActivePrecompileConfig(txallowlist.ContractAddress, big.NewInt(0)) + require.Nil(txAllowListConfig) +} + +func TestPrecompileUpgradeUnmarshalJSON(t *testing.T) { + require := require.New(t) + + upgradeBytes := []byte(` + { + "precompileUpgrades": [ + { + "rewardManagerConfig": { + "blockTimestamp": 1671542573, + "adminAddresses": [ + "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC" + ], + "initialRewardConfig": { + "allowFeeRecipients": true + } + } + }, + { + "contractNativeMinterConfig": { + "blockTimestamp": 1671543172, + "disable": false + } + } + ] + } + `) + + var upgradeConfig UpgradeConfig + err := json.Unmarshal(upgradeBytes, &upgradeConfig) + require.NoError(err) + + require.Len(upgradeConfig.PrecompileUpgrades, 2) + + rewardManagerConf := upgradeConfig.PrecompileUpgrades[0] + require.Equal(rewardManagerConf.Key(), rewardmanager.ConfigKey) + testRewardManagerConfig := rewardmanager.NewConfig( + big.NewInt(1671542573), + []common.Address{common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC")}, + nil, + &rewardmanager.InitialRewardConfig{ + AllowFeeRecipients: true, + }) + require.True(rewardManagerConf.Equal(testRewardManagerConfig)) - deployerConfig = config.GetContractDeployerAllowListConfig(big.NewInt(11)) - assert.NotNil(deployerConfig) + nativeMinterConfig := upgradeConfig.PrecompileUpgrades[1] + require.Equal(nativeMinterConfig.Key(), nativeminter.ConfigKey) + expectedNativeMinterConfig := nativeminter.NewConfig(big.NewInt(1671543172), nil, nil, nil) + require.True(nativeMinterConfig.Equal(expectedNativeMinterConfig)) - txAllowListConfig := config.GetTxAllowListConfig(big.NewInt(0)) - assert.Nil(txAllowListConfig) + // Marshal and unmarshal again and check that the result is the same + upgradeBytes2, err := json.Marshal(upgradeConfig) + require.NoError(err) + var upgradeConfig2 UpgradeConfig + err = json.Unmarshal(upgradeBytes2, &upgradeConfig2) + require.NoError(err) + require.Equal(upgradeConfig, upgradeConfig2) } diff --git a/params/precompile_upgrade.go b/params/precompile_upgrade.go new file mode 100644 index 0000000000..98a32d4d42 --- /dev/null +++ b/params/precompile_upgrade.go @@ -0,0 +1,259 @@ +// (c) 2023 Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package params + +import ( + "encoding/json" + "errors" + "fmt" + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ava-labs/subnet-evm/utils" + "github.com/ethereum/go-ethereum/common" +) + +var errNoKey = errors.New("PrecompileUpgrade cannot be empty") + +// PrecompileUpgrade is a helper struct embedded in UpgradeConfig. +// It is used to unmarshal the json into the correct precompile config type +// based on the key. Keys are defined in each precompile module, and registered in +// precompile/registry/registry.go. +type PrecompileUpgrade struct { + precompileconfig.Config +} + +// UnmarshalJSON unmarshals the json into the correct precompile config type +// based on the key. Keys are defined in each precompile module, and registered in +// params/precompile_modules.go. +// precompile/registry/registry.go. +// Ex: {"feeManagerConfig": {...}} where "feeManagerConfig" is the key +func (u *PrecompileUpgrade) UnmarshalJSON(data []byte) error { + raw := make(map[string]json.RawMessage) + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if len(raw) == 0 { + return errNoKey + } + if len(raw) > 1 { + return fmt.Errorf("PrecompileUpgrade must have exactly one key, got %d", len(raw)) + } + for key, value := range raw { + module, ok := modules.GetPrecompileModule(key) + if !ok { + return fmt.Errorf("unknown precompile config: %s", key) + } + config := module.MakeConfig() + if err := json.Unmarshal(value, config); err != nil { + return err + } + u.Config = config + } + return nil +} + +// MarshalJSON marshal the precompile config into json based on the precompile key. +// Ex: {"feeManagerConfig": {...}} where "feeManagerConfig" is the key +func (u *PrecompileUpgrade) MarshalJSON() ([]byte, error) { + res := make(map[string]precompileconfig.Config) + res[u.Key()] = u.Config + return json.Marshal(res) +} + +// verifyPrecompileUpgrades checks [c.PrecompileUpgrades] is well formed: +// - [upgrades] must specify exactly one key per PrecompileUpgrade +// - the specified blockTimestamps must monotonically increase +// - the specified blockTimestamps must be compatible with those +// specified in the chainConfig by genesis. +// - check a precompile is disabled before it is re-enabled +func (c *ChainConfig) verifyPrecompileUpgrades() error { + // Store this struct to keep track of the last upgrade for each precompile key. + // Required for timestamp and disabled checks. + type lastUpgradeData struct { + blockTimestamp *big.Int + disabled bool + } + + lastPrecompileUpgrades := make(map[string]lastUpgradeData) + + // verify genesis precompiles + for key, config := range c.GenesisPrecompiles { + if err := config.Verify(); err != nil { + return err + } + // if the precompile is disabled at genesis, skip it. + if config.Timestamp() == nil { + continue + } + // check the genesis chain config for any enabled upgrade + lastPrecompileUpgrades[key] = lastUpgradeData{ + disabled: false, + blockTimestamp: config.Timestamp(), + } + } + + // next range over upgrades to verify correct use of disabled and blockTimestamps. + // previousUpgradeTimestamp is used to verify monotonically increasing timestamps. + var previousUpgradeTimestamp *big.Int + for i, upgrade := range c.PrecompileUpgrades { + key := upgrade.Key() + + // lastUpgradeByKey is the previous processed upgrade for this precompile key. + lastUpgradeByKey, ok := lastPrecompileUpgrades[key] + var ( + disabled bool + lastTimestamp *big.Int + ) + if !ok { + disabled = true + lastTimestamp = nil + } else { + disabled = lastUpgradeByKey.disabled + lastTimestamp = lastUpgradeByKey.blockTimestamp + } + upgradeTimestamp := upgrade.Timestamp() + + if upgradeTimestamp == nil { + return fmt.Errorf("PrecompileUpgrade (%s) at [%d]: block timestamp cannot be nil ", key, i) + } + // Verify specified timestamps are monotonically increasing across all precompile keys. + // Note: It is OK for multiple configs of DIFFERENT keys to specify the same timestamp. + if previousUpgradeTimestamp != nil && upgradeTimestamp.Cmp(previousUpgradeTimestamp) < 0 { + return fmt.Errorf("PrecompileUpgrade (%s) at [%d]: config block timestamp (%v) < previous timestamp (%v)", key, i, upgradeTimestamp, previousUpgradeTimestamp) + } + + if disabled == upgrade.IsDisabled() { + return fmt.Errorf("PrecompileUpgrade (%s) at [%d]: disable should be [%v]", key, i, !disabled) + } + // Verify specified timestamps are monotonically increasing across same precompile keys. + // Note: It is NOT OK for multiple configs of the SAME key to specify the same timestamp. + if lastTimestamp != nil && (upgradeTimestamp.Cmp(lastTimestamp) <= 0) { + return fmt.Errorf("PrecompileUpgrade (%s) at [%d]: config block timestamp (%v) <= previous timestamp (%v) of same key", key, i, upgradeTimestamp, lastTimestamp) + } + + if err := upgrade.Verify(); err != nil { + return err + } + + lastPrecompileUpgrades[key] = lastUpgradeData{ + disabled: upgrade.IsDisabled(), + blockTimestamp: upgradeTimestamp, + } + + previousUpgradeTimestamp = upgradeTimestamp + } + + return nil +} + +// GetActivePrecompileConfig returns the most recent precompile config corresponding to [address]. +// If none have occurred, returns nil. +func (c *ChainConfig) GetActivePrecompileConfig(address common.Address, blockTimestamp *big.Int) precompileconfig.Config { + configs := c.GetActivatingPrecompileConfigs(address, nil, blockTimestamp, c.PrecompileUpgrades) + if len(configs) == 0 { + return nil + } + return configs[len(configs)-1] // return the most recent config +} + +// GetActivatingPrecompileConfigs returns all upgrades configured to activate during the state transition from a block with timestamp [from] +// to a block with timestamp [to]. +func (c *ChainConfig) GetActivatingPrecompileConfigs(address common.Address, from *big.Int, to *big.Int, upgrades []PrecompileUpgrade) []precompileconfig.Config { + // Get key from address. + module, ok := modules.GetPrecompileModuleByAddress(address) + if !ok { + return nil + } + configs := make([]precompileconfig.Config, 0) + key := module.ConfigKey + // First check the embedded [upgrade] for precompiles configured + // in the genesis chain config. + if config, ok := c.GenesisPrecompiles[key]; ok { + if utils.IsForkTransition(config.Timestamp(), from, to) { + configs = append(configs, config) + } + } + // Loop over all upgrades checking for the requested precompile config. + for _, upgrade := range upgrades { + if upgrade.Key() == key { + // Check if the precompile activates in the specified range. + if utils.IsForkTransition(upgrade.Timestamp(), from, to) { + configs = append(configs, upgrade.Config) + } + } + } + return configs +} + +// CheckPrecompilesCompatible checks if [precompileUpgrades] are compatible with [c] at [headTimestamp]. +// Returns a ConfigCompatError if upgrades already activated at [headTimestamp] are missing from +// [precompileUpgrades]. Upgrades not already activated may be modified or absent from [precompileUpgrades]. +// Returns nil if [precompileUpgrades] is compatible with [c]. +// Assumes given timestamp is the last accepted block timestamp. +// This ensures that as long as the node has not accepted a block with a different rule set it will allow a +// new upgrade to be applied as long as it activates after the last accepted block. +func (c *ChainConfig) CheckPrecompilesCompatible(precompileUpgrades []PrecompileUpgrade, lastTimestamp *big.Int) *ConfigCompatError { + for _, module := range modules.RegisteredModules() { + if err := c.checkPrecompileCompatible(module.Address, precompileUpgrades, lastTimestamp); err != nil { + return err + } + } + + return nil +} + +// checkPrecompileCompatible verifies that the precompile specified by [address] is compatible between [c] +// and [precompileUpgrades] at [headTimestamp]. +// Returns an error if upgrades already activated at [headTimestamp] are missing from [precompileUpgrades]. +// Upgrades that have already gone into effect cannot be modified or absent from [precompileUpgrades]. +func (c *ChainConfig) checkPrecompileCompatible(address common.Address, precompileUpgrades []PrecompileUpgrade, lastTimestamp *big.Int) *ConfigCompatError { + // All active upgrades (from nil to [lastTimestamp]) must match. + activeUpgrades := c.GetActivatingPrecompileConfigs(address, nil, lastTimestamp, c.PrecompileUpgrades) + newUpgrades := c.GetActivatingPrecompileConfigs(address, nil, lastTimestamp, precompileUpgrades) + + // Check activated upgrades are still present. + for i, upgrade := range activeUpgrades { + if len(newUpgrades) <= i { + // missing upgrade + return newCompatError( + fmt.Sprintf("missing PrecompileUpgrade[%d]", i), + upgrade.Timestamp(), + nil, + ) + } + // All upgrades that have activated must be identical. + if !upgrade.Equal(newUpgrades[i]) { + return newCompatError( + fmt.Sprintf("PrecompileUpgrade[%d]", i), + upgrade.Timestamp(), + newUpgrades[i].Timestamp(), + ) + } + } + // then, make sure newUpgrades does not have additional upgrades + // that are already activated. (cannot perform retroactive upgrade) + if len(newUpgrades) > len(activeUpgrades) { + return newCompatError( + fmt.Sprintf("cannot retroactively enable PrecompileUpgrade[%d]", len(activeUpgrades)), + nil, + newUpgrades[len(activeUpgrades)].Timestamp(), // this indexes to the first element in newUpgrades after the end of activeUpgrades + ) + } + + return nil +} + +// EnabledStatefulPrecompiles returns current stateful precompile configs that are enabled at [blockTimestamp]. +func (c *ChainConfig) EnabledStatefulPrecompiles(blockTimestamp *big.Int) Precompiles { + statefulPrecompileConfigs := make(Precompiles) + for _, module := range modules.RegisteredModules() { + if config := c.GetActivePrecompileConfig(module.Address, blockTimestamp); config != nil && !config.IsDisabled() { + statefulPrecompileConfigs[module.ConfigKey] = config + } + } + + return statefulPrecompileConfigs +} diff --git a/params/upgrade_config_test.go b/params/precompile_upgrade_test.go similarity index 62% rename from params/upgrade_config_test.go rename to params/precompile_upgrade_test.go index 38c93f5386..54078aad3b 100644 --- a/params/upgrade_config_test.go +++ b/params/precompile_upgrade_test.go @@ -7,15 +7,18 @@ import ( "math/big" "testing" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/contracts/deployerallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestVerifyUpgradeConfig(t *testing.T) { admins := []common.Address{{1}} chainConfig := *TestChainConfig - chainConfig.TxAllowListConfig = precompile.NewTxAllowListConfig(big.NewInt(1), admins, nil) + chainConfig.GenesisPrecompiles = Precompiles{ + txallowlist.ConfigKey: txallowlist.NewConfig(big.NewInt(1), admins, nil), + } type test struct { upgrades []PrecompileUpgrade @@ -27,23 +30,23 @@ func TestVerifyUpgradeConfig(t *testing.T) { expectedErrorString: "disable should be [true]", upgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(2), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(2), admins, nil), }, }, }, "upgrade bytes conflicts with genesis (disable before enable)": { - expectedErrorString: "config timestamp (0) <= previous timestamp (1)", + expectedErrorString: "config block timestamp (0) <= previous timestamp (1) of same key", upgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(0)), + Config: txallowlist.NewDisableConfig(big.NewInt(0)), }, }, }, "upgrade bytes conflicts with genesis (disable same time as enable)": { - expectedErrorString: "config timestamp (1) <= previous timestamp (1)", + expectedErrorString: "config block timestamp (1) <= previous timestamp (1) of same key", upgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(1)), + Config: txallowlist.NewDisableConfig(big.NewInt(1)), }, }, }, @@ -59,9 +62,9 @@ func TestVerifyUpgradeConfig(t *testing.T) { err := chainConfig.Verify() if tt.expectedErrorString != "" { - assert.ErrorContains(t, err, tt.expectedErrorString) + require.ErrorContains(t, err, tt.expectedErrorString) } else { - assert.NoError(t, err) + require.NoError(t, err) } }) } @@ -70,8 +73,10 @@ 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.GenesisPrecompiles = Precompiles{ + txallowlist.ConfigKey: txallowlist.NewConfig(big.NewInt(1), admins, nil), + deployerallowlist.ConfigKey: deployerallowlist.NewConfig(big.NewInt(10), admins, nil), + } type test struct { configs []*UpgradeConfig @@ -86,10 +91,10 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(7), admins, nil), }, }, }, @@ -101,20 +106,20 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(8), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(8), admins, nil), }, }, }, @@ -127,20 +132,20 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(8), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(8), admins, nil), }, }, }, @@ -152,17 +157,17 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, }, }, @@ -175,17 +180,17 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, }, }, @@ -198,21 +203,21 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { // uses a different (empty) admin list, not allowed - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), []common.Address{}, nil), + Config: txallowlist.NewConfig(big.NewInt(7), []common.Address{}, nil), }, }, }, @@ -224,20 +229,20 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(7), admins, nil), }, }, }, { PrecompileUpgrades: []PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(6)), + Config: txallowlist.NewDisableConfig(big.NewInt(6)), }, { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(7), admins, nil), + Config: txallowlist.NewConfig(big.NewInt(7), admins, nil), }, }, }, @@ -268,11 +273,9 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { } if tt.expectedErrorString != "" { - assert.ErrorContains(t, err, tt.expectedErrorString) + require.ErrorContains(t, err, tt.expectedErrorString) } else { - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) } } }) diff --git a/params/precompiles.go b/params/precompiles.go new file mode 100644 index 0000000000..5d8ed74bda --- /dev/null +++ b/params/precompiles.go @@ -0,0 +1,36 @@ +// (c) 2023 Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package params + +import ( + "encoding/json" + + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" +) + +type Precompiles map[string]precompileconfig.Config + +// UnmarshalJSON parses the JSON-encoded data into the ChainConfigPrecompiles. +// ChainConfigPrecompiles is a map of precompile module keys to their +// configuration. +func (ccp *Precompiles) UnmarshalJSON(data []byte) error { + raw := make(map[string]json.RawMessage) + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + *ccp = make(Precompiles) + for _, module := range modules.RegisteredModules() { + key := module.ConfigKey + if value, ok := raw[key]; ok { + conf := module.MakeConfig() + if err := json.Unmarshal(value, conf); err != nil { + return err + } + (*ccp)[key] = conf + } + } + return nil +} diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index ce6f80b3d8..bfff56904e 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -45,6 +45,9 @@ import ( // inside of cmd/geth. _ "github.com/ava-labs/subnet-evm/eth/tracers/native" + // Force-load precompiles to trigger registration + _ "github.com/ava-labs/subnet-evm/precompile/registry" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go index 2725596b48..8acca71c79 100644 --- a/plugin/evm/vm_test.go +++ b/plugin/evm/vm_test.go @@ -21,7 +21,11 @@ import ( "github.com/ava-labs/subnet-evm/internal/ethapi" "github.com/ava-labs/subnet-evm/metrics" "github.com/ava-labs/subnet-evm/plugin/evm/message" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/deployerallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/feemanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/rewardmanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" "github.com/ava-labs/subnet-evm/trie" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" @@ -2172,7 +2176,9 @@ 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.GenesisPrecompiles = params.Precompiles{ + deployerallowlist.ConfigKey: deployerallowlist.NewConfig(big.NewInt(time.Now().Unix()), testEthAddrs, nil), + } genesisJSON, err := genesis.MarshalJSON() if err != nil { @@ -2193,9 +2199,9 @@ func TestBuildAllowListActivationBlock(t *testing.T) { if err != nil { t.Fatal(err) } - role := precompile.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) + role := deployerallowlist.GetContractDeployerAllowListStatus(genesisState, testEthAddrs[0]) + if role != allowlist.NoRole { + t.Fatalf("Expected allow list status to be set to no role: %s, but found: %s", allowlist.NoRole, role) } // Send basic transaction to construct a simple block and confirm that the precompile state configuration in the worker behaves correctly. @@ -2223,9 +2229,9 @@ func TestBuildAllowListActivationBlock(t *testing.T) { if err != nil { t.Fatal(err) } - role = precompile.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) + role = deployerallowlist.GetContractDeployerAllowListStatus(blkState, testEthAddrs[0]) + if role != allowlist.AdminRole { + t.Fatalf("Expected allow list status to be set role %s, but found: %s", allowlist.AdminRole, role) } } @@ -2236,7 +2242,9 @@ 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.GenesisPrecompiles = params.Precompiles{ + txallowlist.ConfigKey: txallowlist.NewConfig(big.NewInt(0), testEthAddrs[0:1], nil), + } genesisJSON, err := genesis.MarshalJSON() if err != nil { t.Fatal(err) @@ -2258,13 +2266,13 @@ func TestTxAllowListSuccessfulTx(t *testing.T) { } // Check that address 0 is whitelisted and address 1 is not - role := precompile.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 := txallowlist.GetTxAllowListStatus(genesisState, testEthAddrs[0]) + if role != allowlist.AdminRole { + t.Fatalf("Expected allow list status to be set to admin: %s, but found: %s", allowlist.AdminRole, role) } - role = precompile.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) + role = txallowlist.GetTxAllowListStatus(genesisState, testEthAddrs[1]) + if role != allowlist.NoRole { + t.Fatalf("Expected allow list status to be set to no role: %s, but found: %s", allowlist.NoRole, role) } // Submit a successful transaction @@ -2285,7 +2293,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, vmerrs.ErrSenderAddressNotAllowListed) { t.Fatalf("expected ErrSenderAddressNotAllowListed, got: %s", err) } @@ -2312,7 +2320,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.GenesisPrecompiles = params.Precompiles{ + txallowlist.ConfigKey: txallowlist.NewConfig(big.NewInt(enableAllowListTimestamp.Unix()), testEthAddrs[0:1], nil), + } genesisJSON, err := genesis.MarshalJSON() if err != nil { t.Fatal(err) @@ -2353,13 +2363,13 @@ func TestTxAllowListDisablePrecompile(t *testing.T) { } // Check that address 0 is whitelisted and address 1 is not - role := precompile.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 := txallowlist.GetTxAllowListStatus(genesisState, testEthAddrs[0]) + if role != allowlist.AdminRole { + t.Fatalf("Expected allow list status to be set to admin: %s, but found: %s", allowlist.AdminRole, role) } - role = precompile.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) + role = txallowlist.GetTxAllowListStatus(genesisState, testEthAddrs[1]) + if role != allowlist.NoRole { + t.Fatalf("Expected allow list status to be set to no role: %s, but found: %s", allowlist.NoRole, role) } // Submit a successful transaction @@ -2380,7 +2390,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, vmerrs.ErrSenderAddressNotAllowListed) { t.Fatalf("expected ErrSenderAddressNotAllowListed, got: %s", err) } @@ -2424,7 +2434,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.GenesisPrecompiles = params.Precompiles{ + feemanager.ConfigKey: feemanager.NewConfig(big.NewInt(0), testEthAddrs[0:1], nil, nil), + } // set a lower fee config now testLowFeeConfig := commontype.FeeConfig{ @@ -2462,13 +2474,13 @@ func TestFeeManagerChangeFee(t *testing.T) { } // Check that address 0 is whitelisted and address 1 is not - role := precompile.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) + role := feemanager.GetFeeManagerStatus(genesisState, testEthAddrs[0]) + if role != allowlist.AdminRole { + t.Fatalf("Expected fee manager list status to be set to admin: %s, but found: %s", allowlist.AdminRole, role) } - role = precompile.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) + role = feemanager.GetFeeManagerStatus(genesisState, testEthAddrs[1]) + if role != allowlist.NoRole { + t.Fatalf("Expected fee manager list status to be set to no role: %s, but found: %s", allowlist.NoRole, 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()) @@ -2480,13 +2492,13 @@ func TestFeeManagerChangeFee(t *testing.T) { testHighFeeConfig := testLowFeeConfig testHighFeeConfig.MinBaseFee = big.NewInt(28_000_000_000) - data, err := precompile.PackSetFeeConfig(testHighFeeConfig) + data, err := feemanager.PackSetFeeConfig(testHighFeeConfig) require.NoError(t, err) tx := types.NewTx(&types.DynamicFeeTx{ ChainID: genesis.Config.ChainID, Nonce: uint64(0), - To: &precompile.FeeConfigManagerAddress, + To: &feemanager.ContractAddress, Gas: testLowFeeConfig.GasLimit.Uint64(), Value: common.Big0, GasFeeCap: testLowFeeConfig.MinBaseFee, // give low fee, it should work since we still haven't applied high fees @@ -2522,7 +2534,7 @@ func TestFeeManagerChangeFee(t *testing.T) { tx2 := types.NewTx(&types.DynamicFeeTx{ ChainID: genesis.Config.ChainID, Nonce: uint64(1), - To: &precompile.FeeConfigManagerAddress, + To: &feemanager.ContractAddress, Gas: genesis.Config.FeeConfig.GasLimit.Uint64(), Value: common.Big0, GasFeeCap: testLowFeeConfig.MinBaseFee, // this is too low for applied config, should fail @@ -2664,7 +2676,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.GenesisPrecompiles = params.Precompiles{ + rewardmanager.ConfigKey: rewardmanager.NewConfig(common.Big0, testEthAddrs[0:1], nil, nil), + } genesis.Config.AllowFeeRecipients = true // enable this in genesis to test if this is recognized by the reward manager genesisJSON, err := genesis.MarshalJSON() require.NoError(t, err) @@ -2705,12 +2719,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.ContractAddress, big.NewInt(1), gas, big.NewInt(testMinGasPrice), data) signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm.chainConfig.ChainID), testKeys[0]) require.NoError(t, err) @@ -2804,7 +2818,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.GenesisPrecompiles = params.Precompiles{ + rewardmanager.ConfigKey: rewardmanager.NewConfig(common.Big0, testEthAddrs[0:1], nil, nil), + } genesis.Config.AllowFeeRecipients = false // disable this in genesis genesisJSON, err := genesis.MarshalJSON() require.NoError(t, err) @@ -2841,12 +2857,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.ContractAddress, big.NewInt(1), gas, big.NewInt(testMinGasPrice), data) signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm.chainConfig.ChainID), testKeys[0]) require.NoError(t, err) diff --git a/plugin/evm/vm_upgrade_bytes_test.go b/plugin/evm/vm_upgrade_bytes_test.go index 7925f1388f..4880f0e348 100644 --- a/plugin/evm/vm_upgrade_bytes_test.go +++ b/plugin/evm/vm_upgrade_bytes_test.go @@ -18,7 +18,8 @@ import ( "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/metrics" "github.com/ava-labs/subnet-evm/params" - "github.com/ava-labs/subnet-evm/precompile" + "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" + "github.com/ava-labs/subnet-evm/vmerrs" "github.com/stretchr/testify/assert" ) @@ -28,7 +29,7 @@ func TestVMUpgradeBytesPrecompile(t *testing.T) { upgradeConfig := ¶ms.UpgradeConfig{ PrecompileUpgrades: []params.PrecompileUpgrade{ { - TxAllowListConfig: precompile.NewTxAllowListConfig(big.NewInt(enableAllowListTimestamp.Unix()), testEthAddrs[0:1], nil), + Config: txallowlist.NewConfig(big.NewInt(enableAllowListTimestamp.Unix()), testEthAddrs[0:1], nil), }, }, } @@ -57,7 +58,7 @@ func TestVMUpgradeBytesPrecompile(t *testing.T) { t.Fatal(err) } errs = vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1}) - if err := errs[0]; !errors.Is(err, precompile.ErrSenderAddressNotAllowListed) { + if err := errs[0]; !errors.Is(err, vmerrs.ErrSenderAddressNotAllowListed) { t.Fatalf("expected ErrSenderAddressNotAllowListed, got: %s", err) } @@ -71,7 +72,7 @@ func TestVMUpgradeBytesPrecompile(t *testing.T) { upgradeConfig.PrecompileUpgrades = append( upgradeConfig.PrecompileUpgrades, params.PrecompileUpgrade{ - TxAllowListConfig: precompile.NewDisableTxAllowListConfig(big.NewInt(disableAllowListTimestamp.Unix())), + Config: txallowlist.NewDisableConfig(big.NewInt(disableAllowListTimestamp.Unix())), }, ) upgradeBytesJSON, err = json.Marshal(upgradeConfig) @@ -108,7 +109,7 @@ func TestVMUpgradeBytesPrecompile(t *testing.T) { // Submit a rejected transaction, should throw an error errs = vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1}) - if err := errs[0]; !errors.Is(err, precompile.ErrSenderAddressNotAllowListed) { + if err := errs[0]; !errors.Is(err, vmerrs.ErrSenderAddressNotAllowListed) { t.Fatalf("expected ErrSenderAddressNotAllowListed, got: %s", err) } diff --git a/precompile/allow_list.go b/precompile/allow_list.go deleted file mode 100644 index 5914718ae7..0000000000 --- a/precompile/allow_list.go +++ /dev/null @@ -1,235 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package precompile - -import ( - "errors" - "fmt" - - "github.com/ava-labs/subnet-evm/vmerrs" - "github.com/ethereum/go-ethereum/common" -) - -const ( - SetAdminFuncKey = "setAdmin" - SetEnabledFuncKey = "setEnabled" - SetNoneFuncKey = "setNone" - ReadAllowListFuncKey = "readAllowList" - - ModifyAllowListGasCost = writeGasCostPerSlot - ReadAllowListGasCost = readGasCostPerSlot -) - -var ( - // No role assigned - this is equivalent to common.Hash{} and deletes the key from the DB when set - AllowListNoRole = AllowListRole(common.BigToHash(common.Big0)) - // Enabled - allowed to use state-changing precompile functions without modifying status of other admins or enableds - AllowListEnabled = AllowListRole(common.BigToHash(common.Big1)) - // Admin - allowed to modify both the admin and enabled list, as well as to use state-changing precompile functions - AllowListAdmin = AllowListRole(common.BigToHash(common.Big2)) - - // AllowList function signatures - setAdminSignature = CalculateFunctionSelector("setAdmin(address)") - setEnabledSignature = CalculateFunctionSelector("setEnabled(address)") - setNoneSignature = CalculateFunctionSelector("setNone(address)") - readAllowListSignature = CalculateFunctionSelector("readAllowList(address)") - // Error returned when an invalid write is attempted - ErrCannotModifyAllowList = errors.New("non-admin cannot modify allow list") - - allowListInputLen = common.HashLength -) - -// AllowListConfig specifies the initial set of allow list admins. -type AllowListConfig struct { - AllowListAdmins []common.Address `json:"adminAddresses"` - EnabledAddresses []common.Address `json:"enabledAddresses"` // initial enabled addresses -} - -// Configure initializes the address space of [precompileAddr] by initializing the role of each of -// the addresses in [AllowListAdmins]. -func (c *AllowListConfig) Configure(state StateDB, precompileAddr common.Address) { - for _, enabledAddr := range c.EnabledAddresses { - setAllowListRole(state, precompileAddr, enabledAddr, AllowListEnabled) - } - for _, adminAddr := range c.AllowListAdmins { - setAllowListRole(state, precompileAddr, adminAddr, AllowListAdmin) - } -} - -// Equal returns true iff [other] has the same admins in the same order in its allow list. -func (c *AllowListConfig) Equal(other *AllowListConfig) bool { - if other == nil { - return false - } - if !areEqualAddressLists(c.AllowListAdmins, other.AllowListAdmins) { - return false - } - - return areEqualAddressLists(c.EnabledAddresses, other.EnabledAddresses) -} - -// areEqualAddressLists returns true iff [a] and [b] have the same addresses in the same order. -func areEqualAddressLists(current []common.Address, other []common.Address) bool { - if len(current) != len(other) { - return false - } - for i, address := range current { - if address != other[i] { - return false - } - } - return true -} - -// Verify returns an error if there is an overlapping address between admin and enabled roles -func (c *AllowListConfig) Verify() error { - // return early if either list is empty - if len(c.EnabledAddresses) == 0 || len(c.AllowListAdmins) == 0 { - return nil - } - - addressMap := make(map[common.Address]bool) - for _, enabledAddr := range c.EnabledAddresses { - // check for duplicates - if _, ok := addressMap[enabledAddr]; ok { - return fmt.Errorf("duplicate address %s in enabled list", enabledAddr) - } - addressMap[enabledAddr] = false - } - - for _, adminAddr := range c.AllowListAdmins { - // check for overlap between enabled and admin lists - if inAdmin, ok := addressMap[adminAddr]; ok { - if inAdmin { - return fmt.Errorf("duplicate address %s in admin list", adminAddr) - } else { - return fmt.Errorf("cannot set address %s as both admin and enabled", adminAddr) - } - } - addressMap[adminAddr] = true - } - - return nil -} - -// getAllowListStatus returns the allow list role of [address] for the precompile -// at [precompileAddr] -func getAllowListStatus(state StateDB, precompileAddr common.Address, address common.Address) AllowListRole { - // Generate the state key for [address] - addressKey := address.Hash() - return AllowListRole(state.GetState(precompileAddr, addressKey)) -} - -// setAllowListRole sets the permissions of [address] to [role] for the precompile -// at [precompileAddr]. -// assumes [role] has already been verified as valid. -func setAllowListRole(stateDB StateDB, precompileAddr, address common.Address, role AllowListRole) { - // Generate the state key for [address] - addressKey := address.Hash() - // Assign [role] to the address - // This stores the [role] in the contract storage with address [precompileAddr] - // and [addressKey] hash. It means that any reusage of the [addressKey] for different value - // conflicts with the same slot [role] is stored. - // Precompile implementations must use a different key than [addressKey] - stateDB.SetState(precompileAddr, addressKey, common.Hash(role)) -} - -// PackModifyAllowList packs [address] and [role] into the appropriate arguments for modifying the allow list. -// Note: [role] is not packed in the input value returned, but is instead used as a selector for the function -// selector that should be encoded in the input. -func PackModifyAllowList(address common.Address, role AllowListRole) ([]byte, error) { - // function selector (4 bytes) + hash for address - input := make([]byte, 0, selectorLen+common.HashLength) - - switch role { - case AllowListAdmin: - input = append(input, setAdminSignature...) - case AllowListEnabled: - input = append(input, setEnabledSignature...) - case AllowListNoRole: - input = append(input, setNoneSignature...) - default: - return nil, fmt.Errorf("cannot pack modify list input with invalid role: %s", role) - } - - input = append(input, address.Hash().Bytes()...) - return input, nil -} - -// PackReadAllowList packs [address] into the input data to the read allow list function -func PackReadAllowList(address common.Address) []byte { - input := make([]byte, 0, selectorLen+common.HashLength) - input = append(input, readAllowListSignature...) - input = append(input, address.Hash().Bytes()...) - return input -} - -// createAllowListRoleSetter returns an execution function for setting the allow list status of the input address argument to [role]. -// This execution function is speciifc to [precompileAddr]. -func createAllowListRoleSetter(precompileAddr common.Address, role AllowListRole) 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 { - return nil, 0, err - } - - if len(input) != allowListInputLen { - return nil, remainingGas, fmt.Errorf("invalid input length for modifying allow list: %d", len(input)) - } - - modifyAddress := common.BytesToAddress(input) - - if readOnly { - return nil, remainingGas, vmerrs.ErrWriteProtection - } - - stateDB := evm.GetStateDB() - - // Verify that the caller is in the allow list and therefore has the right to modify it - callerStatus := getAllowListStatus(stateDB, precompileAddr, callerAddr) - if !callerStatus.IsAdmin() { - return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotModifyAllowList, callerAddr) - } - - setAllowListRole(stateDB, precompileAddr, modifyAddress, role) - // Return an empty output and the remaining gas - return []byte{}, remainingGas, nil - } -} - -// createReadAllowList returns an execution function that reads the allow list for the given [precompileAddr]. -// The execution function parses the input into a single address and returns the 32 byte hash that specifies the -// designated role of that address -func createReadAllowList(precompileAddr common.Address) 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 { - return nil, 0, err - } - - if len(input) != allowListInputLen { - return nil, remainingGas, fmt.Errorf("invalid input length for read allow list: %d", len(input)) - } - - readAddress := common.BytesToAddress(input) - role := getAllowListStatus(evm.GetStateDB(), precompileAddr, readAddress) - roleBytes := common.Hash(role).Bytes() - return roleBytes, remainingGas, nil - } -} - -// createAllowListPrecompile returns a StatefulPrecompiledContract with R/W control of an allow list at [precompileAddr] -func createAllowListPrecompile(precompileAddr common.Address) StatefulPrecompiledContract { - // Construct the contract with no fallback function. - allowListFuncs := createAllowListFunctions(precompileAddr) - contract := newStatefulPrecompileWithFunctionSelectors(nil, allowListFuncs) - 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)) - - return []*statefulPrecompileFunction{setAdmin, setEnabled, setNone, read} -} diff --git a/precompile/allowlist/allowlist.go b/precompile/allowlist/allowlist.go new file mode 100644 index 0000000000..cf8dcbdb4c --- /dev/null +++ b/precompile/allowlist/allowlist.go @@ -0,0 +1,172 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package allowlist + +import ( + "errors" + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" +) + +// AllowList is an abstraction that allows other precompiles to manage +// which addresses can call the precompile by maintaining an allowlist +// in the storage trie. Each account may have one of the following roles: +// 1. NoRole - this is equivalent to common.Hash{} and deletes the key from the DB when set +// 2. EnabledRole - allowed to call the precompile +// 3. Admin - allowed to both modify the allowlist and call the precompile + +const ( + SetAdminFuncKey = "setAdmin" + SetEnabledFuncKey = "setEnabled" + SetNoneFuncKey = "setNone" + ReadAllowListFuncKey = "readAllowList" + + ModifyAllowListGasCost = contract.WriteGasCostPerSlot + ReadAllowListGasCost = contract.ReadGasCostPerSlot + + allowListInputLen = common.HashLength +) + +var ( + NoRole = Role(common.BigToHash(common.Big0)) // NoRole - this is equivalent to common.Hash{} and deletes the key from the DB when set + EnabledRole = Role(common.BigToHash(common.Big1)) // EnabledRole - allowed to call the precompile + AdminRole = Role(common.BigToHash(common.Big2)) // Admin - allowed to both modify the allowlist and call the precompile + + // AllowList function signatures + setAdminSignature = contract.CalculateFunctionSelector("setAdmin(address)") + setEnabledSignature = contract.CalculateFunctionSelector("setEnabled(address)") + setNoneSignature = contract.CalculateFunctionSelector("setNone(address)") + readAllowListSignature = contract.CalculateFunctionSelector("readAllowList(address)") + // Error returned when an invalid write is attempted + ErrCannotModifyAllowList = errors.New("non-admin cannot modify allow list") +) + +// GetAllowListStatus returns the allow list role of [address] for the precompile +// at [precompileAddr] +func GetAllowListStatus(state contract.StateDB, precompileAddr common.Address, address common.Address) Role { + // Generate the state key for [address] + addressKey := address.Hash() + return Role(state.GetState(precompileAddr, addressKey)) +} + +// SetAllowListRole sets the permissions of [address] to [role] for the precompile +// at [precompileAddr]. +// assumes [role] has already been verified as valid. +func SetAllowListRole(stateDB contract.StateDB, precompileAddr, address common.Address, role Role) { + // Generate the state key for [address] + addressKey := address.Hash() + // Assign [role] to the address + // This stores the [role] in the contract storage with address [precompileAddr] + // and [addressKey] hash. It means that any reusage of the [addressKey] for different value + // conflicts with the same slot [role] is stored. + // Precompile implementations must use a different key than [addressKey] + stateDB.SetState(precompileAddr, addressKey, common.Hash(role)) +} + +// PackModifyAllowList packs [address] and [role] into the appropriate arguments for modifying the allow list. +// Note: [role] is not packed in the input value returned, but is instead used as a selector for the function +// selector that should be encoded in the input. +func PackModifyAllowList(address common.Address, role Role) ([]byte, error) { + // function selector (4 bytes) + hash for address + input := make([]byte, 0, contract.SelectorLen+common.HashLength) + + switch role { + case AdminRole: + input = append(input, setAdminSignature...) + case EnabledRole: + input = append(input, setEnabledSignature...) + case NoRole: + input = append(input, setNoneSignature...) + default: + return nil, fmt.Errorf("cannot pack modify list input with invalid role: %s", role) + } + + input = append(input, address.Hash().Bytes()...) + return input, nil +} + +// PackReadAllowList packs [address] into the input data to the read allow list function +func PackReadAllowList(address common.Address) []byte { + input := make([]byte, 0, contract.SelectorLen+common.HashLength) + input = append(input, readAllowListSignature...) + input = append(input, address.Hash().Bytes()...) + return input +} + +// createAllowListRoleSetter returns an execution function for setting the allow list status of the input address argument to [role]. +// This execution function is speciifc to [precompileAddr]. +func createAllowListRoleSetter(precompileAddr common.Address, role Role) contract.RunStatefulPrecompileFunc { + return func(evm contract.AccessibleState, callerAddr, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, ModifyAllowListGasCost); err != nil { + return nil, 0, err + } + + if len(input) != allowListInputLen { + return nil, remainingGas, fmt.Errorf("invalid input length for modifying allow list: %d", len(input)) + } + + modifyAddress := common.BytesToAddress(input) + + if readOnly { + return nil, remainingGas, vmerrs.ErrWriteProtection + } + + stateDB := evm.GetStateDB() + + // Verify that the caller is an admin with permission to modify the allow list + callerStatus := GetAllowListStatus(stateDB, precompileAddr, callerAddr) + if !callerStatus.IsAdmin() { + return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotModifyAllowList, callerAddr) + } + + SetAllowListRole(stateDB, precompileAddr, modifyAddress, role) + // Return an empty output and the remaining gas + return []byte{}, remainingGas, nil + } +} + +// createReadAllowList returns an execution function that reads the allow list for the given [precompileAddr]. +// The execution function parses the input into a single address and returns the 32 byte hash that specifies the +// designated role of that address +func createReadAllowList(precompileAddr common.Address) contract.RunStatefulPrecompileFunc { + return func(evm contract.AccessibleState, callerAddr common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, ReadAllowListGasCost); err != nil { + return nil, 0, err + } + + if len(input) != allowListInputLen { + return nil, remainingGas, fmt.Errorf("invalid input length for read allow list: %d", len(input)) + } + + readAddress := common.BytesToAddress(input) + role := GetAllowListStatus(evm.GetStateDB(), precompileAddr, readAddress) + roleBytes := common.Hash(role).Bytes() + return roleBytes, remainingGas, nil + } +} + +// CreateAllowListPrecompile returns a StatefulPrecompiledContract with R/W control of an allow list at [precompileAddr] +func CreateAllowListPrecompile(precompileAddr common.Address) contract.StatefulPrecompiledContract { + // Construct the contract with no fallback function. + allowListFuncs := CreateAllowListFunctions(precompileAddr) + contract, err := contract.NewStatefulPrecompileContract(nil, allowListFuncs) + // TODO Change this to be returned as an error after refactoring this precompile + // to use the new precompile template. + if err != nil { + panic(err) + } + return contract +} + +func CreateAllowListFunctions(precompileAddr common.Address) []*contract.StatefulPrecompileFunction { + setAdmin := contract.NewStatefulPrecompileFunction(setAdminSignature, createAllowListRoleSetter(precompileAddr, AdminRole)) + setEnabled := contract.NewStatefulPrecompileFunction(setEnabledSignature, createAllowListRoleSetter(precompileAddr, EnabledRole)) + setNone := contract.NewStatefulPrecompileFunction(setNoneSignature, createAllowListRoleSetter(precompileAddr, NoRole)) + read := contract.NewStatefulPrecompileFunction(readAllowListSignature, createReadAllowList(precompileAddr)) + + return []*contract.StatefulPrecompileFunction{setAdmin, setEnabled, setNone, read} +} diff --git a/precompile/allowlist/allowlist_test.go b/precompile/allowlist/allowlist_test.go new file mode 100644 index 0000000000..f4c0f5fc21 --- /dev/null +++ b/precompile/allowlist/allowlist_test.go @@ -0,0 +1,58 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package allowlist + +import ( + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ethereum/go-ethereum/common" +) + +var ( + _ precompileconfig.Config = &dummyConfig{} + _ contract.Configurator = &dummyConfigurator{} + + dummyAddr = common.Address{1} +) + +type dummyConfig struct { + *AllowListConfig +} + +func (d *dummyConfig) Key() string { return "dummy" } +func (d *dummyConfig) Timestamp() *big.Int { return common.Big0 } +func (d *dummyConfig) IsDisabled() bool { return false } +func (d *dummyConfig) Equal(other precompileconfig.Config) bool { + return d.AllowListConfig.Equal(other.(*dummyConfig).AllowListConfig) +} + +type dummyConfigurator struct{} + +func (d *dummyConfigurator) MakeConfig() precompileconfig.Config { + return &dummyConfig{} +} + +func (d *dummyConfigurator) Configure( + chainConfig contract.ChainConfig, + precompileConfig precompileconfig.Config, + state contract.StateDB, + blockContext contract.BlockContext, +) error { + cfg := precompileConfig.(*dummyConfig) + return cfg.Configure(state, dummyAddr) +} + +func TestAllowListRun(t *testing.T) { + dummyModule := modules.Module{ + Address: dummyAddr, + Contract: CreateAllowListPrecompile(dummyAddr), + Configurator: &dummyConfigurator{}, + } + RunPrecompileWithAllowListTests(t, dummyModule, state.NewTestStateDB, nil) +} diff --git a/precompile/allowlist/config.go b/precompile/allowlist/config.go new file mode 100644 index 0000000000..90ef49a811 --- /dev/null +++ b/precompile/allowlist/config.go @@ -0,0 +1,79 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package allowlist + +import ( + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" +) + +// AllowListConfig specifies the initial set of addresses with Admin or Enabled roles. +type AllowListConfig struct { + AdminAddresses []common.Address `json:"adminAddresses,omitempty"` // initial admin addresses + EnabledAddresses []common.Address `json:"enabledAddresses,omitempty"` // initial enabled addresses +} + +// Configure initializes the address space of [precompileAddr] by initializing the role of each of +// the addresses in [AllowListAdmins]. +func (c *AllowListConfig) Configure(state contract.StateDB, precompileAddr common.Address) error { + for _, enabledAddr := range c.EnabledAddresses { + SetAllowListRole(state, precompileAddr, enabledAddr, EnabledRole) + } + for _, adminAddr := range c.AdminAddresses { + SetAllowListRole(state, precompileAddr, adminAddr, AdminRole) + } + return nil +} + +// Equal returns true iff [other] has the same admins in the same order in its allow list. +func (c *AllowListConfig) Equal(other *AllowListConfig) bool { + if other == nil { + return false + } + + return areEqualAddressLists(c.AdminAddresses, other.AdminAddresses) && + areEqualAddressLists(c.EnabledAddresses, other.EnabledAddresses) +} + +// areEqualAddressLists returns true iff [a] and [b] have the same addresses in the same order. +func areEqualAddressLists(current []common.Address, other []common.Address) bool { + if len(current) != len(other) { + return false + } + for i, address := range current { + if address != other[i] { + return false + } + } + return true +} + +// Verify returns an error if there is an overlapping address between admin and enabled roles +func (c *AllowListConfig) Verify() error { + addressMap := make(map[common.Address]Role) // tracks which addresses we have seen and their role + + // check for duplicates in enabled list + for _, enabledAddr := range c.EnabledAddresses { + if _, ok := addressMap[enabledAddr]; ok { + return fmt.Errorf("duplicate address %s in enabled list", enabledAddr) + } + addressMap[enabledAddr] = EnabledRole + } + + // check for overlap between enabled and admin lists or duplicates in admin list + for _, adminAddr := range c.AdminAddresses { + if role, ok := addressMap[adminAddr]; ok { + if role == AdminRole { + return fmt.Errorf("duplicate address %s in admin list", adminAddr) + } else { + return fmt.Errorf("cannot set address %s as both admin and enabled", adminAddr) + } + } + addressMap[adminAddr] = AdminRole + } + + return nil +} diff --git a/precompile/allowlist/config_test.go b/precompile/allowlist/config_test.go new file mode 100644 index 0000000000..bc3a3d9d26 --- /dev/null +++ b/precompile/allowlist/config_test.go @@ -0,0 +1,97 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package allowlist + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestVerifyAllowlistAllowList(t *testing.T) { + admins := []common.Address{{1}} + enableds := []common.Address{{2}} + tests := []struct { + name string + config AllowListConfig + expectedError string + }{ + { + name: "invalid allow list config in allowlist", + config: AllowListConfig{admins, admins}, + expectedError: "cannot set address", + }, + { + name: "nil member allow list config in allowlist", + config: AllowListConfig{nil, nil}, + expectedError: "", + }, + { + name: "empty member allow list config in allowlist", + config: AllowListConfig{[]common.Address{}, []common.Address{}}, + expectedError: "", + }, + { + name: "valid allow list config in allowlist", + config: AllowListConfig{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 TestEqualAllowListAllowList(t *testing.T) { + admins := []common.Address{{1}} + enableds := []common.Address{{2}} + tests := []struct { + name string + config *AllowListConfig + other *AllowListConfig + expected bool + }{ + { + name: "non-nil config and nil other", + config: &AllowListConfig{admins, enableds}, + other: nil, + expected: false, + }, + { + name: "different admin", + config: &AllowListConfig{admins, enableds}, + other: &AllowListConfig{[]common.Address{{3}}, enableds}, + expected: false, + }, + { + name: "different enabled", + config: &AllowListConfig{admins, enableds}, + other: &AllowListConfig{admins, []common.Address{{3}}}, + expected: false, + }, + { + name: "same config", + config: &AllowListConfig{admins, enableds}, + other: &AllowListConfig{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/allow_list_role.go b/precompile/allowlist/role.go similarity index 55% rename from precompile/allow_list_role.go rename to precompile/allowlist/role.go index 0c815d0819..aa55007662 100644 --- a/precompile/allow_list_role.go +++ b/precompile/allowlist/role.go @@ -1,17 +1,17 @@ // (c) 2019-2020, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package precompile +package allowlist import "github.com/ethereum/go-ethereum/common" -// Enum constants for valid AllowListRole -type AllowListRole common.Hash +// Enum constants for valid Role +type Role common.Hash // Valid returns true iff [s] represents a valid role. -func (s AllowListRole) Valid() bool { +func (s Role) Valid() bool { switch s { - case AllowListNoRole, AllowListEnabled, AllowListAdmin: + case NoRole, EnabledRole, AdminRole: return true default: return false @@ -19,9 +19,9 @@ func (s AllowListRole) Valid() bool { } // IsNoRole returns true if [s] indicates no specific role. -func (s AllowListRole) IsNoRole() bool { +func (s Role) IsNoRole() bool { switch s { - case AllowListNoRole: + case NoRole: return true default: return false @@ -29,9 +29,9 @@ func (s AllowListRole) IsNoRole() bool { } // IsAdmin returns true if [s] indicates the permission to modify the allow list. -func (s AllowListRole) IsAdmin() bool { +func (s Role) IsAdmin() bool { switch s { - case AllowListAdmin: + case AdminRole: return true default: return false @@ -39,11 +39,25 @@ func (s AllowListRole) IsAdmin() bool { } // IsEnabled returns true if [s] indicates that it has permission to access the resource. -func (s AllowListRole) IsEnabled() bool { +func (s Role) IsEnabled() bool { switch s { - case AllowListAdmin, AllowListEnabled: + case AdminRole, EnabledRole: return true default: return false } } + +// String returns a string representation of [s]. +func (s Role) String() string { + switch s { + case NoRole: + return "NoRole" + case EnabledRole: + return "EnabledRole" + case AdminRole: + return "AdminRole" + default: + return "UnknownRole" + } +} diff --git a/precompile/allowlist/test_allowlist.go b/precompile/allowlist/test_allowlist.go new file mode 100644 index 0000000000..78158e55ee --- /dev/null +++ b/precompile/allowlist/test_allowlist.go @@ -0,0 +1,291 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package allowlist + +import ( + "encoding/json" + "testing" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ava-labs/subnet-evm/precompile/testutils" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +var ( + TestAdminAddr = common.HexToAddress("0x0000000000000000000000000000000000000011") + TestEnabledAddr = common.HexToAddress("0x0000000000000000000000000000000000000022") + TestNoRoleAddr = common.HexToAddress("0x0000000000000000000000000000000000000033") +) + +// mkConfigWithAllowList creates a new config with the correct type for [module] +// by marshalling [cfg] to JSON and then unmarshalling it into the config. +func mkConfigWithAllowList(module modules.Module, cfg *AllowListConfig) precompileconfig.Config { + jsonBytes, err := json.Marshal(cfg) + if err != nil { + panic(err) + } + + moduleCfg := module.MakeConfig() + err = json.Unmarshal(jsonBytes, moduleCfg) + if err != nil { + panic(err) + } + + return moduleCfg +} + +func AllowListTests(module modules.Module) map[string]testutils.PrecompileTest { + contractAddress := module.Address + return map[string]testutils.PrecompileTest{ + "set admin": { + Caller: TestAdminAddr, + BeforeHook: SetDefaultRoles(contractAddress), + InputFn: func(t *testing.T) []byte { + input, err := PackModifyAllowList(TestNoRoleAddr, AdminRole) + require.NoError(t, err) + + return input + }, + SuppliedGas: ModifyAllowListGasCost, + ReadOnly: false, + ExpectedRes: []byte{}, + AfterHook: func(t *testing.T, state contract.StateDB) { + res := GetAllowListStatus(state, contractAddress, TestNoRoleAddr) + require.Equal(t, AdminRole, res) + }, + }, + "set enabled": { + Caller: TestAdminAddr, + BeforeHook: SetDefaultRoles(contractAddress), + InputFn: func(t *testing.T) []byte { + input, err := PackModifyAllowList(TestNoRoleAddr, EnabledRole) + require.NoError(t, err) + + return input + }, + SuppliedGas: ModifyAllowListGasCost, + ReadOnly: false, + ExpectedRes: []byte{}, + AfterHook: func(t *testing.T, state contract.StateDB) { + res := GetAllowListStatus(state, contractAddress, TestNoRoleAddr) + require.Equal(t, EnabledRole, res) + }, + }, + "set no role": { + Caller: TestAdminAddr, + BeforeHook: SetDefaultRoles(contractAddress), + InputFn: func(t *testing.T) []byte { + input, err := PackModifyAllowList(TestEnabledAddr, NoRole) + require.NoError(t, err) + + return input + }, + SuppliedGas: ModifyAllowListGasCost, + ReadOnly: false, + ExpectedRes: []byte{}, + AfterHook: func(t *testing.T, state contract.StateDB) { + res := GetAllowListStatus(state, contractAddress, TestEnabledAddr) + require.Equal(t, NoRole, res) + }, + }, + "set no role from no role": { + Caller: TestNoRoleAddr, + BeforeHook: SetDefaultRoles(contractAddress), + InputFn: func(t *testing.T) []byte { + input, err := PackModifyAllowList(TestEnabledAddr, NoRole) + require.NoError(t, err) + + return input + }, + SuppliedGas: ModifyAllowListGasCost, + ReadOnly: false, + ExpectedErr: ErrCannotModifyAllowList.Error(), + }, + "set enabled from no role": { + Caller: TestNoRoleAddr, + BeforeHook: SetDefaultRoles(contractAddress), + InputFn: func(t *testing.T) []byte { + input, err := PackModifyAllowList(TestNoRoleAddr, EnabledRole) + require.NoError(t, err) + + return input + }, + SuppliedGas: ModifyAllowListGasCost, + ReadOnly: false, + ExpectedErr: ErrCannotModifyAllowList.Error(), + }, + "set admin from no role": { + Caller: TestNoRoleAddr, + BeforeHook: SetDefaultRoles(contractAddress), + InputFn: func(t *testing.T) []byte { + input, err := PackModifyAllowList(TestEnabledAddr, AdminRole) + require.NoError(t, err) + + return input + }, + SuppliedGas: ModifyAllowListGasCost, + ReadOnly: false, + ExpectedErr: ErrCannotModifyAllowList.Error(), + }, + "set no role from enabled": { + Caller: TestEnabledAddr, + BeforeHook: SetDefaultRoles(contractAddress), + InputFn: func(t *testing.T) []byte { + input, err := PackModifyAllowList(TestAdminAddr, NoRole) + require.NoError(t, err) + + return input + }, + SuppliedGas: ModifyAllowListGasCost, + ReadOnly: false, + ExpectedErr: ErrCannotModifyAllowList.Error(), + }, + "set enabled from enabled": { + Caller: TestEnabledAddr, + BeforeHook: SetDefaultRoles(contractAddress), + InputFn: func(t *testing.T) []byte { + input, err := PackModifyAllowList(TestNoRoleAddr, EnabledRole) + require.NoError(t, err) + + return input + }, + SuppliedGas: ModifyAllowListGasCost, + ReadOnly: false, + ExpectedErr: ErrCannotModifyAllowList.Error(), + }, + "set admin from enabled": { + Caller: TestEnabledAddr, + BeforeHook: SetDefaultRoles(contractAddress), + InputFn: func(t *testing.T) []byte { + input, err := PackModifyAllowList(TestNoRoleAddr, AdminRole) + require.NoError(t, err) + + return input + }, + SuppliedGas: ModifyAllowListGasCost, + ReadOnly: false, + ExpectedErr: ErrCannotModifyAllowList.Error(), + }, + "set no role with readOnly enabled": { + Caller: TestAdminAddr, + BeforeHook: SetDefaultRoles(contractAddress), + InputFn: func(t *testing.T) []byte { + input, err := PackModifyAllowList(TestEnabledAddr, NoRole) + require.NoError(t, err) + + return input + }, + SuppliedGas: ModifyAllowListGasCost, + ReadOnly: true, + ExpectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "set no role insufficient gas": { + Caller: TestAdminAddr, + BeforeHook: SetDefaultRoles(contractAddress), + InputFn: func(t *testing.T) []byte { + input, err := PackModifyAllowList(TestEnabledAddr, NoRole) + require.NoError(t, err) + + return input + }, + SuppliedGas: ModifyAllowListGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "read allow list no role": { + Caller: TestNoRoleAddr, + BeforeHook: SetDefaultRoles(contractAddress), + Input: PackReadAllowList(TestNoRoleAddr), + SuppliedGas: ReadAllowListGasCost, + ReadOnly: false, + ExpectedRes: common.Hash(NoRole).Bytes(), + }, + "read allow list admin role": { + Caller: TestAdminAddr, + BeforeHook: SetDefaultRoles(contractAddress), + Input: PackReadAllowList(TestAdminAddr), + SuppliedGas: ReadAllowListGasCost, + ReadOnly: false, + ExpectedRes: common.Hash(AdminRole).Bytes(), + }, + "read allow list with readOnly enabled": { + Caller: TestAdminAddr, + BeforeHook: SetDefaultRoles(contractAddress), + Input: PackReadAllowList(TestNoRoleAddr), + SuppliedGas: ReadAllowListGasCost, + ReadOnly: true, + ExpectedRes: common.Hash(NoRole).Bytes(), + }, + "read allow list out of gas": { + Caller: TestAdminAddr, + BeforeHook: SetDefaultRoles(contractAddress), + Input: PackReadAllowList(TestNoRoleAddr), + SuppliedGas: ReadAllowListGasCost - 1, + ReadOnly: true, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + "initial config sets admins": { + Config: mkConfigWithAllowList( + module, + &AllowListConfig{ + AdminAddresses: []common.Address{TestNoRoleAddr, TestEnabledAddr}, + }, + ), + SuppliedGas: 0, + ReadOnly: false, + AfterHook: func(t *testing.T, state contract.StateDB) { + require.Equal(t, AdminRole, GetAllowListStatus(state, contractAddress, TestNoRoleAddr)) + require.Equal(t, AdminRole, GetAllowListStatus(state, contractAddress, TestEnabledAddr)) + }, + }, + "initial config sets enabled": { + Config: mkConfigWithAllowList( + module, + &AllowListConfig{ + EnabledAddresses: []common.Address{TestNoRoleAddr, TestAdminAddr}, + }, + ), + SuppliedGas: 0, + ReadOnly: false, + AfterHook: func(t *testing.T, state contract.StateDB) { + require.Equal(t, EnabledRole, GetAllowListStatus(state, contractAddress, TestAdminAddr)) + require.Equal(t, EnabledRole, GetAllowListStatus(state, contractAddress, TestNoRoleAddr)) + }, + }, + } +} + +// SetDefaultRoles returns a BeforeHook that sets roles TestAdminAddr and TestEnabledAddr +// to have the AdminRole and EnabledRole respectively. +func SetDefaultRoles(contractAddress common.Address) func(t *testing.T, state contract.StateDB) { + return func(t *testing.T, state contract.StateDB) { + SetAllowListRole(state, contractAddress, TestAdminAddr, AdminRole) + SetAllowListRole(state, contractAddress, TestEnabledAddr, EnabledRole) + require.Equal(t, AdminRole, GetAllowListStatus(state, contractAddress, TestAdminAddr)) + require.Equal(t, EnabledRole, GetAllowListStatus(state, contractAddress, TestEnabledAddr)) + require.Equal(t, NoRole, GetAllowListStatus(state, contractAddress, TestNoRoleAddr)) + } +} + +func RunPrecompileWithAllowListTests(t *testing.T, module modules.Module, newStateDB func(t *testing.T) contract.StateDB, contractTests map[string]testutils.PrecompileTest) { + t.Helper() + tests := AllowListTests(module) + // Add the contract specific tests to the map of tests to run. + for name, test := range contractTests { + if _, exists := tests[name]; exists { + t.Fatalf("duplicate test name: %s", name) + } + tests[name] = test + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + test.Run(t, module, newStateDB(t)) + }) + } +} diff --git a/precompile/config_test.go b/precompile/config_test.go deleted file mode 100644 index 2772009390..0000000000 --- a/precompile/config_test.go +++ /dev/null @@ -1,465 +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, - func() *commontype.FeeConfig { - feeConfig := validFeeConfig - feeConfig.GasLimit = big.NewInt(0) - return &feeConfig - }()), - - 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 deleted file mode 100644 index 0e2d720273..0000000000 --- a/precompile/contract.go +++ /dev/null @@ -1,142 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package precompile - -import ( - "fmt" - "math/big" - - "github.com/ava-labs/avalanchego/snow" - "github.com/ava-labs/subnet-evm/commontype" - "github.com/ethereum/go-ethereum/common" -) - -const ( - selectorLen = 4 -) - -type RunStatefulPrecompileFunc func(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) - -// 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 { - // selector is the 4 byte function selector for this function - // This should be calculated from the function signature using CalculateFunctionSelector - selector []byte - // execute is performed when this function is selected - execute RunStatefulPrecompileFunc -} - -// newStatefulPrecompileFunction creates a stateful precompile function with the given arguments -func newStatefulPrecompileFunction(selector []byte, execute RunStatefulPrecompileFunc) *statefulPrecompileFunction { - return &statefulPrecompileFunction{ - selector: selector, - execute: execute, - } -} - -// statefulPrecompileWithFunctionSelectors implements StatefulPrecompiledContract by using 4 byte function selectors to pass -// off responsibilities to internal execution functions. -// 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 -} - -// newStatefulPrecompileWithFunctionSelectors 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 newStatefulPrecompileWithFunctionSelectors(fallback RunStatefulPrecompileFunc, functions []*statefulPrecompileFunction) StatefulPrecompiledContract { - // Construct the contract and populate [functions]. - contract := &statefulPrecompileWithFunctionSelectors{ - fallback: fallback, - functions: make(map[string]*statefulPrecompileFunction), - } - for _, function := range functions { - _, exists := contract.functions[string(function.selector)] - if exists { - panic(fmt.Errorf("cannot create stateful precompile with duplicated function selector: %q", function.selector)) - } - contract.functions[string(function.selector)] = function - } - - return contract -} - -// Run selects the function using the 4 byte function selector at the start of the input and executes the underlying function on the -// given arguments. -func (s *statefulPrecompileWithFunctionSelectors) Run(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { - // If there is no input data present, call the fallback function if present. - if len(input) == 0 && s.fallback != nil { - return s.fallback(accessibleState, caller, addr, nil, suppliedGas, readOnly) - } - - // Otherwise, an unexpected input size will result in an error. - 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:] - function, ok := s.functions[string(selector)] - if !ok { - return nil, suppliedGas, fmt.Errorf("invalid function selector %#x", selector) - } - - return function.execute(accessibleState, caller, addr, functionInput, suppliedGas, readOnly) -} diff --git a/precompile/contract/contract.go b/precompile/contract/contract.go new file mode 100644 index 0000000000..82bb5fe21c --- /dev/null +++ b/precompile/contract/contract.go @@ -0,0 +1,84 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package contract + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" +) + +const ( + SelectorLen = 4 +) + +type RunStatefulPrecompileFunc func(accessibleState AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) + +// StatefulPrecompileFunction defines a function implemented by a stateful precompile +type StatefulPrecompileFunction struct { + // selector is the 4 byte function selector for this function + // This should be calculated from the function signature using CalculateFunctionSelector + selector []byte + // execute is performed when this function is selected + execute RunStatefulPrecompileFunc +} + +// NewStatefulPrecompileFunction creates a stateful precompile function with the given arguments +func NewStatefulPrecompileFunction(selector []byte, execute RunStatefulPrecompileFunc) *StatefulPrecompileFunction { + return &StatefulPrecompileFunction{ + selector: selector, + execute: execute, + } +} + +// statefulPrecompileWithFunctionSelectors implements StatefulPrecompiledContract by using 4 byte function selectors to pass +// off responsibilities to internal execution functions. +// 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 +} + +// 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) { + // Construct the contract and populate [functions]. + contract := &statefulPrecompileWithFunctionSelectors{ + fallback: fallback, + functions: make(map[string]*StatefulPrecompileFunction), + } + for _, function := range functions { + _, exists := contract.functions[string(function.selector)] + if exists { + return nil, fmt.Errorf("cannot create stateful precompile with duplicated function selector: %q", function.selector) + } + contract.functions[string(function.selector)] = function + } + + return contract, nil +} + +// Run selects the function using the 4 byte function selector at the start of the input and executes the underlying function on the +// given arguments. +func (s *statefulPrecompileWithFunctionSelectors) Run(accessibleState AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + // If there is no input data present, call the fallback function if present. + if len(input) == 0 && s.fallback != nil { + return s.fallback(accessibleState, caller, addr, nil, suppliedGas, readOnly) + } + + // Otherwise, an unexpected input size will result in an error. + 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:] + function, ok := s.functions[string(selector)] + if !ok { + return nil, suppliedGas, fmt.Errorf("invalid function selector %#x", selector) + } + + return function.execute(accessibleState, caller, addr, functionInput, suppliedGas, readOnly) +} diff --git a/precompile/contract/interfaces.go b/precompile/contract/interfaces.go new file mode 100644 index 0000000000..a8402bed75 --- /dev/null +++ b/precompile/contract/interfaces.go @@ -0,0 +1,79 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Defines the interface for the configuration and execution of a precompile contract +package contract + +import ( + "math/big" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/subnet-evm/commontype" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ethereum/go-ethereum/common" +) + +// StatefulPrecompiledContract is the interface for executing a precompiled contract +type StatefulPrecompiledContract interface { + // Run executes the precompiled contract. + Run(accessibleState AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) +} + +// ChainContext defines an interface that provides information to a stateful precompile +// 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) +} + +// AccessibleState defines the interface exposed to stateful precompile contracts +type AccessibleState interface { + GetStateDB() StateDB + GetBlockContext() BlockContext + GetSnowContext() *snow.Context + CallFromPrecompile(caller common.Address, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) +} + +// BlockContext defines an interface that provides information to a stateful precompile +// about the block that activates the upgrade. The precompile can access this information +// to initialize its state. +type BlockContext interface { + Number() *big.Int + Timestamp() *big.Int +} + +type Configurator interface { + MakeConfig() precompileconfig.Config + Configure( + chainConfig ChainConfig, + precompileconfig precompileconfig.Config, + state StateDB, + blockContext BlockContext, + ) error +} diff --git a/precompile/contract/mock_interfaces.go b/precompile/contract/mock_interfaces.go new file mode 100644 index 0000000000..aa4a9dcfaa --- /dev/null +++ b/precompile/contract/mock_interfaces.go @@ -0,0 +1,73 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package contract + +import ( + "math/big" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/subnet-evm/commontype" + "github.com/ethereum/go-ethereum/common" +) + +// TODO: replace with gomock library + +var ( + _ BlockContext = &mockBlockContext{} + _ AccessibleState = &mockAccessibleState{} +) + +type mockBlockContext struct { + blockNumber *big.Int + timestamp uint64 +} + +func NewMockBlockContext(blockNumber *big.Int, timestamp uint64) *mockBlockContext { + return &mockBlockContext{ + blockNumber: blockNumber, + timestamp: timestamp, + } +} + +func (mb *mockBlockContext) Number() *big.Int { return mb.blockNumber } +func (mb *mockBlockContext) Timestamp() *big.Int { return new(big.Int).SetUint64(mb.timestamp) } + +type mockAccessibleState struct { + state StateDB + blockContext *mockBlockContext + snowContext *snow.Context +} + +func NewMockAccessibleState(state StateDB, blockContext *mockBlockContext, snowContext *snow.Context) *mockAccessibleState { + return &mockAccessibleState{ + state: state, + blockContext: blockContext, + snowContext: snowContext, + } +} + +func (m *mockAccessibleState) GetStateDB() StateDB { return m.state } + +func (m *mockAccessibleState) GetBlockContext() BlockContext { return m.blockContext } + +func (m *mockAccessibleState) GetSnowContext() *snow.Context { return m.snowContext } + +func (m *mockAccessibleState) CallFromPrecompile(caller common.Address, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) { + return nil, 0, nil +} + +type mockChainState struct { + feeConfig commontype.FeeConfig + allowedFeeRecipients bool +} + +func (m *mockChainState) GetFeeConfig() commontype.FeeConfig { return m.feeConfig } +func (m *mockChainState) AllowedFeeRecipients() bool { return m.allowedFeeRecipients } + +func NewMockChainState(feeConfig commontype.FeeConfig, allowedFeeRecipients bool) *mockChainState { + return &mockChainState{ + feeConfig: feeConfig, + allowedFeeRecipients: allowedFeeRecipients, + } +} diff --git a/precompile/utils.go b/precompile/contract/utils.go similarity index 58% rename from precompile/utils.go rename to precompile/contract/utils.go index 2476a97e34..9cc50d3155 100644 --- a/precompile/utils.go +++ b/precompile/contract/utils.go @@ -1,22 +1,31 @@ // (c) 2019-2020, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package precompile +package contract import ( "fmt" "regexp" + "strings" + "github.com/ava-labs/subnet-evm/accounts/abi" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) +// Gas costs for stateful precompiles +const ( + WriteGasCostPerSlot = 20_000 + ReadGasCostPerSlot = 5_000 +) + var functionSignatureRegex = regexp.MustCompile(`[\w]+\(((([\w]+)?)|((([\w]+),)+([\w]+)))\)`) // CalculateFunctionSelector returns the 4 byte function selector that results from [functionSignature] // 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,27 +34,27 @@ 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 } return suppliedGas - requiredGas, nil } -// packOrderedHashesWithSelector packs the function selector and ordered list of hashes into [dst] +// 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) { +func PackOrderedHashesWithSelector(dst []byte, functionSelector []byte, hashes []common.Hash) error { copy(dst[:len(functionSelector)], functionSelector) - packOrderedHashes(dst[len(functionSelector):], hashes) + return PackOrderedHashes(dst[len(functionSelector):], hashes) } -// packOrderedHashes packs the ordered list of [hashes] into the [dst] byte buffer. +// 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) { +func PackOrderedHashes(dst []byte, hashes []common.Hash) error { if len(dst) != len(hashes)*common.HashLength { - panic(fmt.Sprintf("destination byte buffer has insufficient length (%d) for %d hashes", len(dst), len(hashes))) + return fmt.Errorf("destination byte buffer has insufficient length (%d) for %d hashes", len(dst), len(hashes)) } var ( @@ -57,13 +66,25 @@ func packOrderedHashes(dst []byte, hashes []common.Hash) { start += common.HashLength end += common.HashLength } + 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] } + +// ParseABI parses the given ABI string and returns the parsed ABI. +// If the ABI is invalid, it panics. +func ParseABI(rawABI string) abi.ABI { + parsed, err := abi.JSON(strings.NewReader(rawABI)) + if err != nil { + panic(err) + } + + return parsed +} diff --git a/precompile/utils_test.go b/precompile/contract/utils_test.go similarity index 98% rename from precompile/utils_test.go rename to precompile/contract/utils_test.go index 3414bc341c..6220af95a8 100644 --- a/precompile/utils_test.go +++ b/precompile/contract/utils_test.go @@ -1,7 +1,7 @@ // (c) 2019-2020, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package precompile +package contract import ( "testing" diff --git a/precompile/contract_deployer_allow_list.go b/precompile/contract_deployer_allow_list.go deleted file mode 100644 index 7d42a270b9..0000000000 --- a/precompile/contract_deployer_allow_list.go +++ /dev/null @@ -1,91 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package precompile - -import ( - "encoding/json" - "math/big" - - "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) -) - -// 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 -} - -// 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{ - AllowListAdmins: admins, - EnabledAddresses: enableds, - }, - UpgradeableConfig: UpgradeableConfig{BlockTimestamp: blockTimestamp}, - } -} - -// NewDisableContractDeployerAllowListConfig returns config for a network upgrade at [blockTimestamp] -// that disables ContractDeployerAllowList. -func NewDisableContractDeployerAllowListConfig(blockTimestamp *big.Int) *ContractDeployerAllowListConfig { - return &ContractDeployerAllowListConfig{ - UpgradeableConfig: UpgradeableConfig{ - BlockTimestamp: blockTimestamp, - Disable: true, - }, - } -} - -// Address returns the address of the contract deployer allow list. -func (c *ContractDeployerAllowListConfig) Address() common.Address { - return ContractDeployerAllowListAddress -} - -// Configure configures [state] with the desired admins based on [c]. -func (c *ContractDeployerAllowListConfig) Configure(_ ChainConfig, state StateDB, _ BlockContext) { - c.AllowListConfig.Configure(state, ContractDeployerAllowListAddress) -} - -// Contract returns the singleton stateful precompiled contract to be used for the allow list. -func (c *ContractDeployerAllowListConfig) Contract() 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 { - // typecast before comparison - other, ok := (s).(*ContractDeployerAllowListConfig) - if !ok { - return false - } - return c.UpgradeableConfig.Equal(&other.UpgradeableConfig) && c.AllowListConfig.Equal(&other.AllowListConfig) -} - -// String returns a string representation of the ContractDeployerAllowListConfig. -func (c *ContractDeployerAllowListConfig) String() string { - bytes, _ := json.Marshal(c) - return string(bytes) -} - -// 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) -} - -// 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) -} diff --git a/precompile/contract_native_minter.go b/precompile/contract_native_minter.go deleted file mode 100644 index 2da1b3bbaa..0000000000 --- a/precompile/contract_native_minter.go +++ /dev/null @@ -1,222 +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) { - for to, amount := range c.InitialMint { - if amount != nil { - bigIntAmount := (*big.Int)(amount) - state.AddBalance(to, bigIntAmount) - } - } - - 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) - packOrderedHashesWithSelector(res, mintSignature, []common.Hash{ - address.Hash(), - common.BigToHash(amount), - }) - - return res, nil -} - -// 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 := newStatefulPrecompileWithFunctionSelectors(nil, enabledFuncs) - return contract -} diff --git a/precompile/contracts/deployerallowlist/config.go b/precompile/contracts/deployerallowlist/config.go new file mode 100644 index 0000000000..c2d5bf28dd --- /dev/null +++ b/precompile/contracts/deployerallowlist/config.go @@ -0,0 +1,58 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package deployerallowlist + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ethereum/go-ethereum/common" +) + +var _ precompileconfig.Config = &Config{} + +// Config contains the configuration for the ContractDeployerAllowList precompile, +// consisting of the initial allowlist and the timestamp for the network upgrade. +type Config struct { + allowlist.AllowListConfig + precompileconfig.Upgrade +} + +// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables +// ContractDeployerAllowList with [admins] and [enableds] as members of the allowlist. +func NewConfig(blockTimestamp *big.Int, admins []common.Address, enableds []common.Address) *Config { + return &Config{ + AllowListConfig: allowlist.AllowListConfig{ + AdminAddresses: admins, + EnabledAddresses: enableds, + }, + Upgrade: precompileconfig.Upgrade{BlockTimestamp: blockTimestamp}, + } +} + +// NewDisableConfig returns config for a network upgrade at [blockTimestamp] +// that disables ContractDeployerAllowList. +func NewDisableConfig(blockTimestamp *big.Int) *Config { + return &Config{ + Upgrade: precompileconfig.Upgrade{ + BlockTimestamp: blockTimestamp, + Disable: true, + }, + } +} + +func (*Config) Key() string { return ConfigKey } + +// Equal returns true if [cfg] is a [*ContractDeployerAllowListConfig] and it has been configured identical to [c]. +func (c *Config) Equal(cfg precompileconfig.Config) bool { + // typecast before comparison + other, ok := (cfg).(*Config) + if !ok { + return false + } + return c.Upgrade.Equal(&other.Upgrade) && c.AllowListConfig.Equal(&other.AllowListConfig) +} + +func (c *Config) Verify() error { return c.AllowListConfig.Verify() } diff --git a/precompile/contracts/deployerallowlist/config_test.go b/precompile/contracts/deployerallowlist/config_test.go new file mode 100644 index 0000000000..caa3d8995a --- /dev/null +++ b/precompile/contracts/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/precompileconfig" + "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 precompileconfig.Config + ExpectedError string + }{ + { + name: "invalid allow list config in deployer allowlist", + config: NewConfig(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 precompileconfig.Config + other precompileconfig.Config + expected bool + }{ + { + name: "non-nil config and nil other", + config: NewConfig(big.NewInt(3), admins, enableds), + other: nil, + expected: false, + }, + { + name: "different type", + config: NewConfig(big.NewInt(3), admins, enableds), + other: precompileconfig.NewNoopStatefulPrecompileConfig(), + expected: false, + }, + { + name: "different admin", + config: NewConfig(big.NewInt(3), admins, enableds), + other: NewConfig(big.NewInt(3), []common.Address{{3}}, enableds), + expected: false, + }, + { + name: "different enabled", + config: NewConfig(big.NewInt(3), admins, enableds), + other: NewConfig(big.NewInt(3), admins, []common.Address{{3}}), + expected: false, + }, + { + name: "different timestamp", + config: NewConfig(big.NewInt(3), admins, enableds), + other: NewConfig(big.NewInt(4), admins, enableds), + expected: false, + }, + { + name: "same config", + config: NewConfig(big.NewInt(3), admins, enableds), + other: NewConfig(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/contracts/deployerallowlist/contract.go b/precompile/contracts/deployerallowlist/contract.go new file mode 100644 index 0000000000..bb4b97e95b --- /dev/null +++ b/precompile/contracts/deployerallowlist/contract.go @@ -0,0 +1,26 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package deployerallowlist + +import ( + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" +) + +// Singleton StatefulPrecompiledContract for W/R access to the contract deployer allow list. +var ContractDeployerAllowListPrecompile contract.StatefulPrecompiledContract = allowlist.CreateAllowListPrecompile(ContractAddress) + +// GetContractDeployerAllowListStatus returns the role of [address] for the contract deployer +// allow list. +func GetContractDeployerAllowListStatus(stateDB contract.StateDB, address common.Address) allowlist.Role { + return allowlist.GetAllowListStatus(stateDB, ContractAddress, address) +} + +// SetContractDeployerAllowListStatus sets the permissions of [address] to [role] for the +// contract deployer allow list. +// assumes [role] has already been verified as valid. +func SetContractDeployerAllowListStatus(stateDB contract.StateDB, address common.Address, role allowlist.Role) { + allowlist.SetAllowListRole(stateDB, ContractAddress, address, role) +} diff --git a/precompile/contracts/deployerallowlist/contract_test.go b/precompile/contracts/deployerallowlist/contract_test.go new file mode 100644 index 0000000000..ba144fd155 --- /dev/null +++ b/precompile/contracts/deployerallowlist/contract_test.go @@ -0,0 +1,15 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package deployerallowlist + +import ( + "testing" + + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/precompile/allowlist" +) + +func TestContractDeployerAllowListRun(t *testing.T) { + allowlist.RunPrecompileWithAllowListTests(t, Module, state.NewTestStateDB, nil) +} diff --git a/precompile/contracts/deployerallowlist/module.go b/precompile/contracts/deployerallowlist/module.go new file mode 100644 index 0000000000..93dd59daef --- /dev/null +++ b/precompile/contracts/deployerallowlist/module.go @@ -0,0 +1,49 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package deployerallowlist + +import ( + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ethereum/go-ethereum/common" +) + +var _ contract.Configurator = &configurator{} + +// ConfigKey is the key used in json config files to specify this precompile config. +// must be unique across all precompiles. +const ConfigKey = "contractDeployerAllowListConfig" + +var ContractAddress = common.HexToAddress("0x0200000000000000000000000000000000000000") + +var Module = modules.Module{ + ConfigKey: ConfigKey, + Address: ContractAddress, + Contract: ContractDeployerAllowListPrecompile, + Configurator: &configurator{}, +} + +type configurator struct{} + +func init() { + if err := modules.RegisterModule(Module); err != nil { + panic(err) + } +} + +func (*configurator) MakeConfig() precompileconfig.Config { + return new(Config) +} + +// Configure configures [state] with the given [cfg] config. +func (c *configurator) Configure(_ contract.ChainConfig, cfg precompileconfig.Config, state contract.StateDB, _ contract.BlockContext) error { + config, ok := cfg.(*Config) + if !ok { + return fmt.Errorf("incorrect config %T: %v", config, config) + } + return config.AllowListConfig.Configure(state, ContractAddress) +} diff --git a/precompile/contracts/feemanager/config.go b/precompile/contracts/feemanager/config.go new file mode 100644 index 0000000000..9debfa7b1f --- /dev/null +++ b/precompile/contracts/feemanager/config.go @@ -0,0 +1,80 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package feemanager + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/commontype" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ethereum/go-ethereum/common" +) + +var _ precompileconfig.Config = &Config{} + +// Config implements the StatefulPrecompileConfig interface while adding in the +// FeeManager specific precompile config. +type Config struct { + allowlist.AllowListConfig // Config for the fee config manager allow list + precompileconfig.Upgrade + InitialFeeConfig *commontype.FeeConfig `json:"initialFeeConfig,omitempty"` // initial fee config to be immediately activated +} + +// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables +// FeeManager with the given [admins] and [enableds] as members of the +// allowlist with [initialConfig] as initial fee config if specified. +func NewConfig(blockTimestamp *big.Int, admins []common.Address, enableds []common.Address, initialConfig *commontype.FeeConfig) *Config { + return &Config{ + AllowListConfig: allowlist.AllowListConfig{ + AdminAddresses: admins, + EnabledAddresses: enableds, + }, + Upgrade: precompileconfig.Upgrade{BlockTimestamp: blockTimestamp}, + InitialFeeConfig: initialConfig, + } +} + +// NewDisableConfig returns config for a network upgrade at [blockTimestamp] +// that disables FeeManager. +func NewDisableConfig(blockTimestamp *big.Int) *Config { + return &Config{ + Upgrade: precompileconfig.Upgrade{ + BlockTimestamp: blockTimestamp, + Disable: true, + }, + } +} + +func (*Config) Key() string { return ConfigKey } + +// Equal returns true if [cfg] is a [*FeeManagerConfig] and it has been configured identical to [c]. +func (c *Config) Equal(cfg precompileconfig.Config) bool { + // typecast before comparison + other, ok := (cfg).(*Config) + if !ok { + return false + } + eq := c.Upgrade.Equal(&other.Upgrade) && c.AllowListConfig.Equal(&other.AllowListConfig) + if !eq { + return false + } + + if c.InitialFeeConfig == nil { + return other.InitialFeeConfig == nil + } + + return c.InitialFeeConfig.Equal(other.InitialFeeConfig) +} + +func (c *Config) Verify() error { + if err := c.AllowListConfig.Verify(); err != nil { + return err + } + if c.InitialFeeConfig == nil { + return nil + } + + return c.InitialFeeConfig.Verify() +} diff --git a/precompile/contracts/feemanager/config_test.go b/precompile/contracts/feemanager/config_test.go new file mode 100644 index 0000000000..4b5654d5d9 --- /dev/null +++ b/precompile/contracts/feemanager/config_test.go @@ -0,0 +1,132 @@ +// (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/precompileconfig" + "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}} + invalidFeeConfig := validFeeConfig + invalidFeeConfig.GasLimit = big.NewInt(0) + tests := []struct { + name string + config precompileconfig.Config + ExpectedError string + }{ + { + name: "invalid allow list config in fee manager allowlist", + config: NewConfig(big.NewInt(3), admins, admins, nil), + ExpectedError: "cannot set address", + }, + { + name: "invalid initial fee manager config", + config: NewConfig(big.NewInt(3), admins, nil, &invalidFeeConfig), + ExpectedError: "gasLimit = 0 cannot be less than or equal to 0", + }, + { + name: "nil initial fee manager config", + config: NewConfig(big.NewInt(3), admins, nil, &commontype.FeeConfig{}), + ExpectedError: "gasLimit cannot be nil", + }, + } + 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 TestEqualFeeManagerConfig(t *testing.T) { + admins := []common.Address{{1}} + enableds := []common.Address{{2}} + tests := []struct { + name string + config precompileconfig.Config + other precompileconfig.Config + expected bool + }{ + { + name: "non-nil config and nil other", + config: NewConfig(big.NewInt(3), admins, enableds, nil), + other: nil, + expected: false, + }, + { + name: "different type", + config: NewConfig(big.NewInt(3), admins, enableds, nil), + other: precompileconfig.NewNoopStatefulPrecompileConfig(), + expected: false, + }, + { + name: "different timestamp", + config: NewConfig(big.NewInt(3), admins, nil, nil), + other: NewConfig(big.NewInt(4), admins, nil, nil), + expected: false, + }, + { + name: "different enabled", + config: NewConfig(big.NewInt(3), admins, nil, nil), + other: NewConfig(big.NewInt(3), admins, enableds, nil), + expected: false, + }, + { + name: "non-nil initial config and nil initial config", + config: NewConfig(big.NewInt(3), admins, nil, &validFeeConfig), + other: NewConfig(big.NewInt(3), admins, nil, nil), + expected: false, + }, + { + name: "different initial config", + config: NewConfig(big.NewInt(3), admins, nil, &validFeeConfig), + other: NewConfig(big.NewInt(3), admins, nil, + func() *commontype.FeeConfig { + c := validFeeConfig + c.GasLimit = big.NewInt(123) + return &c + }()), + expected: false, + }, + { + name: "same config", + config: NewConfig(big.NewInt(3), admins, nil, &validFeeConfig), + other: NewConfig(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/contracts/feemanager/contract.go similarity index 50% rename from precompile/fee_config_manager.go rename to precompile/contracts/feemanager/contract.go index 3436ba360c..a4ecd9a4e4 100644 --- a/precompile/fee_config_manager.go +++ b/precompile/contracts/feemanager/contract.go @@ -1,15 +1,16 @@ // (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/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" ) @@ -32,130 +33,34 @@ 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 = contract.WriteGasCostPerSlot * (numFeeConfigField + 1) // plus one for setting last changed at + GetFeeConfigGasCost = contract.ReadGasCostPerSlot * numFeeConfigField + GetLastChangedAtGasCost = contract.ReadGasCostPerSlot ) var ( - _ StatefulPrecompileConfig = &FeeConfigManagerConfig{} // Singleton StatefulPrecompiledContract for setting fee configs by permissioned callers. - FeeConfigManagerPrecompile StatefulPrecompiledContract = createFeeConfigManagerPrecompile(FeeConfigManagerAddress) + FeeManagerPrecompile contract.StatefulPrecompiledContract = createFeeManagerPrecompile() - setFeeConfigSignature = CalculateFunctionSelector("setFeeConfig(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)") - getFeeConfigSignature = CalculateFunctionSelector("getFeeConfig()") - getFeeConfigLastChangedAtSignature = CalculateFunctionSelector("getFeeConfigLastChangedAt()") + setFeeConfigSignature = contract.CalculateFunctionSelector("setFeeConfig(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)") + getFeeConfigSignature = contract.CalculateFunctionSelector("getFeeConfig()") + getFeeConfigLastChangedAtSignature = contract.CalculateFunctionSelector("getFeeConfigLastChangedAt()") feeConfigLastChangedAtKey = common.Hash{'l', 'c', 'a'} ErrCannotChangeFee = errors.New("non-enabled cannot change fee config") ) -// 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 +// GetFeeManagerStatus returns the role of [address] for the fee config manager list. +func GetFeeManagerStatus(stateDB contract.StateDB, address common.Address) allowlist.Role { + return allowlist.GetAllowListStatus(stateDB, ContractAddress, address) } -// 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) { - // 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() - panic(fmt.Sprintf("invalid feeConfig provided: %s", 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. - panic(fmt.Sprintf("fee config should have been verified in genesis: %s", err)) - } - } - 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) -} - -// SetFeeConfigManagerStatus sets the permissions of [address] to [role] for the +// SetFeeManagerStatus 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 SetFeeManagerStatus(stateDB contract.StateDB, address common.Address, role allowlist.Role) { + allowlist.SetAllowListRole(stateDB, ContractAddress, address, role) } // PackGetFeeConfigInput packs the getFeeConfig signature @@ -171,16 +76,16 @@ func PackGetLastChangedAtInput() []byte { // PackFeeConfig packs [feeConfig] without the selector into the appropriate arguments for fee config operations. func PackFeeConfig(feeConfig commontype.FeeConfig) ([]byte, error) { // input(feeConfig) - return packFeeConfigHelper(feeConfig, false), nil + return packFeeConfigHelper(feeConfig, false) } // PackSetFeeConfig packs [feeConfig] with the selector into the appropriate arguments for setting fee config operations. func PackSetFeeConfig(feeConfig commontype.FeeConfig) ([]byte, error) { // function selector (4 bytes) + input(feeConfig) - return packFeeConfigHelper(feeConfig, true), nil + return packFeeConfigHelper(feeConfig, true) } -func packFeeConfigHelper(feeConfig commontype.FeeConfig, useSelector bool) []byte { +func packFeeConfigHelper(feeConfig commontype.FeeConfig, useSelector bool) ([]byte, error) { hashes := []common.Hash{ common.BigToHash(feeConfig.GasLimit), common.BigToHash(new(big.Int).SetUint64(feeConfig.TargetBlockRate)), @@ -194,25 +99,25 @@ func packFeeConfigHelper(feeConfig commontype.FeeConfig, useSelector bool) []byt if useSelector { res := make([]byte, len(setFeeConfigSignature)+feeConfigInputLen) - packOrderedHashesWithSelector(res, setFeeConfigSignature, hashes) - return res + err := contract.PackOrderedHashesWithSelector(res, setFeeConfigSignature, hashes) + return res, err } res := make([]byte, len(hashes)*common.HashLength) - packOrderedHashes(res, hashes) - return res + err := contract.PackOrderedHashes(res, hashes) + return res, err } // UnpackFeeConfigInput attempts to unpack [input] into the arguments to the fee config precompile // assumes that [input] does not include selector (omits first 4 bytes in PackSetFeeConfigInput) func UnpackFeeConfigInput(input []byte) (commontype.FeeConfig, error) { if len(input) != feeConfigInputLen { - return commontype.FeeConfig{}, fmt.Errorf("invalid input length for fee config input: %d", len(input)) + return commontype.FeeConfig{}, fmt.Errorf("invalid input length for fee config Input: %d", len(input)) } feeConfig := commontype.FeeConfig{} for i := minFeeConfigFieldKey; i <= numFeeConfigField; i++ { listIndex := i - 1 - packedElement := returnPackedHash(input, listIndex) + packedElement := contract.PackedHash(input, listIndex) switch i { case gasLimitKey: feeConfig.GasLimit = new(big.Int).SetBytes(packedElement) @@ -231,6 +136,7 @@ func UnpackFeeConfigInput(input []byte) (commontype.FeeConfig, error) { case blockGasCostStepKey: feeConfig.BlockGasCostStep = new(big.Int).SetBytes(packedElement) default: + // This should never encounter an unknown fee config key panic(fmt.Sprintf("unknown fee config key: %d", i)) } } @@ -238,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 contract.StateDB) commontype.FeeConfig { feeConfig := commontype.FeeConfig{} for i := minFeeConfigFieldKey; i <= numFeeConfigField; i++ { - val := stateDB.GetState(FeeConfigManagerAddress, common.Hash{byte(i)}) + val := stateDB.GetState(ContractAddress, common.Hash{byte(i)}) switch i { case gasLimitKey: feeConfig.GasLimit = new(big.Int).Set(val.Big()) @@ -260,22 +166,23 @@ func GetStoredFeeConfig(stateDB StateDB) commontype.FeeConfig { case blockGasCostStepKey: feeConfig.BlockGasCostStep = new(big.Int).Set(val.Big()) default: + // This should never encounter an unknown fee config key panic(fmt.Sprintf("unknown fee config key: %d", i)) } } return feeConfig } -func GetFeeConfigLastChangedAt(stateDB StateDB) *big.Int { - val := stateDB.GetState(FeeConfigManagerAddress, feeConfigLastChangedAtKey) +func GetFeeConfigLastChangedAt(stateDB contract.StateDB) *big.Int { + val := stateDB.GetState(ContractAddress, feeConfigLastChangedAtKey) return val.Big() } // StoreFeeConfig stores given [feeConfig] and block number in the [blockContext] to the [stateDB]. // A validation on [feeConfig] is done before storing. -func StoreFeeConfig(stateDB StateDB, feeConfig commontype.FeeConfig, blockContext BlockContext) error { +func StoreFeeConfig(stateDB contract.StateDB, feeConfig commontype.FeeConfig, blockContext contract.BlockContext) error { if err := feeConfig.Verify(); err != nil { - return err + return fmt.Errorf("cannot verify fee config: %w", err) } for i := minFeeConfigFieldKey; i <= numFeeConfigField; i++ { @@ -298,24 +205,24 @@ func StoreFeeConfig(stateDB StateDB, feeConfig commontype.FeeConfig, blockContex case blockGasCostStepKey: input = common.BigToHash(feeConfig.BlockGasCostStep) default: + // 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(ContractAddress, 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(ContractAddress, feeConfigLastChangedAtKey, common.BigToHash(blockNumber)) return nil } // setFeeConfig checks if the caller has permissions to set the fee config. // The execution function parses [input] into FeeConfig structure and sets contract storage accordingly. -func setFeeConfig(accessibleState 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 contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, SetFeeConfigGasCost); err != nil { return nil, 0, err } @@ -329,8 +236,8 @@ 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) + // Verify that the caller is in the allow list and therefore has the right to call this function. + callerStatus := GetFeeManagerStatus(stateDB, caller) if !callerStatus.IsEnabled() { return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotChangeFee, caller) } @@ -345,8 +252,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 contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, GetFeeConfigGasCost); err != nil { return nil, 0, err } @@ -363,8 +270,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 contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, GetLastChangedAtGasCost); err != nil { return nil, 0, err } @@ -374,18 +281,23 @@ func getFeeConfigLastChangedAt(accessibleState PrecompileAccessibleState, caller return common.BigToHash(lastChangedAt).Bytes(), remainingGas, err } -// createFeeConfigManagerPrecompile returns a StatefulPrecompiledContract +// createFeeManagerPrecompile returns a StatefulPrecompiledContract // with getters and setters for the chain's fee config. Access to the getters/setters -// is controlled by an allow list for [precompileAddr]. -func createFeeConfigManagerPrecompile(precompileAddr common.Address) StatefulPrecompiledContract { - feeConfigManagerFunctions := createAllowListFunctions(precompileAddr) +// is controlled by an allow list for ContractAddress. +func createFeeManagerPrecompile() contract.StatefulPrecompiledContract { + feeManagerFunctions := allowlist.CreateAllowListFunctions(ContractAddress) - setFeeConfigFunc := newStatefulPrecompileFunction(setFeeConfigSignature, setFeeConfig) - getFeeConfigFunc := newStatefulPrecompileFunction(getFeeConfigSignature, getFeeConfig) - getFeeConfigLastChangedAtFunc := newStatefulPrecompileFunction(getFeeConfigLastChangedAtSignature, getFeeConfigLastChangedAt) + setFeeConfigFunc := contract.NewStatefulPrecompileFunction(setFeeConfigSignature, setFeeConfig) + getFeeConfigFunc := contract.NewStatefulPrecompileFunction(getFeeConfigSignature, getFeeConfig) + getFeeConfigLastChangedAtFunc := contract.NewStatefulPrecompileFunction(getFeeConfigLastChangedAtSignature, getFeeConfigLastChangedAt) - feeConfigManagerFunctions = append(feeConfigManagerFunctions, setFeeConfigFunc, getFeeConfigFunc, getFeeConfigLastChangedAtFunc) + feeManagerFunctions = append(feeManagerFunctions, setFeeConfigFunc, getFeeConfigFunc, getFeeConfigLastChangedAtFunc) // Construct the contract with no fallback function. - contract := newStatefulPrecompileWithFunctionSelectors(nil, feeConfigManagerFunctions) + contract, err := contract.NewStatefulPrecompileContract(nil, feeManagerFunctions) + // TODO Change this to be returned as an error after refactoring this precompile + // to use the new precompile template. + if err != nil { + panic(err) + } return contract } diff --git a/precompile/contracts/feemanager/contract_test.go b/precompile/contracts/feemanager/contract_test.go new file mode 100644 index 0000000000..4911a2368e --- /dev/null +++ b/precompile/contracts/feemanager/contract_test.go @@ -0,0 +1,225 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package feemanager + +import ( + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/commontype" + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/testutils" + "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), +} + +func TestFeeManager(t *testing.T) { + testBlockNumber := big.NewInt(7) + tests := map[string]testutils.PrecompileTest{ + "set config from no role fails": { + Caller: allowlist.TestNoRoleAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackSetFeeConfig(testFeeConfig) + require.NoError(t, err) + + return input + }, + SuppliedGas: SetFeeConfigGasCost, + ReadOnly: false, + ExpectedErr: ErrCannotChangeFee.Error(), + }, + "set config from enabled address": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackSetFeeConfig(testFeeConfig) + require.NoError(t, err) + + return input + }, + SuppliedGas: SetFeeConfigGasCost, + ReadOnly: false, + ExpectedRes: []byte{}, + AfterHook: func(t *testing.T, state contract.StateDB) { + feeConfig := GetStoredFeeConfig(state) + require.Equal(t, testFeeConfig, feeConfig) + }, + }, + "set invalid config from enabled address": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []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, + Config: &Config{ + InitialFeeConfig: &testFeeConfig, + }, + ExpectedErr: "cannot be greater than maxBlockGasCost", + AfterHook: func(t *testing.T, state contract.StateDB) { + feeConfig := GetStoredFeeConfig(state) + require.Equal(t, testFeeConfig, feeConfig) + }, + }, + "set config from admin address": { + Caller: allowlist.TestAdminAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackSetFeeConfig(testFeeConfig) + require.NoError(t, err) + + return input + }, + SuppliedGas: SetFeeConfigGasCost, + ReadOnly: false, + ExpectedRes: []byte{}, + BlockNumber: testBlockNumber.Int64(), + AfterHook: func(t *testing.T, state contract.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: allowlist.TestNoRoleAddr, + BeforeHook: func(t *testing.T, state contract.StateDB) { + allowlist.SetDefaultRoles(Module.Address)(t, state) + err := StoreFeeConfig(state, testFeeConfig, contract.NewMockBlockContext(big.NewInt(6), 0)) + require.NoError(t, err) + }, + Input: PackGetFeeConfigInput(), + SuppliedGas: GetFeeConfigGasCost, + ReadOnly: true, + ExpectedRes: func() []byte { + res, err := PackFeeConfig(testFeeConfig) + require.NoError(t, err) + return res + }(), + AfterHook: func(t *testing.T, state contract.StateDB) { + feeConfig := GetStoredFeeConfig(state) + lastChangedAt := GetFeeConfigLastChangedAt(state) + require.Equal(t, testFeeConfig, feeConfig) + require.EqualValues(t, big.NewInt(6), lastChangedAt) + }, + }, + "get initial fee config": { + Caller: allowlist.TestNoRoleAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + Input: PackGetFeeConfigInput(), + SuppliedGas: GetFeeConfigGasCost, + Config: &Config{ + InitialFeeConfig: &testFeeConfig, + }, + ReadOnly: true, + ExpectedRes: func() []byte { + res, err := PackFeeConfig(testFeeConfig) + require.NoError(t, err) + return res + }(), + BlockNumber: testBlockNumber.Int64(), + AfterHook: func(t *testing.T, state contract.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: allowlist.TestNoRoleAddr, + BeforeHook: func(t *testing.T, state contract.StateDB) { + allowlist.SetDefaultRoles(Module.Address)(t, state) + err := StoreFeeConfig(state, testFeeConfig, contract.NewMockBlockContext(testBlockNumber, 0)) + require.NoError(t, err) + }, + Input: PackGetLastChangedAtInput(), + SuppliedGas: GetLastChangedAtGasCost, + ReadOnly: true, + ExpectedRes: common.BigToHash(testBlockNumber).Bytes(), + AfterHook: func(t *testing.T, state contract.StateDB) { + feeConfig := GetStoredFeeConfig(state) + lastChangedAt := GetFeeConfigLastChangedAt(state) + require.Equal(t, testFeeConfig, feeConfig) + require.Equal(t, testBlockNumber, lastChangedAt) + }, + }, + "readOnly setFeeConfig with noRole fails": { + Caller: allowlist.TestNoRoleAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []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: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []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: allowlist.TestAdminAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []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: allowlist.TestAdminAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackSetFeeConfig(testFeeConfig) + require.NoError(t, err) + + return input + }, + SuppliedGas: SetFeeConfigGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + } + + allowlist.RunPrecompileWithAllowListTests(t, Module, state.NewTestStateDB, tests) +} diff --git a/precompile/contracts/feemanager/module.go b/precompile/contracts/feemanager/module.go new file mode 100644 index 0000000000..ce96933bf1 --- /dev/null +++ b/precompile/contracts/feemanager/module.go @@ -0,0 +1,61 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package feemanager + +import ( + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ethereum/go-ethereum/common" +) + +var _ contract.Configurator = &configurator{} + +// ConfigKey is the key used in json config files to specify this precompile config. +// must be unique across all precompiles. +const ConfigKey = "feeManagerConfig" + +var ContractAddress = common.HexToAddress("0x0200000000000000000000000000000000000003") + +var Module = modules.Module{ + ConfigKey: ConfigKey, + Address: ContractAddress, + Contract: FeeManagerPrecompile, + Configurator: &configurator{}, +} + +type configurator struct{} + +func init() { + if err := modules.RegisterModule(Module); err != nil { + panic(err) + } +} + +func (*configurator) MakeConfig() precompileconfig.Config { + return new(Config) +} + +// Configure configures [state] with the desired admins based on [configIface]. +func (*configurator) Configure(chainConfig contract.ChainConfig, cfg precompileconfig.Config, state contract.StateDB, blockContext contract.BlockContext) error { + config, ok := cfg.(*Config) + if !ok { + return fmt.Errorf("incorrect config %T: %v", config, config) + } + // Store the initial fee config into the state when the fee manager activates. + if config.InitialFeeConfig != nil { + if err := StoreFeeConfig(state, *config.InitialFeeConfig, blockContext); err != nil { + // This should not happen since we already checked this config with Verify() + return fmt.Errorf("cannot configure given initial fee config: %w", err) + } + } else { + if err := StoreFeeConfig(state, chainConfig.GetFeeConfig(), blockContext); err != nil { + // This should not happen since we already checked the chain config in the genesis creation. + return fmt.Errorf("cannot configure fee config in chain config: %w", err) + } + } + return config.AllowListConfig.Configure(state, ContractAddress) +} diff --git a/precompile/contracts/nativeminter/config.go b/precompile/contracts/nativeminter/config.go new file mode 100644 index 0000000000..e2df6bbc82 --- /dev/null +++ b/precompile/contracts/nativeminter/config.go @@ -0,0 +1,95 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package nativeminter + +import ( + "fmt" + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ava-labs/subnet-evm/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" +) + +var _ precompileconfig.Config = &Config{} + +// Config implements the StatefulPrecompileConfig interface while adding in the +// ContractNativeMinter specific precompile config. +type Config struct { + allowlist.AllowListConfig + precompileconfig.Upgrade + InitialMint map[common.Address]*math.HexOrDecimal256 `json:"initialMint,omitempty"` // addresses to receive the initial mint mapped to the amount to mint +} + +// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables +// ContractNativeMinter with the given [admins] and [enableds] as members of the allowlist. Also mints balances according to [initialMint] when the upgrade activates. +func NewConfig(blockTimestamp *big.Int, admins []common.Address, enableds []common.Address, initialMint map[common.Address]*math.HexOrDecimal256) *Config { + return &Config{ + AllowListConfig: allowlist.AllowListConfig{ + AdminAddresses: admins, + EnabledAddresses: enableds, + }, + Upgrade: precompileconfig.Upgrade{BlockTimestamp: blockTimestamp}, + InitialMint: initialMint, + } +} + +// NewDisableConfig returns config for a network upgrade at [blockTimestamp] +// that disables ContractNativeMinter. +func NewDisableConfig(blockTimestamp *big.Int) *Config { + return &Config{ + Upgrade: precompileconfig.Upgrade{ + BlockTimestamp: blockTimestamp, + Disable: true, + }, + } +} +func (*Config) Key() string { return ConfigKey } + +// Equal returns true if [cfg] is a [*ContractNativeMinterConfig] and it has been configured identical to [c]. +func (c *Config) Equal(cfg precompileconfig.Config) bool { + // typecast before comparison + other, ok := (cfg).(*Config) + if !ok { + return false + } + eq := c.Upgrade.Equal(&other.Upgrade) && 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 +} + +func (c *Config) Verify() error { + // ensure that all of the initial mint values in the map are non-nil positive values + for addr, amount := range c.InitialMint { + if amount == nil { + return fmt.Errorf("initial mint cannot contain nil amount for address %s", addr) + } + bigIntAmount := (*big.Int)(amount) + if bigIntAmount.Sign() < 1 { + return fmt.Errorf("initial mint cannot contain invalid amount %v for address %s", bigIntAmount, addr) + } + } + return c.AllowListConfig.Verify() +} diff --git a/precompile/contracts/nativeminter/config_test.go b/precompile/contracts/nativeminter/config_test.go new file mode 100644 index 0000000000..1927a93b9b --- /dev/null +++ b/precompile/contracts/nativeminter/config_test.go @@ -0,0 +1,149 @@ +// (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/precompileconfig" + "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 precompileconfig.Config + ExpectedError string + }{ + { + name: "invalid allow list config in native minter allowlist", + config: NewConfig(big.NewInt(3), admins, admins, nil), + ExpectedError: "cannot set address", + }, + { + name: "duplicate admins in config in native minter allowlist", + config: NewConfig(big.NewInt(3), append(admins, admins[0]), enableds, nil), + ExpectedError: "duplicate address", + }, + { + name: "duplicate enableds in config in native minter allowlist", + config: NewConfig(big.NewInt(3), admins, append(enableds, enableds[0]), nil), + ExpectedError: "duplicate address", + }, + { + name: "nil amount in native minter config", + config: NewConfig(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: NewConfig(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 precompileconfig.Config + other precompileconfig.Config + expected bool + }{ + { + name: "non-nil config and nil other", + config: NewConfig(big.NewInt(3), admins, enableds, nil), + other: nil, + expected: false, + }, + { + name: "different type", + config: NewConfig(big.NewInt(3), admins, enableds, nil), + other: precompileconfig.NewNoopStatefulPrecompileConfig(), + expected: false, + }, + { + name: "different timestamps", + config: NewConfig(big.NewInt(3), admins, nil, nil), + other: NewConfig(big.NewInt(4), admins, nil, nil), + expected: false, + }, + { + name: "different enabled", + config: NewConfig(big.NewInt(3), admins, nil, nil), + other: NewConfig(big.NewInt(3), admins, enableds, nil), + expected: false, + }, + { + name: "different initial mint amounts", + config: NewConfig(big.NewInt(3), admins, nil, + map[common.Address]*math.HexOrDecimal256{ + common.HexToAddress("0x01"): math.NewHexOrDecimal256(1), + }), + other: NewConfig(big.NewInt(3), admins, nil, + map[common.Address]*math.HexOrDecimal256{ + common.HexToAddress("0x01"): math.NewHexOrDecimal256(2), + }), + expected: false, + }, + { + name: "different initial mint addresses", + config: NewConfig(big.NewInt(3), admins, nil, + map[common.Address]*math.HexOrDecimal256{ + common.HexToAddress("0x01"): math.NewHexOrDecimal256(1), + }), + other: NewConfig(big.NewInt(3), admins, nil, + map[common.Address]*math.HexOrDecimal256{ + common.HexToAddress("0x02"): math.NewHexOrDecimal256(1), + }), + expected: false, + }, + { + name: "same config", + config: NewConfig(big.NewInt(3), admins, nil, + map[common.Address]*math.HexOrDecimal256{ + common.HexToAddress("0x01"): math.NewHexOrDecimal256(1), + }), + other: NewConfig(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/contracts/nativeminter/contract.go b/precompile/contracts/nativeminter/contract.go new file mode 100644 index 0000000000..63d6a624da --- /dev/null +++ b/precompile/contracts/nativeminter/contract.go @@ -0,0 +1,118 @@ +// (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/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" + "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 contract.StatefulPrecompiledContract = createNativeMinterPrecompile() + + mintSignature = contract.CalculateFunctionSelector("mintNativeCoin(address,uint256)") // address, amount + ErrCannotMint = errors.New("non-enabled cannot mint") +) + +// GetContractNativeMinterStatus returns the role of [address] for the minter list. +func GetContractNativeMinterStatus(stateDB contract.StateDB, address common.Address) allowlist.Role { + return allowlist.GetAllowListStatus(stateDB, ContractAddress, address) +} + +// SetContractNativeMinterStatus sets the permissions of [address] to [role] for the +// minter list. assumes [role] has already been verified as valid. +func SetContractNativeMinterStatus(stateDB contract.StateDB, address common.Address, role allowlist.Role) { + allowlist.SetAllowListRole(stateDB, ContractAddress, address, role) +} + +// PackMintInput packs [address] and [amount] into the appropriate arguments for minting operation. +// Assumes that [amount] can be represented by 32 bytes. +func PackMintInput(address common.Address, amount *big.Int) ([]byte, error) { + // function selector (4 bytes) + input(hash for address + hash for amount) + res := make([]byte, contract.SelectorLen+mintInputLen) + err := contract.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(contract.PackedHash(input, mintInputAddressSlot)) + assetAmount := new(big.Int).SetBytes(contract.PackedHash(input, mintInputAmountSlot)) + return to, assetAmount, nil +} + +// mintNativeCoin checks if the caller is permissioned for minting operation. +// The execution function parses the [input] into native coin amount and receiver address. +func mintNativeCoin(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, MintGasCost); err != nil { + return nil, 0, err + } + + 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 call this function. + callerStatus := allowlist.GetAllowListStatus(stateDB, ContractAddress, 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 for native coin minting. The precompile +// is accessed controlled by an allow list at [precompileAddr]. +func createNativeMinterPrecompile() contract.StatefulPrecompiledContract { + enabledFuncs := allowlist.CreateAllowListFunctions(ContractAddress) + + mintFunc := contract.NewStatefulPrecompileFunction(mintSignature, mintNativeCoin) + + enabledFuncs = append(enabledFuncs, mintFunc) + // Construct the contract with no fallback function. + contract, err := contract.NewStatefulPrecompileContract(nil, enabledFuncs) + // TODO: Change this to be returned as an error after refactoring this precompile + // to use the new precompile template. + if err != nil { + panic(err) + } + return contract +} diff --git a/precompile/contracts/nativeminter/contract_test.go b/precompile/contracts/nativeminter/contract_test.go new file mode 100644 index 0000000000..a987dc9998 --- /dev/null +++ b/precompile/contracts/nativeminter/contract_test.go @@ -0,0 +1,149 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package nativeminter + +import ( + "testing" + + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/testutils" + "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) { + tests := map[string]testutils.PrecompileTest{ + "mint funds from no role fails": { + Caller: allowlist.TestNoRoleAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackMintInput(allowlist.TestNoRoleAddr, common.Big1) + require.NoError(t, err) + + return input + }, + SuppliedGas: MintGasCost, + ReadOnly: false, + ExpectedErr: ErrCannotMint.Error(), + }, + "mint funds from enabled address": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackMintInput(allowlist.TestEnabledAddr, common.Big1) + require.NoError(t, err) + + return input + }, + SuppliedGas: MintGasCost, + ReadOnly: false, + ExpectedRes: []byte{}, + AfterHook: func(t *testing.T, state contract.StateDB) { + require.Equal(t, common.Big1, state.GetBalance(allowlist.TestEnabledAddr), "expected minted funds") + }, + }, + "initial mint funds": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + Config: &Config{ + InitialMint: map[common.Address]*math.HexOrDecimal256{ + allowlist.TestEnabledAddr: math.NewHexOrDecimal256(2), + }, + }, + AfterHook: func(t *testing.T, state contract.StateDB) { + require.Equal(t, common.Big2, state.GetBalance(allowlist.TestEnabledAddr), "expected minted funds") + }, + }, + "mint funds from admin address": { + Caller: allowlist.TestAdminAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackMintInput(allowlist.TestAdminAddr, common.Big1) + require.NoError(t, err) + + return input + }, + SuppliedGas: MintGasCost, + ReadOnly: false, + ExpectedRes: []byte{}, + AfterHook: func(t *testing.T, state contract.StateDB) { + require.Equal(t, common.Big1, state.GetBalance(allowlist.TestAdminAddr), "expected minted funds") + }, + }, + "mint max big funds": { + Caller: allowlist.TestAdminAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackMintInput(allowlist.TestAdminAddr, math.MaxBig256) + require.NoError(t, err) + + return input + }, + SuppliedGas: MintGasCost, + ReadOnly: false, + ExpectedRes: []byte{}, + AfterHook: func(t *testing.T, state contract.StateDB) { + require.Equal(t, math.MaxBig256, state.GetBalance(allowlist.TestAdminAddr), "expected minted funds") + }, + }, + "readOnly mint with noRole fails": { + Caller: allowlist.TestNoRoleAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackMintInput(allowlist.TestAdminAddr, common.Big1) + require.NoError(t, err) + + return input + }, + SuppliedGas: MintGasCost, + ReadOnly: true, + ExpectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "readOnly mint with allow role fails": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackMintInput(allowlist.TestEnabledAddr, common.Big1) + require.NoError(t, err) + + return input + }, + SuppliedGas: MintGasCost, + ReadOnly: true, + ExpectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "readOnly mint with admin role fails": { + Caller: allowlist.TestAdminAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackMintInput(allowlist.TestAdminAddr, common.Big1) + require.NoError(t, err) + + return input + }, + SuppliedGas: MintGasCost, + ReadOnly: true, + ExpectedErr: vmerrs.ErrWriteProtection.Error(), + }, + "insufficient gas mint from admin": { + Caller: allowlist.TestAdminAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackMintInput(allowlist.TestEnabledAddr, common.Big1) + require.NoError(t, err) + + return input + }, + SuppliedGas: MintGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + } + + allowlist.RunPrecompileWithAllowListTests(t, Module, state.NewTestStateDB, tests) +} diff --git a/precompile/contracts/nativeminter/module.go b/precompile/contracts/nativeminter/module.go new file mode 100644 index 0000000000..6ebd23e63e --- /dev/null +++ b/precompile/contracts/nativeminter/module.go @@ -0,0 +1,57 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package nativeminter + +import ( + "fmt" + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ethereum/go-ethereum/common" +) + +var _ contract.Configurator = &configurator{} + +// ConfigKey is the key used in json config files to specify this precompile config. +// must be unique across all precompiles. +const ConfigKey = "contractNativeMinterConfig" + +var ContractAddress = common.HexToAddress("0x0200000000000000000000000000000000000001") + +var Module = modules.Module{ + ConfigKey: ConfigKey, + Address: ContractAddress, + Contract: ContractNativeMinterPrecompile, + Configurator: &configurator{}, +} + +type configurator struct{} + +func init() { + if err := modules.RegisterModule(Module); err != nil { + panic(err) + } +} + +func (*configurator) MakeConfig() precompileconfig.Config { + return new(Config) +} + +// Configure configures [state] with the desired admins based on [cfg]. +func (*configurator) Configure(_ contract.ChainConfig, cfg precompileconfig.Config, state contract.StateDB, _ contract.BlockContext) error { + config, ok := cfg.(*Config) + if !ok { + return fmt.Errorf("incorrect config %T: %v", config, config) + } + for to, amount := range config.InitialMint { + if amount != nil { + bigIntAmount := (*big.Int)(amount) + state.AddBalance(to, bigIntAmount) + } + } + + return config.AllowListConfig.Configure(state, ContractAddress) +} diff --git a/precompile/contracts/rewardmanager/config.go b/precompile/contracts/rewardmanager/config.go new file mode 100644 index 0000000000..9b7a8bb238 --- /dev/null +++ b/precompile/contracts/rewardmanager/config.go @@ -0,0 +1,118 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Code generated +// This file is a generated precompile contract with stubbed abstract functions. + +package rewardmanager + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ethereum/go-ethereum/common" +) + +var _ precompileconfig.Config = &Config{} + +type InitialRewardConfig struct { + AllowFeeRecipients bool `json:"allowFeeRecipients"` + RewardAddress common.Address `json:"rewardAddress,omitempty"` +} + +func (i *InitialRewardConfig) Equal(other *InitialRewardConfig) bool { + if other == nil { + return false + } + + return i.AllowFeeRecipients == other.AllowFeeRecipients && i.RewardAddress == other.RewardAddress +} + +func (i *InitialRewardConfig) Verify() error { + switch { + case i.AllowFeeRecipients && i.RewardAddress != (common.Address{}): + return ErrCannotEnableBothRewards + default: + return nil + } +} + +func (i *InitialRewardConfig) Configure(state contract.StateDB) error { + // enable allow fee recipients + if i.AllowFeeRecipients { + EnableAllowFeeRecipients(state) + } else if i.RewardAddress == (common.Address{}) { + // if reward address is empty and allow fee recipients is false + // then disable rewards + DisableFeeRewards(state) + } else { + // set reward address + return StoreRewardAddress(state, i.RewardAddress) + } + return nil +} + +// Config implements the StatefulPrecompileConfig interface while adding in the +// RewardManager specific precompile config. +type Config struct { + allowlist.AllowListConfig + precompileconfig.Upgrade + InitialRewardConfig *InitialRewardConfig `json:"initialRewardConfig,omitempty"` +} + +// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables +// RewardManager with the given [admins] and [enableds] as members of the allowlist with [initialConfig] as initial rewards config if specified. +func NewConfig(blockTimestamp *big.Int, admins []common.Address, enableds []common.Address, initialConfig *InitialRewardConfig) *Config { + return &Config{ + AllowListConfig: allowlist.AllowListConfig{ + AdminAddresses: admins, + EnabledAddresses: enableds, + }, + Upgrade: precompileconfig.Upgrade{BlockTimestamp: blockTimestamp}, + InitialRewardConfig: initialConfig, + } +} + +// NewDisableConfig returns config for a network upgrade at [blockTimestamp] +// that disables RewardManager. +func NewDisableConfig(blockTimestamp *big.Int) *Config { + return &Config{ + Upgrade: precompileconfig.Upgrade{ + BlockTimestamp: blockTimestamp, + Disable: true, + }, + } +} + +func (*Config) Key() string { return ConfigKey } + +func (c *Config) Verify() error { + if c.InitialRewardConfig != nil { + if err := c.InitialRewardConfig.Verify(); err != nil { + return err + } + } + return c.AllowListConfig.Verify() +} + +// Equal returns true if [cfg] is a [*RewardManagerConfig] and it has been configured identical to [c]. +func (c *Config) Equal(cfg precompileconfig.Config) bool { + // typecast before comparison + other, ok := (cfg).(*Config) + if !ok { + return false + } + + if c.InitialRewardConfig != nil { + if other.InitialRewardConfig == nil { + return false + } + if !c.InitialRewardConfig.Equal(other.InitialRewardConfig) { + return false + } + } + + return c.Upgrade.Equal(&other.Upgrade) && c.AllowListConfig.Equal(&other.AllowListConfig) +} diff --git a/precompile/contracts/rewardmanager/config_test.go b/precompile/contracts/rewardmanager/config_test.go new file mode 100644 index 0000000000..c78dec4c6f --- /dev/null +++ b/precompile/contracts/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/precompileconfig" + "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 precompileconfig.Config + ExpectedError string + }{ + { + name: "duplicate enableds in config in reward manager allowlist", + config: NewConfig(big.NewInt(3), admins, append(enableds, enableds[0]), nil), + ExpectedError: "duplicate address", + }, + { + name: "both reward mechanisms should not be activated at the same time in reward manager", + config: NewConfig(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 precompileconfig.Config + other precompileconfig.Config + expected bool + }{ + { + name: "non-nil config and nil other", + config: NewConfig(big.NewInt(3), admins, enableds, nil), + other: nil, + expected: false, + }, + { + name: "different type", + config: NewConfig(big.NewInt(3), admins, enableds, nil), + other: precompileconfig.NewNoopStatefulPrecompileConfig(), + expected: false, + }, + { + name: "different timestamp", + config: NewConfig(big.NewInt(3), admins, nil, nil), + other: NewConfig(big.NewInt(4), admins, nil, nil), + expected: false, + }, + { + name: "different enabled", + config: NewConfig(big.NewInt(3), admins, nil, nil), + other: NewConfig(big.NewInt(3), admins, enableds, nil), + expected: false, + }, + { + name: "non-nil initial config and nil initial config", + config: NewConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ + AllowFeeRecipients: true, + }), + other: NewConfig(big.NewInt(3), admins, nil, nil), + expected: false, + }, + { + name: "different initial config", + config: NewConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ + RewardAddress: common.HexToAddress("0x01"), + }), + other: NewConfig(big.NewInt(3), admins, nil, + &InitialRewardConfig{ + RewardAddress: common.HexToAddress("0x02"), + }), + expected: false, + }, + { + name: "same config", + config: NewConfig(big.NewInt(3), admins, nil, &InitialRewardConfig{ + RewardAddress: common.HexToAddress("0x01"), + }), + other: NewConfig(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/contracts/rewardmanager/contract.abi b/precompile/contracts/rewardmanager/contract.abi new file mode 100644 index 0000000000..d21d5bdc6b --- /dev/null +++ b/precompile/contracts/rewardmanager/contract.abi @@ -0,0 +1 @@ +[{"inputs":[],"name":"allowFeeRecipients","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"areFeeRecipientsAllowed","outputs":[{"internalType":"bool","name":"isAllowed","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"currentRewardAddress","outputs":[{"internalType":"address","name":"rewardAddress","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"disableRewards","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"readAllowList","outputs":[{"internalType":"uint256","name":"role","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"setAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"setEnabled","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"setNone","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"setRewardAddress","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/precompile/contracts/rewardmanager/contract.go b/precompile/contracts/rewardmanager/contract.go new file mode 100644 index 0000000000..8d28e0815d --- /dev/null +++ b/precompile/contracts/rewardmanager/contract.go @@ -0,0 +1,299 @@ +// (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 ( + _ "embed" + "errors" + "fmt" + + "github.com/ava-labs/subnet-evm/accounts/abi" + "github.com/ava-labs/subnet-evm/constants" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/vmerrs" + + "github.com/ethereum/go-ethereum/common" +) + +const ( + AllowFeeRecipientsGasCost uint64 = (contract.WriteGasCostPerSlot) + allowlist.ReadAllowListGasCost // write 1 slot + read allow list + AreFeeRecipientsAllowedGasCost uint64 = allowlist.ReadAllowListGasCost + CurrentRewardAddressGasCost uint64 = allowlist.ReadAllowListGasCost + DisableRewardsGasCost uint64 = (contract.WriteGasCostPerSlot) + allowlist.ReadAllowListGasCost // write 1 slot + read allow list + SetRewardAddressGasCost uint64 = (contract.WriteGasCostPerSlot) + allowlist.ReadAllowListGasCost // write 1 slot + read allow list +) + +// Singleton StatefulPrecompiledContract and signatures. +var ( + ErrCannotAllowFeeRecipients = errors.New("non-enabled cannot call allowFeeRecipients") + ErrCannotAreFeeRecipientsAllowed = errors.New("non-enabled cannot call areFeeRecipientsAllowed") + ErrCannotCurrentRewardAddress = errors.New("non-enabled cannot call currentRewardAddress") + ErrCannotDisableRewards = errors.New("non-enabled cannot call disableRewards") + ErrCannotSetRewardAddress = errors.New("non-enabled cannot call setRewardAddress") + + ErrCannotEnableBothRewards = errors.New("cannot enable both fee recipients and reward address at the same time") + ErrEmptyRewardAddress = errors.New("reward address cannot be empty") + + // RewardManagerRawABI contains the raw ABI of RewardManager contract. + //go:embed contract.abi + RewardManagerRawABI string + + RewardManagerABI = contract.ParseABI(RewardManagerRawABI) + RewardManagerPrecompile = createRewardManagerPrecompile() // will be initialized by init function + + rewardAddressStorageKey = common.Hash{'r', 'a', 's', 'k'} + allowFeeRecipientsAddressValue = common.Hash{'a', 'f', 'r', 'a', 'v'} +) + +// GetRewardManagerAllowListStatus returns the role of [address] for the RewardManager list. +func GetRewardManagerAllowListStatus(stateDB contract.StateDB, address common.Address) allowlist.Role { + return allowlist.GetAllowListStatus(stateDB, ContractAddress, address) +} + +// SetRewardManagerAllowListStatus sets the permissions of [address] to [role] for the +// RewardManager list. Assumes [role] has already been verified as valid. +func SetRewardManagerAllowListStatus(stateDB contract.StateDB, address common.Address, role allowlist.Role) { + allowlist.SetAllowListRole(stateDB, ContractAddress, address, role) +} + +// PackAllowFeeRecipients packs the function selector (first 4 func signature bytes). +// This function is mostly used for tests. +func PackAllowFeeRecipients() ([]byte, error) { + return RewardManagerABI.Pack("allowFeeRecipients") +} + +// EnableAllowFeeRecipients enables fee recipients. +func EnableAllowFeeRecipients(stateDB contract.StateDB) { + stateDB.SetState(ContractAddress, rewardAddressStorageKey, allowFeeRecipientsAddressValue) +} + +// DisableRewardAddress disables rewards and burns them by sending to Blackhole Address. +func DisableFeeRewards(stateDB contract.StateDB) { + stateDB.SetState(ContractAddress, rewardAddressStorageKey, constants.BlackholeAddr.Hash()) +} + +func allowFeeRecipients(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, AllowFeeRecipientsGasCost); err != nil { + return nil, 0, err + } + if readOnly { + return nil, remainingGas, vmerrs.ErrWriteProtection + } + // no input provided for this function + + // Allow list is enabled and AllowFeeRecipients is a state-changer function. + // This part of the code restricts the function to be called only by enabled/admin addresses in the allow list. + // You can modify/delete this code if you don't want this function to be restricted by the allow list. + stateDB := accessibleState.GetStateDB() + // Verify that the caller is in the allow list and therefore has the right to call this function. + callerStatus := allowlist.GetAllowListStatus(stateDB, ContractAddress, caller) + if !callerStatus.IsEnabled() { + return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotAllowFeeRecipients, caller) + } + // allow list code ends here. + + // this function does not return an output, leave this one as is + EnableAllowFeeRecipients(stateDB) + packedOutput := []byte{} + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// PackAreFeeRecipientsAllowed packs the include selector (first 4 func signature bytes). +// This function is mostly used for tests. +func PackAreFeeRecipientsAllowed() ([]byte, error) { + return RewardManagerABI.Pack("areFeeRecipientsAllowed") +} + +// PackAreFeeRecipientsAllowedOutput attempts to pack given isAllowed of type bool +// to conform the ABI outputs. +func PackAreFeeRecipientsAllowedOutput(isAllowed bool) ([]byte, error) { + return RewardManagerABI.PackOutput("areFeeRecipientsAllowed", isAllowed) +} + +func areFeeRecipientsAllowed(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, AreFeeRecipientsAllowedGasCost); err != nil { + return nil, 0, err + } + // no input provided for this function + + stateDB := accessibleState.GetStateDB() + var output bool + _, output = GetStoredRewardAddress(stateDB) + + packedOutput, err := PackAreFeeRecipientsAllowedOutput(output) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// PackCurrentRewardAddress packs the include selector (first 4 func signature bytes). +// This function is mostly used for tests. +func PackCurrentRewardAddress() ([]byte, error) { + return RewardManagerABI.Pack("currentRewardAddress") +} + +// PackCurrentRewardAddressOutput attempts to pack given rewardAddress of type common.Address +// to conform the ABI outputs. +func PackCurrentRewardAddressOutput(rewardAddress common.Address) ([]byte, error) { + return RewardManagerABI.PackOutput("currentRewardAddress", rewardAddress) +} + +// 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 contract.StateDB) (common.Address, bool) { + val := stateDB.GetState(ContractAddress, rewardAddressStorageKey) + return common.BytesToAddress(val.Bytes()), val == allowFeeRecipientsAddressValue +} + +// StoredRewardAddress stores the given [val] under rewardAddressStorageKey. +func StoreRewardAddress(stateDB contract.StateDB, val common.Address) error { + // if input is empty, return an error + if val == (common.Address{}) { + return ErrEmptyRewardAddress + } + stateDB.SetState(ContractAddress, rewardAddressStorageKey, val.Hash()) + return nil +} + +// PackSetRewardAddress packs [addr] of type common.Address into the appropriate arguments for setRewardAddress. +// the packed bytes include selector (first 4 func signature bytes). +// This function is mostly used for tests. +func PackSetRewardAddress(addr common.Address) ([]byte, error) { + return RewardManagerABI.Pack("setRewardAddress", addr) +} + +// UnpackSetRewardAddressInput attempts to unpack [input] into the common.Address type argument +// assumes that [input] does not include selector (omits first 4 func signature bytes) +func UnpackSetRewardAddressInput(input []byte) (common.Address, error) { + res, err := RewardManagerABI.UnpackInput("setRewardAddress", input) + if err != nil { + return common.Address{}, err + } + unpacked := *abi.ConvertType(res[0], new(common.Address)).(*common.Address) + return unpacked, nil +} + +func setRewardAddress(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, SetRewardAddressGasCost); err != nil { + return nil, 0, err + } + if readOnly { + return nil, remainingGas, vmerrs.ErrWriteProtection + } + // attempts to unpack [input] into the arguments to the SetRewardAddressInput. + // Assumes that [input] does not include selector + // You can use unpacked [inputStruct] variable in your code + inputStruct, err := UnpackSetRewardAddressInput(input) + if err != nil { + return nil, remainingGas, err + } + + // Allow list is enabled and SetRewardAddress is a state-changer function. + // This part of the code restricts the function to be called only by enabled/admin addresses in the allow list. + // You can modify/delete this code if you don't want this function to be restricted by the allow list. + stateDB := accessibleState.GetStateDB() + // Verify that the caller is in the allow list and therefore has the right to call this function. + callerStatus := allowlist.GetAllowListStatus(stateDB, ContractAddress, caller) + if !callerStatus.IsEnabled() { + return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotSetRewardAddress, caller) + } + // allow list code ends here. + + if err := StoreRewardAddress(stateDB, inputStruct); err != nil { + return nil, remainingGas, err + } + // this function does not return an output, leave this one as is + packedOutput := []byte{} + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +func currentRewardAddress(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, CurrentRewardAddressGasCost); err != nil { + return nil, 0, err + } + + // no input provided for this function + stateDB := accessibleState.GetStateDB() + output, _ := GetStoredRewardAddress(stateDB) + packedOutput, err := PackCurrentRewardAddressOutput(output) + if err != nil { + return nil, remainingGas, err + } + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// PackDisableRewards packs the include selector (first 4 func signature bytes). +// This function is mostly used for tests. +func PackDisableRewards() ([]byte, error) { + return RewardManagerABI.Pack("disableRewards") +} + +func disableRewards(accessibleState contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { + if remainingGas, err = contract.DeductGas(suppliedGas, DisableRewardsGasCost); err != nil { + return nil, 0, err + } + if readOnly { + return nil, remainingGas, vmerrs.ErrWriteProtection + } + // no input provided for this function + + // Allow list is enabled and DisableRewards is a state-changer function. + // This part of the code restricts the function to be called only by enabled/admin addresses in the allow list. + // You can modify/delete this code if you don't want this function to be restricted by the allow list. + stateDB := accessibleState.GetStateDB() + // Verify that the caller is in the allow list and therefore has the right to call this function. + callerStatus := allowlist.GetAllowListStatus(stateDB, ContractAddress, caller) + if !callerStatus.IsEnabled() { + return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotDisableRewards, caller) + } + // allow list code ends here. + DisableFeeRewards(stateDB) + // this function does not return an output, leave this one as is + packedOutput := []byte{} + + // Return the packed output and the remaining gas + return packedOutput, remainingGas, nil +} + +// 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() contract.StatefulPrecompiledContract { + var functions []*contract.StatefulPrecompileFunction + functions = append(functions, allowlist.CreateAllowListFunctions(ContractAddress)...) + abiFunctionMap := map[string]contract.RunStatefulPrecompileFunc{ + "allowFeeRecipients": allowFeeRecipients, + "areFeeRecipientsAllowed": areFeeRecipientsAllowed, + "currentRewardAddress": currentRewardAddress, + "disableRewards": disableRewards, + "setRewardAddress": setRewardAddress, + } + + for name, function := range abiFunctionMap { + method, ok := RewardManagerABI.Methods[name] + if !ok { + panic(fmt.Errorf("given method (%s) does not exist in the ABI", name)) + } + functions = append(functions, contract.NewStatefulPrecompileFunction(method.ID, function)) + } + + // Construct the contract with no fallback function. + statefulContract, err := contract.NewStatefulPrecompileContract(nil, functions) + if err != nil { + panic(err) + } + return statefulContract +} diff --git a/precompile/contracts/rewardmanager/contract_test.go b/precompile/contracts/rewardmanager/contract_test.go new file mode 100644 index 0000000000..db1d61d3ae --- /dev/null +++ b/precompile/contracts/rewardmanager/contract_test.go @@ -0,0 +1,277 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package rewardmanager + +import ( + "testing" + + "github.com/ava-labs/subnet-evm/constants" + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/testutils" + "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestRewardManagerRun(t *testing.T) { + testAddr := common.HexToAddress("0x0123") + + tests := map[string]testutils.PrecompileTest{ + "set allow fee recipients from no role fails": { + Caller: allowlist.TestNoRoleAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []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: allowlist.TestNoRoleAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []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: allowlist.TestNoRoleAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []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: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackAllowFeeRecipients() + require.NoError(t, err) + + return input + }, + SuppliedGas: AllowFeeRecipientsGasCost, + ReadOnly: false, + ExpectedRes: []byte{}, + AfterHook: func(t *testing.T, state contract.StateDB) { + _, isFeeRecipients := GetStoredRewardAddress(state) + require.True(t, isFeeRecipients) + }, + }, + "set reward address from enabled succeeds": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackSetRewardAddress(testAddr) + require.NoError(t, err) + + return input + }, + SuppliedGas: SetRewardAddressGasCost, + ReadOnly: false, + ExpectedRes: []byte{}, + AfterHook: func(t *testing.T, state contract.StateDB) { + address, isFeeRecipients := GetStoredRewardAddress(state) + require.Equal(t, testAddr, address) + require.False(t, isFeeRecipients) + }, + }, + "disable rewards from enabled succeeds": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackDisableRewards() + require.NoError(t, err) + + return input + }, + SuppliedGas: DisableRewardsGasCost, + ReadOnly: false, + ExpectedRes: []byte{}, + AfterHook: func(t *testing.T, state contract.StateDB) { + address, isFeeRecipients := GetStoredRewardAddress(state) + require.False(t, isFeeRecipients) + require.Equal(t, constants.BlackholeAddr, address) + }, + }, + "get current reward address from no role succeeds": { + Caller: allowlist.TestNoRoleAddr, + BeforeHook: func(t *testing.T, state contract.StateDB) { + allowlist.SetDefaultRoles(Module.Address)(t, state) + StoreRewardAddress(state, testAddr) + }, + InputFn: func(t *testing.T) []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: allowlist.TestNoRoleAddr, + BeforeHook: func(t *testing.T, state contract.StateDB) { + allowlist.SetDefaultRoles(Module.Address)(t, state) + EnableAllowFeeRecipients(state) + }, + InputFn: func(t *testing.T) []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: allowlist.TestNoRoleAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackCurrentRewardAddress() + require.NoError(t, err) + return input + }, + SuppliedGas: CurrentRewardAddressGasCost, + Config: &Config{ + InitialRewardConfig: &InitialRewardConfig{ + RewardAddress: testAddr, + }, + }, + ReadOnly: false, + ExpectedRes: func() []byte { + res, err := PackCurrentRewardAddressOutput(testAddr) + require.NoError(t, err) + return res + }(), + }, + "get initial config with allow fee recipients enabled": { + Caller: allowlist.TestNoRoleAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackAreFeeRecipientsAllowed() + require.NoError(t, err) + return input + }, + SuppliedGas: AreFeeRecipientsAllowedGasCost, + Config: &Config{ + InitialRewardConfig: &InitialRewardConfig{ + AllowFeeRecipients: true, + }, + }, + ReadOnly: false, + ExpectedRes: func() []byte { + res, err := PackAreFeeRecipientsAllowedOutput(true) + require.NoError(t, err) + return res + }(), + }, + "readOnly allow fee recipients with allowed role fails": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []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: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []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: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []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: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []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: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []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: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t *testing.T) []byte { + input, err := PackAreFeeRecipientsAllowed() + require.NoError(t, err) + + return input + }, + SuppliedGas: AreFeeRecipientsAllowedGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), + }, + } + + allowlist.RunPrecompileWithAllowListTests(t, Module, state.NewTestStateDB, tests) +} diff --git a/precompile/contracts/rewardmanager/module.go b/precompile/contracts/rewardmanager/module.go new file mode 100644 index 0000000000..477d0d413c --- /dev/null +++ b/precompile/contracts/rewardmanager/module.go @@ -0,0 +1,61 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package rewardmanager + +import ( + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ethereum/go-ethereum/common" +) + +var _ contract.Configurator = &configurator{} + +// ConfigKey is the key used in json config files to specify this precompile config. +// must be unique across all precompiles. +const ConfigKey = "rewardManagerConfig" + +var ContractAddress = common.HexToAddress("0x0200000000000000000000000000000000000004") + +var Module = modules.Module{ + ConfigKey: ConfigKey, + Address: ContractAddress, + Contract: RewardManagerPrecompile, + Configurator: &configurator{}, +} + +type configurator struct{} + +func init() { + if err := modules.RegisterModule(Module); err != nil { + panic(err) + } +} + +func (*configurator) MakeConfig() precompileconfig.Config { + return new(Config) +} + +// Configure configures [state] with the initial state for the precompile. +func (*configurator) Configure(chainConfig contract.ChainConfig, cfg precompileconfig.Config, state contract.StateDB, _ contract.BlockContext) error { + config, ok := cfg.(*Config) + if !ok { + return fmt.Errorf("incorrect config %T: %v", config, config) + } + // configure the RewardManager with the given initial configuration + if config.InitialRewardConfig != nil { + config.InitialRewardConfig.Configure(state) + } else if chainConfig.AllowedFeeRecipients() { + // configure the RewardManager according to chainConfig + EnableAllowFeeRecipients(state) + } else { + // chainConfig does not have any reward address + // if chainConfig does not enable fee recipients + // default to disabling rewards + DisableFeeRewards(state) + } + return config.Configure(state, ContractAddress) +} diff --git a/precompile/contracts/txallowlist/config.go b/precompile/contracts/txallowlist/config.go new file mode 100644 index 0000000000..c6204b41c5 --- /dev/null +++ b/precompile/contracts/txallowlist/config.go @@ -0,0 +1,56 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txallowlist + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ethereum/go-ethereum/common" +) + +var _ precompileconfig.Config = &Config{} + +// Config implements the StatefulPrecompileConfig interface while adding in the +// TxAllowList specific precompile config. +type Config struct { + allowlist.AllowListConfig + precompileconfig.Upgrade +} + +// NewConfig returns a config for a network upgrade at [blockTimestamp] that enables +// TxAllowList with the given [admins] and [enableds] as members of the allowlist. +func NewConfig(blockTimestamp *big.Int, admins []common.Address, enableds []common.Address) *Config { + return &Config{ + AllowListConfig: allowlist.AllowListConfig{ + AdminAddresses: admins, + EnabledAddresses: enableds, + }, + Upgrade: precompileconfig.Upgrade{BlockTimestamp: blockTimestamp}, + } +} + +// NewDisableConfig returns config for a network upgrade at [blockTimestamp] +// that disables TxAllowList. +func NewDisableConfig(blockTimestamp *big.Int) *Config { + return &Config{ + Upgrade: precompileconfig.Upgrade{ + BlockTimestamp: blockTimestamp, + Disable: true, + }, + } +} + +func (c *Config) Key() string { return ConfigKey } + +// Equal returns true if [cfg] is a [*TxAllowListConfig] and it has been configured identical to [c]. +func (c *Config) Equal(cfg precompileconfig.Config) bool { + // typecast before comparison + other, ok := (cfg).(*Config) + if !ok { + return false + } + return c.Upgrade.Equal(&other.Upgrade) && c.AllowListConfig.Equal(&other.AllowListConfig) +} diff --git a/precompile/contracts/txallowlist/config_test.go b/precompile/contracts/txallowlist/config_test.go new file mode 100644 index 0000000000..54ab46ac34 --- /dev/null +++ b/precompile/contracts/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/precompileconfig" + "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 precompileconfig.Config + ExpectedError string + }{ + { + name: "invalid allow list config in tx allowlist", + config: NewConfig(big.NewInt(3), admins, admins), + ExpectedError: "cannot set address", + }, + { + name: "nil member allow list config in tx allowlist", + config: NewConfig(big.NewInt(3), nil, nil), + ExpectedError: "", + }, + { + name: "empty member allow list config in tx allowlist", + config: NewConfig(big.NewInt(3), []common.Address{}, []common.Address{}), + ExpectedError: "", + }, + { + name: "valid allow list config in tx allowlist", + config: NewConfig(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 precompileconfig.Config + other precompileconfig.Config + expected bool + }{ + { + name: "non-nil config and nil other", + config: NewConfig(big.NewInt(3), admins, enableds), + other: nil, + expected: false, + }, + { + name: "different admin", + config: NewConfig(big.NewInt(3), admins, enableds), + other: NewConfig(big.NewInt(3), []common.Address{{3}}, enableds), + expected: false, + }, + { + name: "different enabled", + config: NewConfig(big.NewInt(3), admins, enableds), + other: NewConfig(big.NewInt(3), admins, []common.Address{{3}}), + expected: false, + }, + { + name: "different timestamp", + config: NewConfig(big.NewInt(3), admins, enableds), + other: NewConfig(big.NewInt(4), admins, enableds), + expected: false, + }, + { + name: "same config", + config: NewConfig(big.NewInt(3), admins, enableds), + other: NewConfig(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/contracts/txallowlist/contract.go b/precompile/contracts/txallowlist/contract.go new file mode 100644 index 0000000000..e93d53c6a1 --- /dev/null +++ b/precompile/contracts/txallowlist/contract.go @@ -0,0 +1,25 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txallowlist + +import ( + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" +) + +// Singleton StatefulPrecompiledContract for W/R access to the tx allow list. +var TxAllowListPrecompile contract.StatefulPrecompiledContract = allowlist.CreateAllowListPrecompile(ContractAddress) + +// GetTxAllowListStatus returns the role of [address] for the tx allow list. +func GetTxAllowListStatus(stateDB contract.StateDB, address common.Address) allowlist.Role { + return allowlist.GetAllowListStatus(stateDB, ContractAddress, address) +} + +// SetTxAllowListStatus sets the permissions of [address] to [role] for the +// tx allow list. +// assumes [role] has already been verified as valid. +func SetTxAllowListStatus(stateDB contract.StateDB, address common.Address, role allowlist.Role) { + allowlist.SetAllowListRole(stateDB, ContractAddress, address, role) +} diff --git a/precompile/contracts/txallowlist/contract_test.go b/precompile/contracts/txallowlist/contract_test.go new file mode 100644 index 0000000000..08104024c5 --- /dev/null +++ b/precompile/contracts/txallowlist/contract_test.go @@ -0,0 +1,15 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txallowlist + +import ( + "testing" + + "github.com/ava-labs/subnet-evm/core/state" + "github.com/ava-labs/subnet-evm/precompile/allowlist" +) + +func TestTxAllowListRun(t *testing.T) { + allowlist.RunPrecompileWithAllowListTests(t, Module, state.NewTestStateDB, nil) +} diff --git a/precompile/contracts/txallowlist/module.go b/precompile/contracts/txallowlist/module.go new file mode 100644 index 0000000000..3d58942dea --- /dev/null +++ b/precompile/contracts/txallowlist/module.go @@ -0,0 +1,49 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txallowlist + +import ( + "fmt" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ethereum/go-ethereum/common" +) + +var _ contract.Configurator = &configurator{} + +// ConfigKey is the key used in json config files to specify this precompile config. +// must be unique across all precompiles. +const ConfigKey = "txAllowListConfig" + +var ContractAddress = common.HexToAddress("0x0200000000000000000000000000000000000002") + +var Module = modules.Module{ + ConfigKey: ConfigKey, + Address: ContractAddress, + Contract: TxAllowListPrecompile, + Configurator: &configurator{}, +} + +type configurator struct{} + +func init() { + if err := modules.RegisterModule(Module); err != nil { + panic(err) + } +} + +func (*configurator) MakeConfig() precompileconfig.Config { + return new(Config) +} + +// Configure configures [state] with the initial state for the precompile. +func (*configurator) Configure(chainConfig contract.ChainConfig, cfg precompileconfig.Config, state contract.StateDB, _ contract.BlockContext) error { + config, ok := cfg.(*Config) + if !ok { + return fmt.Errorf("incorrect config %T: %v", config, config) + } + return config.AllowListConfig.Configure(state, ContractAddress) +} diff --git a/precompile/modules/module.go b/precompile/modules/module.go new file mode 100644 index 0000000000..d0a047c94d --- /dev/null +++ b/precompile/modules/module.go @@ -0,0 +1,37 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package modules + +import ( + "bytes" + + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" +) + +type Module struct { + // ConfigKey is the key used in json config files to specify this precompile config. + ConfigKey string + // Address returns the address where the stateful precompile is accessible. + Address common.Address + // Contract returns a thread-safe singleton that can be used as the StatefulPrecompiledContract when + // this config is enabled. + Contract contract.StatefulPrecompiledContract + // Configurator is used to configure the stateful precompile when the config is enabled. + contract.Configurator +} + +type moduleArray []Module + +func (u moduleArray) Len() int { + return len(u) +} + +func (u moduleArray) Swap(i, j int) { + u[i], u[j] = u[j], u[i] +} + +func (m moduleArray) Less(i, j int) bool { + return bytes.Compare(m[i].Address.Bytes(), m[j].Address.Bytes()) < 0 +} diff --git a/precompile/modules/registerer.go b/precompile/modules/registerer.go new file mode 100644 index 0000000000..3ab469ed06 --- /dev/null +++ b/precompile/modules/registerer.go @@ -0,0 +1,98 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package modules + +import ( + "fmt" + "sort" + + "github.com/ava-labs/subnet-evm/constants" + "github.com/ava-labs/subnet-evm/utils" + "github.com/ethereum/go-ethereum/common" +) + +var ( + // registeredModules is a list of Module to preserve order + // for deterministic iteration + registeredModules = make([]Module, 0) + + reservedRanges = []utils.AddressRange{ + { + Start: common.HexToAddress("0x0100000000000000000000000000000000000000"), + End: common.HexToAddress("0x01000000000000000000000000000000000000ff"), + }, + { + Start: common.HexToAddress("0x0200000000000000000000000000000000000000"), + End: common.HexToAddress("0x02000000000000000000000000000000000000ff"), + }, + { + Start: common.HexToAddress("0x0300000000000000000000000000000000000000"), + End: common.HexToAddress("0x03000000000000000000000000000000000000ff"), + }, + } +) + +// ReservedAddress returns true if [addr] is in a reserved range for custom precompiles +func ReservedAddress(addr common.Address) bool { + for _, reservedRange := range reservedRanges { + if reservedRange.Contains(addr) { + return true + } + } + + return false +} + +// RegisterModule registers a stateful precompile module +func RegisterModule(stm Module) error { + address := stm.Address + key := stm.ConfigKey + + if address == constants.BlackholeAddr { + return fmt.Errorf("address %s overlaps with blackhole address", address) + } + if !ReservedAddress(address) { + return fmt.Errorf("address %s not in a reserved range", address) + } + + for _, registeredModule := range registeredModules { + if registeredModule.ConfigKey == key { + return fmt.Errorf("name %s already used by a stateful precompile", key) + } + if registeredModule.Address == address { + return fmt.Errorf("address %s already used by a stateful precompile", address) + } + } + // sort by address to ensure deterministic iteration + registeredModules = insertSortedByAddress(registeredModules, stm) + return nil +} + +func GetPrecompileModuleByAddress(address common.Address) (Module, bool) { + for _, stm := range registeredModules { + if stm.Address == address { + return stm, true + } + } + return Module{}, false +} + +func GetPrecompileModule(key string) (Module, bool) { + for _, stm := range registeredModules { + if stm.ConfigKey == key { + return stm, true + } + } + return Module{}, false +} + +func RegisteredModules() []Module { + return registeredModules +} + +func insertSortedByAddress(data []Module, stm Module) []Module { + data = append(data, stm) + sort.Sort(moduleArray(data)) + return data +} diff --git a/precompile/modules/registerer_test.go b/precompile/modules/registerer_test.go new file mode 100644 index 0000000000..c0e4feb711 --- /dev/null +++ b/precompile/modules/registerer_test.go @@ -0,0 +1,59 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package modules + +import ( + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/constants" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestInsertSortedByAddress(t *testing.T) { + data := make([]Module, 0) + // test that the module is registered in sorted order + module1 := Module{ + Address: common.BigToAddress(big.NewInt(1)), + } + data = insertSortedByAddress(data, module1) + + require.Equal(t, []Module{module1}, data) + + module0 := Module{ + Address: common.BigToAddress(big.NewInt(0)), + } + + data = insertSortedByAddress(data, module0) + require.Equal(t, []Module{module0, module1}, data) + + module3 := Module{ + Address: common.BigToAddress(big.NewInt(3)), + } + + data = insertSortedByAddress(data, module3) + require.Equal(t, []Module{module0, module1, module3}, data) + + module2 := Module{ + Address: common.BigToAddress(big.NewInt(2)), + } + + data = insertSortedByAddress(data, module2) + require.Equal(t, []Module{module0, module1, module2, module3}, data) +} + +func TestRegisterModuleInvalidAddresses(t *testing.T) { + // Test the blockhole address cannot be registered + m := Module{ + Address: constants.BlackholeAddr, + } + err := RegisterModule(m) + require.ErrorContains(t, err, "overlaps with blackhole address") + + // Test an address outside of the reserved ranges cannot be registered + m.Address = common.BigToAddress(big.NewInt(1)) + err = RegisterModule(m) + require.ErrorContains(t, err, "not in a reserved range") +} 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/precompileconfig/config.go b/precompile/precompileconfig/config.go new file mode 100644 index 0000000000..fcee72fc95 --- /dev/null +++ b/precompile/precompileconfig/config.go @@ -0,0 +1,27 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Defines the stateless interface for unmarshalling an arbitrary config of a precompile +package precompileconfig + +import ( + "math/big" +) + +// StatefulPrecompileConfig defines the interface for a stateful precompile to +// be enabled via a network upgrade. +type Config interface { + // Key returns the unique key for the stateful precompile. + Key() string + // Timestamp returns the timestamp at which this stateful precompile should be enabled. + // 1) 0 indicates that the precompile should be enabled from genesis. + // 2) n indicates that the precompile should be enabled in the first block with timestamp >= [n]. + // 3) nil indicates that the precompile is never enabled. + Timestamp() *big.Int + // IsDisabled returns true if this network upgrade should disable the precompile. + IsDisabled() bool + // Equal returns true if the provided argument configures the same precompile with the same parameters. + Equal(Config) bool + // Verify is called on startup and an error is treated as fatal. Configure can assume the Config has passed verification. + Verify() error +} diff --git a/precompile/precompileconfig/mock_config.go b/precompile/precompileconfig/mock_config.go new file mode 100644 index 0000000000..6f1a9debc6 --- /dev/null +++ b/precompile/precompileconfig/mock_config.go @@ -0,0 +1,44 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// TODO: replace with gomock +package precompileconfig + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +var _ Config = &noopStatefulPrecompileConfig{} + +type noopStatefulPrecompileConfig struct { +} + +func NewNoopStatefulPrecompileConfig() *noopStatefulPrecompileConfig { + return &noopStatefulPrecompileConfig{} +} + +func (n *noopStatefulPrecompileConfig) Key() string { + return "" +} + +func (n *noopStatefulPrecompileConfig) Address() common.Address { + return common.Address{} +} + +func (n *noopStatefulPrecompileConfig) Timestamp() *big.Int { + return new(big.Int) +} + +func (n *noopStatefulPrecompileConfig) IsDisabled() bool { + return false +} + +func (n *noopStatefulPrecompileConfig) Equal(Config) bool { + return false +} + +func (n *noopStatefulPrecompileConfig) Verify() error { + return nil +} diff --git a/precompile/upgradeable.go b/precompile/precompileconfig/upgradeable.go similarity index 60% rename from precompile/upgradeable.go rename to precompile/precompileconfig/upgradeable.go index b835585b8b..3ad8c0498b 100644 --- a/precompile/upgradeable.go +++ b/precompile/precompileconfig/upgradeable.go @@ -1,7 +1,7 @@ // (c) 2022 Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package precompile +package precompileconfig import ( "math/big" @@ -9,29 +9,29 @@ import ( "github.com/ava-labs/subnet-evm/utils" ) -// UpgradeableConfig contains the timestamp for the upgrade along with +// Upgrade contains the timestamp for the upgrade along with // a boolean [Disable]. If [Disable] is set, the upgrade deactivates -// the precompile and resets its storage. -type UpgradeableConfig struct { +// the precompile and clears its storage. +type Upgrade struct { BlockTimestamp *big.Int `json:"blockTimestamp"` Disable bool `json:"disable,omitempty"` } // Timestamp returns the timestamp this network upgrade goes into effect. -func (c *UpgradeableConfig) Timestamp() *big.Int { - return c.BlockTimestamp +func (u *Upgrade) Timestamp() *big.Int { + return u.BlockTimestamp } // IsDisabled returns true if the network upgrade deactivates the precompile. -func (c *UpgradeableConfig) IsDisabled() bool { - return c.Disable +func (u *Upgrade) IsDisabled() bool { + return u.Disable } // Equal returns true iff [other] has the same blockTimestamp and has the // same on value for the Disable flag. -func (c *UpgradeableConfig) Equal(other *UpgradeableConfig) bool { +func (u *Upgrade) Equal(other *Upgrade) bool { if other == nil { return false } - return c.Disable == other.Disable && utils.BigNumEqual(c.BlockTimestamp, other.BlockTimestamp) + return u.Disable == other.Disable && utils.BigNumEqual(u.BlockTimestamp, other.BlockTimestamp) } diff --git a/precompile/registry/registry.go b/precompile/registry/registry.go new file mode 100644 index 0000000000..273ebbcde3 --- /dev/null +++ b/precompile/registry/registry.go @@ -0,0 +1,41 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Module to facilitate the registration of precompiles and their configuration. +package registry + +// Force imports of each precompile to ensure each precompile's init function runs and registers itself +// with the registry. +import ( + _ "github.com/ava-labs/subnet-evm/precompile/contracts/deployerallowlist" + + _ "github.com/ava-labs/subnet-evm/precompile/contracts/nativeminter" + + _ "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" + + _ "github.com/ava-labs/subnet-evm/precompile/contracts/feemanager" + + _ "github.com/ava-labs/subnet-evm/precompile/contracts/rewardmanager" + // ADD YOUR PRECOMPILE HERE + // _ "github.com/ava-labs/subnet-evm/precompile/contracts/yourprecompile" +) + +// This list is kept just for reference. The actual addresses defined in respective packages of precompiles. +// Note: it is important that none of these addresses conflict with each other or any other precompiles +// in core/vm/contracts.go. +// The first stateful precompiles were added in coreth to support nativeAssetCall and nativeAssetBalance. New stateful precompiles +// originating in coreth will continue at this prefix, so we reserve this range in subnet-evm so that they can be migrated into +// subnet-evm without issue. +// These start at the address: 0x0100000000000000000000000000000000000000 and will increment by 1. +// Optional precompiles implemented in subnet-evm start at 0x0200000000000000000000000000000000000000 and will increment by 1 +// from here to reduce the risk of conflicts. +// For forks of subnet-evm, users should start at 0x0300000000000000000000000000000000000000 to ensure +// that their own modifications do not conflict with stateful precompiles that may be added to subnet-evm +// in the future. +// ContractDeployerAllowListAddress = common.HexToAddress("0x0200000000000000000000000000000000000000") +// ContractNativeMinterAddress = common.HexToAddress("0x0200000000000000000000000000000000000001") +// TxAllowListAddress = common.HexToAddress("0x0200000000000000000000000000000000000002") +// FeeManagerAddress = common.HexToAddress("0x0200000000000000000000000000000000000003") +// RewardManagerAddress = common.HexToAddress("0x0200000000000000000000000000000000000004") +// ADD YOUR PRECOMPILE HERE +// {YourPrecompile}Address = common.HexToAddress("0x03000000000000000000000000000000000000??") diff --git a/precompile/reward_manager.go b/precompile/reward_manager.go deleted file mode 100644 index f887fa2f37..0000000000 --- a/precompile/reward_manager.go +++ /dev/null @@ -1,456 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Code generated -// This file is a generated precompile contract with stubbed abstract functions. - -package precompile - -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/vmerrs" - - "github.com/ethereum/go-ethereum/common" -) - -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 - - // RewardManagerRawABI contains the raw ABI of RewardManager contract. - RewardManagerRawABI = "[{\"inputs\":[],\"name\":\"allowFeeRecipients\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"areFeeRecipientsAllowed\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"isAllowed\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"currentRewardAddress\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"rewardAddress\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"disableRewards\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"readAllowList\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"role\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"setAdmin\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"setEnabled\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"setNone\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"setRewardAddress\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]" -) - -// 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") - ErrCannotDisableRewards = errors.New("non-enabled cannot call disableRewards") - ErrCannotSetRewardAddress = errors.New("non-enabled cannot call setRewardAddress") - - ErrCannotEnableBothRewards = errors.New("cannot enable both fee recipients and reward address at the same time") - ErrEmptyRewardAddress = errors.New("reward address cannot be empty") - - RewardManagerABI abi.ABI // will be initialized by init function - RewardManagerPrecompile 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) { - // 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 - if err := StoreRewardAddress(state, i.RewardAddress); err != nil { - panic(err) - } - } -} - -// 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 = createRewardManagerPrecompile(RewardManagerAddress) -} - -// 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) { - c.AllowListConfig.Configure(state, RewardManagerAddress) - // configure the RewardManager with the given initial configuration - if c.InitialRewardConfig != nil { - 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) - } -} - -// 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) -} - -// 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) -} - -// PackAllowFeeRecipients packs the function selector (first 4 func signature bytes). -// This function is mostly used for tests. -func PackAllowFeeRecipients() ([]byte, error) { - return RewardManagerABI.Pack("allowFeeRecipients") -} - -// EnableAllowFeeRecipients enables fee recipients. -func EnableAllowFeeRecipients(stateDB StateDB) { - stateDB.SetState(RewardManagerAddress, 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 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 { - return nil, 0, err - } - if readOnly { - return nil, remainingGas, vmerrs.ErrWriteProtection - } - // no input provided for this function - - // Allow list is enabled and AllowFeeRecipients is a state-changer function. - // This part of the code restricts the function to be called only by enabled/admin addresses in the allow list. - // You can modify/delete this code if you don't want this function to be restricted by the allow list. - stateDB := accessibleState.GetStateDB() - // Verify that the caller is in the allow list and therefore has the right to modify it - callerStatus := getAllowListStatus(stateDB, RewardManagerAddress, caller) - if !callerStatus.IsEnabled() { - return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotAllowFeeRecipients, caller) - } - // allow list code ends here. - - // this function does not return an output, leave this one as is - EnableAllowFeeRecipients(stateDB) - packedOutput := []byte{} - - // Return the packed output and the remaining gas - return packedOutput, remainingGas, nil -} - -// PackAreFeeRecipientsAllowed packs the include selector (first 4 func signature bytes). -// This function is mostly used for tests. -func PackAreFeeRecipientsAllowed() ([]byte, error) { - return RewardManagerABI.Pack("areFeeRecipientsAllowed") -} - -// PackAreFeeRecipientsAllowedOutput attempts to pack given isAllowed of type bool -// to conform the ABI outputs. -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 { - return nil, 0, err - } - // no input provided for this function - - stateDB := accessibleState.GetStateDB() - var output bool - _, output = GetStoredRewardAddress(stateDB) - - packedOutput, err := PackAreFeeRecipientsAllowedOutput(output) - if err != nil { - return nil, remainingGas, err - } - - // Return the packed output and the remaining gas - return packedOutput, remainingGas, nil -} - -// PackCurrentRewardAddress packs the include selector (first 4 func signature bytes). -// This function is mostly used for tests. -func PackCurrentRewardAddress() ([]byte, error) { - return RewardManagerABI.Pack("currentRewardAddress") -} - -// PackCurrentRewardAddressOutput attempts to pack given rewardAddress of type common.Address -// to conform the ABI outputs. -func PackCurrentRewardAddressOutput(rewardAddress common.Address) ([]byte, error) { - return RewardManagerABI.PackOutput("currentRewardAddress", rewardAddress) -} - -// 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) - return common.BytesToAddress(val.Bytes()), val == allowFeeRecipientsAddressValue -} - -// StoredRewardAddress stores the given [val] under rewardAddressStorageKey. -func StoreRewardAddress(stateDB StateDB, val common.Address) error { - // if input is empty, return an error - if val == (common.Address{}) { - return ErrEmptyRewardAddress - } - stateDB.SetState(RewardManagerAddress, rewardAddressStorageKey, val.Hash()) - return nil -} - -// PackSetRewardAddress packs [addr] of type common.Address into the appropriate arguments for setRewardAddress. -// the packed bytes include selector (first 4 func signature bytes). -// This function is mostly used for tests. -func PackSetRewardAddress(addr common.Address) ([]byte, error) { - return RewardManagerABI.Pack("setRewardAddress", addr) -} - -// UnpackSetRewardAddressInput attempts to unpack [input] into the common.Address type argument -// assumes that [input] does not include selector (omits first 4 func signature bytes) -func UnpackSetRewardAddressInput(input []byte) (common.Address, error) { - res, err := RewardManagerABI.UnpackInput("setRewardAddress", input) - if err != nil { - return common.Address{}, err - } - unpacked := *abi.ConvertType(res[0], new(common.Address)).(*common.Address) - 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 { - return nil, 0, err - } - if readOnly { - return nil, remainingGas, vmerrs.ErrWriteProtection - } - // attempts to unpack [input] into the arguments to the SetRewardAddressInput. - // Assumes that [input] does not include selector - // You can use unpacked [inputStruct] variable in your code - inputStruct, err := UnpackSetRewardAddressInput(input) - if err != nil { - return nil, remainingGas, err - } - - // Allow list is enabled and SetRewardAddress is a state-changer function. - // This part of the code restricts the function to be called only by enabled/admin addresses in the allow list. - // You can modify/delete this code if you don't want this function to be restricted by the allow list. - stateDB := accessibleState.GetStateDB() - // Verify that the caller is in the allow list and therefore has the right to modify it - callerStatus := getAllowListStatus(stateDB, RewardManagerAddress, caller) - if !callerStatus.IsEnabled() { - return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotSetRewardAddress, caller) - } - // allow list code ends here. - - if err := StoreRewardAddress(stateDB, inputStruct); err != nil { - return nil, remainingGas, err - } - // this function does not return an output, leave this one as is - packedOutput := []byte{} - - // Return the packed output and the remaining gas - 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 { - return nil, 0, err - } - - // no input provided for this function - stateDB := accessibleState.GetStateDB() - output, _ := GetStoredRewardAddress(stateDB) - packedOutput, err := PackCurrentRewardAddressOutput(output) - if err != nil { - return nil, remainingGas, err - } - - // Return the packed output and the remaining gas - return packedOutput, remainingGas, nil -} - -// PackDisableRewards packs the include selector (first 4 func signature bytes). -// This function is mostly used for tests. -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 { - return nil, 0, err - } - if readOnly { - return nil, remainingGas, vmerrs.ErrWriteProtection - } - // no input provided for this function - - // Allow list is enabled and DisableRewards is a state-changer function. - // This part of the code restricts the function to be called only by enabled/admin addresses in the allow list. - // You can modify/delete this code if you don't want this function to be restricted by the allow list. - stateDB := accessibleState.GetStateDB() - // Verify that the caller is in the allow list and therefore has the right to modify it - callerStatus := getAllowListStatus(stateDB, RewardManagerAddress, caller) - if !callerStatus.IsEnabled() { - return nil, remainingGas, fmt.Errorf("%w: %s", ErrCannotDisableRewards, caller) - } - // allow list code ends here. - DisableFeeRewards(stateDB) - // this function does not return an output, leave this one as is - packedOutput := []byte{} - - // Return the packed output and the remaining gas - return packedOutput, remainingGas, nil -} - -// 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 { - var functions []*statefulPrecompileFunction - functions = append(functions, createAllowListFunctions(precompileAddr)...) - - methodAllowFeeRecipients, ok := RewardManagerABI.Methods["allowFeeRecipients"] - if !ok { - panic("given method does not exist in the ABI") - } - functions = append(functions, newStatefulPrecompileFunction(methodAllowFeeRecipients.ID, allowFeeRecipients)) - - methodAreFeeRecipientsAllowed, ok := RewardManagerABI.Methods["areFeeRecipientsAllowed"] - if !ok { - panic("given method does not exist in the ABI") - } - functions = append(functions, newStatefulPrecompileFunction(methodAreFeeRecipientsAllowed.ID, areFeeRecipientsAllowed)) - - methodCurrentRewardAddress, ok := RewardManagerABI.Methods["currentRewardAddress"] - if !ok { - panic("given method does not exist in the ABI") - } - functions = append(functions, newStatefulPrecompileFunction(methodCurrentRewardAddress.ID, currentRewardAddress)) - - methodDisableRewards, ok := RewardManagerABI.Methods["disableRewards"] - if !ok { - panic("given method does not exist in the ABI") - } - functions = append(functions, newStatefulPrecompileFunction(methodDisableRewards.ID, disableRewards)) - - methodSetRewardAddress, ok := RewardManagerABI.Methods["setRewardAddress"] - if !ok { - panic("given method does not exist in the ABI") - } - functions = append(functions, newStatefulPrecompileFunction(methodSetRewardAddress.ID, setRewardAddress)) - - // Construct the contract with no fallback function. - contract := newStatefulPrecompileWithFunctionSelectors(nil, functions) - return contract -} diff --git a/precompile/stateful_precompile_config.go b/precompile/stateful_precompile_config.go deleted file mode 100644 index 7db30060e4..0000000000 --- a/precompile/stateful_precompile_config.go +++ /dev/null @@ -1,57 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package precompile - -import ( - "fmt" - "math/big" - - "github.com/ethereum/go-ethereum/common" -) - -// StatefulPrecompileConfig defines the interface for a stateful precompile to -type StatefulPrecompileConfig interface { - // Address returns the address where the stateful precompile is accessible. - Address() common.Address - // Timestamp returns the timestamp at which this stateful precompile should be enabled. - // 1) 0 indicates that the precompile should be enabled from genesis. - // 2) n indicates that the precompile should be enabled in the first block with timestamp >= [n]. - // 3) nil indicates that the precompile is never enabled. - Timestamp() *big.Int - // IsDisabled returns true if this network upgrade should disable the precompile. - IsDisabled() bool - // Equal returns true if the provided argument configures the same precompile with the same parameters. - Equal(StatefulPrecompileConfig) bool - // Configure is called on the first block where the stateful precompile should be enabled. - // This allows the stateful precompile to configure its own state via [StateDB] and [BlockContext] as necessary. - // This function must be deterministic since it will impact the EVM state. If a change to the - // config causes a change to the state modifications made in Configure, then it cannot be safely - // made to the config after the network upgrade has gone into effect. - // - // Configure is called on the first block where the stateful precompile should be enabled. This - // provides the config the ability to set its initial state and should only modify the state within - // its own address space. - Configure(ChainConfig, StateDB, BlockContext) - // 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 - - fmt.Stringer -} - -// Configure sets the nonce and code to non-empty values then calls Configure on [precompileConfig] to make the necessary -// state update to enable the StatefulPrecompile. -// Assumes that [precompileConfig] is non-nil. -func Configure(chainConfig ChainConfig, blockContext BlockContext, precompileConfig StatefulPrecompileConfig, state StateDB) { - // Set the nonce of the precompile's address (as is done when a contract is created) to ensure - // that it is marked as non-empty and will not be cleaned up when the statedb is finalized. - state.SetNonce(precompileConfig.Address(), 1) - // Set the code of the precompile's address to a non-zero length byte slice to ensure that the precompile - // can be called from within Solidity contracts. Solidity adds a check before invoking a contract to ensure - // that it does not attempt to invoke a non-existent contract. - state.SetCode(precompileConfig.Address(), []byte{0x1}) - precompileConfig.Configure(chainConfig, state, blockContext) -} diff --git a/precompile/testutils/test_precompile.go b/precompile/testutils/test_precompile.go new file mode 100644 index 0000000000..6e7fe23c1c --- /dev/null +++ b/precompile/testutils/test_precompile.go @@ -0,0 +1,86 @@ +// (c) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package testutils + +import ( + "math/big" + "testing" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/subnet-evm/commontype" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/precompile/precompileconfig" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// PrecompileTest is a test case for a precompile +type PrecompileTest struct { + // Caller is the address of the precompile caller + Caller common.Address + // Input the raw input bytes to the precompile + Input []byte + // InputFn is a function that returns the raw input bytes to the precompile + // If specified, Input will be ignored. + InputFn func(t *testing.T) []byte + // SuppliedGas is the amount of gas supplied to the precompile + SuppliedGas uint64 + // ReadOnly is whether the precompile should be called in read only + // mode. If true, the precompile should not modify the state. + ReadOnly bool + // Config is the config to use for the precompile + // It should be the same precompile config that is used in the + // precompile's configurator. + // If nil, Configure will not be called. + Config precompileconfig.Config + // BeforeHook is called before the precompile is called. + BeforeHook func(t *testing.T, state contract.StateDB) + // AfterHook is called after the precompile is called. + AfterHook func(t *testing.T, state contract.StateDB) + // ExpectedRes is the expected raw byte result returned by the precompile + ExpectedRes []byte + // ExpectedErr is the expected error returned by the precompile + ExpectedErr string + // BlockNumber is the block number to use for the precompile's block context + BlockNumber int64 +} + +func (test PrecompileTest) Run(t *testing.T, module modules.Module, state contract.StateDB) { + t.Helper() + contractAddress := module.Address + + if test.BeforeHook != nil { + test.BeforeHook(t, state) + } + + blockContext := contract.NewMockBlockContext(big.NewInt(test.BlockNumber), 0) + accesibleState := contract.NewMockAccessibleState(state, blockContext, snow.DefaultContextTest()) + chainConfig := contract.NewMockChainState(commontype.ValidTestFeeConfig, false) + + if test.Config != nil { + err := module.Configure(chainConfig, test.Config, state, blockContext) + require.NoError(t, err) + } + + input := test.Input + if test.InputFn != nil { + input = test.InputFn(t) + } + + if input != nil { + ret, remainingGas, err := module.Contract.Run(accesibleState, test.Caller, contractAddress, 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.AfterHook != nil { + test.AfterHook(t, state) + } +} diff --git a/precompile/tx_allow_list.go b/precompile/tx_allow_list.go deleted file mode 100644 index c67e07011f..0000000000 --- a/precompile/tx_allow_list.go +++ /dev/null @@ -1,94 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package precompile - -import ( - "encoding/json" - "errors" - "math/big" - - "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) - - ErrSenderAddressNotAllowListed = errors.New("cannot issue transaction from non-allow listed address") -) - -// TxAllowListConfig wraps [AllowListConfig] and uses it to implement the StatefulPrecompileConfig -// interface while adding in the TxAllowList specific precompile address. -type TxAllowListConfig struct { - AllowListConfig - UpgradeableConfig -} - -// NewTxAllowListConfig returns a config for a network upgrade at [blockTimestamp] that enables -// TxAllowList with the given [admins] and [enableds] as members of the allowlist. -func NewTxAllowListConfig(blockTimestamp *big.Int, admins []common.Address, enableds []common.Address) *TxAllowListConfig { - return &TxAllowListConfig{ - AllowListConfig: AllowListConfig{ - AllowListAdmins: admins, - EnabledAddresses: enableds, - }, - UpgradeableConfig: UpgradeableConfig{BlockTimestamp: blockTimestamp}, - } -} - -// NewDisableTxAllowListConfig returns config for a network upgrade at [blockTimestamp] -// that disables TxAllowList. -func NewDisableTxAllowListConfig(blockTimestamp *big.Int) *TxAllowListConfig { - return &TxAllowListConfig{ - UpgradeableConfig: UpgradeableConfig{ - BlockTimestamp: blockTimestamp, - Disable: true, - }, - } -} - -// Address returns the address of the contract deployer allow list. -func (c *TxAllowListConfig) Address() common.Address { - return TxAllowListAddress -} - -// Configure configures [state] with the desired admins based on [c]. -func (c *TxAllowListConfig) Configure(_ ChainConfig, state StateDB, _ BlockContext) { - c.AllowListConfig.Configure(state, TxAllowListAddress) -} - -// Contract returns the singleton stateful precompiled contract to be used for the allow list. -func (c *TxAllowListConfig) Contract() 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 { - // typecast before comparison - other, ok := (s).(*TxAllowListConfig) - if !ok { - return false - } - return c.UpgradeableConfig.Equal(&other.UpgradeableConfig) && c.AllowListConfig.Equal(&other.AllowListConfig) -} - -// String returns a string representation of the TxAllowListConfig. -func (c *TxAllowListConfig) String() string { - bytes, _ := json.Marshal(c) - return string(bytes) -} - -// 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) -} - -// 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) -} diff --git a/precompile/reserved_range.go b/utils/address_range.go similarity index 82% rename from precompile/reserved_range.go rename to utils/address_range.go index 4bbc953b54..940c39e8a1 100644 --- a/precompile/reserved_range.go +++ b/utils/address_range.go @@ -1,7 +1,7 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// (c) 2021-2023, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package precompile +package utils import ( "bytes" @@ -16,6 +16,7 @@ type AddressRange struct { } // Contains returns true iff [addr] is contained within the (inclusive) +// range of addresses defined by [a]. func (a *AddressRange) Contains(addr common.Address) bool { addrBytes := addr.Bytes() return bytes.Compare(addrBytes, a.Start[:]) >= 0 && bytes.Compare(addrBytes, a.End[:]) <= 0 diff --git a/vmerrs/vmerrs.go b/vmerrs/vmerrs.go index 5ab30248ff..ea3871852c 100644 --- a/vmerrs/vmerrs.go +++ b/vmerrs/vmerrs.go @@ -32,19 +32,20 @@ import ( // List evm execution errors var ( - ErrOutOfGas = errors.New("out of gas") - ErrCodeStoreOutOfGas = errors.New("contract creation code storage out of gas") - ErrDepth = errors.New("max call depth exceeded") - ErrInsufficientBalance = errors.New("insufficient balance for transfer") - ErrContractAddressCollision = errors.New("contract address collision") - ErrExecutionReverted = errors.New("execution reverted") - ErrMaxCodeSizeExceeded = errors.New("max code size exceeded") - ErrInvalidJump = errors.New("invalid jump destination") - ErrWriteProtection = errors.New("write protection") - ErrReturnDataOutOfBounds = errors.New("return data out of bounds") - ErrGasUintOverflow = errors.New("gas uint64 overflow") - ErrInvalidCode = errors.New("invalid code: must not begin with 0xef") - ErrNonceUintOverflow = errors.New("nonce uint64 overflow") - ErrAddrProhibited = errors.New("prohibited address cannot be sender or created contract address") - ErrInvalidCoinbase = errors.New("invalid coinbase") + ErrOutOfGas = errors.New("out of gas") + ErrCodeStoreOutOfGas = errors.New("contract creation code storage out of gas") + ErrDepth = errors.New("max call depth exceeded") + ErrInsufficientBalance = errors.New("insufficient balance for transfer") + ErrContractAddressCollision = errors.New("contract address collision") + ErrExecutionReverted = errors.New("execution reverted") + ErrMaxCodeSizeExceeded = errors.New("max code size exceeded") + ErrInvalidJump = errors.New("invalid jump destination") + ErrWriteProtection = errors.New("write protection") + ErrReturnDataOutOfBounds = errors.New("return data out of bounds") + ErrGasUintOverflow = errors.New("gas uint64 overflow") + ErrInvalidCode = errors.New("invalid code: must not begin with 0xef") + ErrNonceUintOverflow = errors.New("nonce uint64 overflow") + ErrAddrProhibited = errors.New("prohibited address cannot be sender or created contract address") + ErrInvalidCoinbase = errors.New("invalid coinbase") + ErrSenderAddressNotAllowListed = errors.New("cannot issue transaction from non-allow listed address") )