Skip to content
LogoLogo

Relay On-Chain

The Symbiotic Relay system implements a complete signature aggregation workflow from validator set derivation through on-chain commitment of the ValSetHeader data structure. This enables provable attestation checks on any chain for arbitrary data signed by the validator set quorum.

The system provides a modular smart contract framework that lets you manage validator sets dynamically, handle cryptographic keys, aggregate signatures, and commit cross-chain state.

Architecture

Symbiotic provides a set of predefined smart contracts representing the following modules:

  • VotingPowerProvider - provides basic data about operators, vaults, and their voting power, enabling various onboarding schemes
  • KeyRegistry - verifies and manages operators' keys; currently, these key types are supported:
  • ValSetDriver - used by the off-chain part of Symbiotic Relay for validator set derivation and maintenance
  • Settlement - requires committing a compressed validator set (header) each epoch, but allows verifying signatures made by the validator set; currently supports the following verification mechanics:
    • SimpleVerifier - requires the whole validator set as an input during verification, but in a compressed and efficient way, making it the best choice for up to 125 validators
    • ZKVerifier - uses ZK verification made with gnark, allowing larger validator sets with an almost constant verification gas cost

Permissions

Relay contracts have three ready-to-use permission models:

To use these permission models, inherit one of the above contracts and add the checkPermission modifier to functions that require access control.

Examples

ValSetDriver with OzOwnable
MyValSetDriver.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
 
import {ValSetDriver} from "../src/modules/valset-driver/ValSetDriver.sol";
import {OzOwnable} from "../src/modules/common/permissions/OzOwnable.sol";
import {IEpochManager} from "../src/interfaces/modules/valset-driver/IEpochManager.sol";
import {IValSetDriver} from "../src/interfaces/modules/valset-driver/IValSetDriver.sol";
 
contract MyValSetDriver is ValSetDriver, OzOwnable {
    function initialize(
        ValSetDriverInitParams memory valSetDriverInitParams,
        address owner
    ) public virtual initializer {
        __ValSetDriver_init(valSetDriverInitParams);
        __OzOwnable_init(ozOwnableInitParams);
    }
}
Settlement with OzAccessControl
MySettlement.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
 
import {Settlement} from "../src/modules/settlement/Settlement.sol";
import {OzAccessControl} from "../src/modules/common/permissions/OzAccessControl.sol";
import {ISettlement} from "../src/interfaces/modules/settlement/ISettlement.sol";
 
contract MySettlement is Settlement, OzAccessControl {
    bytes32 public constant SET_SIG_VERIFIER_ROLE = keccak256("SET_SIG_VERIFIER_ROLE");
    bytes32 public constant SET_GENESIS_ROLE = keccak256("SET_GENESIS_ROLE");
 
    function initialize(
        SettlementInitParams memory settlementInitParams,
        address defaultAdmin
    ) public virtual initializer {
        __Settlement_init(settlementInitParams);
 
        _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
 
        _setSelectorRole(ISettlement.setSigVerifier.selector, SET_SIG_VERIFIER_ROLE);
        _setSelectorRole(ISettlement.setGenesis.selector, SET_GENESIS_ROLE);
    }
}

VotingPowerProvider Extensions

Multiple voting power extensions can be combined to achieve different properties of the VotingPowerProvider:

  • OperatorsWhitelist - only whitelisted operators can register
  • OperatorsBlacklist - blacklisted operators are unregistered and are forbidden to return back
  • OperatorsJail - operators can be jailed for some amount of time and register back after that
  • SharedVaults - shared (with other networks) vaults (like the ones with NetworkRestakeDelegator) can be added
  • OperatorVaults - vaults that are attached to a single operator can be added
  • MultiToken - possible to add new supported tokens on the go
  • OpNetVaultAutoDeploy - enable auto-creation of the configured by you vault on each operator registration
  • Ready bindings are also available for slashing and rewards

Examples

Single-Operator Vaults Added by Owner
MyVotingPowerProvider.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
 
import {VotingPowerProvider} from "../src/modules/voting-power/VotingPowerProvider.sol";
import {OzOwnable} from "../src/modules/common/permissions/OzOwnable.sol";
import {EqualStakeVPCalc} from "../src/modules/voting-power/common/voting-power-calc/EqualStakeVPCalc.sol";
import {OperatorVaults} from "../src/modules/voting-power/extensions/OperatorVaults.sol";
 
