From 38eaaab96cccce0737980cfec2e7863237e014e6 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:14:17 -0400 Subject: [PATCH] feat!: RulesHooks.CanCreateContract() accepts and returns gas (#28) * refactor!: `RulesHooks.CanCreateContract()` via `defer` `TestCanCreateContract` is unchanged and merely moved to the appropriate file. * feat!: `RulesHooks.CanCreateContract()` accepts and returns gas * chore: move `TestCanCreateContract` to original file Simplifies code review * chore: pacify linter * refactor!: revert to non-deferred call (not at start) --- core/vm/contracts.libevm_test.go | 17 +++++++++++------ core/vm/evm.go | 17 +++++++++++++---- libevm/hookstest/stub.go | 8 ++++---- params/example.libevm_test.go | 7 ++++--- params/hooks.libevm.go | 11 +++++++---- 5 files changed, 39 insertions(+), 21 deletions(-) diff --git a/core/vm/contracts.libevm_test.go b/core/vm/contracts.libevm_test.go index 29358900a9..c500f07edb 100644 --- a/core/vm/contracts.libevm_test.go +++ b/core/vm/contracts.libevm_test.go @@ -296,12 +296,15 @@ func TestCanCreateContract(t *testing.T) { account := rng.Address() slot := rng.Hash() + const gasLimit uint64 = 1e6 + gasUsage := rng.Uint64n(gasLimit) + makeErr := func(cc *libevm.AddressContext, stateVal common.Hash) error { return fmt.Errorf("Origin: %v Caller: %v Contract: %v State: %v", cc.Origin, cc.Caller, cc.Self, stateVal) } hooks := &hookstest.Stub{ - CanCreateContractFn: func(cc *libevm.AddressContext, s libevm.StateReader) error { - return makeErr(cc, s.GetState(account, slot)) + CanCreateContractFn: func(cc *libevm.AddressContext, gas uint64, s libevm.StateReader) (uint64, error) { + return gas - gasUsage, makeErr(cc, s.GetState(account, slot)) }, } hooks.Register(t) @@ -309,7 +312,7 @@ func TestCanCreateContract(t *testing.T) { origin := rng.Address() caller := rng.Address() value := rng.Hash() - code := rng.Bytes(8) + code := []byte{byte(vm.STOP)} salt := rng.Hash() create := crypto.CreateAddress(caller, 0) @@ -323,14 +326,14 @@ func TestCanCreateContract(t *testing.T) { { name: "Create", create: func(evm *vm.EVM) ([]byte, common.Address, uint64, error) { - return evm.Create(vm.AccountRef(caller), code, 1e6, uint256.NewInt(0)) + return evm.Create(vm.AccountRef(caller), code, gasLimit, uint256.NewInt(0)) }, wantErr: makeErr(&libevm.AddressContext{Origin: origin, Caller: caller, Self: create}, value), }, { name: "Create2", create: func(evm *vm.EVM) ([]byte, common.Address, uint64, error) { - return evm.Create2(vm.AccountRef(caller), code, 1e6, uint256.NewInt(0), new(uint256.Int).SetBytes(salt[:])) + return evm.Create2(vm.AccountRef(caller), code, gasLimit, uint256.NewInt(0), new(uint256.Int).SetBytes(salt[:])) }, wantErr: makeErr(&libevm.AddressContext{Origin: origin, Caller: caller, Self: create2}, value), }, @@ -342,8 +345,10 @@ func TestCanCreateContract(t *testing.T) { state.SetState(account, slot, value) evm.TxContext.Origin = origin - _, _, _, err := tt.create(evm) + _, _, gasRemaining, err := tt.create(evm) require.EqualError(t, err, tt.wantErr.Error()) + // require prints uint64s in hex + require.Equal(t, int(gasLimit-gasUsage), int(gasRemaining), "gas remaining") //nolint:gosec // G115 won't overflow as <= 1e6 }) } } diff --git a/core/vm/evm.go b/core/vm/evm.go index b846b20cbc..605b1cd2e5 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -428,10 +428,6 @@ func (c *codeAndHash) Hash() common.Hash { // create creates a new contract using code as deployment code. func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *uint256.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error) { - cc := &libevm.AddressContext{Origin: evm.Origin, Caller: caller.Address(), Self: address} - if err := evm.chainRules.Hooks().CanCreateContract(cc, evm.StateDB); err != nil { - return nil, common.Address{}, gas, err - } // Depth check execution. Fail if we're trying to execute above the // limit. if evm.depth > int(params.CallCreateDepth) { @@ -455,6 +451,19 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) { return nil, common.Address{}, 0, ErrContractAddressCollision } + + //libevm:start + // + // This check MUST be placed after the caller's nonce is incremented but + // before all other state-modifying behaviour, even if changes may be + // reverted to the snapshot. + addrs := &libevm.AddressContext{Origin: evm.Origin, Caller: caller.Address(), Self: address} + gas, err := evm.chainRules.Hooks().CanCreateContract(addrs, gas, evm.StateDB) + if err != nil { + return nil, common.Address{}, gas, err + } + //libevm:end + // Create a new account on the state snapshot := evm.StateDB.Snapshot() evm.StateDB.CreateAccount(address) diff --git a/libevm/hookstest/stub.go b/libevm/hookstest/stub.go index 719a16664f..5c090387ea 100644 --- a/libevm/hookstest/stub.go +++ b/libevm/hookstest/stub.go @@ -27,7 +27,7 @@ func Register[C params.ChainConfigHooks, R params.RulesHooks](tb testing.TB, ext type Stub struct { PrecompileOverrides map[common.Address]libevm.PrecompiledContract CanExecuteTransactionFn func(common.Address, *common.Address, libevm.StateReader) error - CanCreateContractFn func(*libevm.AddressContext, libevm.StateReader) error + CanCreateContractFn func(*libevm.AddressContext, uint64, libevm.StateReader) (uint64, error) } // Register is a convenience wrapper for registering s as both the @@ -63,11 +63,11 @@ func (s Stub) CanExecuteTransaction(from common.Address, to *common.Address, sr // CanCreateContract proxies arguments to the s.CanCreateContractFn function if // non-nil, otherwise it acts as a noop. -func (s Stub) CanCreateContract(cc *libevm.AddressContext, sr libevm.StateReader) error { +func (s Stub) CanCreateContract(cc *libevm.AddressContext, gas uint64, sr libevm.StateReader) (uint64, error) { if f := s.CanCreateContractFn; f != nil { - return f(cc, sr) + return f(cc, gas, sr) } - return nil + return gas, nil } var _ interface { diff --git a/params/example.libevm_test.go b/params/example.libevm_test.go index dd264e43c7..631e528bbe 100644 --- a/params/example.libevm_test.go +++ b/params/example.libevm_test.go @@ -105,11 +105,12 @@ func (r RulesExtra) PrecompileOverride(addr common.Address) (_ libevm.Precompile // CanCreateContract implements the required [params.RuleHooks] method. Access // to state allows it to be configured on-chain however this is an optional // implementation detail. -func (r RulesExtra) CanCreateContract(*libevm.AddressContext, libevm.StateReader) error { +func (r RulesExtra) CanCreateContract(_ *libevm.AddressContext, gas uint64, _ libevm.StateReader) (uint64, error) { if time.Unix(int64(r.timestamp), 0).UTC().Day() != int(time.Tuesday) { //nolint:gosec // G115 timestamp won't overflow int64 for millions of years so this is someone else's problem - return errors.New("uh oh") + // Consumes all remaining gas. + return 0, errors.New("uh oh") } - return nil + return gas, nil } // This example demonstrates how the rest of this file would be used from a diff --git a/params/hooks.libevm.go b/params/hooks.libevm.go index 74fd88ccef..75fabbd3cc 100644 --- a/params/hooks.libevm.go +++ b/params/hooks.libevm.go @@ -32,7 +32,9 @@ type RulesHooks interface { // RulesAllowlistHooks are a subset of [RulesHooks] that gate actions, signalled // by returning a nil (allowed) or non-nil (blocked) error. type RulesAllowlistHooks interface { - CanCreateContract(*libevm.AddressContext, libevm.StateReader) error + // CanCreateContract is called after the deployer's nonce is incremented but + // before all other state-modifying actions. + CanCreateContract(_ *libevm.AddressContext, gas uint64, _ libevm.StateReader) (gasRemaining uint64, _ error) CanExecuteTransaction(from common.Address, to *common.Address, _ libevm.StateReader) error } @@ -71,9 +73,10 @@ func (NOOPHooks) CanExecuteTransaction(_ common.Address, _ *common.Address, _ li return nil } -// CanCreateContract allows all (otherwise valid) contract deployment. -func (NOOPHooks) CanCreateContract(*libevm.AddressContext, libevm.StateReader) error { - return nil +// CanCreateContract allows all (otherwise valid) contract deployment, not +// consuming any more gas. +func (NOOPHooks) CanCreateContract(_ *libevm.AddressContext, gas uint64, _ libevm.StateReader) (uint64, error) { + return gas, nil } // PrecompileOverride instructs the EVM interpreter to use the default