diff --git a/core/vm/eips.go b/core/vm/eips.go
index 6e7259d405443fdbe9ac0e957ea657eac63e9421..075f5b7606691cf4ea32c105c4f01e643931cd66 100644
--- a/core/vm/eips.go
+++ b/core/vm/eips.go
@@ -27,6 +27,8 @@ import (
 // defined jump tables are not polluted.
 func EnableEIP(eipNum int, jt *JumpTable) error {
 	switch eipNum {
+	case 2200:
+		enable2200(jt)
 	case 1884:
 		enable1884(jt)
 	case 1344:
@@ -83,3 +85,8 @@ func opChainID(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memo
 	stack.push(chainId)
 	return nil, nil
 }
+
+// enable2200 applies EIP-2200 (Rebalance net-metered SSTORE)
+func enable2200(jt *JumpTable) {
+	jt[SSTORE].dynamicGas = gasSStoreEIP2200
+}
diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go
index b2999fdea7f45e965fbd06787789c8cfd81309fe..1d3c4f1003e04b46e0410d9f2e6a7f7599ab5de3 100644
--- a/core/vm/gas_table.go
+++ b/core/vm/gas_table.go
@@ -17,6 +17,8 @@
 package vm
 
 import (
+	"errors"
+
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/common/math"
 	"github.com/ethereum/go-ethereum/params"
@@ -160,6 +162,61 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi
 	return params.NetSstoreDirtyGas, nil
 }
 
+// 0. If *gasleft* is less than or equal to 2300, fail the current call.
+// 1. If current value equals new value (this is a no-op), SSTORE_NOOP_GAS gas is deducted.
+// 2. If current value does not equal new value:
+//   2.1. If original value equals current value (this storage slot has not been changed by the current execution context):
+//     2.1.1. If original value is 0, SSTORE_INIT_GAS gas is deducted.
+//     2.1.2. Otherwise, SSTORE_CLEAN_GAS gas is deducted. If new value is 0, add SSTORE_CLEAR_REFUND to refund counter.
+//   2.2. If original value does not equal current value (this storage slot is dirty), SSTORE_DIRTY_GAS gas is deducted. Apply both of the following clauses:
+//     2.2.1. If original value is not 0:
+//       2.2.1.1. If current value is 0 (also means that new value is not 0), subtract SSTORE_CLEAR_REFUND gas from refund counter. We can prove that refund counter will never go below 0.
+//       2.2.1.2. If new value is 0 (also means that current value is not 0), add SSTORE_CLEAR_REFUND gas to refund counter.
+//     2.2.2. If original value equals new value (this storage slot is reset):
+//       2.2.2.1. If original value is 0, add SSTORE_INIT_REFUND to refund counter.
+//       2.2.2.2. Otherwise, add SSTORE_CLEAN_REFUND gas to refund counter.
+func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+	// If we fail the minimum gas availability invariant, fail (0)
+	if contract.Gas <= params.SstoreSentryGasEIP2200 {
+		return 0, errors.New("not enough gas for reentrancy sentry")
+	}
+	// Gas sentry honoured, do the actual gas calculation based on the stored value
+	var (
+		y, x    = stack.Back(1), stack.Back(0)
+		current = evm.StateDB.GetState(contract.Address(), common.BigToHash(x))
+	)
+	value := common.BigToHash(y)
+
+	if current == value { // noop (1)
+		return params.SstoreNoopGasEIP2200, nil
+	}
+	original := evm.StateDB.GetCommittedState(contract.Address(), common.BigToHash(x))
+	if original == current {
+		if original == (common.Hash{}) { // create slot (2.1.1)
+			return params.SstoreInitGasEIP2200, nil
+		}
+		if value == (common.Hash{}) { // delete slot (2.1.2b)
+			evm.StateDB.AddRefund(params.SstoreClearRefundEIP2200)
+		}
+		return params.SstoreCleanGasEIP2200, nil // write existing slot (2.1.2)
+	}
+	if original != (common.Hash{}) {
+		if current == (common.Hash{}) { // recreate slot (2.2.1.1)
+			evm.StateDB.SubRefund(params.SstoreClearRefundEIP2200)
+		} else if value == (common.Hash{}) { // delete slot (2.2.1.2)
+			evm.StateDB.AddRefund(params.SstoreClearRefundEIP2200)
+		}
+	}
+	if original == value {
+		if original == (common.Hash{}) { // reset to original inexistent slot (2.2.2.1)
+			evm.StateDB.AddRefund(params.SstoreInitRefundEIP2200)
+		} else { // reset to original existing slot (2.2.2.2)
+			evm.StateDB.AddRefund(params.SstoreCleanRefundEIP2200)
+		}
+	}
+	return params.SstoreDirtyGasEIP2200, nil // dirty update (2.2)
+}
+
 func makeGasLog(n uint64) gasFunc {
 	return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
 		requestedSize, overflow := bigUint64(stack.Back(1))
diff --git a/core/vm/gas_table_test.go b/core/vm/gas_table_test.go
index 2c1e11894fdddd157f6c33595d89ee00ac0d6a48..5d443de0eae6940d04e9030502427740ddce7048 100644
--- a/core/vm/gas_table_test.go
+++ b/core/vm/gas_table_test.go
@@ -16,7 +16,17 @@
 
 package vm
 
-import "testing"
+import (
+	"math"
+	"math/big"
+	"testing"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/core/rawdb"
+	"github.com/ethereum/go-ethereum/core/state"
+	"github.com/ethereum/go-ethereum/params"
+)
 
 func TestMemoryGasCost(t *testing.T) {
 	tests := []struct {
@@ -37,3 +47,61 @@ func TestMemoryGasCost(t *testing.T) {
 		}
 	}
 }
+
+var eip2200Tests = []struct {
+	original byte
+	gaspool  uint64
+	input    string
+	used     uint64
+	refund   uint64
+	failure  error
+}{
+	{0, math.MaxUint64, "0x60006000556000600055", 1612, 0, nil},                // 0 -> 0 -> 0
+	{0, math.MaxUint64, "0x60006000556001600055", 20812, 0, nil},               // 0 -> 0 -> 1
+	{0, math.MaxUint64, "0x60016000556000600055", 20812, 19200, nil},           // 0 -> 1 -> 0
+	{0, math.MaxUint64, "0x60016000556002600055", 20812, 0, nil},               // 0 -> 1 -> 2
+	{0, math.MaxUint64, "0x60016000556001600055", 20812, 0, nil},               // 0 -> 1 -> 1
+	{1, math.MaxUint64, "0x60006000556000600055", 5812, 15000, nil},            // 1 -> 0 -> 0
+	{1, math.MaxUint64, "0x60006000556001600055", 5812, 4200, nil},             // 1 -> 0 -> 1
+	{1, math.MaxUint64, "0x60006000556002600055", 5812, 0, nil},                // 1 -> 0 -> 2
+	{1, math.MaxUint64, "0x60026000556000600055", 5812, 15000, nil},            // 1 -> 2 -> 0
+	{1, math.MaxUint64, "0x60026000556003600055", 5812, 0, nil},                // 1 -> 2 -> 3
+	{1, math.MaxUint64, "0x60026000556001600055", 5812, 4200, nil},             // 1 -> 2 -> 1
+	{1, math.MaxUint64, "0x60026000556002600055", 5812, 0, nil},                // 1 -> 2 -> 2
+	{1, math.MaxUint64, "0x60016000556000600055", 5812, 15000, nil},            // 1 -> 1 -> 0
+	{1, math.MaxUint64, "0x60016000556002600055", 5812, 0, nil},                // 1 -> 1 -> 2
+	{1, math.MaxUint64, "0x60016000556001600055", 1612, 0, nil},                // 1 -> 1 -> 1
+	{0, math.MaxUint64, "0x600160005560006000556001600055", 40818, 19200, nil}, // 0 -> 1 -> 0 -> 1
+	{1, math.MaxUint64, "0x600060005560016000556000600055", 10818, 19200, nil}, // 1 -> 0 -> 1 -> 0
+	{1, 2306, "0x6001600055", 2306, 0, ErrOutOfGas},                            // 1 -> 1 (2300 sentry + 2xPUSH)
+	{1, 2307, "0x6001600055", 806, 0, nil},                                     // 1 -> 1 (2301 sentry + 2xPUSH)
+}
+
+func TestEIP2200(t *testing.T) {
+	for i, tt := range eip2200Tests {
+		address := common.BytesToAddress([]byte("contract"))
+
+		statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()))
+		statedb.CreateAccount(address)
+		statedb.SetCode(address, hexutil.MustDecode(tt.input))
+		statedb.SetState(address, common.Hash{}, common.BytesToHash([]byte{tt.original}))
+		statedb.Finalise(true) // Push the state into the "original" slot
+
+		vmctx := Context{
+			CanTransfer: func(StateDB, common.Address, *big.Int) bool { return true },
+			Transfer:    func(StateDB, common.Address, common.Address, *big.Int) {},
+		}
+		vmenv := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}})
+
+		_, gas, err := vmenv.Call(AccountRef(common.Address{}), address, nil, tt.gaspool, new(big.Int))
+		if err != tt.failure {
+			t.Errorf("test %d: failure mismatch: have %v, want %v", i, err, tt.failure)
+		}
+		if used := tt.gaspool - gas; used != tt.used {
+			t.Errorf("test %d: gas used mismatch: have %v, want %v", i, used, tt.used)
+		}
+		if refund := vmenv.StateDB.GetRefund(); refund != tt.refund {
+			t.Errorf("test %d: gas refund mismatch: have %v, want %v", i, refund, tt.refund)
+		}
+	}
+}
diff --git a/params/protocol_params.go b/params/protocol_params.go
index 3022b700df9a2b73b86b42c8619156c70108f753..11b858a61c4f52cab9cdc797e60b6c31e149a9f4 100644
--- a/params/protocol_params.go
+++ b/params/protocol_params.go
@@ -52,6 +52,15 @@ const (
 	NetSstoreResetRefund      uint64 = 4800  // Once per SSTORE operation for resetting to the original non-zero value
 	NetSstoreResetClearRefund uint64 = 19800 // Once per SSTORE operation for resetting to the original zero value
 
+	SstoreSentryGasEIP2200   uint64 = 2300  // Minimum gas required to be present for an SSTORE call, not consumed
+	SstoreNoopGasEIP2200     uint64 = 800   // Once per SSTORE operation if the value doesn't change.
+	SstoreDirtyGasEIP2200    uint64 = 800   // Once per SSTORE operation if a dirty value is changed.
+	SstoreInitGasEIP2200     uint64 = 20000 // Once per SSTORE operation from clean zero to non-zero
+	SstoreInitRefundEIP2200  uint64 = 19200 // Once per SSTORE operation for resetting to the original zero value
+	SstoreCleanGasEIP2200    uint64 = 5000  // Once per SSTORE operation from clean non-zero to something else
+	SstoreCleanRefundEIP2200 uint64 = 4200  // Once per SSTORE operation for resetting to the original non-zero value
+	SstoreClearRefundEIP2200 uint64 = 15000 // Once per SSTORE operation for clearing an originally existing storage slot
+
 	JumpdestGas   uint64 = 1     // Once per JUMPDEST operation.
 	EpochDuration uint64 = 30000 // Duration between proof-of-work epochs.