contract MyVotingPowerProvider is VotingPowerProvider, OzOwnable, EqualStakeVPCalc, OperatorVaults {
    constructor(address operatorRegistry, address vaultFactory) VotingPowerProvider(operatorRegistry, vaultFactory) {}
 
    function initialize(
        VotingPowerProviderInitParams memory votingPowerProviderInitParams,
        OzOwnableInitParams memory ozOwnableInitParams
    ) public virtual initializer {
        __VotingPowerProvider_init(votingPowerProviderInitParams);
        __OzOwnable_init(ozOwnableInitParams);
    }
}
Shared Vaults Whitelisted under AccessControl
MyVotingPowerProvider.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
 
import {VotingPowerProvider} from "../src/modules/voting-power/VotingPowerProvider.sol";
import {OzAccessControl} from "../src/modules/common/permissions/OzAccessControl.sol";
import {EqualStakeVPCalc} from "../src/modules/voting-power/common/voting-power-calc/EqualStakeVPCalc.sol";
import {SharedVaults} from "../src/modules/voting-power/extensions/SharedVaults.sol";
import {OperatorsWhitelist} from "../src/modules/voting-power/extensions/OperatorsWhitelist.sol";
import {ISharedVaults} from "../src/interfaces/modules/voting-power/extensions/ISharedVaults.sol";
 
contract MyVotingPowerProvider is VotingPowerProvider, OzAccessControl, EqualStakeVPCalc, SharedVaults, OperatorsWhitelist {
    bytes32 public constant REGISTER_SHARED_VAULT = keccak256("REGISTER_SHARED_VAULT");
    bytes32 public constant UNREGISTER_SHARED_VAULT = keccak256("UNREGISTER_SHARED_VAULT");
    bytes32 public constant SET_WHITELIST_STATUS = keccak256("SET_WHITELIST_STATUS");
    bytes32 public constant WHITELIST_OPERATOR = keccak256("WHITELIST_OPERATOR");
    bytes32 public constant UNWHITELIST_OPERATOR = keccak256("UNWHITELIST_OPERATOR");
 
    constructor(address operatorRegistry, address vaultFactory) VotingPowerProvider(operatorRegistry, vaultFactory) {}
 
    function initialize(
        VotingPowerProviderInitParams memory votingPowerProviderInitParams,
        address defaultAdmin
    ) public virtual initializer {
        __VotingPowerProvider_init(votingPowerProviderInitParams);
 
        _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
        _setSelectorRole(ISharedVaults.registerSharedVault.selector, REGISTER_SHARED_VAULT);
        _setSelectorRole(ISharedVaults.unregisterSharedVault.selector, UNREGISTER_SHARED_VAULT);
        _setSelectorRole(ISharedVaults.setWhitelistStatus.selector, SET_WHITELIST_STATUS);
        _setSelectorRole(ISharedVaults.whitelistOperator.selector, WHITELIST_OPERATOR);
        _setSelectorRole(ISharedVaults.unwhitelistOperator.selector, UNWHITELIST_OPERATOR);
    }
 
    function _registerOperatorImpl(
        address operator
    ) internal virtual override(VotingPowerProvider, OperatorsWhitelist) {
        super._registerOperatorImpl(operator);
    }
}

VotingPowerProvider Power Calculators

VotingPowerProvider always inherits a virtual VotingPowerCalculators contracts that has to be implemented in the resulting contract. Symbiotic provides several stake-to-votingPower conversion mechanisms you can separately or combine:

Examples

MyVotingPowerProvider.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
 
import {VotingPowerProvider} from "../src/modules/voting-power/VotingPowerProvider.sol";
import {PricedTokensChainlinkVPCalc} from
    "../src/modules/voting-power/common/voting-power-calc/PricedTokensChainlinkVPCalc.sol";
    import {OzOwnable} from "../src/modules/common/permissions/OzOwnable.sol";
import {WeightedTokensVPCalc} from "../src/modules/voting-power/common/voting-power-calc/WeightedTokensVPCalc.sol";
import {WeightedVaultsVPCalc} from "../src/modules/voting-power/common/voting-power-calc/WeightedVaultsVPCalc.sol";
import {VotingPowerCalcManager} from "../src/modules/voting-power/base/VotingPowerCalcManager.sol";
 
