diff --git a/accounts/abi/bind/precompilebind/precompile_module_template.go b/accounts/abi/bind/precompilebind/precompile_module_template.go index 31d77383f1..e9dd8e7275 100644 --- a/accounts/abi/bind/precompilebind/precompile_module_template.go +++ b/accounts/abi/bind/precompilebind/precompile_module_template.go @@ -22,7 +22,7 @@ import ( var _ contract.Configurator = &configurator{} -// ConfigKey is the key used in json config files to specify this precompile precompileconfig. +// ConfigKey is the key used in json config files to specify this precompile config. // must be unique across all precompiles. const ConfigKey = "{{decapitalise .Contract.Type}}Config" @@ -50,7 +50,7 @@ func init() { } // MakeConfig returns a new precompile config instance. -// This is required for Marshal/Unmarshal the precompile config. +// This is required to Marshal/Unmarshal the precompile config. func (*configurator) MakeConfig() precompileconfig.Config { return new(Config) } diff --git a/precompile/allowlist/allowlist.go b/precompile/allowlist/allowlist.go index ec4f53c047..e0135ddc25 100644 --- a/precompile/allowlist/allowlist.go +++ b/precompile/allowlist/allowlist.go @@ -170,14 +170,10 @@ func CreateAllowListPrecompile(precompileAddr common.Address) contract.StatefulP func CreateAllowListFunctions(precompileAddr common.Address) []*contract.StatefulPrecompileFunction { setAdmin := contract.NewStatefulPrecompileFunction(setAdminSignature, createAllowListRoleSetter(precompileAddr, AdminRole)) - setManager := contract.NewStatefulPrecompileFunctionWithActivator(setManagerSignature, createAllowListRoleSetter(precompileAddr, ManagerRole), isManagerRoleActivated) + setManager := contract.NewStatefulPrecompileFunctionWithActivator(setManagerSignature, createAllowListRoleSetter(precompileAddr, ManagerRole), contract.IsDUpgradeActivated) setEnabled := contract.NewStatefulPrecompileFunction(setEnabledSignature, createAllowListRoleSetter(precompileAddr, EnabledRole)) setNone := contract.NewStatefulPrecompileFunction(setNoneSignature, createAllowListRoleSetter(precompileAddr, NoRole)) read := contract.NewStatefulPrecompileFunction(readAllowListSignature, createReadAllowList(precompileAddr)) return []*contract.StatefulPrecompileFunction{setAdmin, setManager, setEnabled, setNone, read} } - -func isManagerRoleActivated(evm contract.AccessibleState) bool { - return evm.GetChainConfig().IsDUpgrade(evm.GetBlockContext().Timestamp()) -} diff --git a/precompile/contract/contract.go b/precompile/contract/contract.go index f843e2a15b..0be76a8955 100644 --- a/precompile/contract/contract.go +++ b/precompile/contract/contract.go @@ -22,7 +22,6 @@ type ActivationFunc func(AccessibleState) bool // 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 diff --git a/precompile/contract/test_utils.go b/precompile/contract/test_utils.go new file mode 100644 index 0000000000..cf38b5db77 --- /dev/null +++ b/precompile/contract/test_utils.go @@ -0,0 +1,49 @@ +// (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" +) + +// PackOrderedHashesWithSelector packs the function selector and ordered list of hashes into [dst] +// byte slice. +// assumes that [dst] has sufficient room for [functionSelector] and [hashes]. +// Kept for testing backwards compatibility. +func PackOrderedHashesWithSelector(dst []byte, functionSelector []byte, hashes []common.Hash) error { + copy(dst[:len(functionSelector)], functionSelector) + return PackOrderedHashes(dst[len(functionSelector):], hashes) +} + +// PackOrderedHashes packs the ordered list of [hashes] into the [dst] byte buffer. +// assumes that [dst] has sufficient space to pack [hashes] or else this function will panic. +// Kept for testing backwards compatibility. +func PackOrderedHashes(dst []byte, hashes []common.Hash) error { + if len(dst) != len(hashes)*common.HashLength { + return fmt.Errorf("destination byte buffer has insufficient length (%d) for %d hashes", len(dst), len(hashes)) + } + + var ( + start = 0 + end = common.HashLength + ) + for _, hash := range hashes { + copy(dst[start:end], hash.Bytes()) + start += common.HashLength + end += common.HashLength + } + return nil +} + +// 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. +// Kept for testing backwards compatibility. +func PackedHash(packed []byte, index int) []byte { + start := common.HashLength * index + end := start + common.HashLength + return packed[start:end] +} diff --git a/precompile/contract/utils.go b/precompile/contract/utils.go index b294a7adfe..a61edc394f 100644 --- a/precompile/contract/utils.go +++ b/precompile/contract/utils.go @@ -10,7 +10,6 @@ import ( "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" ) @@ -42,42 +41,6 @@ func DeductGas(suppliedGas uint64, requiredGas uint64) (uint64, error) { return suppliedGas - requiredGas, nil } -// PackOrderedHashesWithSelector packs the function selector and ordered list of hashes into [dst] -// byte slice. -// assumes that [dst] has sufficient room for [functionSelector] and [hashes]. -func PackOrderedHashesWithSelector(dst []byte, functionSelector []byte, hashes []common.Hash) error { - copy(dst[:len(functionSelector)], functionSelector) - return PackOrderedHashes(dst[len(functionSelector):], hashes) -} - -// PackOrderedHashes packs the ordered list of [hashes] into the [dst] byte buffer. -// assumes that [dst] has sufficient space to pack [hashes] or else this function will panic. -func PackOrderedHashes(dst []byte, hashes []common.Hash) error { - if len(dst) != len(hashes)*common.HashLength { - return fmt.Errorf("destination byte buffer has insufficient length (%d) for %d hashes", len(dst), len(hashes)) - } - - var ( - start = 0 - end = common.HashLength - ) - for _, hash := range hashes { - copy(dst[start:end], hash.Bytes()) - start += common.HashLength - end += common.HashLength - } - return nil -} - -// 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 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 { @@ -88,3 +51,7 @@ func ParseABI(rawABI string) abi.ABI { return parsed } + +func IsDUpgradeActivated(evm AccessibleState) bool { + return evm.GetChainConfig().IsDUpgrade(evm.GetBlockContext().Timestamp()) +} diff --git a/precompile/contracts/deployerallowlist/module.go b/precompile/contracts/deployerallowlist/module.go index bdb0de8f18..17f7431ab0 100644 --- a/precompile/contracts/deployerallowlist/module.go +++ b/precompile/contracts/deployerallowlist/module.go @@ -35,11 +35,14 @@ func init() { } } +// MakeConfig returns a new precompile config instance. +// This is required to Marshal/Unmarshal the precompile config. func (*configurator) MakeConfig() precompileconfig.Config { return new(Config) } -// Configure configures [state] with the given [cfg] config. +// Configure configures [state] with the given [cfg] precompileconfig. +// This function is called by the EVM once per precompile contract activation. func (c *configurator) Configure(chainConfig precompileconfig.ChainConfig, cfg precompileconfig.Config, state contract.StateDB, blockContext contract.ConfigurationBlockContext) error { config, ok := cfg.(*Config) if !ok { diff --git a/precompile/contracts/feemanager/config.go b/precompile/contracts/feemanager/config.go index 50dd7305ed..9dcfc307d2 100644 --- a/precompile/contracts/feemanager/config.go +++ b/precompile/contracts/feemanager/config.go @@ -46,6 +46,8 @@ func NewDisableConfig(blockTimestamp *uint64) *Config { } } +// Key returns the key for the FeeManager precompileconfig. +// This should be the same key as used in the precompile module. func (*Config) Key() string { return ConfigKey } // Equal returns true if [cfg] is a [*FeeManagerConfig] and it has been configured identical to [c]. @@ -67,6 +69,7 @@ func (c *Config) Equal(cfg precompileconfig.Config) bool { return c.InitialFeeConfig.Equal(other.InitialFeeConfig) } +// Verify tries to verify Config and returns an error accordingly. func (c *Config) Verify(chainConfig precompileconfig.ChainConfig) error { if err := c.AllowListConfig.Verify(chainConfig, c.Upgrade); err != nil { return err diff --git a/precompile/contracts/feemanager/contract.abi b/precompile/contracts/feemanager/contract.abi new file mode 100644 index 0000000000..e32f94e09e --- /dev/null +++ b/precompile/contracts/feemanager/contract.abi @@ -0,0 +1,169 @@ +[ + { + "inputs": [], + "name": "getFeeConfig", + "outputs": [ + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "targetBlockRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minBaseFee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "targetGas", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "baseFeeChangeDenominator", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minBlockGasCost", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxBlockGasCost", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "blockGasCostStep", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getFeeConfigLastChangedAt", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "stateMutability": "view", + "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": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "targetBlockRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minBaseFee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "targetGas", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "baseFeeChangeDenominator", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minBlockGasCost", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxBlockGasCost", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "blockGasCostStep", + "type": "uint256" + } + ], + "name": "setFeeConfig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "setNone", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] diff --git a/precompile/contracts/feemanager/contract.go b/precompile/contracts/feemanager/contract.go index bcf8d674a7..195ba973e0 100644 --- a/precompile/contracts/feemanager/contract.go +++ b/precompile/contracts/feemanager/contract.go @@ -4,10 +4,12 @@ package feemanager import ( + _ "embed" "errors" "fmt" "math/big" + "github.com/ava-labs/subnet-evm/accounts/abi" "github.com/ava-labs/subnet-evm/commontype" "github.com/ava-labs/subnet-evm/precompile/allowlist" "github.com/ava-labs/subnet-evm/precompile/contract" @@ -43,15 +45,30 @@ var ( // Singleton StatefulPrecompiledContract for setting fee configs by permissioned callers. FeeManagerPrecompile contract.StatefulPrecompiledContract = createFeeManagerPrecompile() - 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") + ErrInvalidLen = errors.New("invalid input length for fee config Input") + + // IFeeManagerRawABI contains the raw ABI of FeeManager contract. + //go:embed contract.abi + FeeManagerRawABI string + + FeeManagerABI = contract.ParseABI(FeeManagerRawABI) ) +// FeeConfigABIStruct is the ABI struct for FeeConfig type. +type FeeConfigABIStruct struct { + GasLimit *big.Int + TargetBlockRate *big.Int + MinBaseFee *big.Int + TargetGas *big.Int + BaseFeeChangeDenominator *big.Int + MinBlockGasCost *big.Int + MaxBlockGasCost *big.Int + BlockGasCostStep *big.Int +} + // 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) @@ -63,86 +80,6 @@ func SetFeeManagerStatus(stateDB contract.StateDB, address common.Address, role allowlist.SetAllowListRole(stateDB, ContractAddress, address, role) } -// PackGetFeeConfigInput packs the getFeeConfig signature -func PackGetFeeConfigInput() []byte { - return getFeeConfigSignature -} - -// PackGetLastChangedAtInput packs the getFeeConfigLastChangedAt signature -func PackGetLastChangedAtInput() []byte { - return getFeeConfigLastChangedAtSignature -} - -// 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) -} - -// 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) -} - -func packFeeConfigHelper(feeConfig commontype.FeeConfig, useSelector bool) ([]byte, error) { - hashes := []common.Hash{ - common.BigToHash(feeConfig.GasLimit), - common.BigToHash(new(big.Int).SetUint64(feeConfig.TargetBlockRate)), - common.BigToHash(feeConfig.MinBaseFee), - common.BigToHash(feeConfig.TargetGas), - common.BigToHash(feeConfig.BaseFeeChangeDenominator), - common.BigToHash(feeConfig.MinBlockGasCost), - common.BigToHash(feeConfig.MaxBlockGasCost), - common.BigToHash(feeConfig.BlockGasCostStep), - } - - if useSelector { - res := make([]byte, len(setFeeConfigSignature)+feeConfigInputLen) - err := contract.PackOrderedHashesWithSelector(res, setFeeConfigSignature, hashes) - return res, err - } - - res := make([]byte, len(hashes)*common.HashLength) - 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)) - } - feeConfig := commontype.FeeConfig{} - for i := minFeeConfigFieldKey; i <= numFeeConfigField; i++ { - listIndex := i - 1 - packedElement := contract.PackedHash(input, listIndex) - switch i { - case gasLimitKey: - feeConfig.GasLimit = new(big.Int).SetBytes(packedElement) - case targetBlockRateKey: - feeConfig.TargetBlockRate = new(big.Int).SetBytes(packedElement).Uint64() - case minBaseFeeKey: - feeConfig.MinBaseFee = new(big.Int).SetBytes(packedElement) - case targetGasKey: - feeConfig.TargetGas = new(big.Int).SetBytes(packedElement) - case baseFeeChangeDenominatorKey: - feeConfig.BaseFeeChangeDenominator = new(big.Int).SetBytes(packedElement) - case minBlockGasCostKey: - feeConfig.MinBlockGasCost = new(big.Int).SetBytes(packedElement) - case maxBlockGasCostKey: - feeConfig.MaxBlockGasCost = new(big.Int).SetBytes(packedElement) - 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)) - } - } - return feeConfig, nil -} - // GetStoredFeeConfig returns fee config from contract storage in given state func GetStoredFeeConfig(stateDB contract.StateDB) commontype.FeeConfig { feeConfig := commontype.FeeConfig{} @@ -219,6 +156,52 @@ func StoreFeeConfig(stateDB contract.StateDB, feeConfig commontype.FeeConfig, bl return nil } +// PackSetFeeConfig packs [inputStruct] of type SetFeeConfigInput into the appropriate arguments for setFeeConfig. +func PackSetFeeConfig(input commontype.FeeConfig) ([]byte, error) { + inputStruct := FeeConfigABIStruct{ + GasLimit: input.GasLimit, + TargetBlockRate: new(big.Int).SetUint64(input.TargetBlockRate), + MinBaseFee: input.MinBaseFee, + TargetGas: input.TargetGas, + BaseFeeChangeDenominator: input.BaseFeeChangeDenominator, + MinBlockGasCost: input.MinBlockGasCost, + MaxBlockGasCost: input.MaxBlockGasCost, + BlockGasCostStep: input.BlockGasCostStep, + } + return FeeManagerABI.Pack("setFeeConfig", inputStruct.GasLimit, inputStruct.TargetBlockRate, inputStruct.MinBaseFee, inputStruct.TargetGas, inputStruct.BaseFeeChangeDenominator, inputStruct.MinBlockGasCost, inputStruct.MaxBlockGasCost, inputStruct.BlockGasCostStep) +} + +// UnpackSetFeeConfigInput attempts to unpack [input] as SetFeeConfigInput +// assumes that [input] does not include selector (omits first 4 func signature bytes) +// if [skipLenCheck] is false, it will return an error if the length of [input] is not [feeConfigInputLen] +func UnpackSetFeeConfigInput(input []byte, skipLenCheck bool) (commontype.FeeConfig, error) { + // Initially we had this check to ensure that the input was the correct length. + // However solidity does not always pack the input to the correct length, and allows + // for extra padding bytes to be added to the end of the input. Therefore, we have removed + // this check with the DUpgrade. We still need to keep this check for backwards compatibility. + if !skipLenCheck && len(input) != feeConfigInputLen { + return commontype.FeeConfig{}, fmt.Errorf("%w: %d", ErrInvalidLen, len(input)) + } + inputStruct := FeeConfigABIStruct{} + err := FeeManagerABI.UnpackInputIntoInterface(&inputStruct, "setFeeConfig", input) + if err != nil { + return commontype.FeeConfig{}, err + } + + result := commontype.FeeConfig{ + GasLimit: inputStruct.GasLimit, + TargetBlockRate: inputStruct.TargetBlockRate.Uint64(), + MinBaseFee: inputStruct.MinBaseFee, + TargetGas: inputStruct.TargetGas, + BaseFeeChangeDenominator: inputStruct.BaseFeeChangeDenominator, + MinBlockGasCost: inputStruct.MinBlockGasCost, + MaxBlockGasCost: inputStruct.MaxBlockGasCost, + BlockGasCostStep: inputStruct.BlockGasCostStep, + } + + return result, 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 contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { @@ -230,7 +213,8 @@ func setFeeConfig(accessibleState contract.AccessibleState, caller common.Addres return nil, remainingGas, vmerrs.ErrWriteProtection } - feeConfig, err := UnpackFeeConfigInput(input) + // We skip the fixed length check with DUpgrade + feeConfig, err := UnpackSetFeeConfigInput(input, contract.IsDUpgradeActivated(accessibleState)) if err != nil { return nil, remainingGas, err } @@ -250,6 +234,63 @@ func setFeeConfig(accessibleState contract.AccessibleState, caller common.Addres return []byte{}, remainingGas, nil } +// PackGetFeeConfig packs the include selector (first 4 func signature bytes). +// This function is mostly used for tests. +func PackGetFeeConfig() ([]byte, error) { + return FeeManagerABI.Pack("getFeeConfig") +} + +// PackGetFeeConfigOutput attempts to pack given [outputStruct] of type GetFeeConfigOutput +// to conform the ABI outputs. +func PackGetFeeConfigOutput(output commontype.FeeConfig) ([]byte, error) { + outputStruct := FeeConfigABIStruct{ + GasLimit: output.GasLimit, + TargetBlockRate: new(big.Int).SetUint64(output.TargetBlockRate), + MinBaseFee: output.MinBaseFee, + TargetGas: output.TargetGas, + BaseFeeChangeDenominator: output.BaseFeeChangeDenominator, + MinBlockGasCost: output.MinBlockGasCost, + MaxBlockGasCost: output.MaxBlockGasCost, + BlockGasCostStep: output.BlockGasCostStep, + } + return FeeManagerABI.PackOutput("getFeeConfig", + outputStruct.GasLimit, + outputStruct.TargetBlockRate, + outputStruct.MinBaseFee, + outputStruct.TargetGas, + outputStruct.BaseFeeChangeDenominator, + outputStruct.MinBlockGasCost, + outputStruct.MaxBlockGasCost, + outputStruct.BlockGasCostStep, + ) +} + +// UnpackGetFeeConfigOutput attempts to unpack [output] as GetFeeConfigOutput +// assumes that [output] does not include selector (omits first 4 func signature bytes) +func UnpackGetFeeConfigOutput(output []byte, skipLenCheck bool) (commontype.FeeConfig, error) { + if !skipLenCheck && len(output) != feeConfigInputLen { + return commontype.FeeConfig{}, fmt.Errorf("%w: %d", ErrInvalidLen, len(output)) + } + outputStruct := FeeConfigABIStruct{} + err := FeeManagerABI.UnpackIntoInterface(&outputStruct, "getFeeConfig", output) + + if err != nil { + return commontype.FeeConfig{}, err + } + + result := commontype.FeeConfig{ + GasLimit: outputStruct.GasLimit, + TargetBlockRate: outputStruct.TargetBlockRate.Uint64(), + MinBaseFee: outputStruct.MinBaseFee, + TargetGas: outputStruct.TargetGas, + BaseFeeChangeDenominator: outputStruct.BaseFeeChangeDenominator, + MinBlockGasCost: outputStruct.MinBlockGasCost, + MaxBlockGasCost: outputStruct.MaxBlockGasCost, + BlockGasCostStep: outputStruct.BlockGasCostStep, + } + return result, nil +} + // 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 contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { @@ -259,7 +300,7 @@ func getFeeConfig(accessibleState contract.AccessibleState, caller common.Addres feeConfig := GetStoredFeeConfig(accessibleState.GetStateDB()) - output, err := PackFeeConfig(feeConfig) + output, err := PackGetFeeConfigOutput(feeConfig) if err != nil { return nil, remainingGas, err } @@ -268,6 +309,29 @@ func getFeeConfig(accessibleState contract.AccessibleState, caller common.Addres return output, remainingGas, err } +// PackGetFeeConfigLastChangedAt packs the include selector (first 4 func signature bytes). +// This function is mostly used for tests. +func PackGetFeeConfigLastChangedAt() ([]byte, error) { + return FeeManagerABI.Pack("getFeeConfigLastChangedAt") +} + +// PackGetFeeConfigLastChangedAtOutput attempts to pack given blockNumber of type *big.Int +// to conform the ABI outputs. +func PackGetFeeConfigLastChangedAtOutput(blockNumber *big.Int) ([]byte, error) { + return FeeManagerABI.PackOutput("getFeeConfigLastChangedAt", blockNumber) +} + +// UnpackGetFeeConfigLastChangedAtOutput attempts to unpack given [output] into the *big.Int type output +// assumes that [output] does not include selector (omits first 4 func signature bytes) +func UnpackGetFeeConfigLastChangedAtOutput(output []byte) (*big.Int, error) { + res, err := FeeManagerABI.Unpack("getFeeConfigLastChangedAt", output) + if err != nil { + return new(big.Int), err + } + unpacked := *abi.ConvertType(res[0], new(*big.Int)).(**big.Int) + return unpacked, nil +} + // 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 contract.AccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { @@ -276,28 +340,37 @@ func getFeeConfigLastChangedAt(accessibleState contract.AccessibleState, caller } lastChangedAt := GetFeeConfigLastChangedAt(accessibleState.GetStateDB()) + packedOutput, err := PackGetFeeConfigLastChangedAtOutput(lastChangedAt) + if err != nil { + return nil, remainingGas, err + } - // Return an empty output and the remaining gas - return common.BigToHash(lastChangedAt).Bytes(), remainingGas, err + return packedOutput, remainingGas, err } -// 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 ContractAddress. +// createFeeManagerPrecompile returns a StatefulPrecompiledContract with getters and setters for the precompile. +// Access to the getters/setters is controlled by an allow list for ContractAddress. func createFeeManagerPrecompile() contract.StatefulPrecompiledContract { - feeManagerFunctions := allowlist.CreateAllowListFunctions(ContractAddress) + var functions []*contract.StatefulPrecompileFunction + functions = append(functions, allowlist.CreateAllowListFunctions(ContractAddress)...) - setFeeConfigFunc := contract.NewStatefulPrecompileFunction(setFeeConfigSignature, setFeeConfig) - getFeeConfigFunc := contract.NewStatefulPrecompileFunction(getFeeConfigSignature, getFeeConfig) - getFeeConfigLastChangedAtFunc := contract.NewStatefulPrecompileFunction(getFeeConfigLastChangedAtSignature, getFeeConfigLastChangedAt) + abiFunctionMap := map[string]contract.RunStatefulPrecompileFunc{ + "getFeeConfig": getFeeConfig, + "getFeeConfigLastChangedAt": getFeeConfigLastChangedAt, + "setFeeConfig": setFeeConfig, + } - feeManagerFunctions = append(feeManagerFunctions, setFeeConfigFunc, getFeeConfigFunc, getFeeConfigLastChangedAtFunc) + for name, function := range abiFunctionMap { + method, ok := FeeManagerABI.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. - 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. + statefulContract, err := contract.NewStatefulPrecompileContract(nil, functions) if err != nil { panic(err) } - return contract + return statefulContract } diff --git a/precompile/contracts/feemanager/contract_test.go b/precompile/contracts/feemanager/contract_test.go index 4c5d5a5902..6a51951843 100644 --- a/precompile/contracts/feemanager/contract_test.go +++ b/precompile/contracts/feemanager/contract_test.go @@ -11,6 +11,7 @@ import ( "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/precompileconfig" "github.com/ava-labs/subnet-evm/precompile/testutils" "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" @@ -116,6 +117,7 @@ var ( ExpectedRes: []byte{}, SetupBlockContext: func(mbc *contract.MockBlockContext) { mbc.EXPECT().Number().Return(testBlockNumber).AnyTimes() + mbc.EXPECT().Timestamp().Return(uint64(0)).AnyTimes() }, AfterHook: func(t testing.TB, state contract.StateDB) { feeConfig := GetStoredFeeConfig(state) @@ -133,11 +135,16 @@ var ( err := StoreFeeConfig(state, testFeeConfig, blockContext) require.NoError(t, err) }, - Input: PackGetFeeConfigInput(), + InputFn: func(t testing.TB) []byte { + input, err := PackGetFeeConfig() + require.NoError(t, err) + + return input + }, SuppliedGas: GetFeeConfigGasCost, ReadOnly: true, ExpectedRes: func() []byte { - res, err := PackFeeConfig(testFeeConfig) + res, err := PackGetFeeConfigOutput(testFeeConfig) if err != nil { panic(err) } @@ -151,16 +158,21 @@ var ( }, }, "get initial fee config": { - Caller: allowlist.TestNoRoleAddr, - BeforeHook: allowlist.SetDefaultRoles(Module.Address), - Input: PackGetFeeConfigInput(), + Caller: allowlist.TestNoRoleAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + input, err := PackGetFeeConfig() + require.NoError(t, err) + + return input + }, SuppliedGas: GetFeeConfigGasCost, Config: &Config{ InitialFeeConfig: &testFeeConfig, }, ReadOnly: true, ExpectedRes: func() []byte { - res, err := PackFeeConfig(testFeeConfig) + res, err := PackGetFeeConfigOutput(testFeeConfig) if err != nil { panic(err) } @@ -185,10 +197,21 @@ var ( err := StoreFeeConfig(state, testFeeConfig, blockContext) require.NoError(t, err) }, - Input: PackGetLastChangedAtInput(), + InputFn: func(t testing.TB) []byte { + input, err := PackGetFeeConfigLastChangedAt() + require.NoError(t, err) + + return input + }, SuppliedGas: GetLastChangedAtGasCost, ReadOnly: true, - ExpectedRes: common.BigToHash(testBlockNumber).Bytes(), + ExpectedRes: func() []byte { + res, err := PackGetFeeConfigLastChangedAtOutput(testBlockNumber) + if err != nil { + panic(err) + } + return res + }(), AfterHook: func(t testing.TB, state contract.StateDB) { feeConfig := GetStoredFeeConfig(state) lastChangedAt := GetFeeConfigLastChangedAt(state) @@ -248,6 +271,58 @@ var ( ReadOnly: false, ExpectedErr: vmerrs.ErrOutOfGas.Error(), }, + "set config with extra padded bytes should fail before DUpgrade": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + input, err := PackSetFeeConfig(testFeeConfig) + require.NoError(t, err) + + input = append(input, make([]byte, 32)...) + return input + }, + ChainConfigFn: func(t testing.TB) precompileconfig.ChainConfig { + config := precompileconfig.NewMockChainConfig(gomock.NewController(t)) + config.EXPECT().IsDUpgrade(gomock.Any()).Return(false).AnyTimes() + return config + }, + SuppliedGas: SetFeeConfigGasCost, + ReadOnly: false, + ExpectedErr: ErrInvalidLen.Error(), + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().Number().Return(testBlockNumber).AnyTimes() + mbc.EXPECT().Timestamp().Return(uint64(0)).AnyTimes() + }, + }, + "set config with extra padded bytes should succeed with DUpgrade": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + input, err := PackSetFeeConfig(testFeeConfig) + require.NoError(t, err) + + input = append(input, make([]byte, 32)...) + return input + }, + ChainConfigFn: func(t testing.TB) precompileconfig.ChainConfig { + config := precompileconfig.NewMockChainConfig(gomock.NewController(t)) + config.EXPECT().IsDUpgrade(gomock.Any()).Return(true).AnyTimes() + return config + }, + SuppliedGas: SetFeeConfigGasCost, + ReadOnly: false, + ExpectedRes: []byte{}, + SetupBlockContext: func(mbc *contract.MockBlockContext) { + mbc.EXPECT().Number().Return(testBlockNumber).AnyTimes() + mbc.EXPECT().Timestamp().Return(uint64(0)).AnyTimes() + }, + AfterHook: func(t testing.TB, state contract.StateDB) { + feeConfig := GetStoredFeeConfig(state) + require.Equal(t, testFeeConfig, feeConfig) + lastChangedAt := GetFeeConfigLastChangedAt(state) + require.EqualValues(t, testBlockNumber, lastChangedAt) + }, + }, } ) diff --git a/precompile/contracts/feemanager/module.go b/precompile/contracts/feemanager/module.go index 8d0e1ea70d..e67e5e1115 100644 --- a/precompile/contracts/feemanager/module.go +++ b/precompile/contracts/feemanager/module.go @@ -20,6 +20,7 @@ const ConfigKey = "feeManagerConfig" var ContractAddress = common.HexToAddress("0x0200000000000000000000000000000000000003") +// Module is the precompile module. It is used to register the precompile contract. var Module = modules.Module{ ConfigKey: ConfigKey, Address: ContractAddress, @@ -30,16 +31,21 @@ var Module = modules.Module{ 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 to Marshal/Unmarshal the precompile config. func (*configurator) MakeConfig() precompileconfig.Config { return new(Config) } -// Configure configures [state] with the desired admins based on [configIface]. +// Configure configures [state] with the given [cfg] precompileconfig. +// This function is called by the EVM once per precompile contract activation. func (*configurator) Configure(chainConfig precompileconfig.ChainConfig, cfg precompileconfig.Config, state contract.StateDB, blockContext contract.ConfigurationBlockContext) error { config, ok := cfg.(*Config) if !ok { diff --git a/precompile/contracts/feemanager/unpack_pack_test.go b/precompile/contracts/feemanager/unpack_pack_test.go new file mode 100644 index 0000000000..d758674ff3 --- /dev/null +++ b/precompile/contracts/feemanager/unpack_pack_test.go @@ -0,0 +1,471 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package feemanager + +import ( + "fmt" + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/accounts/abi" + "github.com/ava-labs/subnet-evm/commontype" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/stretchr/testify/require" +) + +var ( + setFeeConfigSignature = contract.CalculateFunctionSelector("setFeeConfig(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)") + getFeeConfigSignature = contract.CalculateFunctionSelector("getFeeConfig()") + getFeeConfigLastChangedAtSignature = contract.CalculateFunctionSelector("getFeeConfigLastChangedAt()") +) + +func FuzzPackGetFeeConfigOutputEqualTest(f *testing.F) { + f.Add([]byte{}, uint64(0)) + f.Add(big.NewInt(0).Bytes(), uint64(0)) + f.Add(big.NewInt(1).Bytes(), uint64(math.MaxUint64)) + f.Add(math.MaxBig256.Bytes(), uint64(0)) + f.Add(math.MaxBig256.Sub(math.MaxBig256, common.Big1).Bytes(), uint64(0)) + f.Add(math.MaxBig256.Add(math.MaxBig256, common.Big1).Bytes(), uint64(0)) + f.Fuzz(func(t *testing.T, bigIntBytes []byte, blockRate uint64) { + bigIntVal := new(big.Int).SetBytes(bigIntBytes) + feeConfig := commontype.FeeConfig{ + GasLimit: bigIntVal, + TargetBlockRate: blockRate, + MinBaseFee: bigIntVal, + TargetGas: bigIntVal, + BaseFeeChangeDenominator: bigIntVal, + MinBlockGasCost: bigIntVal, + MaxBlockGasCost: bigIntVal, + BlockGasCostStep: bigIntVal, + } + doCheckOutputs := true + // we can only check if outputs are correct if the value is less than MaxUint256 + // otherwise the value will be truncated when packed, + // and thus unpacked output will not be equal to the value + if bigIntVal.Cmp(abi.MaxUint256) > 0 { + doCheckOutputs = false + } + testOldPackGetFeeConfigOutputEqual(t, feeConfig, doCheckOutputs) + }) +} + +func TestOldPackGetFeeConfigOutputEqual(t *testing.T) { + testOldPackGetFeeConfigOutputEqual(t, testFeeConfig, true) +} +func TestPackGetFeeConfigOutputPanic(t *testing.T) { + require.Panics(t, func() { + _, _ = OldPackFeeConfig(commontype.FeeConfig{}) + }) + require.Panics(t, func() { + _, _ = PackGetFeeConfigOutput(commontype.FeeConfig{}) + }) +} + +func TestPackGetFeeConfigOutput(t *testing.T) { + testInputBytes, err := PackGetFeeConfigOutput(testFeeConfig) + require.NoError(t, err) + tests := []struct { + name string + input []byte + skipLenCheck bool + expectedErr string + expectedOldErr string + expectedOutput commontype.FeeConfig + }{ + { + name: "empty input", + input: []byte{}, + skipLenCheck: false, + expectedErr: ErrInvalidLen.Error(), + expectedOldErr: ErrInvalidLen.Error(), + }, + { + name: "empty input skip len check", + input: []byte{}, + skipLenCheck: true, + expectedErr: "attempting to unmarshall an empty string", + expectedOldErr: ErrInvalidLen.Error(), + }, + { + name: "input with extra bytes", + input: append(testInputBytes, make([]byte, 32)...), + skipLenCheck: false, + expectedErr: ErrInvalidLen.Error(), + expectedOldErr: ErrInvalidLen.Error(), + }, + { + name: "input with extra bytes skip len check", + input: append(testInputBytes, make([]byte, 32)...), + skipLenCheck: true, + expectedErr: "", + expectedOldErr: ErrInvalidLen.Error(), + expectedOutput: testFeeConfig, + }, + { + name: "input with extra bytes (not divisible by 32)", + input: append(testInputBytes, make([]byte, 33)...), + skipLenCheck: false, + expectedErr: ErrInvalidLen.Error(), + expectedOldErr: ErrInvalidLen.Error(), + }, + { + name: "input with extra bytes (not divisible by 32) skip len check", + input: append(testInputBytes, make([]byte, 33)...), + skipLenCheck: true, + expectedErr: "improperly formatted output", + expectedOldErr: ErrInvalidLen.Error(), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + unpacked, err := UnpackGetFeeConfigOutput(test.input, test.skipLenCheck) + if test.expectedErr != "" { + require.ErrorContains(t, err, test.expectedErr) + } else { + require.NoError(t, err) + require.True(t, test.expectedOutput.Equal(&unpacked), "not equal: expectedOutput %v, unpacked %v", test.expectedOutput, unpacked) + } + oldUnpacked, oldErr := OldUnpackFeeConfig(test.input) + if test.expectedOldErr != "" { + require.ErrorContains(t, oldErr, test.expectedOldErr) + } else { + require.NoError(t, oldErr) + require.True(t, test.expectedOutput.Equal(&oldUnpacked), "not equal: expectedOutput %v, oldUnpacked %v", test.expectedOutput, oldUnpacked) + } + }) + } +} + +func TestGetFeeConfig(t *testing.T) { + // Compare OldPackGetFeeConfigInput vs PackGetFeeConfig + // to see if they are equivalent + input := OldPackGetFeeConfigInput() + + input2, err := PackGetFeeConfig() + require.NoError(t, err) + + require.Equal(t, input, input2) +} + +func TestGetLastChangedAtInput(t *testing.T) { + // Compare OldPackGetFeeConfigInput vs PackGetFeeConfigLastChangedAt + // to see if they are equivalent + + input := OldPackGetLastChangedAtInput() + + input2, err := PackGetFeeConfigLastChangedAt() + require.NoError(t, err) + + require.Equal(t, input, input2) +} + +func FuzzPackGetLastChangedAtOutput(f *testing.F) { + f.Add([]byte{}) + f.Add(big.NewInt(0).Bytes()) + f.Add(big.NewInt(1).Bytes()) + f.Add(math.MaxBig256.Bytes()) + f.Add(math.MaxBig256.Sub(math.MaxBig256, common.Big1).Bytes()) + f.Add(math.MaxBig256.Add(math.MaxBig256, common.Big1).Bytes()) + f.Fuzz(func(t *testing.T, bigIntBytes []byte) { + bigIntVal := new(big.Int).SetBytes(bigIntBytes) + doCheckOutputs := true + // we can only check if outputs are correct if the value is less than MaxUint256 + // otherwise the value will be truncated when packed, + // and thus unpacked output will not be equal to the value + if bigIntVal.Cmp(abi.MaxUint256) > 0 { + doCheckOutputs = false + } + testOldPackGetLastChangedAtOutputEqual(t, bigIntVal, doCheckOutputs) + }) +} + +func FuzzPackSetFeeConfigEqualTest(f *testing.F) { + f.Add([]byte{}, uint64(0)) + f.Add(big.NewInt(0).Bytes(), uint64(0)) + f.Add(big.NewInt(1).Bytes(), uint64(math.MaxUint64)) + f.Add(math.MaxBig256.Bytes(), uint64(0)) + f.Add(math.MaxBig256.Sub(math.MaxBig256, common.Big1).Bytes(), uint64(0)) + f.Add(math.MaxBig256.Add(math.MaxBig256, common.Big1).Bytes(), uint64(0)) + f.Fuzz(func(t *testing.T, bigIntBytes []byte, blockRate uint64) { + bigIntVal := new(big.Int).SetBytes(bigIntBytes) + feeConfig := commontype.FeeConfig{ + GasLimit: bigIntVal, + TargetBlockRate: blockRate, + MinBaseFee: bigIntVal, + TargetGas: bigIntVal, + BaseFeeChangeDenominator: bigIntVal, + MinBlockGasCost: bigIntVal, + MaxBlockGasCost: bigIntVal, + BlockGasCostStep: bigIntVal, + } + doCheckOutputs := true + // we can only check if outputs are correct if the value is less than MaxUint256 + // otherwise the value will be truncated when packed, + // and thus unpacked output will not be equal to the value + if bigIntVal.Cmp(abi.MaxUint256) > 0 { + doCheckOutputs = false + } + testOldPackSetFeeConfigInputEqual(t, feeConfig, doCheckOutputs) + }) +} + +func TestOldPackSetFeeConfigInputEqual(t *testing.T) { + testOldPackSetFeeConfigInputEqual(t, testFeeConfig, true) +} + +func TestPackSetFeeConfigInputPanic(t *testing.T) { + require.Panics(t, func() { + _, _ = OldPackSetFeeConfig(commontype.FeeConfig{}) + }) + require.Panics(t, func() { + _, _ = PackSetFeeConfig(commontype.FeeConfig{}) + }) +} + +func TestPackSetFeeConfigInput(t *testing.T) { + testInputBytes, err := PackSetFeeConfig(testFeeConfig) + require.NoError(t, err) + // exclude 4 bytes for function selector + testInputBytes = testInputBytes[4:] + tests := []struct { + name string + input []byte + skipLenCheck bool + expectedErr string + expectedOldErr string + expectedOutput commontype.FeeConfig + }{ + { + name: "empty input", + input: []byte{}, + skipLenCheck: false, + expectedErr: ErrInvalidLen.Error(), + expectedOldErr: ErrInvalidLen.Error(), + }, + { + name: "empty input skip len check", + input: []byte{}, + skipLenCheck: true, + expectedErr: "attempting to unmarshall an empty string", + expectedOldErr: ErrInvalidLen.Error(), + }, + { + name: "input with extra bytes", + input: append(testInputBytes, make([]byte, 32)...), + skipLenCheck: false, + expectedErr: ErrInvalidLen.Error(), + expectedOldErr: ErrInvalidLen.Error(), + }, + { + name: "input with extra bytes skip len check", + input: append(testInputBytes, make([]byte, 32)...), + skipLenCheck: true, + expectedErr: "", + expectedOldErr: ErrInvalidLen.Error(), + expectedOutput: testFeeConfig, + }, + { + name: "input with extra bytes (not divisible by 32)", + input: append(testInputBytes, make([]byte, 33)...), + skipLenCheck: false, + expectedErr: ErrInvalidLen.Error(), + expectedOldErr: ErrInvalidLen.Error(), + }, + { + name: "input with extra bytes (not divisible by 32) skip len check", + input: append(testInputBytes, make([]byte, 33)...), + skipLenCheck: true, + expectedErr: "improperly formatted input", + expectedOldErr: ErrInvalidLen.Error(), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + unpacked, err := UnpackSetFeeConfigInput(test.input, test.skipLenCheck) + if test.expectedErr != "" { + require.ErrorContains(t, err, test.expectedErr) + } else { + require.NoError(t, err) + require.True(t, test.expectedOutput.Equal(&unpacked), "not equal: expectedOutput %v, unpacked %v", test.expectedOutput, unpacked) + } + oldUnpacked, oldErr := OldUnpackFeeConfig(test.input) + if test.expectedOldErr != "" { + require.ErrorContains(t, oldErr, test.expectedOldErr) + } else { + require.NoError(t, oldErr) + require.True(t, test.expectedOutput.Equal(&oldUnpacked), "not equal: expectedOutput %v, oldUnpacked %v", test.expectedOutput, oldUnpacked) + } + }) + } +} + +func TestFunctionSignatures(t *testing.T) { + abiSetFeeConfig := FeeManagerABI.Methods["setFeeConfig"] + require.Equal(t, setFeeConfigSignature, abiSetFeeConfig.ID) + + abiGetFeeConfig := FeeManagerABI.Methods["getFeeConfig"] + require.Equal(t, getFeeConfigSignature, abiGetFeeConfig.ID) + + abiGetFeeConfigLastChangedAt := FeeManagerABI.Methods["getFeeConfigLastChangedAt"] + require.Equal(t, getFeeConfigLastChangedAtSignature, abiGetFeeConfigLastChangedAt.ID) +} + +func testOldPackGetFeeConfigOutputEqual(t *testing.T, feeConfig commontype.FeeConfig, checkOutputs bool) { + t.Helper() + t.Run(fmt.Sprintf("TestGetFeeConfigOutput, feeConfig %v", feeConfig), func(t *testing.T) { + input, err := OldPackFeeConfig(feeConfig) + input2, err2 := PackGetFeeConfigOutput(feeConfig) + if err != nil { + require.ErrorContains(t, err2, err.Error()) + return + } + require.NoError(t, err2) + require.Equal(t, input, input2) + + config, err := OldUnpackFeeConfig(input) + unpacked, err2 := UnpackGetFeeConfigOutput(input, false) + if err != nil { + require.ErrorContains(t, err2, err.Error()) + return + } + require.NoError(t, err2) + require.True(t, config.Equal(&unpacked), "not equal: config %v, unpacked %v", feeConfig, unpacked) + if checkOutputs { + require.True(t, feeConfig.Equal(&unpacked), "not equal: feeConfig %v, unpacked %v", feeConfig, unpacked) + } + }) +} + +func testOldPackGetLastChangedAtOutputEqual(t *testing.T, blockNumber *big.Int, checkOutputs bool) { + t.Helper() + t.Run(fmt.Sprintf("TestGetLastChangedAtOutput, blockNumber %v", blockNumber), func(t *testing.T) { + input := OldPackGetLastChangedAtOutput(blockNumber) + input2, err2 := PackGetFeeConfigLastChangedAtOutput(blockNumber) + require.NoError(t, err2) + require.Equal(t, input, input2) + + value, err := OldUnpackGetLastChangedAtOutput(input) + unpacked, err2 := UnpackGetFeeConfigLastChangedAtOutput(input) + if err != nil { + require.ErrorContains(t, err2, err.Error()) + return + } + require.NoError(t, err2) + require.True(t, value.Cmp(unpacked) == 0, "not equal: value %v, unpacked %v", value, unpacked) + if checkOutputs { + require.True(t, blockNumber.Cmp(unpacked) == 0, "not equal: blockNumber %v, unpacked %v", blockNumber, unpacked) + } + }) +} + +func testOldPackSetFeeConfigInputEqual(t *testing.T, feeConfig commontype.FeeConfig, checkOutputs bool) { + t.Helper() + t.Run(fmt.Sprintf("TestSetFeeConfigInput, feeConfig %v", feeConfig), func(t *testing.T) { + input, err := OldPackSetFeeConfig(feeConfig) + input2, err2 := PackSetFeeConfig(feeConfig) + if err != nil { + require.ErrorContains(t, err2, err.Error()) + return + } + require.NoError(t, err2) + require.Equal(t, input, input2) + + value, err := OldUnpackFeeConfig(input) + unpacked, err2 := UnpackSetFeeConfigInput(input, false) + if err != nil { + require.ErrorContains(t, err2, err.Error()) + return + } + require.NoError(t, err2) + require.True(t, value.Equal(&unpacked), "not equal: value %v, unpacked %v", value, unpacked) + if checkOutputs { + require.True(t, feeConfig.Equal(&unpacked), "not equal: feeConfig %v, unpacked %v", feeConfig, unpacked) + } + }) +} + +func OldPackFeeConfig(feeConfig commontype.FeeConfig) ([]byte, error) { + return packFeeConfigHelper(feeConfig, false) +} + +func OldUnpackFeeConfig(input []byte) (commontype.FeeConfig, error) { + if len(input) != feeConfigInputLen { + return commontype.FeeConfig{}, fmt.Errorf("%w: %d", ErrInvalidLen, len(input)) + } + feeConfig := commontype.FeeConfig{} + for i := minFeeConfigFieldKey; i <= numFeeConfigField; i++ { + listIndex := i - 1 + packedElement := contract.PackedHash(input, listIndex) + switch i { + case gasLimitKey: + feeConfig.GasLimit = new(big.Int).SetBytes(packedElement) + case targetBlockRateKey: + feeConfig.TargetBlockRate = new(big.Int).SetBytes(packedElement).Uint64() + case minBaseFeeKey: + feeConfig.MinBaseFee = new(big.Int).SetBytes(packedElement) + case targetGasKey: + feeConfig.TargetGas = new(big.Int).SetBytes(packedElement) + case baseFeeChangeDenominatorKey: + feeConfig.BaseFeeChangeDenominator = new(big.Int).SetBytes(packedElement) + case minBlockGasCostKey: + feeConfig.MinBlockGasCost = new(big.Int).SetBytes(packedElement) + case maxBlockGasCostKey: + feeConfig.MaxBlockGasCost = new(big.Int).SetBytes(packedElement) + 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)) + } + } + return feeConfig, nil +} + +func packFeeConfigHelper(feeConfig commontype.FeeConfig, useSelector bool) ([]byte, error) { + hashes := []common.Hash{ + common.BigToHash(feeConfig.GasLimit), + common.BigToHash(new(big.Int).SetUint64(feeConfig.TargetBlockRate)), + common.BigToHash(feeConfig.MinBaseFee), + common.BigToHash(feeConfig.TargetGas), + common.BigToHash(feeConfig.BaseFeeChangeDenominator), + common.BigToHash(feeConfig.MinBlockGasCost), + common.BigToHash(feeConfig.MaxBlockGasCost), + common.BigToHash(feeConfig.BlockGasCostStep), + } + + if useSelector { + res := make([]byte, len(setFeeConfigSignature)+feeConfigInputLen) + err := contract.PackOrderedHashesWithSelector(res, setFeeConfigSignature, hashes) + return res, err + } + + res := make([]byte, len(hashes)*common.HashLength) + err := contract.PackOrderedHashes(res, hashes) + return res, err +} + +// PackGetFeeConfigInput packs the getFeeConfig signature +func OldPackGetFeeConfigInput() []byte { + return getFeeConfigSignature +} + +// PackGetLastChangedAtInput packs the getFeeConfigLastChangedAt signature +func OldPackGetLastChangedAtInput() []byte { + return getFeeConfigLastChangedAtSignature +} + +func OldPackGetLastChangedAtOutput(lastChangedAt *big.Int) []byte { + return common.BigToHash(lastChangedAt).Bytes() +} + +func OldUnpackGetLastChangedAtOutput(input []byte) (*big.Int, error) { + return new(big.Int).SetBytes(input), nil +} + +func OldPackSetFeeConfig(feeConfig commontype.FeeConfig) ([]byte, error) { + // function selector (4 bytes) + input(feeConfig) + return packFeeConfigHelper(feeConfig, true) +} diff --git a/precompile/contracts/nativeminter/config.go b/precompile/contracts/nativeminter/config.go index e9b358abfd..38a65ee6c8 100644 --- a/precompile/contracts/nativeminter/config.go +++ b/precompile/contracts/nativeminter/config.go @@ -16,7 +16,7 @@ import ( var _ precompileconfig.Config = &Config{} -// Config implements the StatefulPrecompileConfig interface while adding in the +// Config implements the precompileconfig.Config interface while adding in the // ContractNativeMinter specific precompile config. type Config struct { allowlist.AllowListConfig @@ -49,6 +49,9 @@ func NewDisableConfig(blockTimestamp *uint64) *Config { }, } } + +// Key returns the key for the ContractNativeMinter precompileconfig. +// This should be the same key as used in the precompile module. func (*Config) Key() string { return ConfigKey } // Equal returns true if [cfg] is a [*ContractNativeMinterConfig] and it has been configured identical to [c]. diff --git a/precompile/contracts/nativeminter/config_test.go b/precompile/contracts/nativeminter/config_test.go index c0f9958e36..91a6784e0b 100644 --- a/precompile/contracts/nativeminter/config_test.go +++ b/precompile/contracts/nativeminter/config_test.go @@ -20,6 +20,15 @@ func TestVerify(t *testing.T) { enableds := []common.Address{allowlist.TestEnabledAddr} managers := []common.Address{allowlist.TestManagerAddr} tests := map[string]testutils.ConfigVerifyTest{ + "valid config": { + Config: NewConfig(utils.NewUint64(3), admins, enableds, managers, nil), + ChainConfig: func() precompileconfig.ChainConfig { + config := precompileconfig.NewMockChainConfig(gomock.NewController(t)) + config.EXPECT().IsDUpgrade(gomock.Any()).Return(true).AnyTimes() + return config + }(), + ExpectedError: "", + }, "invalid allow list config in native minter allowlist": { Config: NewConfig(utils.NewUint64(3), admins, admins, nil, nil), ExpectedError: "cannot set address", @@ -67,7 +76,7 @@ func TestEqual(t *testing.T) { Other: precompileconfig.NewMockConfig(gomock.NewController(t)), Expected: false, }, - "different timestamps": { + "different timestamp": { Config: NewConfig(utils.NewUint64(3), admins, nil, nil, nil), Other: NewConfig(utils.NewUint64(4), admins, nil, nil, nil), Expected: false, diff --git a/precompile/contracts/nativeminter/contract.abi b/precompile/contracts/nativeminter/contract.abi new file mode 100644 index 0000000000..ebb091259d --- /dev/null +++ b/precompile/contracts/nativeminter/contract.abi @@ -0,0 +1,91 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "mintNativeCoin", + "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": "setManager", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "setNone", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] diff --git a/precompile/contracts/nativeminter/contract.go b/precompile/contracts/nativeminter/contract.go index 63d6a624da..4a77985131 100644 --- a/precompile/contracts/nativeminter/contract.go +++ b/precompile/contracts/nativeminter/contract.go @@ -4,6 +4,7 @@ package nativeminter import ( + _ "embed" "errors" "fmt" "math/big" @@ -15,20 +16,28 @@ import ( ) const ( - mintInputAddressSlot = iota - mintInputAmountSlot - mintInputLen = common.HashLength + common.HashLength MintGasCost = 30_000 ) +type MintNativeCoinInput struct { + Addr common.Address + Amount *big.Int +} + 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") + ErrInvalidLen = errors.New("invalid input length for minting") + + // NativeMinterRawABI contains the raw ABI of NativeMinter contract. + //go:embed contract.abi + NativeMinterRawABI string + + NativeMinterABI = contract.ParseABI(NativeMinterRawABI) ) // GetContractNativeMinterStatus returns the role of [address] for the minter list. @@ -42,28 +51,26 @@ func SetContractNativeMinterStatus(stateDB contract.StateDB, address common.Addr 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 +// PackMintNativeCoin packs [address] and [amount] into the appropriate arguments for mintNativeCoin. +func PackMintNativeCoin(address common.Address, amount *big.Int) ([]byte, error) { + return NativeMinterABI.Pack("mintNativeCoin", address, amount) } -// 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)) +// UnpackMintNativeCoinInput attempts to unpack [input] as address and amount. +// assumes that [input] does not include selector (omits first 4 func signature bytes) +// if [skipLenCheck] is false, it will return an error if the length of [input] is not [mintInputLen] +func UnpackMintNativeCoinInput(input []byte, skipLenCheck bool) (common.Address, *big.Int, error) { + // Initially we had this check to ensure that the input was the correct length. + // However solidity does not always pack the input to the correct length, and allows + // for extra padding bytes to be added to the end of the input. Therefore, we have removed + // this check with the DUpgrade. We still need to keep this check for backwards compatibility. + if !skipLenCheck && len(input) != mintInputLen { + return common.Address{}, nil, fmt.Errorf("%w: %d", ErrInvalidLen, len(input)) } - to := common.BytesToAddress(contract.PackedHash(input, mintInputAddressSlot)) - assetAmount := new(big.Int).SetBytes(contract.PackedHash(input, mintInputAmountSlot)) - return to, assetAmount, nil + inputStruct := MintNativeCoinInput{} + err := NativeMinterABI.UnpackInputIntoInterface(&inputStruct, "mintNativeCoin", input) + + return inputStruct.Addr, inputStruct.Amount, err } // mintNativeCoin checks if the caller is permissioned for minting operation. @@ -77,7 +84,8 @@ func mintNativeCoin(accessibleState contract.AccessibleState, caller common.Addr return nil, remainingGas, vmerrs.ErrWriteProtection } - to, amount, err := UnpackMintInput(input) + // We skip the fixed length check with DUpgrade + to, amount, err := UnpackMintNativeCoinInput(input, contract.IsDUpgradeActivated(accessibleState)) if err != nil { return nil, remainingGas, err } @@ -99,20 +107,27 @@ func mintNativeCoin(accessibleState contract.AccessibleState, caller common.Addr return []byte{}, remainingGas, nil } -// createNativeMinterPrecompile returns a StatefulPrecompiledContract for native coin minting. The precompile -// is accessed controlled by an allow list at [precompileAddr]. +// createNativeMinterPrecompile returns a StatefulPrecompiledContract with getters and setters for the precompile. +// Access to the getters/setters is controlled by an allow list for ContractAddress. func createNativeMinterPrecompile() contract.StatefulPrecompiledContract { - enabledFuncs := allowlist.CreateAllowListFunctions(ContractAddress) + var functions []*contract.StatefulPrecompileFunction + functions = append(functions, allowlist.CreateAllowListFunctions(ContractAddress)...) - mintFunc := contract.NewStatefulPrecompileFunction(mintSignature, mintNativeCoin) + abiFunctionMap := map[string]contract.RunStatefulPrecompileFunc{ + "mintNativeCoin": mintNativeCoin, + } - enabledFuncs = append(enabledFuncs, mintFunc) + for name, function := range abiFunctionMap { + method, ok := NativeMinterABI.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. - 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. + statefulContract, err := contract.NewStatefulPrecompileContract(nil, functions) if err != nil { panic(err) } - return contract + return statefulContract } diff --git a/precompile/contracts/nativeminter/contract_test.go b/precompile/contracts/nativeminter/contract_test.go index 15f62b2597..6f6a30a82c 100644 --- a/precompile/contracts/nativeminter/contract_test.go +++ b/precompile/contracts/nativeminter/contract_test.go @@ -9,156 +9,205 @@ import ( "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/precompileconfig" "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" + "go.uber.org/mock/gomock" ) -var tests = map[string]testutils.PrecompileTest{ - "mint funds from no role fails": { - Caller: allowlist.TestNoRoleAddr, - BeforeHook: allowlist.SetDefaultRoles(Module.Address), - InputFn: func(t testing.TB) []byte { - input, err := PackMintInput(allowlist.TestNoRoleAddr, common.Big1) - require.NoError(t, err) +var ( + tests = map[string]testutils.PrecompileTest{ + "calling mintNativeCoin from NoRole should fail": { + Caller: allowlist.TestNoRoleAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + input, err := PackMintNativeCoin(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.TB) []byte { - input, err := PackMintInput(allowlist.TestEnabledAddr, common.Big1) - require.NoError(t, err) - - return input - }, - SuppliedGas: MintGasCost, - ReadOnly: false, - ExpectedRes: []byte{}, - AfterHook: func(t testing.TB, 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), + return input }, + SuppliedGas: MintGasCost, + ReadOnly: false, + ExpectedErr: ErrCannotMint.Error(), }, - AfterHook: func(t testing.TB, state contract.StateDB) { - require.Equal(t, common.Big2, state.GetBalance(allowlist.TestEnabledAddr), "expected minted funds") + "calling mintNativeCoin from Enabled should succeed": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + input, err := PackMintNativeCoin(allowlist.TestEnabledAddr, common.Big1) + require.NoError(t, err) + + return input + }, + SuppliedGas: MintGasCost, + ReadOnly: false, + ExpectedRes: []byte{}, + AfterHook: func(t testing.TB, state contract.StateDB) { + require.Equal(t, common.Big1, state.GetBalance(allowlist.TestEnabledAddr), "expected minted funds") + }, }, - }, - "mint funds from manager role succeeds": { - Caller: allowlist.TestManagerAddr, - BeforeHook: allowlist.SetDefaultRoles(Module.Address), - InputFn: func(t testing.TB) []byte { - input, err := PackMintInput(allowlist.TestEnabledAddr, common.Big1) - require.NoError(t, err) - - return input + "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.TB, state contract.StateDB) { + require.Equal(t, common.Big2, state.GetBalance(allowlist.TestEnabledAddr), "expected minted funds") + }, }, - SuppliedGas: MintGasCost, - ReadOnly: false, - ExpectedRes: []byte{}, - AfterHook: func(t testing.TB, state contract.StateDB) { - require.Equal(t, common.Big1, state.GetBalance(allowlist.TestEnabledAddr), "expected minted funds") + "calling mintNativeCoin from Manager should succeed": { + Caller: allowlist.TestManagerAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + input, err := PackMintNativeCoin(allowlist.TestEnabledAddr, common.Big1) + require.NoError(t, err) + + return input + }, + SuppliedGas: MintGasCost, + ReadOnly: false, + ExpectedRes: []byte{}, + AfterHook: func(t testing.TB, state contract.StateDB) { + require.Equal(t, common.Big1, state.GetBalance(allowlist.TestEnabledAddr), "expected minted funds") + }, }, - }, - "mint funds from admin address": { - Caller: allowlist.TestAdminAddr, - BeforeHook: allowlist.SetDefaultRoles(Module.Address), - InputFn: func(t testing.TB) []byte { - input, err := PackMintInput(allowlist.TestAdminAddr, common.Big1) - require.NoError(t, err) - - return input + "calling mintNativeCoin from Admin should succeed": { + Caller: allowlist.TestAdminAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + input, err := PackMintNativeCoin(allowlist.TestAdminAddr, common.Big1) + require.NoError(t, err) + + return input + }, + SuppliedGas: MintGasCost, + ReadOnly: false, + ExpectedRes: []byte{}, + AfterHook: func(t testing.TB, state contract.StateDB) { + require.Equal(t, common.Big1, state.GetBalance(allowlist.TestAdminAddr), "expected minted funds") + }, }, - SuppliedGas: MintGasCost, - ReadOnly: false, - ExpectedRes: []byte{}, - AfterHook: func(t testing.TB, 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.TB) []byte { + input, err := PackMintNativeCoin(allowlist.TestAdminAddr, math.MaxBig256) + require.NoError(t, err) + + return input + }, + SuppliedGas: MintGasCost, + ReadOnly: false, + ExpectedRes: []byte{}, + AfterHook: func(t testing.TB, state contract.StateDB) { + require.Equal(t, math.MaxBig256, state.GetBalance(allowlist.TestAdminAddr), "expected minted funds") + }, }, - }, - "mint max big funds": { - Caller: allowlist.TestAdminAddr, - BeforeHook: allowlist.SetDefaultRoles(Module.Address), - InputFn: func(t testing.TB) []byte { - input, err := PackMintInput(allowlist.TestAdminAddr, math.MaxBig256) - require.NoError(t, err) - - return input + "readOnly mint with noRole fails": { + Caller: allowlist.TestNoRoleAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + input, err := PackMintNativeCoin(allowlist.TestAdminAddr, common.Big1) + require.NoError(t, err) + + return input + }, + SuppliedGas: MintGasCost, + ReadOnly: true, + ExpectedErr: vmerrs.ErrWriteProtection.Error(), }, - SuppliedGas: MintGasCost, - ReadOnly: false, - ExpectedRes: []byte{}, - AfterHook: func(t testing.TB, state contract.StateDB) { - require.Equal(t, math.MaxBig256, state.GetBalance(allowlist.TestAdminAddr), "expected minted funds") + "readOnly mint with allow role fails": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + input, err := PackMintNativeCoin(allowlist.TestEnabledAddr, common.Big1) + require.NoError(t, err) + + return input + }, + SuppliedGas: MintGasCost, + ReadOnly: true, + ExpectedErr: vmerrs.ErrWriteProtection.Error(), }, - }, - "readOnly mint with noRole fails": { - Caller: allowlist.TestNoRoleAddr, - BeforeHook: allowlist.SetDefaultRoles(Module.Address), - InputFn: func(t testing.TB) []byte { - input, err := PackMintInput(allowlist.TestAdminAddr, common.Big1) - require.NoError(t, err) - - return input + "readOnly mint with admin role fails": { + Caller: allowlist.TestAdminAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + input, err := PackMintNativeCoin(allowlist.TestAdminAddr, common.Big1) + require.NoError(t, err) + + return input + }, + SuppliedGas: MintGasCost, + ReadOnly: true, + ExpectedErr: vmerrs.ErrWriteProtection.Error(), }, - 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.TB) []byte { - input, err := PackMintInput(allowlist.TestEnabledAddr, common.Big1) - require.NoError(t, err) - - return input + "insufficient gas mint from admin": { + Caller: allowlist.TestAdminAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + InputFn: func(t testing.TB) []byte { + input, err := PackMintNativeCoin(allowlist.TestEnabledAddr, common.Big1) + require.NoError(t, err) + + return input + }, + SuppliedGas: MintGasCost - 1, + ReadOnly: false, + ExpectedErr: vmerrs.ErrOutOfGas.Error(), }, - 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.TB) []byte { - input, err := PackMintInput(allowlist.TestAdminAddr, common.Big1) - require.NoError(t, err) - - return input + "mint with extra padded bytes should fail before DUpgrade": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + ChainConfigFn: func(t testing.TB) precompileconfig.ChainConfig { + config := precompileconfig.NewMockChainConfig(gomock.NewController(t)) + config.EXPECT().IsDUpgrade(gomock.Any()).Return(false).AnyTimes() + return config + }, + InputFn: func(t testing.TB) []byte { + input, err := PackMintNativeCoin(allowlist.TestEnabledAddr, common.Big1) + require.NoError(t, err) + + // Add extra bytes to the end of the input + input = append(input, make([]byte, 32)...) + + return input + }, + SuppliedGas: MintGasCost, + ReadOnly: false, + ExpectedErr: ErrInvalidLen.Error(), }, - 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.TB) []byte { - input, err := PackMintInput(allowlist.TestEnabledAddr, common.Big1) - require.NoError(t, err) - - return input + "mint with extra padded bytes should succeed with DUpgrade": { + Caller: allowlist.TestEnabledAddr, + BeforeHook: allowlist.SetDefaultRoles(Module.Address), + ChainConfigFn: func(t testing.TB) precompileconfig.ChainConfig { + config := precompileconfig.NewMockChainConfig(gomock.NewController(t)) + config.EXPECT().IsDUpgrade(gomock.Any()).Return(true).AnyTimes() + return config + }, + InputFn: func(t testing.TB) []byte { + input, err := PackMintNativeCoin(allowlist.TestEnabledAddr, common.Big1) + require.NoError(t, err) + + // Add extra bytes to the end of the input + input = append(input, make([]byte, 32)...) + + return input + }, + ExpectedRes: []byte{}, + SuppliedGas: MintGasCost, + ReadOnly: false, + AfterHook: func(t testing.TB, state contract.StateDB) { + require.Equal(t, common.Big1, state.GetBalance(allowlist.TestEnabledAddr), "expected minted funds") + }, }, - SuppliedGas: MintGasCost - 1, - ReadOnly: false, - ExpectedErr: vmerrs.ErrOutOfGas.Error(), - }, -} + } +) func TestContractNativeMinterRun(t *testing.T) { allowlist.RunPrecompileWithAllowListTests(t, Module, state.NewTestStateDB, tests) diff --git a/precompile/contracts/nativeminter/module.go b/precompile/contracts/nativeminter/module.go index eb49cb1a23..ce62cee149 100644 --- a/precompile/contracts/nativeminter/module.go +++ b/precompile/contracts/nativeminter/module.go @@ -21,6 +21,7 @@ const ConfigKey = "contractNativeMinterConfig" var ContractAddress = common.HexToAddress("0x0200000000000000000000000000000000000001") +// Module is the precompile module. It is used to register the precompile contract. var Module = modules.Module{ ConfigKey: ConfigKey, Address: ContractAddress, @@ -36,11 +37,14 @@ func init() { } } +// MakeConfig returns a new precompile config instance. +// This is required to Marshal/Unmarshal the precompile config. func (*configurator) MakeConfig() precompileconfig.Config { return new(Config) } -// Configure configures [state] with the desired admins based on [cfg]. +// Configure configures [state] with the given [cfg] precompileconfig. +// This function is called by the EVM once per precompile contract activation. func (*configurator) Configure(chainConfig precompileconfig.ChainConfig, cfg precompileconfig.Config, state contract.StateDB, blockContext contract.ConfigurationBlockContext) error { config, ok := cfg.(*Config) if !ok { diff --git a/precompile/contracts/nativeminter/unpack_pack_test.go b/precompile/contracts/nativeminter/unpack_pack_test.go new file mode 100644 index 0000000000..860249f118 --- /dev/null +++ b/precompile/contracts/nativeminter/unpack_pack_test.go @@ -0,0 +1,183 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package nativeminter + +import ( + "fmt" + "math/big" + "testing" + + "github.com/ava-labs/subnet-evm/accounts/abi" + "github.com/ava-labs/subnet-evm/constants" + "github.com/ava-labs/subnet-evm/precompile/contract" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" +) + +var ( + mintSignature = contract.CalculateFunctionSelector("mintNativeCoin(address,uint256)") // address, amount +) + +func FuzzPackMintNativeCoinEqualTest(f *testing.F) { + key, err := crypto.GenerateKey() + require.NoError(f, err) + addr := crypto.PubkeyToAddress(key.PublicKey) + testAddrBytes := addr.Bytes() + f.Add(testAddrBytes, common.Big0.Bytes()) + f.Add(testAddrBytes, common.Big1.Bytes()) + f.Add(testAddrBytes, abi.MaxUint256.Bytes()) + f.Add(testAddrBytes, new(big.Int).Sub(abi.MaxUint256, common.Big1).Bytes()) + f.Add(testAddrBytes, new(big.Int).Add(abi.MaxUint256, common.Big1).Bytes()) + f.Add(constants.BlackholeAddr.Bytes(), common.Big2.Bytes()) + f.Fuzz(func(t *testing.T, b []byte, bigIntBytes []byte) { + bigIntVal := new(big.Int).SetBytes(bigIntBytes) + doCheckOutputs := true + // we can only check if outputs are correct if the value is less than MaxUint256 + // otherwise the value will be truncated when packed, + // and thus unpacked output will not be equal to the value + if bigIntVal.Cmp(abi.MaxUint256) > 0 { + doCheckOutputs = false + } + testOldPackMintNativeCoinEqual(t, common.BytesToAddress(b), bigIntVal, doCheckOutputs) + }) +} + +func TestUnpackMintNativeCoinInput(t *testing.T) { + testInputBytes, err := PackMintNativeCoin(constants.BlackholeAddr, common.Big2) + require.NoError(t, err) + // exclude 4 bytes for function selector + testInputBytes = testInputBytes[4:] + tests := []struct { + name string + input []byte + skipLenCheck bool + expectedErr string + expectedOldErr string + expectedAddr common.Address + expectedAmount *big.Int + }{ + { + name: "empty input", + input: []byte{}, + skipLenCheck: false, + expectedErr: ErrInvalidLen.Error(), + expectedOldErr: ErrInvalidLen.Error(), + }, + { + name: "empty input skip len check", + input: []byte{}, + skipLenCheck: true, + expectedErr: "attempting to unmarshall an empty string", + expectedOldErr: ErrInvalidLen.Error(), + }, + { + name: "input with extra bytes", + input: append(testInputBytes, make([]byte, 32)...), + skipLenCheck: false, + expectedErr: ErrInvalidLen.Error(), + expectedOldErr: ErrInvalidLen.Error(), + }, + { + name: "input with extra bytes skip len check", + input: append(testInputBytes, make([]byte, 32)...), + skipLenCheck: true, + expectedErr: "", + expectedOldErr: ErrInvalidLen.Error(), + expectedAddr: constants.BlackholeAddr, + expectedAmount: common.Big2, + }, + { + name: "input with extra bytes (not divisible by 32)", + input: append(testInputBytes, make([]byte, 33)...), + skipLenCheck: false, + expectedErr: ErrInvalidLen.Error(), + expectedOldErr: ErrInvalidLen.Error(), + }, + { + name: "input with extra bytes (not divisible by 32) skip len check", + input: append(testInputBytes, make([]byte, 33)...), + skipLenCheck: true, + expectedErr: "improperly formatted input", + expectedOldErr: ErrInvalidLen.Error(), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + unpackedAddress, unpackedAmount, err := UnpackMintNativeCoinInput(test.input, test.skipLenCheck) + if test.expectedErr != "" { + require.ErrorContains(t, err, test.expectedErr) + } else { + require.NoError(t, err) + require.Equal(t, test.expectedAddr, unpackedAddress) + require.True(t, test.expectedAmount.Cmp(unpackedAmount) == 0, "expected %s, got %s", test.expectedAmount.String(), unpackedAmount.String()) + } + oldUnpackedAddress, oldUnpackedAmount, oldErr := OldUnpackMintNativeCoinInput(test.input) + if test.expectedOldErr != "" { + require.ErrorContains(t, oldErr, test.expectedOldErr) + } else { + require.NoError(t, oldErr) + require.Equal(t, test.expectedAddr, oldUnpackedAddress) + require.True(t, test.expectedAmount.Cmp(oldUnpackedAmount) == 0, "expected %s, got %s", test.expectedAmount.String(), oldUnpackedAmount.String()) + } + }) + } +} + +func TestFunctionSignatures(t *testing.T) { + // Test that the mintNativeCoin signature is correct + abiMintNativeCoin := NativeMinterABI.Methods["mintNativeCoin"] + require.Equal(t, mintSignature, abiMintNativeCoin.ID) +} + +func testOldPackMintNativeCoinEqual(t *testing.T, addr common.Address, amount *big.Int, checkOutputs bool) { + t.Helper() + t.Run(fmt.Sprintf("TestUnpackAndPacks, addr: %s, amount: %s", addr.String(), amount.String()), func(t *testing.T) { + input, err := OldPackMintNativeCoinInput(addr, amount) + input2, err2 := PackMintNativeCoin(addr, amount) + if err != nil { + require.ErrorContains(t, err2, err.Error()) + return + } + require.NoError(t, err2) + require.Equal(t, input, input2) + + input = input[4:] + to, assetAmount, err := OldUnpackMintNativeCoinInput(input) + unpackedAddr, unpackedAmount, err2 := UnpackMintNativeCoinInput(input, false) + if err != nil { + require.ErrorContains(t, err2, err.Error()) + return + } + require.NoError(t, err2) + require.Equal(t, to, unpackedAddr) + require.Equal(t, assetAmount.Bytes(), unpackedAmount.Bytes()) + if checkOutputs { + require.Equal(t, addr, to) + require.Equal(t, amount.Bytes(), assetAmount.Bytes()) + } + }) +} + +func OldPackMintNativeCoinInput(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 +} + +func OldUnpackMintNativeCoinInput(input []byte) (common.Address, *big.Int, error) { + mintInputAddressSlot := 0 + mintInputAmountSlot := 1 + if len(input) != mintInputLen { + return common.Address{}, nil, fmt.Errorf("%w: %d", ErrInvalidLen, len(input)) + } + to := common.BytesToAddress(contract.PackedHash(input, mintInputAddressSlot)) + assetAmount := new(big.Int).SetBytes(contract.PackedHash(input, mintInputAmountSlot)) + return to, assetAmount, nil +} diff --git a/precompile/contracts/rewardmanager/contract.abi b/precompile/contracts/rewardmanager/contract.abi index d21d5bdc6b..544767ec0b 100644 --- a/precompile/contracts/rewardmanager/contract.abi +++ b/precompile/contracts/rewardmanager/contract.abi @@ -1 +1,113 @@ -[{"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 +[ + { + "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" + } + ] diff --git a/precompile/contracts/txallowlist/module.go b/precompile/contracts/txallowlist/module.go index 3a87a12157..f7333613c7 100644 --- a/precompile/contracts/txallowlist/module.go +++ b/precompile/contracts/txallowlist/module.go @@ -35,11 +35,14 @@ func init() { } } +// MakeConfig returns a new precompile config instance. +// This is required to Marshal/Unmarshal the precompile config. func (*configurator) MakeConfig() precompileconfig.Config { return new(Config) } -// Configure configures [state] with the initial state for the precompile. +// Configure configures [state] with the given [cfg] precompileconfig. +// This function is called by the EVM once per precompile contract activation. func (*configurator) Configure(chainConfig precompileconfig.ChainConfig, cfg precompileconfig.Config, state contract.StateDB, blockContext contract.ConfigurationBlockContext) error { config, ok := cfg.(*Config) if !ok { diff --git a/precompile/testutils/test_precompile.go b/precompile/testutils/test_precompile.go index c0c0a797c2..7a9b6c7358 100644 --- a/precompile/testutils/test_precompile.go +++ b/precompile/testutils/test_precompile.go @@ -50,6 +50,9 @@ type PrecompileTest struct { // ChainConfig is the chain config to use for the precompile's block context // If nil, the default chain config will be used. ChainConfig precompileconfig.ChainConfig + // ChainConfigFn is a function that returns the chain config to use for the precompile's block context + // If specified, ChainConfig will be ignored. + ChainConfigFn func(t testing.TB) precompileconfig.ChainConfig } type PrecompileRunparams struct { @@ -91,6 +94,9 @@ func (test PrecompileTest) setup(t testing.TB, module modules.Module, state cont } chainConfig := test.ChainConfig + if test.ChainConfigFn != nil { + chainConfig = test.ChainConfigFn(t) + } if chainConfig == nil { mockChainConfig := precompileconfig.NewMockChainConfig(ctrl) mockChainConfig.EXPECT().GetFeeConfig().AnyTimes().Return(commontype.ValidTestFeeConfig) diff --git a/x/warp/module.go b/x/warp/module.go index 98d4278178..37b7451184 100644 --- a/x/warp/module.go +++ b/x/warp/module.go @@ -15,7 +15,7 @@ import ( var _ contract.Configurator = &configurator{} -// ConfigKey is the key used in json config files to specify this precompile precompileconfig. +// ConfigKey is the key used in json config files to specify this precompile config. // must be unique across all precompiles. const ConfigKey = "warpConfig" @@ -41,7 +41,7 @@ func init() { } // MakeConfig returns a new precompile config instance. -// This is required for Marshal/Unmarshal the precompile config. +// This is required to Marshal/Unmarshal the precompile config. func (*configurator) MakeConfig() precompileconfig.Config { return new(Config) }