Skip to main content

Middleware SDK Development

warning

Status: Work in Progress (WIP)
This middleware framework is currently under audits. Use with caution and ensure thorough testing.

Introduction

Visit the Symbiotic Middleware SDK repository on GitHub

The Middleware SDK provides a simple way to create middleware contracts that represent your network's logic. By inheriting from BaseMiddleware and base managers, your contract gains access to core functionality like operator management, vault handling, key storage, and access control. The framework is designed to be modular - you can pick and choose which components you need.

For a high-level overview of how networks are represented in the middleware framework, see the Network abstract representation.

Core Components

To develop middleware for your network, your contract should inherit from provided extensions:

  1. KeyManager: Manages operator keys (e.g., KeyManager256, KeyManagerBytes, or NoKeyManager).
  2. CaptureTimestampManager: Determines the reference timestamp or epoch for historical state queries (e.g., TimestampCapture, EpochCapture).
  3. StakePowerManager: Converts stake amounts to a voting power. By default, EqualStakePower is provided, but custom logic can be implemented. This can be used to weight vaults differently, potentially integrating with price oracles or other external data sources.
  4. AccessManager: Controls access to restricted functions. Multiple implementations exist (e.g., OwnableAccessManager or OzAccessControl). For complex role-based permissions, OzAccessControl can be used to assign roles and role admins.
  5. Operators: Extends BaseMiddleware to manage operator registration, validation, lifecycle including pausing/unpausing and operator-specific vaults.
  6. SharedVaults: (Optional) Extends BaseMiddleware to manage a set of vault addresses used by multiple operators. Vaults can be registered, paused, unpaused, and have their stake allocated across operators.
  7. Subnetworks: (Optional) Extends BaseMiddleware to manage registration, pausing, and unregistration of subnetworks.
  8. Custom/Third-Party Extensions: You can develop your own custom extensions or integrate third-party extensions.
contract MyMiddleware is SomeKeyManager, SomeStakePowerManager, SomeAccessManager, SomeOperatorsExtension, SharedVaults, Subnetworks {
// ...
}

Middleware Architecture

For more details, refer to the Extensions Overview documentation.

Initialization and Upgradeability

All modules and extensions are written in an upgradeable fashion:

  • Use the initializer modifier for your middleware’s initialization function.
  • Inside your initializer, you must call __BaseMiddleware_init and, if you are using any extensions that require initialization, call their __{ExtensionName}_init functions with the onlyInitializing modifier.
  • To prevent re-initialization in the implementation contract, call _disableInitializers() in the constructor.
  • If you are deploying a non-upgradeable middleware, you can call the initializer functions directly from the constructor.

contract MyMiddleware is BaseMiddleware, SomeExtension {
function initialize(...) external initializer {
__BaseMiddleware_init(...);
__SomeExtension_init(...);
// Additional initialization steps for your middleware
}

constructor() {
_disableInitializers();
}
}

Extensions Hooks

For customizing logic, hooks are provided in many extensions. Override these hooks in your middleware and always call super._hook_name() to ensure base functionality is preserved unless you intend to fully replace it.

For example, _beforeRegisterOperator, _beforeUnpauseOperatorVault, or _beforeUnregisterSubnetwork can be overridden to enforce additional checks or logging.

It’s considered a best practice to:

  • Override only the hooks you need.
  • Call super to maintain upstream logic and state updates.
function _beforeRegisterOperator(address operator, address vault) internal override {
super._beforeRegisterOperator(operator, vault);
// Additional logic here
}

Custom Logic

For custom logic implementation, you can leverage the base middleware's internal functions through its inherited manager contracts:

  • KeyManager: Handles operator key registration and validation
  • CaptureTimestampManager: Manages timestamp snapshots for state transitions
  • AccessManager: Controls function access and permissions
  • OperatorManager: Manages operator registration, pausing, unpausing, and unregistration
  • VaultManager: Manages vault registration, pausing, unpausing, and unregistration
  • StakePowerManager: Manages stake power allocation and distribution
  • NetworkStorage: Stores the network address
  • SlashingWindowStorage: Stores the slashing window

Each manager provides internal functions that can be called from your middleware to implement custom business logic while maintaining security and consistency.

Middleware Architecture

warning

Historical Data Constraints

On-chain historic data is only accessible from the range [now - SLASHING_WINDOW, now]. If you need older off-chain data, you must query it via standard Ethereum node API calls (eth_call with a specified block number).

warning

Slashing Logic

The SDK provides internal functions for slashing within a single vault and tools for executing slashing. However, the logic and conditions under which slashing occurs are left to the middleware developer. You must implement your own slashing policies by using the provided internal hooks and manager functions.

info

Public Getters

Public getter functions (e.g., for lists of operators, vaults, or active sets) are available in the BaseMiddlewareReader contract. The BaseMiddleware fallback can delegate calls to a deployed BaseMiddlewareReader, making all reader functions accessible without cluttering your middleware code. All getters are defined in the IBaseMiddlewareReader interface. For more details, see the BaseMiddlewareReader API Reference.

  • In your initializer, provide the address of the deployed BaseMiddlewareReader.
  • All reader functions become accessible via fallback calls, no additional code needed.
danger

StakePowerManager

The StakePowerManager handles converting vault stakes into voting powers. Since vaults can hold different assets (ETH, ERC20 tokens, etc.) with varying market values, we need a way to normalize these into a voting power. By default, we provide a 1:1 conversion of stake to voting power, but this is often insufficient for multi-asset systems. You can either override this directly in your middleware to implement custom voting power calculations, or use third-party stake power managers to handle more complex scenarios involving different assets, prices, and market values.

Examples of Middlewares

Four middleware implementations are currently provided as examples:

  1. SimplePosMiddleware: Demonstrates a straightforward Proof-of-Stake design.
  2. SqrtTaskMiddleware: Shows how to integrate computational tasks and slash incorrect answers.
  3. SelfRegisterMiddleware: Allows operators to self-register using ECDSA signatures.
  4. SelfRegisterEd25519Middleware: Similar to SelfRegisterMiddleware, but with EdDSA signatures for Ed25519 keys.

Study these examples to understand how to compose extensions and implement custom logic.