contract MyVotingPowerProvider is VotingPowerProvider, OzOwnable, PricedTokensChainlinkVPCalc, WeightedTokensVPCalc, WeightedVaultsVPCalc {
    constructor(address operatorRegistry, address vaultFactory) VotingPowerProvider(operatorRegistry, vaultFactory) {}
 
    function initialize(
        VotingPowerProviderInitParams memory votingPowerProviderInitParams,
        OzOwnableInitParams memory ozOwnableInitParams
    ) public virtual initializer {
        __VotingPowerProvider_init(votingPowerProviderInitParams);
        __OzOwnable_init(ozOwnableInitParams);
    }
 
    function stakeToVotingPowerAt(
        address vault,
        uint256 stake,
        bytes memory extraData,
        uint48 timestamp
    )
        public
        view
        override(VotingPowerCalcManager, PricedTokensChainlinkVPCalc, WeightedTokensVPCalc, WeightedVaultsVPCalc)
        returns (uint256)
    {
        return super.stakeToVotingPowerAt(vault, stake, extraData, timestamp);
    }
 
    function stakeToVotingPower(
        address vault,
        uint256 stake,
        bytes memory extraData
    )
        public
        view
        override(VotingPowerCalcManager, PricedTokensChainlinkVPCalc, WeightedTokensVPCalc, WeightedVaultsVPCalc)
        returns (uint256)
    {
        return super.stakeToVotingPower(vault, stake, extraData);
    }
}

Deployment

The deployment tooling is in the script/ folder. It consists of RelayDeploy.sol Foundry script template and relay-deploy.sh bash script (Relay smart contracts use external libraries, so it's not currently possible to use solely Foundry script for multi-chain deployment).

  • RelayDeploy.sol - abstract base that wires common Symbiotic Core helpers and exposes four deployment hooks: KeyRegistry, VotingPowerProvider, Settlement, and ValSetDriver
  • relay-deploy.sh - orchestrates per-contract multi-chain deployments (uses Python to parse toml file)

The script deploys Relay modules under OpenZeppelin's TransparentUpgradeableProxy using CreateX, providing better control for production deployments and simpler approaches for development.

Configure on-chain deployment

Implement your MyRelayDeploy.sol (see example):

  • Include the deployment configuration of your Relay modules
  • Implement all virtual functions of RelayDeploy.sol
  • In the constructor, input the path of the toml file
  • Use additional helpers such as getCore(), getKeyRegistry(), getVotingPowerProvider(), etc. (see full list in RelayDeploy.sol)

Choose multi-chain setup

Implement your my-relay-deploy.toml (see example):

  • Include RPC URLs needed for deployment and specify which modules to deploy on which chains
  • Do not replace [1234567890] placeholder with endpoint_url = ""
  • Contracts are deployed in this order: 1. KeyRegistry 2. VotingPowerProvider 3. Settlement 4. ValSetDriver

Run the deployment

Execute the deployment script, e.g.:

bash
./script/relay-deploy.sh ./script/examples/MyRelayDeploy.sol ./script/examples/my-relay-deploy.toml --broadcast --ledger

At the end, your toml file will contain the addresses of the deployed Relay modules.

Integrate

Symbiotic Relay provides comprehensive tooling that works independently, so you can focus on your stake-backed application logic.

Verify Message

Your application contract can verify any message using a validator set at any point in time via:

MyApp.sol
import {ISettlement} from "@symbioticfi/relay-contracts/src/interfaces/modules/settlement/ISettlement.sol";
 
function verifyMessage(bytes calldata message, uint48 epoch, bytes calldata proof) public returns (bool) {
    return ISettlement(SETTLEMENT).verifyQuorumSigAt(
            abi.encode(keccak256(message)),
            15, // default key tag - BN254
            (uint248(1e18) * 2) / 3 + 1, // default quorum threshold - 2/3 + 1
            proof,
            epoch,
            new bytes(0)
    );
}

Use Validator Set Data

Your application contract can use the validator set at any point in time using SSZ proof verification, for example:

MyApp.sol
import {ValSetVerifier} from "@symbioticfi/relay-contracts/src/libraries/utils/ValSetVerifier.sol";
import {Math} from "openzeppelin-contracts/contracts/utils/math/Math.sol";
 
function verifyOperatorVotingPower(
    ValSetVerifier.SszProof calldata validatorRootProof,
    uint256 validatorRootLocalIndex,
    bytes32 validatorSetRoot,
    ValSetVerifier.SszProof calldata operatorProof,
    address operator,
    ValSetVerifier.SszProof calldata votingPowerProof,
    uint256 votingPower
) public returns (bool) {
    return operatorProof.leaf == bytes32(uint256(uint160(operator)) << 96)
        && ValSetVerifier.verifyOperator(
        validatorRootProof, validatorRootLocalIndex, validatorSetRoot, operatorProof
    ) && votingPowerProof.leaf == bytes32(votingPower << (256 - (Math.log2(votingPower) / 8 + 1) * 8))
        && ValSetVerifier.verifyVotingPower(
        validatorRootProof, validatorRootLocalIndex, validatorSetRoot, votingPowerProof
    );
}

Next Steps

Relay Off-Chain

Proceed to the the development of your protocol's off-chain part using Relay.