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/core/vm/evm.go b/core/vm/evm.go index 088b18aaa4ff..4ad258034f5a 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" ) @@ -121,6 +122,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 +149,19 @@ 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. register native minter to 0x0000000000000000000000000000000000001000 + 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 } @@ -186,7 +202,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 +240,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 +302,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 +347,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 +396,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..bf0399b55b88 --- /dev/null +++ b/core/vm/precompile_manager.go @@ -0,0 +1,183 @@ +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)) + } + + // 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. + // 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(contract), + 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 _, 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; 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 _, abiMethod := range abiMethods { + 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, + } + } + + pm.precompiles[addr] = p + pm.pMethods[addr] = precompileMethods + return nil +} diff --git a/genesis.json b/genesis.json index ff586448127f..00ab252ad4c2 100644 --- a/genesis.json +++ b/genesis.json @@ -19,7 +19,13 @@ "difficulty": "10000000", "gasLimit": "8000000", "alloc": { - "0x46B77EFDFB20979E1C29ec98DcE73e3eCbF64102": { "balance": "300000000000000000000" } + "0x46B77EFDFB20979E1C29ec98DcE73e3eCbF64102": { "balance": "300000000000000000000" }, + "0x0000000000000000000000000000000000001000": { + "balance": "0", + "storage": { + "0x00": "0x00000000000000000000000046B77EFDFB20979E1C29ec98DcE73e3eCbF64102" + } + } }, "astriaOverrideGenesisExtraData": true, "astriaSequencerInitialHeight": 1, 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/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..f86ab6b9c6bb --- /dev/null +++ b/precompile/contracts/nativeminter/nativeminter.go @@ -0,0 +1,127 @@ +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 +} + +func NewNativeMinter() *NativeMinter { + return &NativeMinter{ + StatefulPrecompiledContract: precompile.NewStatefulPrecompiledContract( + bindings.NativeMinterABI, + ), + } +} + +// StorageSlots is a struct that holds the storage slots for the contract +type StorageSlots struct { + Owner common.Hash + Minter common.Hash +} + +// 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 +} + +var ZeroAddress = common.Address{} + +// 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 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 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 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 contract +func (c *NativeMinter) RenounceOwnership(ctx precompile.StatefulContext) (bool, error) { + 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()) + 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 +} 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) + } +} 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/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 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/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.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_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) +} 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 +}