diff --git a/cmd/neofs-ir/defaults.go b/cmd/neofs-ir/defaults.go
index 516192baa..1a466ab49 100644
--- a/cmd/neofs-ir/defaults.go
+++ b/cmd/neofs-ir/defaults.go
@@ -80,6 +80,7 @@ func defaultConfiguration(cfg *viper.Viper) {
 	cfg.SetDefault("workers.container", "10")
 	cfg.SetDefault("workers.alphabet", "10")
 	cfg.SetDefault("workers.reputation", "10")
+	cfg.SetDefault("workers.subnet", "10")
 
 	cfg.SetDefault("netmap_cleaner.enabled", true)
 	cfg.SetDefault("netmap_cleaner.threshold", 3)
diff --git a/go.mod b/go.mod
index 0806974ab..9f262722e 100644
--- a/go.mod
+++ b/go.mod
@@ -11,9 +11,9 @@ require (
 	github.com/mr-tron/base58 v1.2.0
 	github.com/multiformats/go-multiaddr v0.4.0
 	github.com/nspcc-dev/hrw v1.0.9
-	github.com/nspcc-dev/neo-go v0.97.4-pre.0.20211123163659-b25c3775e847
+	github.com/nspcc-dev/neo-go v0.97.4-pre.0.20211126130906-87f5113c031b
 	github.com/nspcc-dev/neofs-api-go/v2 v2.11.0-pre.0.20211124141318-d93828f46514
-	github.com/nspcc-dev/neofs-sdk-go v0.0.0-20211126123811-1dde267424aa
+	github.com/nspcc-dev/neofs-sdk-go v0.0.0-20211126125208-279a5a1e0bfe
 	github.com/nspcc-dev/tzhash v1.4.0
 	github.com/panjf2000/ants/v2 v2.4.0
 	github.com/paulmach/orb v0.2.2
diff --git a/go.sum b/go.sum
index 014590ee1..d3e97d650 100644
Binary files a/go.sum and b/go.sum differ
diff --git a/pkg/innerring/innerring.go b/pkg/innerring/innerring.go
index 07914d57c..471769db4 100644
--- a/pkg/innerring/innerring.go
+++ b/pkg/innerring/innerring.go
@@ -119,6 +119,8 @@ type (
 		//
 		// TODO: unify with workers.
 		runners []func(chan<- error)
+
+		subnetHandler
 	}
 
 	chainParams struct {
@@ -857,6 +859,10 @@ func New(ctx context.Context, log *zap.Logger, cfg *viper.Viper) (*Server, error
 		log.Info("no Control server endpoint specified, service is disabled")
 	}
 
+	server.initSubnet(subnetConfig{
+		queueSize: cfg.GetUint32("workers.subnet"),
+	})
+
 	return server, nil
 }
 
diff --git a/pkg/innerring/processors/netmap/handlers.go b/pkg/innerring/processors/netmap/handlers.go
index 4d91f882b..c2a1b50c4 100644
--- a/pkg/innerring/processors/netmap/handlers.go
+++ b/pkg/innerring/processors/netmap/handlers.go
@@ -6,7 +6,7 @@ import (
 	timerEvent "github.com/nspcc-dev/neofs-node/pkg/innerring/timers"
 	"github.com/nspcc-dev/neofs-node/pkg/morph/event"
 	netmapEvent "github.com/nspcc-dev/neofs-node/pkg/morph/event/netmap"
-	"github.com/nspcc-dev/neofs-node/pkg/morph/event/subnet"
+	subnetevents "github.com/nspcc-dev/neofs-node/pkg/morph/event/subnet"
 	"go.uber.org/zap"
 )
 
@@ -102,7 +102,7 @@ func (np *Processor) handleCleanupTick(ev event.Event) {
 }
 
 func (np *Processor) handleRemoveNode(ev event.Event) {
-	removeNode := ev.(subnet.RemoveNode)
+	removeNode := ev.(subnetevents.RemoveNode)
 
 	np.log.Info("notification",
 		zap.String("type", "remove node from subnet"),
diff --git a/pkg/innerring/processors/subnet/common.go b/pkg/innerring/processors/subnet/common.go
new file mode 100644
index 000000000..08f3df0f1
--- /dev/null
+++ b/pkg/innerring/processors/subnet/common.go
@@ -0,0 +1,22 @@
+package subnetevents
+
+import (
+	"fmt"
+
+	subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
+)
+
+// common interface of subnet notifications with subnet ID.
+type eventWithID interface {
+	// ReadID reads identifier of the subnet.
+	ReadID(*subnetid.ID) error
+}
+
+// an error which is returned on zero subnet operation attempt.
+type zeroSubnetOp struct {
+	op string
+}
+
+func (x zeroSubnetOp) Error() string {
+	return fmt.Sprintf("zero subnet %s", x.op)
+}
diff --git a/pkg/innerring/processors/subnet/common_test.go b/pkg/innerring/processors/subnet/common_test.go
new file mode 100644
index 000000000..3834114f8
--- /dev/null
+++ b/pkg/innerring/processors/subnet/common_test.go
@@ -0,0 +1,19 @@
+package subnetevents
+
+import subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
+
+type idEvent struct {
+	id subnetid.ID
+
+	idErr error
+}
+
+func (x idEvent) ReadID(id *subnetid.ID) error {
+	if x.idErr != nil {
+		return x.idErr
+	}
+
+	*id = x.id
+
+	return nil
+}
diff --git a/pkg/innerring/processors/subnet/delete.go b/pkg/innerring/processors/subnet/delete.go
new file mode 100644
index 000000000..e27d68093
--- /dev/null
+++ b/pkg/innerring/processors/subnet/delete.go
@@ -0,0 +1,44 @@
+package subnetevents
+
+import (
+	"fmt"
+
+	subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
+)
+
+// Delete represents notification about NeoFS subnet removal.
+// Generated by a contract when intending to delete a subnet.
+type Delete interface {
+	// Contains ID of the subnet to be removed.
+	eventWithID
+}
+
+// DeleteValidator asserts intent to remove a subnet.
+type DeleteValidator struct{}
+
+// Assert processes the attempt to remove a subnet. Approves the removal through nil return.
+//
+// All read errors of Delete are forwarded.
+//
+// Returns an error on:
+//   * zero subnet creation;
+//   * empty ID or different from the one wired into info;
+//   * empty owner ID or different from the one wired into info.
+func (x DeleteValidator) Assert(event Delete) error {
+	var err error
+
+	// read ID
+	var id subnetid.ID
+	if err = event.ReadID(&id); err != nil {
+		return fmt.Errorf("read ID: %w", err)
+	}
+
+	// prevent zero subnet removal
+	if subnetid.IsZero(id) {
+		return zeroSubnetOp{
+			op: "removal",
+		}
+	}
+
+	return nil
+}
diff --git a/pkg/innerring/processors/subnet/delete_test.go b/pkg/innerring/processors/subnet/delete_test.go
new file mode 100644
index 000000000..04db0e34a
--- /dev/null
+++ b/pkg/innerring/processors/subnet/delete_test.go
@@ -0,0 +1,44 @@
+package subnetevents
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
+)
+
+type delete struct {
+	idEvent
+}
+
+func TestDeleteValidator_Assert(t *testing.T) {
+	var (
+		v DeleteValidator
+
+		e delete
+
+		err error
+	)
+
+	// read ID error
+	e.idErr = errors.New("id err")
+
+	err = v.Assert(e)
+	require.ErrorIs(t, err, e.idErr)
+
+	e.idErr = nil
+
+	// zero subnet ID
+	subnetid.MakeZero(&e.id)
+
+	err = v.Assert(e)
+	require.ErrorAs(t, err, new(zeroSubnetOp))
+
+	const idNum = 13
+	e.id.SetNumber(idNum)
+
+	err = v.Assert(e)
+	require.NoError(t, err)
+}
diff --git a/pkg/innerring/processors/subnet/put.go b/pkg/innerring/processors/subnet/put.go
new file mode 100644
index 000000000..5676446dc
--- /dev/null
+++ b/pkg/innerring/processors/subnet/put.go
@@ -0,0 +1,82 @@
+package subnetevents
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/nspcc-dev/neofs-sdk-go/owner"
+	"github.com/nspcc-dev/neofs-sdk-go/subnet"
+	subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
+)
+
+// Put represents notification about NeoFS subnet creation.
+// Generated by a contract when intending to create a subnet.
+type Put interface {
+	// Contains ID of the subnet to be created.
+	eventWithID
+
+	// ReadCreator reads user ID of the subnet creator.
+	// Returns an error if ID is missing.
+	ReadCreator(id *owner.ID) error
+
+	// ReadInfo reads information about subnet to be created.
+	ReadInfo(info *subnet.Info) error
+}
+
+// PutValidator asserts intent to create a subnet.
+type PutValidator struct{}
+
+// errDiffOwner is returned when subnet owners differ.
+var errDiffOwner = errors.New("diff subnet owners")
+
+// errDiffID is returned when subnet IDs differ.
+var errDiffID = errors.New("diff subnet IDs")
+
+// Assert processes the attempt to create a subnet. Approves the creation through nil return.
+//
+// All read errors of Put are forwarded.
+//
+// Returns an error on:
+//   * zero subnet creation;
+//   * empty ID or different from the one wired into info;
+//   * empty owner ID or different from the one wired into info.
+func (x PutValidator) Assert(event Put) error {
+	var err error
+
+	// read ID
+	var id subnetid.ID
+	if err = event.ReadID(&id); err != nil {
+		return fmt.Errorf("read ID: %w", err)
+	}
+
+	// prevent zero subnet creation
+	if subnetid.IsZero(id) {
+		return zeroSubnetOp{
+			op: "creation",
+		}
+	}
+
+	// read creator's user ID in NeoFS system
+	var creator owner.ID
+	if err = event.ReadCreator(&creator); err != nil {
+		return fmt.Errorf("read creator: %w", err)
+	}
+
+	// read information about the subnet
+	var info subnet.Info
+	if err = event.ReadInfo(&info); err != nil {
+		return fmt.Errorf("read info: %w", err)
+	}
+
+	// check if explicit ID equals to the one from info
+	if !subnet.IDEquals(info, id) {
+		return errDiffID
+	}
+
+	// check if explicit creator equals to the one from info
+	if !subnet.IsOwner(info, creator) {
+		return errDiffOwner
+	}
+
+	return nil
+}
diff --git a/pkg/innerring/processors/subnet/put_test.go b/pkg/innerring/processors/subnet/put_test.go
new file mode 100644
index 000000000..50c5f31b1
--- /dev/null
+++ b/pkg/innerring/processors/subnet/put_test.go
@@ -0,0 +1,115 @@
+package subnetevents
+
+import (
+	"errors"
+	"testing"
+
+	ownertest "github.com/nspcc-dev/neofs-sdk-go/owner/test"
+	"github.com/stretchr/testify/require"
+
+	"github.com/nspcc-dev/neofs-sdk-go/owner"
+	"github.com/nspcc-dev/neofs-sdk-go/subnet"
+	subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
+)
+
+type put struct {
+	idEvent
+
+	creator owner.ID
+
+	creatorErr error
+
+	info subnet.Info
+
+	infoErr error
+}
+
+func (x put) ReadCreator(id *owner.ID) error {
+	if x.creatorErr != nil {
+		return x.creatorErr
+	}
+
+	*id = x.creator
+
+	return nil
+}
+
+func (x put) ReadInfo(info *subnet.Info) error {
+	if x.infoErr != nil {
+		return x.infoErr
+	}
+
+	*info = x.info
+
+	return nil
+}
+
+func TestPutValidator_Assert(t *testing.T) {
+	var (
+		v PutValidator
+
+		e put
+
+		err error
+	)
+
+	// read ID error
+	e.idErr = errors.New("id err")
+
+	err = v.Assert(e)
+	require.ErrorIs(t, err, e.idErr)
+
+	e.idErr = nil
+
+	// zero subnet ID
+	subnetid.MakeZero(&e.id)
+
+	err = v.Assert(e)
+	require.ErrorAs(t, err, new(zeroSubnetOp))
+
+	const idNum = 13
+	e.id.SetNumber(idNum)
+
+	// read creator error
+	e.creatorErr = errors.New("creator err")
+
+	err = v.Assert(e)
+	require.ErrorIs(t, err, e.creatorErr)
+
+	e.creatorErr = nil
+
+	// read info error
+	e.infoErr = errors.New("info err")
+
+	err = v.Assert(e)
+	require.ErrorIs(t, err, e.infoErr)
+
+	e.infoErr = nil
+
+	// diff explicit ID and the one in info
+	var id2 subnetid.ID
+
+	id2.SetNumber(idNum + 1)
+
+	e.info.SetID(id2)
+
+	err = v.Assert(e)
+	require.ErrorIs(t, err, errDiffID)
+
+	e.info.SetID(e.id)
+
+	// diff explicit creator and the one in info
+	var creator2 owner.ID
+
+	creator2 = *ownertest.GenerateID()
+
+	e.info.SetOwner(creator2)
+
+	err = v.Assert(e)
+	require.ErrorIs(t, err, errDiffOwner)
+
+	e.info.SetOwner(e.creator)
+
+	err = v.Assert(e)
+	require.NoError(t, err)
+}
diff --git a/pkg/innerring/subnet.go b/pkg/innerring/subnet.go
new file mode 100644
index 000000000..d709ce28b
--- /dev/null
+++ b/pkg/innerring/subnet.go
@@ -0,0 +1,332 @@
+package innerring
+
+import (
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"errors"
+	"fmt"
+
+	"github.com/nspcc-dev/neo-go/pkg/core/mempoolevent"
+	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
+	irsubnet "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/subnet"
+	morphsubnet "github.com/nspcc-dev/neofs-node/pkg/morph/client/subnet"
+	"github.com/nspcc-dev/neofs-node/pkg/morph/event"
+	subnetevents "github.com/nspcc-dev/neofs-node/pkg/morph/event/subnet"
+	"github.com/nspcc-dev/neofs-node/pkg/util"
+	"github.com/nspcc-dev/neofs-sdk-go/owner"
+	"github.com/nspcc-dev/neofs-sdk-go/subnet"
+	subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
+	"github.com/panjf2000/ants/v2"
+	"go.uber.org/zap"
+)
+
+// IR server's component to handle Subnet contract notifications.
+type subnetHandler struct {
+	workerPool util.WorkerPool
+
+	morphClient morphsubnet.Client
+
+	putValidator irsubnet.PutValidator
+
+	delValidator irsubnet.DeleteValidator
+}
+
+// configuration of subnet component.
+type subnetConfig struct {
+	queueSize uint32
+}
+
+// makes IR server to catch Subnet notifications from sidechain listener,
+// and to release corresponding processing queue on stop.
+func (s *Server) initSubnet(cfg subnetConfig) {
+	s.registerStarter(func() error {
+		var err error
+
+		// initialize queue for processing of the events from Subnet contract
+		s.subnetHandler.workerPool, err = ants.NewPool(int(cfg.queueSize), ants.WithNonblocking(true))
+		if err != nil {
+			return fmt.Errorf("subnet queue initialization: %w", err)
+		}
+
+		// initialize morph client of Subnet contract
+		clientMode := morphsubnet.NotaryAlphabet
+
+		if s.sideNotaryConfig.disabled {
+			clientMode = morphsubnet.NonNotary
+		}
+
+		var initPrm morphsubnet.InitPrm
+
+		initPrm.SetBaseClient(s.morphClient)
+		initPrm.SetContractAddress(s.contracts.subnet)
+		initPrm.SetMode(clientMode)
+
+		err = s.subnetHandler.morphClient.Init(initPrm)
+		if err != nil {
+			return fmt.Errorf("init morph subnet client: %w", err)
+		}
+
+		s.listenSubnet()
+
+		return nil
+	})
+
+	s.registerCloser(func() error {
+		s.stopSubnet()
+		return nil
+	})
+}
+
+// releases the Subnet contract notification processing queue.
+func (s *Server) stopSubnet() {
+	s.workerPool.Release()
+}
+
+// names of listened notification events from Subnet contract.
+const (
+	// subnet creation
+	subnetCreateEvName = "put"
+	// subnet removal
+	subnetRemoveEvName = "delete"
+)
+
+// makes IR server to listen notifications of Subnet contract.
+// All required resources must be initialized before (initSubnet).
+// Works in one of two modes (configured): notary and non-notary.
+//
+// All handlers are executed only if local node is an alphabet one.
+//
+// Events (notary):
+//   * put (parser: subnetevents.ParseNotaryPut, handler: catchSubnetCreation);
+//   * delete (parser: subnetevents.ParseNotaryDelete, handler: catchSubnetRemoval).
+//
+// Events (non-notary):
+//   * put (parser: subnetevents.ParsePut, handler: catchSubnetCreation);
+//   * delete (parser: subnetevents.ParseDelete, handler: catchSubnetCreation).
+func (s *Server) listenSubnet() {
+	if s.sideNotaryConfig.disabled {
+		s.listenSubnetWithoutNotary()
+		return
+	}
+
+	var (
+		parserInfo  event.NotaryParserInfo
+		handlerInfo event.NotaryHandlerInfo
+	)
+
+	parserInfo.SetScriptHash(s.contracts.subnet)
+	handlerInfo.SetScriptHash(s.contracts.subnet)
+
+	listenEvent := func(notifyName string, parser event.NotaryParser, handler event.Handler) {
+		notifyTyp := event.NotaryTypeFromString(notifyName)
+
+		parserInfo.SetMempoolType(mempoolevent.TransactionAdded)
+		handlerInfo.SetMempoolType(mempoolevent.TransactionAdded)
+
+		parserInfo.SetParser(parser)
+		handlerInfo.SetHandler(handler)
+
+		parserInfo.SetRequestType(notifyTyp)
+		handlerInfo.SetRequestType(notifyTyp)
+
+		s.morphListener.SetNotaryParser(parserInfo)
+		s.morphListener.RegisterNotaryHandler(handlerInfo)
+	}
+
+	// subnet creation
+	listenEvent(subnetCreateEvName, subnetevents.ParseNotaryPut, s.onlyAlphabetEventHandler(s.catchSubnetCreation))
+	// subnet removal
+	listenEvent(subnetRemoveEvName, subnetevents.ParseNotaryDelete, s.onlyAlphabetEventHandler(s.catchSubnetRemoval))
+}
+
+func (s *Server) listenSubnetWithoutNotary() {
+	var (
+		parserInfo  event.NotificationParserInfo
+		handlerInfo event.NotificationHandlerInfo
+	)
+
+	parserInfo.SetScriptHash(s.contracts.subnet)
+	handlerInfo.SetScriptHash(s.contracts.subnet)
+
+	listenEvent := func(notifyName string, parser event.NotificationParser, handler event.Handler) {
+		notifyTyp := event.TypeFromString(notifyName)
+
+		parserInfo.SetType(notifyTyp)
+		handlerInfo.SetType(notifyTyp)
+
+		parserInfo.SetParser(parser)
+		handlerInfo.SetHandler(handler)
+
+		s.morphListener.SetNotificationParser(parserInfo)
+		s.morphListener.RegisterNotificationHandler(handlerInfo)
+	}
+
+	// subnet creation
+	listenEvent(subnetCreateEvName, subnetevents.ParsePut, s.onlyAlphabetEventHandler(s.catchSubnetCreation))
+	// subnet removal
+	listenEvent(subnetRemoveEvName, subnetevents.ParseDelete, s.onlyAlphabetEventHandler(s.catchSubnetRemoval))
+}
+
+// catchSubnetCreation catches event of subnet creation from listener and queues the processing.
+func (s *Server) catchSubnetCreation(e event.Event) {
+	err := s.subnetHandler.workerPool.Submit(func() {
+		s.handleSubnetCreation(e)
+	})
+	if err != nil {
+		s.log.Error("subnet creation queue failure",
+			zap.String("error", err.Error()),
+		)
+	}
+}
+
+// implements irsubnet.Put event interface required by irsubnet.PutValidator.
+type putSubnetEvent struct {
+	ev subnetevents.Put
+}
+
+// ReadID unmarshals subnet ID from a binary NeoFS API protocol's format.
+func (x putSubnetEvent) ReadID(id *subnetid.ID) error {
+	return id.Unmarshal(x.ev.ID())
+}
+
+var errMissingSubnetOwner = errors.New("missing subnet owner")
+
+// ReadCreator unmarshals subnet creator from a binary NeoFS API protocol's format.
+// Returns an error if byte array is empty.
+func (x putSubnetEvent) ReadCreator(id *owner.ID) error {
+	data := x.ev.Owner()
+
+	if len(data) == 0 {
+		return errMissingSubnetOwner
+	}
+
+	key, err := keys.NewPublicKeyFromBytes(data, elliptic.P256())
+	if err != nil {
+		return err
+	}
+
+	wal, err := owner.NEO3WalletFromPublicKey((*ecdsa.PublicKey)(key))
+	if err != nil {
+		return err
+	}
+
+	// it would be better if we could do it not like this
+	*id = *owner.NewIDFromNeo3Wallet(wal)
+
+	return nil
+}
+
+// ReadInfo unmarshal subnet info from a binary NeoFS API protocol's format.
+func (x putSubnetEvent) ReadInfo(info *subnet.Info) error {
+	return info.Unmarshal(x.ev.Info())
+}
+
+// handleSubnetCreation handles event of subnet creation parsed via subnetevents.ParsePut.
+//
+// Validates the event using irsubnet.PutValidator. Logs message about (dis)agreement.
+func (s *Server) handleSubnetCreation(e event.Event) {
+	putEv := e.(subnetevents.Put) // panic occurs only if we registered handler incorrectly
+
+	err := s.subnetHandler.putValidator.Assert(putSubnetEvent{
+		ev: putEv,
+	})
+	if err != nil {
+		s.log.Info("discard subnet creation",
+			zap.String("reason", err.Error()),
+		)
+
+		return
+	}
+
+	notaryMainTx := putEv.NotaryMainTx()
+
+	isNotary := notaryMainTx != nil
+	if isNotary {
+		// re-sign notary request
+		err = s.morphClient.NotarySignAndInvokeTX(notaryMainTx)
+	} else {
+		// send new transaction
+		var prm morphsubnet.PutPrm
+
+		prm.SetID(putEv.ID())
+		prm.SetOwner(putEv.Owner())
+		prm.SetInfo(putEv.Info())
+		prm.SetTxHash(putEv.TxHash())
+
+		_, err = s.subnetHandler.morphClient.Put(prm)
+	}
+
+	if err != nil {
+		s.log.Error("approve subnet creation",
+			zap.Bool("notary", isNotary),
+			zap.String("error", err.Error()),
+		)
+
+		return
+	}
+}
+
+// catchSubnetRemoval catches event of subnet removal from listener and queues the processing.
+func (s *Server) catchSubnetRemoval(e event.Event) {
+	err := s.subnetHandler.workerPool.Submit(func() {
+		s.handleSubnetCreation(e)
+	})
+	if err != nil {
+		s.log.Error("subnet removal queue failure",
+			zap.String("error", err.Error()),
+		)
+	}
+}
+
+// implements irsubnet.Delete event interface required by irsubnet.DeleteValidator.
+type deleteSubnetEvent struct {
+	ev subnetevents.Delete
+}
+
+// ReadID unmarshals subnet ID from a binary NeoFS API protocol's format.
+func (x deleteSubnetEvent) ReadID(id *subnetid.ID) error {
+	return id.Unmarshal(x.ev.ID())
+}
+
+// handleSubnetRemoval handles event of subnet removal parsed via subnetevents.ParseDelete.
+func (s *Server) handleSubnetRemoval(e event.Event) {
+	delEv := e.(subnetevents.Delete) // panic occurs only if we registered handler incorrectly
+
+	err := s.subnetHandler.delValidator.Assert(deleteSubnetEvent{
+		ev: delEv,
+	})
+	if err != nil {
+		s.log.Info("discard subnet removal",
+			zap.String("reason", err.Error()),
+		)
+
+		return
+	}
+
+	notaryMainTx := delEv.NotaryMainTx()
+
+	isNotary := notaryMainTx != nil
+	if isNotary {
+		// re-sign notary request
+		err = s.morphClient.NotarySignAndInvokeTX(notaryMainTx)
+	} else {
+		// send new transaction
+		var prm morphsubnet.DeletePrm
+
+		prm.SetID(delEv.ID())
+		prm.SetTxHash(delEv.TxHash())
+
+		_, err = s.subnetHandler.morphClient.Delete(prm)
+	}
+
+	if err != nil {
+		s.log.Error("approve subnet removal",
+			zap.Bool("notary", isNotary),
+			zap.String("error", err.Error()),
+		)
+
+		return
+	}
+
+	// TODO: handle removal of the subnet in netmap candidates
+}
diff --git a/pkg/morph/client/subnet/client.go b/pkg/morph/client/subnet/client.go
new file mode 100644
index 000000000..bbd10dd09
--- /dev/null
+++ b/pkg/morph/client/subnet/client.go
@@ -0,0 +1,91 @@
+package morphsubnet
+
+import (
+	"fmt"
+
+	"github.com/nspcc-dev/neo-go/pkg/util"
+	"github.com/nspcc-dev/neofs-node/pkg/morph/client"
+)
+
+// Client represents Subnet contract client.
+//
+// Client should be preliminary initialized (see Init method).
+type Client struct {
+	client *client.StaticClient
+}
+
+// InitPrm groups parameters of Client's initialization.
+type InitPrm struct {
+	base *client.Client
+
+	addr util.Uint160
+
+	modeSet bool
+	mode    Mode
+}
+
+// SetBaseClient sets basic morph client.
+func (x *InitPrm) SetBaseClient(base *client.Client) {
+	x.base = base
+}
+
+// SetContractAddress sets address of Subnet contract in NeoFS sidechain.
+func (x *InitPrm) SetContractAddress(addr util.Uint160) {
+	x.addr = addr
+}
+
+// Mode regulates client work mode.
+type Mode uint8
+
+const (
+	_ Mode = iota
+
+	// NonNotary makes client to work in non-notary environment.
+	NonNotary
+
+	// NotaryAlphabet makes client to use its internal key for signing the notary requests.
+	NotaryAlphabet
+
+	// NotaryNonAlphabet makes client to not use its internal key for signing the notary requests.
+	NotaryNonAlphabet
+
+	lastMode
+)
+
+// SetMode makes client to work with non-notary sidechain.
+// By default, NonNotary is used.
+func (x *InitPrm) SetMode(mode Mode) {
+	x.modeSet = true
+	x.mode = mode
+}
+
+// Init initializes client with specified parameters.
+//
+// Base client must be set.
+func (x *Client) Init(prm InitPrm) error {
+	if prm.base == nil {
+		panic("missing base morph client")
+	}
+
+	if !prm.modeSet {
+		prm.mode = NonNotary
+	}
+
+	var opts []client.StaticClientOption
+
+	switch prm.mode {
+	default:
+		panic(fmt.Sprintf("invalid work mode %d", prm.mode))
+	case NonNotary:
+	case NotaryNonAlphabet:
+		opts = []client.StaticClientOption{client.TryNotary()}
+	case NotaryAlphabet:
+		opts = []client.StaticClientOption{client.TryNotary(), client.AsAlphabet()}
+	}
+
+	var err error
+
+	x.client, err = client.NewStatic(prm.base, prm.addr, 0, opts...)
+
+	return err
+}
diff --git a/pkg/morph/client/subnet/delete.go b/pkg/morph/client/subnet/delete.go
new file mode 100644
index 000000000..e52ed8828
--- /dev/null
+++ b/pkg/morph/client/subnet/delete.go
@@ -0,0 +1,40 @@
+package morphsubnet
+
+import (
+	"github.com/nspcc-dev/neo-go/pkg/util"
+	"github.com/nspcc-dev/neofs-node/pkg/morph/client"
+)
+
+// DeletePrm groups parameters of Delete method of Subnet contract.
+type DeletePrm struct {
+	cliPrm client.InvokePrm
+
+	args [1]interface{}
+}
+
+// SetTxHash sets hash of the transaction which spawned the notification.
+// Ignore this parameter for new requests.
+func (x *DeletePrm) SetTxHash(hash util.Uint256) {
+	x.cliPrm.SetHash(hash)
+}
+
+// SetID sets identifier of the subnet to be removed in a binary NeoFS API protocol format.
+func (x *DeletePrm) SetID(id []byte) {
+	x.args[0] = id
+}
+
+// DeleteRes groups resulting values of Delete method of Subnet contract.
+type DeleteRes struct{}
+
+// Delete removes subnet though the call of the corresponding method of the Subnet contract.
+func (x Client) Delete(prm DeletePrm) (*DeleteRes, error) {
+	prm.cliPrm.SetMethod("delete")
+	prm.cliPrm.SetArgs(prm.args[:]...)
+
+	err := x.client.Invoke(prm.cliPrm)
+	if err != nil {
+		return nil, err
+	}
+
+	return new(DeleteRes), nil
+}
diff --git a/pkg/morph/client/subnet/get.go b/pkg/morph/client/subnet/get.go
new file mode 100644
index 000000000..b5fab8dff
--- /dev/null
+++ b/pkg/morph/client/subnet/get.go
@@ -0,0 +1,57 @@
+package morphsubnet
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/nspcc-dev/neofs-node/pkg/morph/client"
+)
+
+// GetPrm groups parameters of Get method of Subnet contract.
+type GetPrm struct {
+	cliPrm client.TestInvokePrm
+
+	args [1]interface{}
+}
+
+// SetID sets identifier of the subnet to be read in a binary NeoFS API protocol format.
+func (x *GetPrm) SetID(id []byte) {
+	x.args[0] = id
+}
+
+// GetRes groups resulting values of Get method of Subnet contract.
+type GetRes struct {
+	info []byte
+}
+
+// Info returns information about the subnet in a binary format of NeoFS API protocol.
+func (x GetRes) Info() []byte {
+	return x.info
+}
+
+var errEmptyResponse = errors.New("empty response")
+
+// Get reads the subnet through the call of the corresponding method of the Subnet contract.
+func (x *Client) Get(prm GetPrm) (*GetRes, error) {
+	prm.cliPrm.SetMethod("get")
+	prm.cliPrm.SetArgs(prm.args[:]...)
+
+	res, err := x.client.TestInvoke(prm.cliPrm)
+	if err != nil {
+		fmt.Println()
+		return nil, err
+	}
+
+	if len(res) == 0 {
+		return nil, errEmptyResponse
+	}
+
+	data, err := client.BytesFromStackItem(res[0])
+	if err != nil {
+		return nil, err
+	}
+
+	return &GetRes{
+		info: data,
+	}, nil
+}
diff --git a/pkg/morph/client/subnet/put.go b/pkg/morph/client/subnet/put.go
new file mode 100644
index 000000000..fc3dcfa02
--- /dev/null
+++ b/pkg/morph/client/subnet/put.go
@@ -0,0 +1,50 @@
+package morphsubnet
+
+import (
+	"github.com/nspcc-dev/neo-go/pkg/util"
+	"github.com/nspcc-dev/neofs-node/pkg/morph/client"
+)
+
+// PutPrm groups parameters of Put method of Subnet contract.
+type PutPrm struct {
+	cliPrm client.InvokePrm
+
+	args [3]interface{}
+}
+
+// SetTxHash sets hash of the transaction which spawned the notification.
+// Ignore this parameter for new requests.
+func (x *PutPrm) SetTxHash(hash util.Uint256) {
+	x.cliPrm.SetHash(hash)
+}
+
+// SetID sets identifier of the created subnet in a binary NeoFS API protocol format.
+func (x *PutPrm) SetID(id []byte) {
+	x.args[0] = id
+}
+
+// SetOwner sets identifier of the subnet owner in a binary NeoFS API protocol format.
+func (x *PutPrm) SetOwner(id []byte) {
+	x.args[1] = id
+}
+
+// SetInfo sets information about the created subnet in a binary NeoFS API protocol format.
+func (x *PutPrm) SetInfo(id []byte) {
+	x.args[2] = id
+}
+
+// PutRes groups resulting values of Put method of Subnet contract.
+type PutRes struct{}
+
+// Put creates subnet though the call of the corresponding method of the Subnet contract.
+func (x Client) Put(prm PutPrm) (*PutRes, error) {
+	prm.cliPrm.SetMethod("put")
+	prm.cliPrm.SetArgs(prm.args[:]...)
+
+	err := x.client.Invoke(prm.cliPrm)
+	if err != nil {
+		return nil, err
+	}
+
+	return new(PutRes), nil
+}
diff --git a/pkg/morph/event/subnet/delete.go b/pkg/morph/event/subnet/delete.go
new file mode 100644
index 000000000..4c174abb0
--- /dev/null
+++ b/pkg/morph/event/subnet/delete.go
@@ -0,0 +1,109 @@
+package subnetevents
+
+import (
+	"fmt"
+
+	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
+	"github.com/nspcc-dev/neo-go/pkg/network/payload"
+	"github.com/nspcc-dev/neo-go/pkg/rpc/response/result/subscriptions"
+	"github.com/nspcc-dev/neo-go/pkg/util"
+	"github.com/nspcc-dev/neofs-node/pkg/morph/client"
+	"github.com/nspcc-dev/neofs-node/pkg/morph/event"
+)
+
+// Delete structures information about the notification generated by Delete method of Subnet contract.
+type Delete struct {
+	notaryRequest *payload.P2PNotaryRequest
+
+	txHash util.Uint256
+
+	id []byte
+}
+
+// MorphEvent implements Neo:Morph Event interface.
+func (Delete) MorphEvent() {}
+
+// ID returns identifier of the removed subnet in a binary format of NeoFS API protocol.
+func (x Delete) ID() []byte {
+	return x.id
+}
+
+// TxHash returns hash of the transaction which thrown the notification event.
+// Makes sense only in non-notary environments (see NotaryMainTx).
+func (x Delete) TxHash() util.Uint256 {
+	return x.txHash
+}
+
+// NotaryMainTx returns main transaction of the request in the Notary service.
+// Returns nil in non-notary environments.
+func (x Delete) NotaryMainTx() *transaction.Transaction {
+	if x.notaryRequest != nil {
+		return x.notaryRequest.MainTransaction
+	}
+
+	return nil
+}
+
+const itemNumDelete = 1
+
+// ParseDelete parses the notification about the removal of a subnet which has been thrown
+// by the appropriate method of the Subnet contract.
+//
+// Resulting event is of Delete type.
+func ParseDelete(e *subscriptions.NotificationEvent) (event.Event, error) {
+	var (
+		ev  Delete
+		err error
+	)
+
+	items, err := event.ParseStackArray(e)
+	if err != nil {
+		return nil, fmt.Errorf("parse stack array: %w", err)
+	}
+
+	if ln := len(items); ln != itemNumDelete {
+		return nil, event.WrongNumberOfParameters(itemNumDelete, ln)
+	}
+
+	// parse ID
+	ev.id, err = client.BytesFromStackItem(items[0])
+	if err != nil {
+		return nil, fmt.Errorf("id item: %w", err)
+	}
+
+	ev.txHash = e.Container
+
+	return ev, nil
+}
+
+// ParseNotaryDelete parses the notary notification about the removal of a subnet which has been
+// thrown by the appropriate method of the Subnet contract.
+//
+// Resulting event is of Delete type.
+func ParseNotaryDelete(e event.NotaryEvent) (event.Event, error) {
+	var ev Delete
+
+	ev.notaryRequest = e.Raw()
+	if ev.notaryRequest == nil {
+		panic(fmt.Sprintf("nil %T in notary environment", ev.notaryRequest))
+	}
+
+	var (
+		err error
+
+		prms = e.Params()
+	)
+
+	if ln := len(prms); ln != itemNumDelete {
+		return nil, event.WrongNumberOfParameters(itemNumDelete, ln)
+	}
+
+	ev.id, err = event.BytesFromOpcode(prms[0])
+	if err != nil {
+		return nil, fmt.Errorf("id param: %w", err)
+	}
+
+	ev.notaryRequest = e.Raw()
+
+	return ev, nil
+}
diff --git a/pkg/morph/event/subnet/delete_test.go b/pkg/morph/event/subnet/delete_test.go
new file mode 100644
index 000000000..28c9c8471
--- /dev/null
+++ b/pkg/morph/event/subnet/delete_test.go
@@ -0,0 +1,42 @@
+package subnetevents_test
+
+import (
+	"testing"
+
+	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
+	subnetevents "github.com/nspcc-dev/neofs-node/pkg/morph/event/subnet"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParseDelete(t *testing.T) {
+	id := []byte("id")
+
+	t.Run("wrong number of items", func(t *testing.T) {
+		prms := []stackitem.Item{
+			stackitem.NewByteArray(nil),
+			stackitem.NewByteArray(nil),
+		}
+
+		_, err := subnetevents.ParseDelete(createNotifyEventFromItems(prms))
+		require.Error(t, err)
+	})
+
+	t.Run("wrong id item", func(t *testing.T) {
+		_, err := subnetevents.ParseDelete(createNotifyEventFromItems([]stackitem.Item{
+			stackitem.NewMap(),
+		}))
+
+		require.Error(t, err)
+	})
+
+	t.Run("correct behavior", func(t *testing.T) {
+		ev, err := subnetevents.ParseDelete(createNotifyEventFromItems([]stackitem.Item{
+			stackitem.NewByteArray(id),
+		}))
+		require.NoError(t, err)
+
+		v := ev.(subnetevents.Delete)
+
+		require.Equal(t, id, v.ID())
+	})
+}
diff --git a/pkg/morph/event/subnet/put.go b/pkg/morph/event/subnet/put.go
new file mode 100644
index 000000000..ed962d101
--- /dev/null
+++ b/pkg/morph/event/subnet/put.go
@@ -0,0 +1,144 @@
+package subnetevents
+
+import (
+	"fmt"
+
+	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
+	"github.com/nspcc-dev/neo-go/pkg/network/payload"
+	"github.com/nspcc-dev/neo-go/pkg/rpc/response/result/subscriptions"
+	"github.com/nspcc-dev/neo-go/pkg/util"
+	"github.com/nspcc-dev/neofs-node/pkg/morph/client"
+	"github.com/nspcc-dev/neofs-node/pkg/morph/event"
+)
+
+// Put structures information about the notification generated by Put method of Subnet contract.
+type Put struct {
+	notaryRequest *payload.P2PNotaryRequest
+
+	txHash util.Uint256
+
+	id []byte
+
+	ownerID []byte
+
+	info []byte
+}
+
+// MorphEvent implements Neo:Morph Event interface.
+func (Put) MorphEvent() {}
+
+// ID returns identifier of the creating subnet in a binary format of NeoFS API protocol.
+func (x Put) ID() []byte {
+	return x.id
+}
+
+// Owner returns subnet owner's ID in a binary format of NeoFS API protocol.
+func (x Put) Owner() []byte {
+	return x.ownerID
+}
+
+// Info returns information about the subnet in a binary format of NeoFS API protocol.
+func (x Put) Info() []byte {
+	return x.info
+}
+
+// TxHash returns hash of the transaction which thrown the notification event.
+// Makes sense only in non-notary environments (see NotaryMainTx).
+func (x Put) TxHash() util.Uint256 {
+	return x.txHash
+}
+
+// NotaryMainTx returns main transaction of the request in the Notary service.
+// Returns nil in non-notary environments.
+func (x Put) NotaryMainTx() *transaction.Transaction {
+	if x.notaryRequest != nil {
+		return x.notaryRequest.MainTransaction
+	}
+
+	return nil
+}
+
+// number of items in notification about subnet creation.
+const itemNumPut = 3
+
+// ParsePut parses the notification about the creation of a subnet which has been thrown
+// by the appropriate method of the subnet contract.
+//
+// Resulting event is of Put type.
+func ParsePut(e *subscriptions.NotificationEvent) (event.Event, error) {
+	var (
+		put Put
+		err error
+	)
+
+	items, err := event.ParseStackArray(e)
+	if err != nil {
+		return nil, fmt.Errorf("parse stack array: %w", err)
+	}
+
+	if ln := len(items); ln != itemNumPut {
+		return nil, event.WrongNumberOfParameters(itemNumPut, ln)
+	}
+
+	// parse ID
+	put.id, err = client.BytesFromStackItem(items[0])
+	if err != nil {
+		return nil, fmt.Errorf("id item: %w", err)
+	}
+
+	// parse owner
+	put.ownerID, err = client.BytesFromStackItem(items[1])
+	if err != nil {
+		return nil, fmt.Errorf("owner item: %w", err)
+	}
+
+	// parse public key
+	put.info, err = client.BytesFromStackItem(items[2])
+	if err != nil {
+		return nil, fmt.Errorf("info item: %w", err)
+	}
+
+	put.txHash = e.Container
+
+	return put, nil
+}
+
+// ParseNotaryPut parses the notary notification about the creation of a subnet which has been
+// thrown by the appropriate method of the subnet contract.
+//
+// Resulting event is of Put type.
+func ParseNotaryPut(e event.NotaryEvent) (event.Event, error) {
+	var put Put
+
+	put.notaryRequest = e.Raw()
+	if put.notaryRequest == nil {
+		panic(fmt.Sprintf("nil %T in notary environment", put.notaryRequest))
+	}
+
+	var (
+		err error
+
+		prms = e.Params()
+	)
+
+	if ln := len(prms); ln != itemNumPut {
+		return nil, event.WrongNumberOfParameters(itemNumPut, ln)
+	}
+
+	put.info, err = event.BytesFromOpcode(prms[0])
+	if err != nil {
+		return nil, fmt.Errorf("info param: %w", err)
+	}
+
+	put.ownerID, err = event.BytesFromOpcode(prms[1])
+	if err != nil {
+		return nil, fmt.Errorf("creator param: %w", err)
+	}
+
+	put.id, err = event.BytesFromOpcode(prms[2])
+	if err != nil {
+		return nil, fmt.Errorf("id param: %w", err)
+	}
+
+	return put, nil
+}
diff --git a/pkg/morph/event/subnet/put_test.go b/pkg/morph/event/subnet/put_test.go
new file mode 100644
index 000000000..85e7fc5cc
--- /dev/null
+++ b/pkg/morph/event/subnet/put_test.go
@@ -0,0 +1,69 @@
+package subnetevents_test
+
+import (
+	"testing"
+
+	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
+	subnetevents "github.com/nspcc-dev/neofs-node/pkg/morph/event/subnet"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParsePut(t *testing.T) {
+	var (
+		id    = []byte("id")
+		owner = []byte("owner")
+		info  = []byte("info")
+	)
+
+	t.Run("wrong number of items", func(t *testing.T) {
+		prms := []stackitem.Item{
+			stackitem.NewByteArray(nil),
+			stackitem.NewByteArray(nil),
+		}
+
+		_, err := subnetevents.ParsePut(createNotifyEventFromItems(prms))
+		require.Error(t, err)
+	})
+
+	t.Run("wrong id item", func(t *testing.T) {
+		_, err := subnetevents.ParsePut(createNotifyEventFromItems([]stackitem.Item{
+			stackitem.NewMap(),
+		}))
+
+		require.Error(t, err)
+	})
+
+	t.Run("wrong owner item", func(t *testing.T) {
+		_, err := subnetevents.ParsePut(createNotifyEventFromItems([]stackitem.Item{
+			stackitem.NewByteArray(id),
+			stackitem.NewMap(),
+		}))
+
+		require.Error(t, err)
+	})
+
+	t.Run("wrong info item", func(t *testing.T) {
+		_, err := subnetevents.ParsePut(createNotifyEventFromItems([]stackitem.Item{
+			stackitem.NewByteArray(id),
+			stackitem.NewByteArray(owner),
+			stackitem.NewMap(),
+		}))
+
+		require.Error(t, err)
+	})
+
+	t.Run("correct behavior", func(t *testing.T) {
+		ev, err := subnetevents.ParsePut(createNotifyEventFromItems([]stackitem.Item{
+			stackitem.NewByteArray(id),
+			stackitem.NewByteArray(owner),
+			stackitem.NewByteArray(info),
+		}))
+		require.NoError(t, err)
+
+		v := ev.(subnetevents.Put)
+
+		require.Equal(t, id, v.ID())
+		require.Equal(t, owner, v.Owner())
+		require.Equal(t, info, v.Info())
+	})
+}
diff --git a/pkg/morph/event/subnet/remove_node.go b/pkg/morph/event/subnet/remove_node.go
index 81b2b7acc..229df2477 100644
--- a/pkg/morph/event/subnet/remove_node.go
+++ b/pkg/morph/event/subnet/remove_node.go
@@ -1,4 +1,4 @@
-package subnet
+package subnetevents
 
 import (
 	"fmt"
diff --git a/pkg/morph/event/subnet/remove_node_test.go b/pkg/morph/event/subnet/remove_node_test.go
index 40c3d76cb..17410a012 100644
--- a/pkg/morph/event/subnet/remove_node_test.go
+++ b/pkg/morph/event/subnet/remove_node_test.go
@@ -1,4 +1,4 @@
-package subnet
+package subnetevents_test
 
 import (
 	"testing"
@@ -7,6 +7,7 @@ import (
 	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
 	"github.com/nspcc-dev/neo-go/pkg/rpc/response/result/subscriptions"
 	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
+	. "github.com/nspcc-dev/neofs-node/pkg/morph/event/subnet"
 	subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
 	"github.com/stretchr/testify/require"
 )