From f4225173014c006d5f431c084b00b043e4644401 Mon Sep 17 00:00:00 2001 From: Josh Dechant Date: Mon, 26 Feb 2024 09:36:36 -0500 Subject: [PATCH 1/9] initial pass at adding stateful precompiles to evm --- core/vm/evm.go | 24 +++-- core/vm/interface.go | 8 ++ core/vm/precompile_manager.go | 161 ++++++++++++++++++++++++++++++++ precompile/errors.go | 7 ++ precompile/interface.go | 34 +++++++ precompile/stateful_context.go | 82 ++++++++++++++++ precompile/stateful_contract.go | 31 ++++++ 7 files changed, 339 insertions(+), 8 deletions(-) create mode 100644 core/vm/precompile_manager.go create mode 100644 precompile/errors.go create mode 100644 precompile/interface.go create mode 100644 precompile/stateful_context.go create mode 100644 precompile/stateful_contract.go diff --git a/core/vm/evm.go b/core/vm/evm.go index 088b18aaa4ff..58ed6ee0c3aa 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -121,6 +121,8 @@ type EVM struct { // available gas is calculated in gasCall* according to the 63/64 rule and later // applied in opCall*. callGasTemp uint64 + // stateful precompiles + precompileManager PrecompileManager } // NewEVM returns a new EVM. The returned EVM is not thread safe and should @@ -146,6 +148,12 @@ func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig chainRules: chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time), } evm.interpreter = NewEVMInterpreter(evm) + evm.precompileManager = NewPrecompileManager(evm) + + // register precompiles here, e.g: + // evm.precompileManager.Register(common.HexToAddress("0x1000"), nativeminter.NewNativeMinter()) + // evm.precompileManager.Register(common.HexToAddress("0x1001"), compress.NewCompress()) + return evm } @@ -186,7 +194,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas return nil, gas, ErrInsufficientBalance } snapshot := evm.StateDB.Snapshot() - p, isPrecompile := evm.precompile(addr) + isPrecompile := evm.precompileManager.IsPrecompile(addr) debug := evm.Config.Tracer != nil if !evm.StateDB.Exist(addr) { @@ -224,7 +232,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas } if isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas) + ret, gas, err = evm.precompileManager.Run(addr, input, caller.Address(), value, gas, false) } else { // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. @@ -286,8 +294,8 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, } // It is allowed to call precompiles, even via delegatecall - if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas) + if isPrecompile := evm.precompileManager.IsPrecompile(addr); isPrecompile { + ret, gas, err = evm.precompileManager.Run(addr, input, caller.Address(), value, gas, false) } else { addrCopy := addr // Initialise a new contract and set the code that is to be used by the EVM. @@ -331,8 +339,8 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by } // It is allowed to call precompiles, even via delegatecall - if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas) + if isPrecompile := evm.precompileManager.IsPrecompile(addr); isPrecompile { + ret, gas, err = evm.precompileManager.Run(addr, input, caller.Address(), nil, gas, false) } else { addrCopy := addr // Initialise a new contract and make initialise the delegate values @@ -380,8 +388,8 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte }(gas) } - if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas) + if isPrecompile := evm.precompileManager.IsPrecompile(addr); isPrecompile { + ret, gas, err = evm.precompileManager.Run(addr, input, caller.Address(), new(big.Int), gas, true) } else { // At this point, we use a copy of address. If we don't, the go compiler will // leak the 'contract' to the outer scope, and make allocation for 'contract' diff --git a/core/vm/interface.go b/core/vm/interface.go index 26814d3d2f0e..bf77aab346d8 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -22,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/precompile" ) // StateDB is an EVM database for full state querying. @@ -92,3 +93,10 @@ type CallContext interface { // Create creates a new contract Create(env *EVM, me ContractRef, data []byte, gas, value *big.Int) ([]byte, common.Address, error) } + +// PrecompileManager registers and runs stateful precompiles +type PrecompileManager interface { + IsPrecompile(addr common.Address) bool + Run(addr common.Address, input []byte, caller common.Address, value *big.Int, suppliedGas uint64, readonly bool) (ret []byte, remainingGas uint64, err error) + Register(addr common.Address, p precompile.StatefulPrecompiledContract) error +} diff --git a/core/vm/precompile_manager.go b/core/vm/precompile_manager.go new file mode 100644 index 000000000000..5068a28c23d2 --- /dev/null +++ b/core/vm/precompile_manager.go @@ -0,0 +1,161 @@ +package vm + +import ( + "fmt" + "math/big" + "reflect" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/precompile" +) + +type methodID [4]byte + +type statefulMethod struct { + abiMethod abi.Method + reflectMethod reflect.Method +} + +type precompileMethods map[methodID]*statefulMethod + +type precompileManager struct { + evm *EVM + precompiles map[common.Address]precompile.StatefulPrecompiledContract + pMethods map[common.Address]precompileMethods +} + +func NewPrecompileManager(evm *EVM) PrecompileManager { + precompiles := make(map[common.Address]precompile.StatefulPrecompiledContract) + pMethods := make(map[common.Address]precompileMethods) + return &precompileManager{ + evm: evm, + precompiles: precompiles, + pMethods: pMethods, + } +} + +func (pm *precompileManager) IsPrecompile(addr common.Address) bool { + _, isEvmPrecompile := pm.evm.precompile(addr) + if isEvmPrecompile { + return true + } + + _, isStatefulPrecompile := pm.precompiles[addr] + return isStatefulPrecompile +} + +func (pm *precompileManager) Run( + addr common.Address, + input []byte, + caller common.Address, + value *big.Int, + suppliedGas uint64, + readOnly bool, +) (ret []byte, remainingGas uint64, err error) { + + // run core evm precompile + p, isEvmPrecompile := pm.evm.precompile(addr) + if isEvmPrecompile { + return RunPrecompiledContract(p, input, suppliedGas) + } + + contract, ok := pm.precompiles[addr] + if !ok { + return nil, 0, fmt.Errorf("no precompiled contract at address %v", addr.Hex()) + } + + // Extract the method ID from the input + methodId := methodID(input) + // Try to get the method from the precompiled contracts using the method ID + method, exists := pm.pMethods[addr][methodId] + if !exists { + return nil, 0, fmt.Errorf("no method with id %v in precompiled contract at address %v", methodId, addr.Hex()) + } + + gasCost := contract.RequiredGas(input) + if gasCost > suppliedGas { + return nil, 0, ErrOutOfGas + } + + // Unpack the input arguments using the ABI method's inputs + unpackedArgs, err := method.abiMethod.Inputs.Unpack(input[4:]) + if err != nil { + return nil, 0, err + } + + // Convert the unpacked args to reflect values. + reflectedUnpackedArgs := make([]reflect.Value, 0, len(unpackedArgs)) + for _, unpacked := range unpackedArgs { + reflectedUnpackedArgs = append(reflectedUnpackedArgs, reflect.ValueOf(unpacked)) + } + + ctx := precompile.NewStatefulContext(pm.evm.StateDB, addr, caller, value) + + // Make sure the readOnly is only set if we aren't in readOnly yet. + // This also makes sure that the readOnly flag isn't removed for child calls. + if readOnly && !ctx.IsReadOnly() { + ctx.SetReadOnly(true) + defer func() { ctx.SetReadOnly(false) }() + } + + results := method.reflectMethod.Func.Call(append([]reflect.Value{reflect.ValueOf(ctx)}, reflectedUnpackedArgs...)) + + // check if precompile returned an error + if len(results) > 0 { + if err, ok := results[len(results)-1].Interface().(error); ok && err != nil { + return nil, 0, err + } + } + + // Pack the result + var output []byte + if len(results) > 1 { + interfaceArgs := make([]interface{}, len(results)-1) + for i, v := range results[:len(results)-1] { + interfaceArgs[i] = v.Interface() + } + output, err = method.abiMethod.Outputs.Pack(interfaceArgs...) + if err != nil { + return nil, 0, err + } + } + + suppliedGas -= gasCost + return output, suppliedGas, nil +} + +func (pm *precompileManager) Register(addr common.Address, p precompile.StatefulPrecompiledContract) error { + if _, exists := pm.precompiles[addr]; exists { + return fmt.Errorf("precompiled contract already exists at address %v", addr.Hex()) + } + + // niaeve implementation; abi method names must match precompile method names 1:1 + // NOTE, this does not allow using solidity method overloading + abiMethods := p.GetABI().Methods + contractType := reflect.ValueOf(p).Type() + precompileMethods := make(precompileMethods) + for i := 0; i < contractType.NumMethod(); i++ { + method := contractType.Method(i) + abiMethodName := strings.ToLower(string(method.Name[0])) + method.Name[1:] + if _, exists := abiMethods[abiMethodName]; exists { + methodId := methodID(abiMethods[abiMethodName].ID) + precompileMethods[methodId] = &statefulMethod{ + abiMethod: abiMethods[abiMethodName], + reflectMethod: method, + } + } + } + + // Sanity check, ensure all abi methods are implemented + for _, abiMethod := range abiMethods { + if _, exists := precompileMethods[methodID(abiMethod.ID)]; !exists { + return fmt.Errorf("precompiled contract does not implement abi method %s", abiMethod.Name) + } + } + + pm.precompiles[addr] = p + pm.pMethods[addr] = precompileMethods + return nil +} diff --git a/precompile/errors.go b/precompile/errors.go new file mode 100644 index 000000000000..4858fdfe011f --- /dev/null +++ b/precompile/errors.go @@ -0,0 +1,7 @@ +package precompile + +import "errors" + +var ( + ErrWriteProtection = errors.New("write protection") +) diff --git a/precompile/interface.go b/precompile/interface.go new file mode 100644 index 000000000000..65117a16f261 --- /dev/null +++ b/precompile/interface.go @@ -0,0 +1,34 @@ +package precompile + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +type StateDB interface { + SubBalance(common.Address, *big.Int) + AddBalance(common.Address, *big.Int) + GetBalance(common.Address) *big.Int + GetState(common.Address, common.Hash) common.Hash + SetState(common.Address, common.Hash, common.Hash) +} + +type StatefulContext interface { + SetState(common.Hash, common.Hash) error + GetState(common.Hash) common.Hash + SubBalance(common.Address, *big.Int) error + AddBalance(common.Address, *big.Int) error + GetBalance(common.Address) *big.Int + Address() common.Address + MsgSender() common.Address + MsgValue() *big.Int + IsReadOnly() bool + SetReadOnly(bool) +} + +type StatefulPrecompiledContract interface { + GetABI() abi.ABI + RequiredGas(input []byte) uint64 // RequiredPrice calculates the contract gas use +} diff --git a/precompile/stateful_context.go b/precompile/stateful_context.go new file mode 100644 index 000000000000..c2bb72dc0f37 --- /dev/null +++ b/precompile/stateful_context.go @@ -0,0 +1,82 @@ +package precompile + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +type statefulContext struct { + state StateDB + address common.Address + msgSender common.Address + msgValue *big.Int + readOnly bool +} + +func NewStatefulContext( + state StateDB, + address common.Address, + msgSender common.Address, + msgValue *big.Int, +) StatefulContext { + return &statefulContext{ + state: state, + address: address, + msgSender: msgSender, + msgValue: msgValue, + readOnly: false, + } +} + +func (sc *statefulContext) SetState(key common.Hash, value common.Hash) error { + if sc.readOnly { + return ErrWriteProtection + } + sc.state.SetState(sc.address, key, value) + return nil +} + +func (sc *statefulContext) GetState(key common.Hash) common.Hash { + return sc.state.GetState(sc.address, key) +} + +func (sc *statefulContext) SubBalance(address common.Address, amount *big.Int) error { + if sc.readOnly { + return ErrWriteProtection + } + sc.state.SubBalance(address, amount) + return nil +} + +func (sc *statefulContext) AddBalance(address common.Address, amount *big.Int) error { + if sc.readOnly { + return ErrWriteProtection + } + sc.state.AddBalance(address, amount) + return nil +} + +func (sc *statefulContext) GetBalance(address common.Address) *big.Int { + return sc.state.GetBalance(address) +} + +func (sc *statefulContext) Address() common.Address { + return sc.address +} + +func (sc *statefulContext) MsgSender() common.Address { + return sc.msgSender +} + +func (sc *statefulContext) MsgValue() *big.Int { + return sc.msgValue +} + +func (sc *statefulContext) IsReadOnly() bool { + return sc.readOnly +} + +func (sc *statefulContext) SetReadOnly(readOnly bool) { + sc.readOnly = readOnly +} diff --git a/precompile/stateful_contract.go b/precompile/stateful_contract.go new file mode 100644 index 000000000000..c5ddcb5704b6 --- /dev/null +++ b/precompile/stateful_contract.go @@ -0,0 +1,31 @@ +package precompile + +import ( + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" +) + +type statefulPrecompiledContract struct { + abi abi.ABI +} + +func NewStatefulPrecompiledContract(abiStr string) StatefulPrecompiledContract { + abi, err := abi.JSON(strings.NewReader(abiStr)) + if err != nil { + panic(err) + } + return &statefulPrecompiledContract{ + abi: abi, + } +} + +func (spc *statefulPrecompiledContract) GetABI() abi.ABI { + return spc.abi +} + +func (spc *statefulPrecompiledContract) RequiredGas(input []byte) uint64 { + // This is a placeholder implementation. The actual gas required would depend on the specific contract. + // You should replace this with the actual implementation. + return 0 +} From 1a7c6b79e853dd2be8abb9d67f44287bc1c0733a Mon Sep 17 00:00:00 2001 From: Josh Dechant Date: Tue, 27 Feb 2024 08:56:50 -0500 Subject: [PATCH 2/9] simpler precompile abi matching logic --- core/vm/evm.go | 1 + core/vm/precompile_manager.go | 44 +++++++++++++++++++++-------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/core/vm/evm.go b/core/vm/evm.go index 58ed6ee0c3aa..4826f0057415 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -153,6 +153,7 @@ func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig // register precompiles here, e.g: // evm.precompileManager.Register(common.HexToAddress("0x1000"), nativeminter.NewNativeMinter()) // evm.precompileManager.Register(common.HexToAddress("0x1001"), compress.NewCompress()) + // evm.precompileManager.Register(common.HexToAddress("0x1002"), jsonutil.NewJsonUtil()) return evm } diff --git a/core/vm/precompile_manager.go b/core/vm/precompile_manager.go index 5068a28c23d2..88bdb68b6283 100644 --- a/core/vm/precompile_manager.go +++ b/core/vm/precompile_manager.go @@ -127,31 +127,41 @@ func (pm *precompileManager) Run( } func (pm *precompileManager) Register(addr common.Address, p precompile.StatefulPrecompiledContract) error { + if _, isEvmPrecompile := pm.evm.precompile(addr); isEvmPrecompile { + return fmt.Errorf("precompiled contract already exists at address %v", addr.Hex()) + } + if _, exists := pm.precompiles[addr]; exists { return fmt.Errorf("precompiled contract already exists at address %v", addr.Hex()) } - // niaeve implementation; abi method names must match precompile method names 1:1 - // NOTE, this does not allow using solidity method overloading + // niaeve implementation; parsed abi method names must match precompile method names 1:1 + // + // Note on method naming: + // Method name is the abi method name used for internal representation. It's derived from + // the abi raw name and a suffix will be added in the case of a function overload. + // + // e.g. + // These are two functions that have the same name: + // * foo(int,int) + // * foo(uint,uint) + // The method name of the first one will be resolved as Foo while the second one + // will be resolved as Foo0. + // + // Alternatively could require each precompile to define the func mapping instead of doing this magic abiMethods := p.GetABI().Methods contractType := reflect.ValueOf(p).Type() precompileMethods := make(precompileMethods) - for i := 0; i < contractType.NumMethod(); i++ { - method := contractType.Method(i) - abiMethodName := strings.ToLower(string(method.Name[0])) + method.Name[1:] - if _, exists := abiMethods[abiMethodName]; exists { - methodId := methodID(abiMethods[abiMethodName].ID) - precompileMethods[methodId] = &statefulMethod{ - abiMethod: abiMethods[abiMethodName], - reflectMethod: method, - } - } - } - - // Sanity check, ensure all abi methods are implemented for _, abiMethod := range abiMethods { - if _, exists := precompileMethods[methodID(abiMethod.ID)]; !exists { - return fmt.Errorf("precompiled contract does not implement abi method %s", abiMethod.Name) + mName := strings.ToUpper(string(abiMethod.Name[0])) + abiMethod.Name[1:] + reflectMethod, exists := contractType.MethodByName(mName) + if !exists { + return fmt.Errorf("precompiled contract does not implement abi method %s with signature %s", abiMethod.Name, abiMethod.RawName) + } + mID := methodID(abiMethod.ID) + precompileMethods[mID] = &statefulMethod{ + abiMethod: abiMethod, + reflectMethod: reflectMethod, } } From 9fd3f6494e6ea129bbdfa79e67ef05da46bb9441 Mon Sep 17 00:00:00 2001 From: Josh Dechant Date: Tue, 27 Feb 2024 09:29:23 -0500 Subject: [PATCH 3/9] add initial native minter precompile --- core/vm/evm.go | 10 +- genesis.json | 3 +- params/config.go | 1 + precompile/bindings/i_nativeminter.abigen.go | 348 ++++++++++++++++++ .../contracts/interfaces/INativeMinter.sol | 32 ++ .../contracts/nativeminter/nativeminter.go | 142 +++++++ 6 files changed, 533 insertions(+), 3 deletions(-) create mode 100644 precompile/bindings/i_nativeminter.abigen.go create mode 100644 precompile/contracts/interfaces/INativeMinter.sol create mode 100644 precompile/contracts/nativeminter/nativeminter.go diff --git a/core/vm/evm.go b/core/vm/evm.go index 4826f0057415..929e1c3fe95a 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/precompile/contracts/nativeminter" "github.com/holiman/uint256" ) @@ -150,8 +151,13 @@ func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig evm.interpreter = NewEVMInterpreter(evm) evm.precompileManager = NewPrecompileManager(evm) - // register precompiles here, e.g: - // evm.precompileManager.Register(common.HexToAddress("0x1000"), nativeminter.NewNativeMinter()) + // register precompiles here + + // register native minter to 0x0000000000000000000000000000000000001000 + evm.precompileManager.Register( + common.HexToAddress("0x1000"), + nativeminter.NewNativeMinter(common.BigToAddress(chainConfig.AstriaNativeMinterInitialOwner)), + ) // evm.precompileManager.Register(common.HexToAddress("0x1001"), compress.NewCompress()) // evm.precompileManager.Register(common.HexToAddress("0x1002"), jsonutil.NewJsonUtil()) diff --git a/genesis.json b/genesis.json index ff586448127f..5f865cda991b 100644 --- a/genesis.json +++ b/genesis.json @@ -24,5 +24,6 @@ "astriaOverrideGenesisExtraData": true, "astriaSequencerInitialHeight": 1, "astriaDataAvailabilityInitialHeight": 1, - "astriaDataAvailabilityHeightVariance": 50 + "astriaDataAvailabilityHeightVariance": 50, + "astriaNativeMinterInitialOwner": "0x46B77EFDFB20979E1C29ec98DcE73e3eCbF64102" } diff --git a/params/config.go b/params/config.go index 342d87f8be14..234ae6d33ece 100644 --- a/params/config.go +++ b/params/config.go @@ -343,6 +343,7 @@ type ChainConfig struct { AstriaSequencerInitialHeight uint32 `json:"astriaSequencerInitialHeight"` AstriaCelestiaInitialHeight uint32 `json:"astriaCelestiaInitialHeight"` AstriaCelestiaHeightVariance uint32 `json:"astriaCelestiaHeightVariance,omitempty"` + AstriaNativeMinterInitialOwner *big.Int `json:"astriaNativeMinterInitialOwner,omitempty"` } func (c *ChainConfig) AstriaExtraData() []byte { diff --git a/precompile/bindings/i_nativeminter.abigen.go b/precompile/bindings/i_nativeminter.abigen.go new file mode 100644 index 000000000000..167fb8571bb7 --- /dev/null +++ b/precompile/bindings/i_nativeminter.abigen.go @@ -0,0 +1,348 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package bindings + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// NativeMinterMetaData contains all meta data concerning the NativeMinter contract. +var NativeMinterMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"function\",\"name\":\"burn\",\"inputs\":[{\"name\":\"addr\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"mint\",\"inputs\":[{\"name\":\"addr\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"minter\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"owner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"renounceOwnership\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setMinter\",\"inputs\":[{\"name\":\"newMinter\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"transferOwnership\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"nonpayable\"}]", +} + +// NativeMinterABI is the input ABI used to generate the binding from. +// Deprecated: Use NativeMinterMetaData.ABI instead. +var NativeMinterABI = NativeMinterMetaData.ABI + +// NativeMinter is an auto generated Go binding around an Ethereum contract. +type NativeMinter struct { + NativeMinterCaller // Read-only binding to the contract + NativeMinterTransactor // Write-only binding to the contract + NativeMinterFilterer // Log filterer for contract events +} + +// NativeMinterCaller is an auto generated read-only Go binding around an Ethereum contract. +type NativeMinterCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// NativeMinterTransactor is an auto generated write-only Go binding around an Ethereum contract. +type NativeMinterTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// NativeMinterFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type NativeMinterFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// NativeMinterSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type NativeMinterSession struct { + Contract *NativeMinter // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// NativeMinterCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type NativeMinterCallerSession struct { + Contract *NativeMinterCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// NativeMinterTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type NativeMinterTransactorSession struct { + Contract *NativeMinterTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// NativeMinterRaw is an auto generated low-level Go binding around an Ethereum contract. +type NativeMinterRaw struct { + Contract *NativeMinter // Generic contract binding to access the raw methods on +} + +// NativeMinterCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type NativeMinterCallerRaw struct { + Contract *NativeMinterCaller // Generic read-only contract binding to access the raw methods on +} + +// NativeMinterTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type NativeMinterTransactorRaw struct { + Contract *NativeMinterTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewNativeMinter creates a new instance of NativeMinter, bound to a specific deployed contract. +func NewNativeMinter(address common.Address, backend bind.ContractBackend) (*NativeMinter, error) { + contract, err := bindNativeMinter(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &NativeMinter{NativeMinterCaller: NativeMinterCaller{contract: contract}, NativeMinterTransactor: NativeMinterTransactor{contract: contract}, NativeMinterFilterer: NativeMinterFilterer{contract: contract}}, nil +} + +// NewNativeMinterCaller creates a new read-only instance of NativeMinter, bound to a specific deployed contract. +func NewNativeMinterCaller(address common.Address, caller bind.ContractCaller) (*NativeMinterCaller, error) { + contract, err := bindNativeMinter(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &NativeMinterCaller{contract: contract}, nil +} + +// NewNativeMinterTransactor creates a new write-only instance of NativeMinter, bound to a specific deployed contract. +func NewNativeMinterTransactor(address common.Address, transactor bind.ContractTransactor) (*NativeMinterTransactor, error) { + contract, err := bindNativeMinter(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &NativeMinterTransactor{contract: contract}, nil +} + +// NewNativeMinterFilterer creates a new log filterer instance of NativeMinter, bound to a specific deployed contract. +func NewNativeMinterFilterer(address common.Address, filterer bind.ContractFilterer) (*NativeMinterFilterer, error) { + contract, err := bindNativeMinter(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &NativeMinterFilterer{contract: contract}, nil +} + +// bindNativeMinter binds a generic wrapper to an already deployed contract. +func bindNativeMinter(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := NativeMinterMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_NativeMinter *NativeMinterRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _NativeMinter.Contract.NativeMinterCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_NativeMinter *NativeMinterRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _NativeMinter.Contract.NativeMinterTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_NativeMinter *NativeMinterRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _NativeMinter.Contract.NativeMinterTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_NativeMinter *NativeMinterCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _NativeMinter.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_NativeMinter *NativeMinterTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _NativeMinter.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_NativeMinter *NativeMinterTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _NativeMinter.Contract.contract.Transact(opts, method, params...) +} + +// Minter is a free data retrieval call binding the contract method 0x07546172. +// +// Solidity: function minter() view returns(address) +func (_NativeMinter *NativeMinterCaller) Minter(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _NativeMinter.contract.Call(opts, &out, "minter") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Minter is a free data retrieval call binding the contract method 0x07546172. +// +// Solidity: function minter() view returns(address) +func (_NativeMinter *NativeMinterSession) Minter() (common.Address, error) { + return _NativeMinter.Contract.Minter(&_NativeMinter.CallOpts) +} + +// Minter is a free data retrieval call binding the contract method 0x07546172. +// +// Solidity: function minter() view returns(address) +func (_NativeMinter *NativeMinterCallerSession) Minter() (common.Address, error) { + return _NativeMinter.Contract.Minter(&_NativeMinter.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_NativeMinter *NativeMinterCaller) Owner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _NativeMinter.contract.Call(opts, &out, "owner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_NativeMinter *NativeMinterSession) Owner() (common.Address, error) { + return _NativeMinter.Contract.Owner(&_NativeMinter.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_NativeMinter *NativeMinterCallerSession) Owner() (common.Address, error) { + return _NativeMinter.Contract.Owner(&_NativeMinter.CallOpts) +} + +// Burn is a paid mutator transaction binding the contract method 0x9dc29fac. +// +// Solidity: function burn(address addr, uint256 amount) returns(bool) +func (_NativeMinter *NativeMinterTransactor) Burn(opts *bind.TransactOpts, addr common.Address, amount *big.Int) (*types.Transaction, error) { + return _NativeMinter.contract.Transact(opts, "burn", addr, amount) +} + +// Burn is a paid mutator transaction binding the contract method 0x9dc29fac. +// +// Solidity: function burn(address addr, uint256 amount) returns(bool) +func (_NativeMinter *NativeMinterSession) Burn(addr common.Address, amount *big.Int) (*types.Transaction, error) { + return _NativeMinter.Contract.Burn(&_NativeMinter.TransactOpts, addr, amount) +} + +// Burn is a paid mutator transaction binding the contract method 0x9dc29fac. +// +// Solidity: function burn(address addr, uint256 amount) returns(bool) +func (_NativeMinter *NativeMinterTransactorSession) Burn(addr common.Address, amount *big.Int) (*types.Transaction, error) { + return _NativeMinter.Contract.Burn(&_NativeMinter.TransactOpts, addr, amount) +} + +// Mint is a paid mutator transaction binding the contract method 0x40c10f19. +// +// Solidity: function mint(address addr, uint256 amount) returns(bool) +func (_NativeMinter *NativeMinterTransactor) Mint(opts *bind.TransactOpts, addr common.Address, amount *big.Int) (*types.Transaction, error) { + return _NativeMinter.contract.Transact(opts, "mint", addr, amount) +} + +// Mint is a paid mutator transaction binding the contract method 0x40c10f19. +// +// Solidity: function mint(address addr, uint256 amount) returns(bool) +func (_NativeMinter *NativeMinterSession) Mint(addr common.Address, amount *big.Int) (*types.Transaction, error) { + return _NativeMinter.Contract.Mint(&_NativeMinter.TransactOpts, addr, amount) +} + +// Mint is a paid mutator transaction binding the contract method 0x40c10f19. +// +// Solidity: function mint(address addr, uint256 amount) returns(bool) +func (_NativeMinter *NativeMinterTransactorSession) Mint(addr common.Address, amount *big.Int) (*types.Transaction, error) { + return _NativeMinter.Contract.Mint(&_NativeMinter.TransactOpts, addr, amount) +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns(bool) +func (_NativeMinter *NativeMinterTransactor) RenounceOwnership(opts *bind.TransactOpts) (*types.Transaction, error) { + return _NativeMinter.contract.Transact(opts, "renounceOwnership") +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns(bool) +func (_NativeMinter *NativeMinterSession) RenounceOwnership() (*types.Transaction, error) { + return _NativeMinter.Contract.RenounceOwnership(&_NativeMinter.TransactOpts) +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns(bool) +func (_NativeMinter *NativeMinterTransactorSession) RenounceOwnership() (*types.Transaction, error) { + return _NativeMinter.Contract.RenounceOwnership(&_NativeMinter.TransactOpts) +} + +// SetMinter is a paid mutator transaction binding the contract method 0xfca3b5aa. +// +// Solidity: function setMinter(address newMinter) returns(bool) +func (_NativeMinter *NativeMinterTransactor) SetMinter(opts *bind.TransactOpts, newMinter common.Address) (*types.Transaction, error) { + return _NativeMinter.contract.Transact(opts, "setMinter", newMinter) +} + +// SetMinter is a paid mutator transaction binding the contract method 0xfca3b5aa. +// +// Solidity: function setMinter(address newMinter) returns(bool) +func (_NativeMinter *NativeMinterSession) SetMinter(newMinter common.Address) (*types.Transaction, error) { + return _NativeMinter.Contract.SetMinter(&_NativeMinter.TransactOpts, newMinter) +} + +// SetMinter is a paid mutator transaction binding the contract method 0xfca3b5aa. +// +// Solidity: function setMinter(address newMinter) returns(bool) +func (_NativeMinter *NativeMinterTransactorSession) SetMinter(newMinter common.Address) (*types.Transaction, error) { + return _NativeMinter.Contract.SetMinter(&_NativeMinter.TransactOpts, newMinter) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns(bool) +func (_NativeMinter *NativeMinterTransactor) TransferOwnership(opts *bind.TransactOpts, newOwner common.Address) (*types.Transaction, error) { + return _NativeMinter.contract.Transact(opts, "transferOwnership", newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns(bool) +func (_NativeMinter *NativeMinterSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _NativeMinter.Contract.TransferOwnership(&_NativeMinter.TransactOpts, newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns(bool) +func (_NativeMinter *NativeMinterTransactorSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _NativeMinter.Contract.TransferOwnership(&_NativeMinter.TransactOpts, newOwner) +} diff --git a/precompile/contracts/interfaces/INativeMinter.sol b/precompile/contracts/interfaces/INativeMinter.sol new file mode 100644 index 000000000000..56606414ce29 --- /dev/null +++ b/precompile/contracts/interfaces/INativeMinter.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +interface INativeMinter { + + /////////////////////////////////////// READ METHODS ////////////////////////////////////////// + + // Returns the current owner of the contract + function owner() view external returns (address); + + // Returns the current minter of the contract + function minter() view external returns (address); + + ////////////////////////////////////// WRITE METHODS ////////////////////////////////////////// + + // Mints the specified amount of tokens to the specified address + function mint(address addr, uint256 amount) external returns (bool); + + // Burns the specified amount of tokens from the specified address + function burn(address addr, uint256 amount) external returns (bool); + + // Sets a new minter for the contract + function setMinter(address newMinter) external returns (bool); + + // Transfers the ownership of the contract to a new owner + function transferOwnership(address newOwner) external returns (bool); + + // Renounces ownership of the contract + function renounceOwnership() external returns (bool); + +} diff --git a/precompile/contracts/nativeminter/nativeminter.go b/precompile/contracts/nativeminter/nativeminter.go new file mode 100644 index 000000000000..6a4979351935 --- /dev/null +++ b/precompile/contracts/nativeminter/nativeminter.go @@ -0,0 +1,142 @@ +package nativeminter + +import ( + "bytes" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/precompile" + "github.com/ethereum/go-ethereum/precompile/bindings" +) + +type NativeMinter struct { + precompile.StatefulPrecompiledContract + initialOwner common.Address +} + +func NewNativeMinter(initialOwner common.Address) *NativeMinter { + return &NativeMinter{ + StatefulPrecompiledContract: precompile.NewStatefulPrecompiledContract( + bindings.NativeMinterABI, + ), + initialOwner: initialOwner, + } +} + +// StorageSlots is a struct that holds the storage slots for the HypNative contract +type StorageSlots struct { + Owner common.Hash + Minter common.Hash + OwnerRenounced common.Hash +} + +// Slots is a global variable that holds the storage slots for the NativeMinter contract +var Slots = StorageSlots{ + Owner: common.BytesToHash([]byte{0x00}), // slot 0 + Minter: common.BytesToHash([]byte{0x01}), // slot 1 + OwnerRenounced: common.BytesToHash([]byte{0x02}), // slot 2 +} + +var ZeroAddress = common.Address{} + +// Owner returns the owner of the NativeMinter contract +func (c *NativeMinter) Owner(ctx precompile.StatefulContext) (common.Address, error) { + return common.BytesToAddress(ctx.GetState(Slots.Owner).Bytes()), nil +} + +// Minter returns the minter of the NativeMinter contract +func (c *NativeMinter) Minter(ctx precompile.StatefulContext) (common.Address, error) { + return common.BytesToAddress(ctx.GetState(Slots.Minter).Bytes()), nil +} + +// SetMinter sets a new minter for the HypNative contract +func (c *NativeMinter) SetMinter(ctx precompile.StatefulContext, newMinter common.Address) (bool, error) { + if err := c.senderIsOwner(ctx); err != nil { + return false, err + } + + ctx.SetState(Slots.Minter, common.BytesToHash(newMinter.Bytes())) + + return true, nil +} + +// TransferOwnership transfers the ownership of the HypNative contract to a new owner +func (c *NativeMinter) TransferOwnership(ctx precompile.StatefulContext, newOwner common.Address) (bool, error) { + if bytes.Equal(newOwner.Bytes(), ZeroAddress.Bytes()) { + return false, errors.New("new owner is the zero address") + } + + return c.internalTransferOwnership(ctx, newOwner) +} + +// RenounceOwnership renounces the ownership of the HypNative contract +func (c *NativeMinter) RenounceOwnership(ctx precompile.StatefulContext) (bool, error) { + ctx.SetState(Slots.OwnerRenounced, common.BytesToHash([]byte{0x01})) + return c.internalTransferOwnership(ctx, ZeroAddress) +} + +func (c *NativeMinter) internalTransferOwnership(ctx precompile.StatefulContext, newOwner common.Address) (bool, error) { + if err := c.senderIsOwner(ctx); err != nil { + return false, err + } + + ctx.SetState(Slots.Owner, common.BytesToHash(newOwner.Bytes())) + + return true, nil +} + +// Mint new tokens to the specified address +func (c *NativeMinter) Mint(ctx precompile.StatefulContext, addr common.Address, amount *big.Int) (bool, error) { + if err := c.senderIsMinter(ctx); err != nil { + return false, err + } + + ctx.AddBalance(addr, amount) + + return true, nil +} + +// Burn tokens from the specified address +func (c *NativeMinter) Burn(ctx precompile.StatefulContext, addr common.Address, amount *big.Int) (bool, error) { + if err := c.senderIsMinter(ctx); err != nil { + return false, err + } + + // check balance is valid + balance := ctx.GetBalance(addr) + if balance.Cmp(amount) < 0 { + return false, errors.New("insufficient balance for burn") + } + + ctx.SubBalance(addr, amount) + + return true, nil +} + +// Access control funcs +func (c *NativeMinter) senderIsOwner(ctx precompile.StatefulContext) error { + owner := common.BytesToAddress(ctx.GetState(Slots.Owner).Bytes()) + ownerRenounced := ctx.GetState(Slots.OwnerRenounced) + + // owner not explicity set and not renounced; use initial owner + if owner.Cmp(ZeroAddress) == 0 && bytes.Equal(ownerRenounced.Bytes(), common.BytesToHash([]byte{0x00}).Bytes()) { + if owner.Cmp(c.initialOwner) != 0 { + return errors.New("caller is not the owner") + } + return nil + } + + if owner.Cmp(ctx.MsgSender()) != 0 { + return errors.New("caller is not the owner") + } + return nil +} + +func (c *NativeMinter) senderIsMinter(ctx precompile.StatefulContext) error { + allowedMinter := common.BytesToAddress(ctx.GetState(Slots.Minter).Bytes()) + if allowedMinter.Cmp(ctx.MsgSender()) != 0 { + return errors.New("caller is not the minter") + } + return nil +} From 55166c912322cec259332e683482e0827e54496f Mon Sep 17 00:00:00 2001 From: Josh Dechant Date: Tue, 27 Feb 2024 22:19:27 -0500 Subject: [PATCH 4/9] add basic tests for StatefulContext --- precompile/mocks/state_db.go | 55 +++++++++++++++++ precompile/stateful_context_test.go | 92 +++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 precompile/mocks/state_db.go create mode 100644 precompile/stateful_context_test.go diff --git a/precompile/mocks/state_db.go b/precompile/mocks/state_db.go new file mode 100644 index 000000000000..7afb762ae803 --- /dev/null +++ b/precompile/mocks/state_db.go @@ -0,0 +1,55 @@ +package mocks + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/precompile" +) + +type mockStateDB struct { + balances map[common.Address]*big.Int + states map[common.Address]map[common.Hash]common.Hash +} + +func NewMockStateDB() precompile.StateDB { + return &mockStateDB{ + balances: make(map[common.Address]*big.Int), + states: make(map[common.Address]map[common.Hash]common.Hash), + } +} + +func (m *mockStateDB) SubBalance(address common.Address, amount *big.Int) { + if _, ok := m.balances[address]; !ok { + m.balances[address] = big.NewInt(0) + } + m.balances[address].Sub(m.balances[address], amount) +} + +func (m *mockStateDB) AddBalance(address common.Address, amount *big.Int) { + if _, ok := m.balances[address]; !ok { + m.balances[address] = big.NewInt(0) + } + m.balances[address].Add(m.balances[address], amount) +} + +func (m *mockStateDB) GetBalance(address common.Address) *big.Int { + if _, ok := m.balances[address]; !ok { + m.balances[address] = big.NewInt(0) + } + return new(big.Int).Set(m.balances[address]) +} + +func (m *mockStateDB) GetState(address common.Address, hash common.Hash) common.Hash { + if _, ok := m.states[address]; !ok { + m.states[address] = make(map[common.Hash]common.Hash) + } + return m.states[address][hash] +} + +func (m *mockStateDB) SetState(address common.Address, hash common.Hash, value common.Hash) { + if _, ok := m.states[address]; !ok { + m.states[address] = make(map[common.Hash]common.Hash) + } + m.states[address][hash] = value +} diff --git a/precompile/stateful_context_test.go b/precompile/stateful_context_test.go new file mode 100644 index 000000000000..1e848f1d6eb3 --- /dev/null +++ b/precompile/stateful_context_test.go @@ -0,0 +1,92 @@ +package precompile_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/precompile" + "github.com/ethereum/go-ethereum/precompile/mocks" + "github.com/stretchr/testify/assert" +) + +func setupStatefulContext() precompile.StatefulContext { + state := mocks.NewMockStateDB() + address := common.HexToAddress("0x123") + msgSender := common.HexToAddress("0x456") + msgValue := big.NewInt(1000) + + return precompile.NewStatefulContext(state, address, msgSender, msgValue) +} + +func TestAddress(t *testing.T) { + ctx := setupStatefulContext() + assert.Equal(t, common.HexToAddress("0x123"), ctx.Address()) +} + +func TestMsgSender(t *testing.T) { + ctx := setupStatefulContext() + assert.Equal(t, common.HexToAddress("0x456"), ctx.MsgSender()) +} + +func TestMsgValue(t *testing.T) { + ctx := setupStatefulContext() + assert.Equal(t, big.NewInt(1000), ctx.MsgValue()) +} + +func TestIsReadOnly(t *testing.T) { + ctx := setupStatefulContext() + + assert.False(t, ctx.IsReadOnly()) + + ctx.SetReadOnly(true) + assert.True(t, ctx.IsReadOnly()) +} + +func TestSetState(t *testing.T) { + ctx := setupStatefulContext() + + key := common.HexToHash("0x789") + value := common.HexToHash("0xabc") + + assert.Equal(t, common.HexToHash("0x00"), ctx.GetState(key)) + + ctx.SetReadOnly(true) + err := ctx.SetState(key, value) + assert.Error(t, err) + + ctx.SetReadOnly(false) + err = ctx.SetState(key, value) + assert.NoError(t, err) + + assert.Equal(t, value, ctx.GetState(key)) +} + +func TestBalance(t *testing.T) { + ctx := setupStatefulContext() + + initialBalance := ctx.GetBalance(common.HexToAddress("0x123")) + assert.Equal(t, big.NewInt(0), initialBalance) + + amount := big.NewInt(500) + + err := ctx.AddBalance(common.HexToAddress("0x123"), amount) + assert.NoError(t, err) + assert.Equal(t, big.NewInt(500), ctx.GetBalance(common.HexToAddress("0x123"))) + + err = ctx.AddBalance(common.HexToAddress("0x123"), amount) + assert.NoError(t, err) + assert.Equal(t, big.NewInt(1000), ctx.GetBalance(common.HexToAddress("0x123"))) + + err = ctx.SubBalance(common.HexToAddress("0x123"), amount) + assert.NoError(t, err) + assert.Equal(t, big.NewInt(500), ctx.GetBalance(common.HexToAddress("0x123"))) + + ctx.SetReadOnly(true) + + err = ctx.AddBalance(common.HexToAddress("0x123"), amount) + assert.Error(t, err) + + err = ctx.SubBalance(common.HexToAddress("0x123"), amount) + assert.Error(t, err) +} From a1d82ee4b37087870af648bda0490b27eec0a7a0 Mon Sep 17 00:00:00 2001 From: Josh Dechant Date: Wed, 6 Mar 2024 14:29:54 -0500 Subject: [PATCH 5/9] move initial owner config to genesis storage alloc --- core/vm/evm.go | 5 ++- genesis.json | 11 ++++-- params/config.go | 1 - .../contracts/nativeminter/nativeminter.go | 39 ++++++------------- 4 files changed, 23 insertions(+), 33 deletions(-) diff --git a/core/vm/evm.go b/core/vm/evm.go index 929e1c3fe95a..4ad258034f5a 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -153,11 +153,12 @@ func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig // register precompiles here - // register native minter to 0x0000000000000000000000000000000000001000 + // e.g. register native minter to 0x0000000000000000000000000000000000001000 evm.precompileManager.Register( common.HexToAddress("0x1000"), - nativeminter.NewNativeMinter(common.BigToAddress(chainConfig.AstriaNativeMinterInitialOwner)), + nativeminter.NewNativeMinter(), ) + // evm.precompileManager.Register(common.HexToAddress("0x1001"), compress.NewCompress()) // evm.precompileManager.Register(common.HexToAddress("0x1002"), jsonutil.NewJsonUtil()) diff --git a/genesis.json b/genesis.json index 5f865cda991b..00ab252ad4c2 100644 --- a/genesis.json +++ b/genesis.json @@ -19,11 +19,16 @@ "difficulty": "10000000", "gasLimit": "8000000", "alloc": { - "0x46B77EFDFB20979E1C29ec98DcE73e3eCbF64102": { "balance": "300000000000000000000" } + "0x46B77EFDFB20979E1C29ec98DcE73e3eCbF64102": { "balance": "300000000000000000000" }, + "0x0000000000000000000000000000000000001000": { + "balance": "0", + "storage": { + "0x00": "0x00000000000000000000000046B77EFDFB20979E1C29ec98DcE73e3eCbF64102" + } + } }, "astriaOverrideGenesisExtraData": true, "astriaSequencerInitialHeight": 1, "astriaDataAvailabilityInitialHeight": 1, - "astriaDataAvailabilityHeightVariance": 50, - "astriaNativeMinterInitialOwner": "0x46B77EFDFB20979E1C29ec98DcE73e3eCbF64102" + "astriaDataAvailabilityHeightVariance": 50 } diff --git a/params/config.go b/params/config.go index 234ae6d33ece..342d87f8be14 100644 --- a/params/config.go +++ b/params/config.go @@ -343,7 +343,6 @@ type ChainConfig struct { AstriaSequencerInitialHeight uint32 `json:"astriaSequencerInitialHeight"` AstriaCelestiaInitialHeight uint32 `json:"astriaCelestiaInitialHeight"` AstriaCelestiaHeightVariance uint32 `json:"astriaCelestiaHeightVariance,omitempty"` - AstriaNativeMinterInitialOwner *big.Int `json:"astriaNativeMinterInitialOwner,omitempty"` } func (c *ChainConfig) AstriaExtraData() []byte { diff --git a/precompile/contracts/nativeminter/nativeminter.go b/precompile/contracts/nativeminter/nativeminter.go index 6a4979351935..f86ab6b9c6bb 100644 --- a/precompile/contracts/nativeminter/nativeminter.go +++ b/precompile/contracts/nativeminter/nativeminter.go @@ -12,45 +12,41 @@ import ( type NativeMinter struct { precompile.StatefulPrecompiledContract - initialOwner common.Address } -func NewNativeMinter(initialOwner common.Address) *NativeMinter { +func NewNativeMinter() *NativeMinter { return &NativeMinter{ StatefulPrecompiledContract: precompile.NewStatefulPrecompiledContract( bindings.NativeMinterABI, ), - initialOwner: initialOwner, } } -// StorageSlots is a struct that holds the storage slots for the HypNative contract +// StorageSlots is a struct that holds the storage slots for the contract type StorageSlots struct { - Owner common.Hash - Minter common.Hash - OwnerRenounced common.Hash + Owner common.Hash + Minter common.Hash } -// Slots is a global variable that holds the storage slots for the NativeMinter contract +// Slots is a global variable that holds the storage slots for the contract var Slots = StorageSlots{ - Owner: common.BytesToHash([]byte{0x00}), // slot 0 - Minter: common.BytesToHash([]byte{0x01}), // slot 1 - OwnerRenounced: common.BytesToHash([]byte{0x02}), // slot 2 + Owner: common.BytesToHash([]byte{0x00}), // slot 0 + Minter: common.BytesToHash([]byte{0x01}), // slot 1 } var ZeroAddress = common.Address{} -// Owner returns the owner of the NativeMinter contract +// Owner returns the owner of the contract func (c *NativeMinter) Owner(ctx precompile.StatefulContext) (common.Address, error) { return common.BytesToAddress(ctx.GetState(Slots.Owner).Bytes()), nil } -// Minter returns the minter of the NativeMinter contract +// Minter returns the minter of the contract func (c *NativeMinter) Minter(ctx precompile.StatefulContext) (common.Address, error) { return common.BytesToAddress(ctx.GetState(Slots.Minter).Bytes()), nil } -// SetMinter sets a new minter for the HypNative contract +// SetMinter sets a new minter for the contract func (c *NativeMinter) SetMinter(ctx precompile.StatefulContext, newMinter common.Address) (bool, error) { if err := c.senderIsOwner(ctx); err != nil { return false, err @@ -61,7 +57,7 @@ func (c *NativeMinter) SetMinter(ctx precompile.StatefulContext, newMinter commo return true, nil } -// TransferOwnership transfers the ownership of the HypNative contract to a new owner +// TransferOwnership transfers the ownership of the contract to a new owner func (c *NativeMinter) TransferOwnership(ctx precompile.StatefulContext, newOwner common.Address) (bool, error) { if bytes.Equal(newOwner.Bytes(), ZeroAddress.Bytes()) { return false, errors.New("new owner is the zero address") @@ -70,9 +66,8 @@ func (c *NativeMinter) TransferOwnership(ctx precompile.StatefulContext, newOwne return c.internalTransferOwnership(ctx, newOwner) } -// RenounceOwnership renounces the ownership of the HypNative contract +// RenounceOwnership renounces the ownership of the contract func (c *NativeMinter) RenounceOwnership(ctx precompile.StatefulContext) (bool, error) { - ctx.SetState(Slots.OwnerRenounced, common.BytesToHash([]byte{0x01})) return c.internalTransferOwnership(ctx, ZeroAddress) } @@ -117,16 +112,6 @@ func (c *NativeMinter) Burn(ctx precompile.StatefulContext, addr common.Address, // Access control funcs func (c *NativeMinter) senderIsOwner(ctx precompile.StatefulContext) error { owner := common.BytesToAddress(ctx.GetState(Slots.Owner).Bytes()) - ownerRenounced := ctx.GetState(Slots.OwnerRenounced) - - // owner not explicity set and not renounced; use initial owner - if owner.Cmp(ZeroAddress) == 0 && bytes.Equal(ownerRenounced.Bytes(), common.BytesToHash([]byte{0x00}).Bytes()) { - if owner.Cmp(c.initialOwner) != 0 { - return errors.New("caller is not the owner") - } - return nil - } - if owner.Cmp(ctx.MsgSender()) != 0 { return errors.New("caller is not the owner") } From 870b51c53b311775785d1bbf64a9a687deb6942e Mon Sep 17 00:00:00 2001 From: Josh Dechant Date: Wed, 6 Mar 2024 14:31:33 -0500 Subject: [PATCH 6/9] add precompile gen script --- .gitignore | 2 ++ precompile/README.md | 9 +++++++++ precompile/foundry.toml | 10 ++++++++++ precompile/gen.sh | 13 +++++++++++++ 4 files changed, 34 insertions(+) create mode 100644 precompile/README.md create mode 100644 precompile/foundry.toml create mode 100755 precompile/gen.sh diff --git a/.gitignore b/.gitignore index 3f27cdc00f07..258ff3ac75d7 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,5 @@ profile.cov logs/ tests/spec-tests/ + +precompile/out/ diff --git a/precompile/README.md b/precompile/README.md new file mode 100644 index 000000000000..dfae49e90ded --- /dev/null +++ b/precompile/README.md @@ -0,0 +1,9 @@ +# Writing a Precompile Contract + +1. Create Solidity interface in `contracts/interfaces`, e.g, IExampleContract.sol + +2. Generate bindings with `./gen.sh` + +3. Implement the precompile in Go. The struct should implement the `StatefulPrecompiledContract` interface and methods defined in the Solidity interface. + +See NativeMinter as an example implementation diff --git a/precompile/foundry.toml b/precompile/foundry.toml new file mode 100644 index 000000000000..d2b280d46cdb --- /dev/null +++ b/precompile/foundry.toml @@ -0,0 +1,10 @@ +[profile.default] +fuzz_runs = 1024 +evm_version = 'shanghai' +solc_version = '0.8.24' +cache = false +force = false +optimizer = false + +[profile.ci] +fuzz_runs = 8192 diff --git a/precompile/gen.sh b/precompile/gen.sh new file mode 100755 index 000000000000..7ed7366d734d --- /dev/null +++ b/precompile/gen.sh @@ -0,0 +1,13 @@ +forge build --extra-output-files bin --extra-output-files abi --root . + +for dir in ./out/*/ +do + NAME=$(basename $dir) + NAME=${NAME%.sol} + NAME_LOWER=$(echo "${NAME:1}" | tr '[:upper:]' '[:lower:]') + abigen --pkg bindings \ + --abi ./out/$NAME.sol/$NAME.abi.json \ + --bin ./out/$NAME.sol/$NAME.bin \ + --out ./bindings/i_${NAME_LOWER}.abigen.go \ + --type ${NAME:1} +done From 72ca0875bf023f457928d52c0b965d9949464639 Mon Sep 17 00:00:00 2001 From: Josh Dechant Date: Wed, 6 Mar 2024 14:31:43 -0500 Subject: [PATCH 7/9] add native minter tests --- .../nativeminter/nativeminter_test.go | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 precompile/contracts/nativeminter/nativeminter_test.go diff --git a/precompile/contracts/nativeminter/nativeminter_test.go b/precompile/contracts/nativeminter/nativeminter_test.go new file mode 100644 index 000000000000..f7cfcc067ff1 --- /dev/null +++ b/precompile/contracts/nativeminter/nativeminter_test.go @@ -0,0 +1,188 @@ +package nativeminter + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/precompile" + "github.com/ethereum/go-ethereum/precompile/mocks" +) + +var ( + zero = big.NewInt(0) + + SelfAddrForTest = common.HexToAddress("0x1000") + OwnerForTest = common.BytesToAddress([]byte("0xOwner")) + MinterForTest = common.BytesToAddress([]byte("0xMinter")) + RecipientForTest = common.BytesToAddress([]byte("0xRecipient")) +) + +func TestInvalidMintAndBurn(t *testing.T) { + stateDB := mocks.NewMockStateDB() + minter := NewNativeMinter() + + recipient := RecipientForTest + amount := big.NewInt(100) + + ctx := precompile.NewStatefulContext(stateDB, SelfAddrForTest, MinterForTest, amount) + + _, err := minter.Mint(ctx, recipient, amount) + if err == nil { + t.Fatalf("Expected error when minting, got nil") + } + + ctx.AddBalance(recipient, amount) + + _, err = minter.Burn(ctx, recipient, amount) + if err == nil { + t.Fatalf("Expected error when minting, got nil") + } +} + +func TestValidMintAndBurn(t *testing.T) { + stateDB := mocks.NewMockStateDB() + minter := NewNativeMinter() + + recipient := RecipientForTest + amount := big.NewInt(100) + + ctx := precompile.NewStatefulContext(stateDB, SelfAddrForTest, MinterForTest, amount) + + // set minter in state db for testing + ctx.SetState(Slots.Minter, common.BytesToHash(MinterForTest.Bytes())) + + _, err := minter.Mint(ctx, recipient, amount) + if err != nil { + t.Fatalf("Minting failed with error: %v", err) + } + + balance := stateDB.GetBalance(recipient) + if balance.Cmp(amount) != 0 { + t.Fatalf("Expected balance of %v, got %v", amount, balance) + } + + _, err = minter.Burn(ctx, recipient, amount) + if err != nil { + t.Fatalf("Burning failed with error: %v", err) + } + + balance = stateDB.GetBalance(recipient) + if balance.Cmp(zero) != 0 { + t.Fatalf("Expected balance of %v, got %v", zero, balance) + } +} + +func TestTransferOwnership(t *testing.T) { + stateDB := mocks.NewMockStateDB() + minter := NewNativeMinter() + + owner := OwnerForTest + newOwner := common.BytesToAddress([]byte("0xNewOwner")) + + ctx := precompile.NewStatefulContext(stateDB, SelfAddrForTest, owner, big.NewInt(0)) + + _, err := minter.TransferOwnership(ctx, newOwner) + if err == nil { + t.Fatalf("Expected error when transferring ownership, got nil") + } + + // set owner in state db for testing + ctx.SetState(Slots.Owner, common.BytesToHash(owner.Bytes())) + + // check owner is set + setOwner, err := minter.Owner(ctx) + if err != nil { + t.Fatalf("Owner failed with error: %v", err) + } + if setOwner.Cmp(owner) != 0 { + t.Fatalf("Expected owner to be %v, got %v", owner, setOwner) + } + + _, err = minter.TransferOwnership(ctx, newOwner) + if err != nil { + t.Fatalf("TransferOwnership failed with error: %v", err) + } + + // Check if the new owner is set correctly + setOwner, err = minter.Owner(ctx) + if err != nil { + t.Fatalf("Owner failed with error: %v", err) + } + if setOwner.Cmp(newOwner) != 0 { + t.Fatalf("Expected new owner to be %v, got %v", newOwner, setOwner) + } +} + +func TestRenounceOwnership(t *testing.T) { + stateDB := mocks.NewMockStateDB() + minter := NewNativeMinter() + + owner := OwnerForTest + + ctx := precompile.NewStatefulContext(stateDB, SelfAddrForTest, owner, big.NewInt(0)) + + _, err := minter.RenounceOwnership(ctx) + if err == nil { + t.Fatalf("Expected error when renouncing ownership, got nil") + } + + // set owner in state db for testing + ctx.SetState(Slots.Owner, common.BytesToHash(owner.Bytes())) + + _, err = minter.RenounceOwnership(ctx) + if err != nil { + t.Fatalf("RenounceOwnership failed with error: %v", err) + } + + // Check if the owner is set to zero address after renouncing ownership + setOwner, err := minter.Owner(ctx) + if err != nil { + t.Fatalf("Owner failed with error: %v", err) + } + if setOwner.Cmp(ZeroAddress) != 0 { + t.Fatalf("Expected owner to be zero address after renouncing ownership, got %v", setOwner) + } +} + +func TestSetMinter(t *testing.T) { + stateDB := mocks.NewMockStateDB() + minter := NewNativeMinter() + + owner := OwnerForTest + newMinter := MinterForTest + + ctx := precompile.NewStatefulContext(stateDB, SelfAddrForTest, owner, big.NewInt(0)) + + // check current minter is zero address (not set) + setMinter, err := minter.Minter(ctx) + if err != nil { + t.Fatalf("Owner failed with error: %v", err) + } + if setMinter.Cmp(ZeroAddress) != 0 { + t.Fatalf("Expected minter to be zero address, got %v", setMinter) + } + + // should fail as owner has not been set + _, err = minter.SetMinter(ctx, newMinter) + if err == nil { + t.Fatalf("Expected error when renouncing ownership, got nil") + } + + // set owner in state db for testing + ctx.SetState(Slots.Owner, common.BytesToHash(owner.Bytes())) + + _, err = minter.SetMinter(ctx, newMinter) + if err != nil { + t.Fatalf("SetMinter failed with error: %v", err) + } + + // Check if the minter has been properly set + setMinter, err = minter.Minter(ctx) + if err != nil { + t.Fatalf("Owner failed with error: %v", err) + } + if setMinter.Cmp(newMinter) != 0 { + t.Fatalf("Expected minter to be %v, got %v", newMinter, setMinter) + } +} From dbb333cdb7cd0cfbf57806449a4f53a2baec9832 Mon Sep 17 00:00:00 2001 From: Josh Dechant Date: Mon, 1 Apr 2024 11:48:19 -0400 Subject: [PATCH 8/9] Fix reflection callback --- core/vm/precompile_manager.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/vm/precompile_manager.go b/core/vm/precompile_manager.go index 88bdb68b6283..2d3b52cd318f 100644 --- a/core/vm/precompile_manager.go +++ b/core/vm/precompile_manager.go @@ -100,7 +100,13 @@ func (pm *precompileManager) Run( defer func() { ctx.SetReadOnly(false) }() } - results := method.reflectMethod.Func.Call(append([]reflect.Value{reflect.ValueOf(ctx)}, reflectedUnpackedArgs...)) + results := method.reflectMethod.Func.Call(append( + []reflect.Value{ + reflect.ValueOf(contract), + reflect.ValueOf(ctx), + }, + reflectedUnpackedArgs..., + )) // check if precompile returned an error if len(results) > 0 { From 53cf1bf2d741902b89f521a5cfb5fa88855a2516 Mon Sep 17 00:00:00 2001 From: Josh Dechant Date: Sat, 6 Apr 2024 21:46:38 -0400 Subject: [PATCH 9/9] ensure stateful precompiles conform to EIP-161 --- core/vm/precompile_manager.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/vm/precompile_manager.go b/core/vm/precompile_manager.go index 2d3b52cd318f..bf0399b55b88 100644 --- a/core/vm/precompile_manager.go +++ b/core/vm/precompile_manager.go @@ -91,6 +91,12 @@ func (pm *precompileManager) Run( reflectedUnpackedArgs = append(reflectedUnpackedArgs, reflect.ValueOf(unpacked)) } + // set precompile nonce to 1 to avoid state deletion for being considered an empty account + // this conforms precompile contracts to EIP-161 + if pm.evm.StateDB.GetNonce(addr) == 0 { + pm.evm.StateDB.SetNonce(addr, 1) + } + ctx := precompile.NewStatefulContext(pm.evm.StateDB, addr, caller, value) // Make sure the readOnly is only set if we aren't in readOnly yet.