Middleware SDK Development
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:
- KeyManager: Manages operator keys (e.g.,
KeyManager256
,KeyManagerBytes
, orNoKeyManager
). - CaptureTimestampManager: Determines the reference timestamp or epoch for historical state queries (e.g.,
TimestampCapture
,EpochCapture
). - 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. - AccessManager: Controls access to restricted functions. Multiple implementations exist (e.g.,
OwnableAccessManager
orOzAccessControl
). For complex role-based permissions,OzAccessControl
can be used to assign roles and role admins. - Operators: Extends
BaseMiddleware
to manage operator registration, validation, lifecycle including pausing/unpausing and operator-specific vaults. - 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. - Subnetworks: (Optional) Extends
BaseMiddleware
to manage registration, pausing, and unregistration of subnetworks. - 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 {
// ...
}
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 theonlyInitializing
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.
- Upgradeable
- Non-Upgradeable
contract MyMiddleware is BaseMiddleware, SomeExtension {
function initialize(...) external initializer {
__BaseMiddleware_init(...);
__SomeExtension_init(...);
// Additional initialization steps for your middleware
}
constructor() {
_disableInitializers();
}
}
contract MyMiddleware is BaseMiddleware, SomeExtension {
function initialize(...) internal initializer {
__BaseMiddleware_init(...);
__SomeExtension_init(...);
// Additional initialization steps for your middleware
}
constructor(...) {
initialize(...);
}
}
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 validationCaptureTimestampManager
: Manages timestamp snapshots for state transitionsAccessManager
: Controls function access and permissionsOperatorManager
: Manages operator registration, pausing, unpausing, and unregistrationVaultManager
: Manages vault registration, pausing, unpausing, and unregistrationStakePowerManager
: Manages stake power allocation and distributionNetworkStorage
: Stores the network addressSlashingWindowStorage
: 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.
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).
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.
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.
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:
- SimplePosMiddleware: Demonstrates a straightforward Proof-of-Stake design.
- SqrtTaskMiddleware: Shows how to integrate computational tasks and slash incorrect answers.
- SelfRegisterMiddleware: Allows operators to self-register using ECDSA signatures.
- SelfRegisterEd25519Middleware: Similar to
SelfRegisterMiddleware
, but with EdDSA signatures for Ed25519 keys.
Study these examples to understand how to compose extensions and implement custom logic.