{"judging_docs_hash":"2668590b5fd3c5aae6b3120a0e1e7b79524bd673","id":967,"starts_at":1750345200,"score_sequence":262,"languages":[],"scope":[{"repo":"symbioticfi/middleware-sdk","branch_name":"main","commit_hash":"57f80a92a614f4df812cd0495e3b214bc5d954ec","total_nsloc":4345,"files":[{"name":"examples/MyKeyRegistry.sol","nsloc":7},{"name":"examples/MyNetwork.sol","nsloc":11},{"name":"examples/MySettlement.sol","nsloc":10},{"name":"examples/MyValSetDriver.sol","nsloc":10},{"name":"examples/MyVotingPowerProvider.sol","nsloc":13},{"name":"pkg/proof/circuit.go","nsloc":160},{"name":"src/contracts/libraries/keys/KeyBlsBn254.sol","nsloc":65},{"name":"src/contracts/libraries/keys/KeyEcdsaSecp256k1.sol","nsloc":40},{"name":"src/contracts/libraries/sigs/SigBlsBn254.sol","nsloc":47},{"name":"src/contracts/libraries/sigs/SigEcdsaSecp256k1.sol","nsloc":20},{"name":"src/contracts/libraries/structs/Checkpoints.sol","nsloc":299},{"name":"src/contracts/libraries/structs/PersistentSet.sol","nsloc":156},{"name":"src/contracts/libraries/utils/InputNormalizer.sol","nsloc":22},{"name":"src/contracts/libraries/utils/KeyTags.sol","nsloc":75},{"name":"src/contracts/libraries/utils/ValSetVerifier.sol","nsloc":273},{"name":"src/contracts/modules/base/NetworkManager.sol","nsloc":32},{"name":"src/contracts/modules/base/OzEIP712.sol","nsloc":20},{"name":"src/contracts/modules/base/PermissionManager.sol","nsloc":9},{"name":"src/contracts/modules/common/permissions/OzAccessControl.sol","nsloc":26},{"name":"src/contracts/modules/common/permissions/OzAccessManaged.sol","nsloc":12},{"name":"src/contracts/modules/common/permissions/OzOwnable.sol","nsloc":12},{"name":"src/contracts/modules/key-registry/KeyRegistry.sol","nsloc":203},{"name":"src/contracts/modules/network/Network.sol","nsloc":205},{"name":"src/contracts/modules/settlement/Settlement.sol","nsloc":205},{"name":"src/contracts/modules/settlement/sig-verifiers/libraries/ExtraDataStorageHelper.sol","nsloc":18},{"name":"src/contracts/modules/settlement/sig-verifiers/SigVerifierBlsBn254Simple.sol","nsloc":135},{"name":"src/contracts/modules/settlement/sig-verifiers/SigVerifierBlsBn254ZK.sol","nsloc":105},{"name":"src/contracts/modules/valset-driver/EpochManager.sol","nsloc":115},{"name":"src/contracts/modules/valset-driver/ValSetDriver.sol","nsloc":335},{"name":"src/contracts/modules/voting-power/base/VotingPowerCalcManager.sol","nsloc":5},{"name":"src/contracts/modules/voting-power/common/voting-power-calc/EqualStakeVPCalc.sol","nsloc":14},{"name":"src/contracts/modules/voting-power/extensions/BaseRewards.sol","nsloc":45},{"name":"src/contracts/modules/voting-power/extensions/BaseSlashing.sol","nsloc":45},{"name":"src/contracts/modules/voting-power/extensions/logic/BaseRewardsLogic.sol","nsloc":22},{"name":"src/contracts/modules/voting-power/extensions/logic/BaseSlashingLogic.sol","nsloc":95},{"name":"src/contracts/modules/voting-power/extensions/logic/OpNetVaultAutoDeployLogic.sol","nsloc":168},{"name":"src/contracts/modules/voting-power/extensions/MultiToken.sol","nsloc":12},{"name":"src/contracts/modules/voting-power/extensions/OperatorsBlacklist.sol","nsloc":40},{"name":"src/contracts/modules/voting-power/extensions/OperatorsJail.sol","nsloc":41},{"name":"src/contracts/modules/voting-power/extensions/OperatorsWhitelist.sol","nsloc":58},{"name":"src/contracts/modules/voting-power/extensions/OperatorVaults.sol","nsloc":12},{"name":"src/contracts/modules/voting-power/extensions/OpNetVaultAutoDeploy.sol","nsloc":51},{"name":"src/contracts/modules/voting-power/extensions/SharedVaults.sol","nsloc":12},{"name":"src/contracts/modules/voting-power/logic/VotingPowerProviderLogic.sol","nsloc":418},{"name":"src/contracts/modules/voting-power/VotingPowerProvider.sol","nsloc":181},{"name":"src/interfaces/modules/base/INetworkManager.sol","nsloc":13},{"name":"src/interfaces/modules/base/IOzEIP712.sol","nsloc":9},{"name":"src/interfaces/modules/base/IPermissionManager.sol","nsloc":2},{"name":"src/interfaces/modules/common/permissions/IOzAccessControl.sol","nsloc":9},{"name":"src/interfaces/modules/common/permissions/IOzAccessManaged.sol","nsloc":8},{"name":"src/interfaces/modules/common/permissions/IOzOwnable.sol","nsloc":7},{"name":"src/interfaces/modules/key-registry/IKeyRegistry.sol","nsloc":36},{"name":"src/interfaces/modules/network/INetwork.sol","nsloc":40},{"name":"src/interfaces/modules/network/ISetMaxNetworkLimitHook.sol","nsloc":3},{"name":"src/interfaces/modules/settlement/ISettlement.sol","nsloc":42},{"name":"src/interfaces/modules/settlement/sig-verifiers/ISigVerifierBlsBn254Simple.sol","nsloc":11},{"name":"src/interfaces/modules/settlement/sig-verifiers/ISigVerifierBlsBn254ZK.sol","nsloc":12},{"name":"src/interfaces/modules/settlement/sig-verifiers/ISigVerifier.sol","nsloc":3},{"name":"src/interfaces/modules/settlement/sig-verifiers/zk/IVerifier.sol","nsloc":3},{"name":"src/interfaces/modules/valset-driver/IEpochManager.sol","nsloc":17},{"name":"src/interfaces/modules/valset-driver/IValSetDriver.sol","nsloc":74},{"name":"src/interfaces/modules/voting-power/base/IVotingPowerCalcManager.sol","nsloc":3},{"name":"src/interfaces/modules/voting-power/common/voting-power-calc/IEqualStakeVPCalc.sol","nsloc":2},{"name":"src/interfaces/modules/voting-power/extensions/IBaseRewards.sol","nsloc":16},{"name":"src/interfaces/modules/voting-power/extensions/IBaseSlashing.sol","nsloc":30},{"name":"src/interfaces/modules/voting-power/extensions/IMultiToken.sol","nsloc":3},{"name":"src/interfaces/modules/voting-power/extensions/IOperatorsBlacklist.sol","nsloc":11},{"name":"src/interfaces/modules/voting-power/extensions/IOperatorsJail.sol","nsloc":11},{"name":"src/interfaces/modules/voting-power/extensions/IOperatorsWhitelist.sol","nsloc":16},{"name":"src/interfaces/modules/voting-power/extensions/IOperatorVaults.sol","nsloc":3},{"name":"src/interfaces/modules/voting-power/extensions/IOpNetVaultAutoDeploy.sol","nsloc":29},{"name":"src/interfaces/modules/voting-power/extensions/ISharedVaults.sol","nsloc":3},{"name":"src/interfaces/modules/voting-power/IVotingPowerProvider.sol","nsloc":70}]}],"template_repo_name":"sherlock-audit/2025-06-symbiotic-relay","judging_prize_pool":0,"num_competition_issues":530,"rewards":100000,"rewards_tiers":[{"min_count":0,"unlock_percentage":100.0,"id":914,"severity":{"name":"Medium","text_color":"423d38","id":2.0,"color":"f0b375"}}],"is_best_efforts":false,"calc_completed":true,"update_nsloc":null,"type_label":"Public","is_judging_v3":true,"uses_live_issues":true,"logo_url":"https://sherlock-files.ams3.digitaloceanspaces.com/contests/symbiotic.png","reserve_auditor_message":null,"short_description":"Symbiotic Core (https://github.com/symbioticfi/core) provides a modular onchain framework for creating flexible staking solutions, including collateral choice (native tokens, restaked assets, or multi-asset), as well as reward, slashing and redistribution logic.\n\nSymbiotic Relay serves as an extension to the Symbiotic Core that radically simplifies integrating Symbiotic's universal staking primitives and enables leveraging stake across any execution environment, expanding the design space for multichain-native decentralized protocols.","lead_judge_fixed_pay":9000,"lead_senior_auditor_handle":"0x73696d616f","judging_stopped_at":1755603000,"prize_pool":50900,"status":"FINISHED","description":null,"reserved_auditors_fixed_pays":[],"ends_at":1752159600,"requires_kyc":false,"bug_bounty_contest":false,"escalation_ends_at":1753369041,"lead_judge_handle":"oot2k","nsloc":4345,"is_judging_visible":false,"first_blood_pool":2600,"allows_signups":false,"escalation_started_at":1753282641,"private":false,"lead_judge_avatar_url":"https://sherlock-files.ams3.digitaloceanspaces.com/twitter_images/f465d6ad-cbc9-4218-8cf4-d6127e906008.jpg","title":"Symbiotic Relay","token":"USDC","lead_senior_auditor_avatar_url":"https://sherlock-files.ams3.digitaloceanspaces.com/twitter_images/6ae07656-3ef9-4f30-9667-7a2e92b12411.jpg","judging_repo_name":"sherlock-audit/2025-06-symbiotic-relay-judging","report":"# Issue M-1: Attacker will manipulate voting power calculations as `getOperatorVotingPower()` and `getOperatorVotingPowerAt()` functions lack vault validation \n\nSource: https://github.com/sherlock-audit/2025-06-symbiotic-relay-judging/issues/196 \n\nThis issue has been acknowledged by the team but won't be fixed at this time.\n\n## Found by \n0xapple, 0xmaverick, Drynooo, Jeffy, PASCAL, Ziusz, katz, patitonar, roshark, zark\n\n### Summary\n\nThe missing vault validation in `VotingPowerProvider::getOperatorVotingPower()` and `VotingPowerProvider::getOperatorVotingPowerAt()` functions that receives the `vault` external parameter will cause incorrect voting power calculations as an attacker can provide unregistered or invalid vault addresses to gain unauthorized voting influence\n\n### Root Cause\n\nIn `VotingPowerProviderLogic::getOperatorVotingPower(address operator, address vault, bytes memory extraData)` and `VotingPowerProviderLogic::getOperatorVotingPowerAt(address operator, address vault, bytes memory extraData, uint48 timestamp, bytes memory hints)` the `vault` parameter is not validated to be a registered vault before processing voting power calculations.\n\nhttps://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/main/middleware-sdk/src/contracts/modules/voting-power/VotingPowerProvider.sol#L273-L295\n\nThe functions only validate that the vault's collateral token is registered via `isTokenRegistered(IVault(vault).collateral())`, but they do not verify that the vault itself is properly registered in the system.\n\nA vault can be unregistered by:\n- SharedVaults::unregisterSharedVault()\n- OperatorVaults::unregisterOperatorVault()\n\n### Internal Pre-conditions\n\nA vault with valid collateral token exists but is not registered in the `VotingPowerProvider` contract, OR the vault was registered but unregistered later.\n\n### External Pre-conditions\n\nN/A\n\n### Attack Path\n\n1. Attacker identifies an unregistered vault that has a registered collateral token\n2. Attacker calls some contract that calls `VotingPowerProvider::getOperatorVotingPower()` with the unregistered vault address\n3. Function calculates voting power using the retrieved stake and returns it as valid voting power\n4. Attacker uses this voting power, gaining influence they should not have\n\n### Impact\n\nThe protocol using `VotingPowerProvider` suffers incorrect voting power calculations as attackers gain unauthorized voting influence through unregistered vaults.\n\n### PoC\n\n_No response_\n\n### Mitigation\n\nAdd vault validation to both `getOperatorVotingPower()` and `getOperatorVotingPowerAt()` functions by checking if the vault is registered before processing voting power calculations:\n\n```solidity\n// Add vault validation\nif (!isSharedVaultRegistered(vault) && !isOperatorVaultRegistered(vault)) {\n    return 0;\n}\n```\n\n# Issue M-2: Enabling the whitelist can grant a malicious operator a temporary whitelisted status \n\nSource: https://github.com/sherlock-audit/2025-06-symbiotic-relay-judging/issues/361 \n\n## Found by \nZiusz, klaus, maigadoh, zark\n\n### Summary\n\nN/A\n\n### Root Cause\n\nThe root cause of this vulnerability is that in order for `unwhitelist` call to success, the operator must be whitelisted.\n\n```solidity\n    function _unwhitelistOperator(\n        address operator\n    ) internal virtual {\n        if (!isOperatorWhitelisted(operator)) {\n            revert OperatorsWhitelist_OperatorNotWhitelisted();\n        }\n        _getOperatorsWhitelistStorage()._whitelisted[operator] = false;\n        if (isWhitelistEnabled() && isOperatorRegistered(operator)) {\n            _unregisterOperator(operator);\n        }\n\n        emit UnwhitelistOperator(operator);\n    }\n```\n[Link to code](https://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/435a21fd81bcd588439feef3108580f535b9e5eb/middleware-sdk/src/contracts/modules/voting-power/extensions/OperatorsWhitelist.sol#L96C1-L108C6)\n\n### Internal Pre-conditions\n\nN/A\n\n### External Pre-conditions\n\nN/A\n\n### Attack Path\n\n1. Whitelist mode is off.\n2. Owner enables whitelist mode.\n3. Malicious operator front runs owner by registering.\n4. Now, if owner wants to unregister him, he must grant him the whitelist role and then unwhitelist him.\n5. This would mean that the malicious operator would be granted the whitelist role for at least one block.\n6. Since this is an SDK, it is very possible that the whitelist role will be connected with further abilities.\n\n### Impact\n\nMalicious actor can frontrun the whitelist mode enabled and in this way force the owners to whitelist him (in order to unwhitelist him) for at least one block. So the malicious actor can gain any abilities that a whitelisted operator can do without this being the goal of the owners of the network. If we take into account that the owner of the `Middleware` may be the `Network` which has time delays in the calls, this worsen the situation.\n\n### PoC\n\nN/A\n\n### Mitigation\n\nAllow the `unwhitelist` of an operator even if he is not whitelisted but the whitelist mode is on.\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/symbioticfi/relay-contracts/pull/33/commits/e655574ab282e2c42065166f1d6f8ef4a5004665\n\n\n\n\n# Issue M-3: `autoDeployedVault` mapping is not updated after `unregisterOperatorVault` \n\nSource: https://github.com/sherlock-audit/2025-06-symbiotic-relay-judging/issues/362 \n\n## Found by \n0xShoonya, snjax, zark\n\n### Summary\n\n`unregisterOperatorVault` deletes a vault in `VotingPowerProvider` but it doesn't check the `_autoDeployedVault` mapping in `OpNetVaultAutoDeploy` extension, so an operator whose auto-deployed vault is removed remains flagged as owning that vault and can neither receive a fresh auto deployment nor rely on the old address.\n\n### Root Cause\n\nThe vault unregistration in `VotingPowerProviderLogic` updates only `_operatorVaults` and `_allOperatorVaults`. When the vault had originally been created by `OpNetVaultAutoDeploy.createVault`, its address was also stored in `_autoDeployedVault[operator]`. Because `unregisterOperatorVault` is not overridden in the extension and holds no hook back into it, that mapping key is left unchanged.\n\n```solidity\n// OpNetVaultAutoDeploy.sol\n    function _registerOperatorImpl(\n        address operator\n    ) internal virtual override {\n        super._registerOperatorImpl(operator);\n        if (isAutoDeployEnabled() && getAutoDeployedVault(operator) == address(0)) {\n            (address vault, address delegator,) = OpNetVaultAutoDeployLogic.createVault(operator);\n            _registerOperatorVault(operator, vault);\n            if (isSetMaxNetworkLimitHookEnabled()) {\n                ISetMaxNetworkLimitHook(NETWORK()).setMaxNetworkLimit(\n                    delegator, SUBNETWORK_IDENTIFIER(), type(uint256).max\n                );\n            }\n        }\n    }\n```\n[Link to code](https://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/435a21fd81bcd588439feef3108580f535b9e5eb/middleware-sdk/src/contracts/modules/voting-power/extensions/OpNetVaultAutoDeploy.sol#L92C1-L105C6)\n\n```solidity\n// VotingPowerProviderLogic.sol\n    function unregisterOperatorVault(address operator, address vault) public {\n        IVotingPowerProvider.VotingPowerProviderStorage storage $ = _getVotingPowerProviderStorage();\n        if (!$._operatorVaults[operator].remove(Time.timestamp(), vault)) {\n            revert IVotingPowerProvider.VotingPowerProvider_OperatorVaultNotRegistered();\n        }\n        $._allOperatorVaults.remove(Time.timestamp(), vault);\n\n        emit IVotingPowerProvider.UnregisterOperatorVault(operator, vault);\n    }\n```\n[Link to code](https://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/435a21fd81bcd588439feef3108580f535b9e5eb/middleware-sdk/src/contracts/modules/voting-power/logic/VotingPowerProviderLogic.sol#L481C1-L489C6)\n\nSo after all, calls to `getAutoDeployedVault(operator)` therefore return a vault that the core module now considers unregistered, and `_registerOperatorImpl` skips auto-deployment because it sees a non-zero pointer.\n\n### Internal Pre-conditions\n\nAuto deployment must be enabled, the configuration valid and an operator has previously registered, triggering `createVault` and populating `_autoDeployedVault`.\n\n### External Pre-conditions\n\n`unregisterOperatorVault(operator, vault)` being invoked for that vault created with auto deployment.\n\n### Attack Path\n\n1. An operator registers while auto-deployment is enabled, causing `createVault` to store vault in `_autoDeployedVault[operator]` and register it in the provider storage.\n2. A caller executes `unregisterOperatorVault(operator, vault)`, which removes the vault from `_operatorVaults` and `_allOperatorVaults` but leaves `_autoDeployedVault[operator]` unchanged.\n3. Because `getAutoDeployedVault(operator)` still returns a non-zero address, the system assumes the operator already has a vault and skips creating a new one, while the active vault lists no longer include that old address. Also, if someone queries the `getAutoDeployedVault` of the operator, an incorrect unregistered vault would be returned.\n\n### Impact\n\nThe impact of this issue is that the `OpNetVaultAutoDeploy::getAutoDeployedVault` of the operator would return an incorrect unregistered vault while in the same time the operator would never be able to create a new auto deployed vault. \n\n\n### PoC\n\nN/A\n\n### Mitigation\n\nIn order to mitigate this code \"asymmetry\", it is recommended to overwrite the `_unregisterOperator` in `OpNetVaultAutoDeploy` (as it is done with the `_registerOperatorImpl`) and if the operator and the vaults matches in `_autoDeployedVault`, then unregister it from there as well.\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/symbioticfi/relay-contracts/pull/33/commits/84fa428dbd8b113c56f92bf91481997d6eedc288\n\n\n\n\n# Issue M-4: Most KeyRegistry, VotingPowerProvider functions can be DoSed \n\nSource: https://github.com/sherlock-audit/2025-06-symbiotic-relay-judging/issues/403 \n\nThis issue has been acknowledged by the team but won't be fixed at this time.\n\n## Found by \n00xJi, 0x73696d616f, 0xShoonya, 0xpetern, Cybrid, Drynooo, MaCree, Mimis, Mishkat6451, Raihan, X0sauce, Ziusz, albahaca0000, befree3x, coin2own, francoHacker, harry, hunt1, j3x, kangaroo, klaus, maigadoh, maxim371, montecristo, pashap9990, redbeans, roadToWatsonN101, snowflake30518, themartto, zark\n\n### Summary\n\n`VotingPowerProvider::registerOperator()` is [permissionless](https://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/main/middleware-sdk/src/contracts/modules/voting-power/VotingPowerProvider.sol#L340-L342), so anybody can call it with any number of accounts to DoS all functions that gather operator information, such as `getOperatorsAt()`, `getOperators()`. Same for the vaults of the operators `getOperatorVaults()`, which can be registered via `_registerOperatorVault()` (part of the `OperatorVaults` extension or the auto deployment `OpNetVaultAutoDeploy` extension).\n\nThe `KeyRegistry` also has this issue, as [`setKey()`](https://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/main/middleware-sdk/src/contracts/modules/key-registry/KeyRegistry.sol#L202) is permissionless, and [`getKeys()`](https://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/main/middleware-sdk/src/contracts/modules/key-registry/KeyRegistry.sol#L152) will be DoSed as anybody can create any number of ethereum addresses as operators and keys to DoS the function.\n\nAs part of the SDK, these functions are key and should be paginated. They will be DoSed on chain due to the gas limit and off chain due to rpc timeout limits.\n\n### Root Cause\n\nIn `KeyRegistry`, `VotingPowerProvider`, there is no limit on some array elements nor pagination.\n\n### Internal Pre-conditions\n\nNone.\n\n### External Pre-conditions\n\nNone.\n\n### Attack Path\n\n1. Attacker creates multiple wallets to DoS the function calls.\n\n### Impact\n\nKey view functions for key, operators are DoSed.\n\n### PoC\n\n_No response_\n\n### Mitigation\n\nAdd pagination.\n\n# Issue M-5: Changing the epoch duration will completely break the vault and the slashers \n\nSource: https://github.com/sherlock-audit/2025-06-symbiotic-relay-judging/issues/410 \n\nThis issue has been acknowledged by the team but won't be fixed at this time.\n\n## Found by \n0x73696d616f, zark\n\n### Summary\n\nThe [vault](https://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/main/middleware-sdk/src/contracts/modules/voting-power/extensions/logic/OpNetVaultAutoDeployLogic.sol#L19-L20) and slashers in scope of the protocol in `OpNetVaultAutoDeploy` are `BASE_VAULT_VERSION` and `TOKENIZED_VAULT_VERSION`, [`SlasherType.INSTANT`](https://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/main/middleware-sdk/src/contracts/modules/voting-power/extensions/logic/OpNetVaultAutoDeployLogic.sol#L203) and [`SlasherType.VETO`](https://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/main/middleware-sdk/src/contracts/modules/voting-power/extensions/logic/OpNetVaultAutoDeployLogic.sol#L214).\n\nThe issue is that the epoch duration can not be updated in these contracts, they always use the same value. As a result, whenever it is changed in the epoch manager via `EpochManager::setEpochDuration()`, it will completely ruin the vault's staking and slashing mechanism, as they rely on the epoch duration. For example, the instant slasher requires the slashing to be [within](https://github.com/symbioticfi/core/blob/main/src/contracts/slasher/Slasher.sol#L36-L39) 1 epoch duration, but if the epoch duration is now bigger/smaller, slashing will fail for a significant period of time. [Slashing](https://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/main/middleware-sdk/src/contracts/modules/voting-power/extensions/logic/BaseSlashingLogic.sol#L34-L59) also has several checks which don't allow slashing whenever the operator, token or vault is not registered in that epoch, which can make slashing impossible via `slashVault()` in case of epoch duration change.\n\nAdditionally, the [slashing window](https://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/main/middleware-sdk/src/contracts/modules/voting-power/extensions/logic/OpNetVaultAutoDeployLogic.sol#L147-L149) must be bigger than the epoch duration, but the slasher's and vault's epoch duration can't be changed, so this will also not work properly. The same happens for the [veto duration](https://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/main/middleware-sdk/src/contracts/modules/voting-power/extensions/logic/OpNetVaultAutoDeployLogic.sol#L218) of the veto [slasher](https://github.com/symbioticfi/core/blob/main/src/contracts/slasher/VetoSlasher.sol#L274-L276).\n\n`Vault::onSlash()` requires the epoch to be [no older](https://github.com/symbioticfi/core/blob/main/src/contracts/vault/Vault.sol#L224-L226) than `currentEpoch_ - 1`, but a different duration means this will fail. For example, if the duration is halved, calling at duration / 2 + 1 after the current epoch will revert as this is already 2 epochs in the past.\n\n### Root Cause\n\nIn `EpochManager:115`, setting a new epoch duration will always break the vault and their slasher.\n\n### Internal Pre-conditions\n\nNone\n\n### External Pre-conditions\n\nNone\n\n### Attack Path\n\n1. `EpochManager::setEpochDuration()` is called.\n\n### Impact\n\nEpoch duration is out of sync between the vaults, slashers and the epoch manager, so slashing will fail or be impossible.\n\n### PoC\n\n_No response_\n\n### Mitigation\n\nNon trivial.\n\n# Issue M-6: BlsBn254 is not available in certain chains due to hardcoded gas limit \n\nSource: https://github.com/sherlock-audit/2025-06-symbiotic-relay-judging/issues/422 \n\n## Found by \n0x73696d616f, Drynooo, klaus\n\n### Summary\n\nEcParing precompile always fails on certain chains due to the hardcoded gas limit, so BlsBn254 is not available on certain chains.\n\n### Root Cause\n\n\n- [contracts/libraries/sigs/SigBlsBn254.sol#L39](https://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/435a21fd81bcd588439feef3108580f535b9e5eb/middleware-sdk/src/contracts/libraries/sigs/SigBlsBn254.sol#L39)\n- [contracts/libraries/sigs/SigBlsBn254.sol#L65](https://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/435a21fd81bcd588439feef3108580f535b9e5eb/middleware-sdk/src/contracts/libraries/sigs/SigBlsBn254.sol#L65)\n\n`SigBlsBn254.verify`  uses the hardcoded `PAIRING_CHECK_GAS_LIMIT(= 120_000)` when calling `BN254.safePairing`. This is the gas cost of EcParing based on `34000 * k + 45000` when k=2, as defined in [EIP-1108](https://eips.ethereum.org/EIPS/eip-1108).\n\n```solidity\n@>  uint256 internal constant PAIRING_CHECK_GAS_LIMIT = 120_000;\n\n    function verify(\n        bytes memory keyBytes,\n        bytes memory message,\n        bytes memory signature,\n        bytes memory extraData\n    ) internal view returns (bool) {\n        ...\n\n        (bool success, bool result) = BN254.safePairing(\n            signatureG1.plus(keyG1.scalar_mul(alpha)),\n            BN254.negGeneratorG2(),\n            messageG1.plus(BN254.generatorG1().scalar_mul(alpha)),\n            keyG2,\n@>          PAIRING_CHECK_GAS_LIMIT\n        );\n        return success && result;\n    }\n\n    ////////////////////////////\n    // BN254.safePairing\n    function safePairing(\n        G1Point memory a1,\n        G2Point memory a2,\n        G1Point memory b1,\n        G2Point memory b2,\n@>      uint256 pairingGas\n    ) internal view returns (bool, bool) {\n        ...\n\n        assembly {\n@>          success := staticcall(pairingGas, 8, input, mul(12, 0x20), out, 0x20)\n        }\n        ...\n    }\n```\n\n\nThe gas cost for precompile may change or vary by chain. For example, ZKSync (included in the chain to be deployed) updated the EcAdd, EcMul, and EcPairing precompiles and changed the gas cost in the [ZIP-11. V28 Precompile Upgrade](https://www.tally.xyz/gov/zksync/proposal/54063168049426383294336598998322383147338444177076559098597792110160570100155?govId=eip155:324:0x76705327e682F2d96943280D99464Ab61219e34f) upgrade in May 2025.\n\nThe above code cause problems in ZKSync. The following is the new EcPairing code updated at [ZKSync V28](https://github.com/matter-labs/era-contracts/blob/release-v28/system-contracts/contracts/precompiles/EcPairing.yul). The gas cost is calculated via `80000 * k`. When k=2, the required gas is 160_000, which is higher than the `PAIRING_CHECK_GAS_LIMIT`. EcParing always fails when gas is insufficient, so the BlsBn254 signature check in ZKSync will always fail.\n\n```yul\nfunction ECPAIRING_BASE_GAS_COST() -> ret {\n    ret := 0\n}\n\nfunction ECPAIRING_PAIR_GAS_COST() -> ret {\n@>  ret := 80000\n}\n\nfunction ecpairingGasCost(pairs) -> ret{\n@>  let gasPerPairs := mul(ECPAIRING_PAIR_GAS_COST(), pairs)\n    ret := add(ECPAIRING_BASE_GAS_COST(), gasPerPairs)\n}\n```\n\n\n### Internal Pre-conditions\n\n1. Use BlsBn254 for signing.\n\n### External Pre-conditions\n\n1. The EcPairing precompile gas cost at the deployed chain does not follow [EIP-1108](https://eips.ethereum.org/EIPS/eip-1108).\n\n\n### Attack Path\n\nThe issue is caused by a bug.\n\n### Impact\n\nBlsBn254 is not available on some chains.\n\n### PoC\n\n\nThe [ZIP-11. V28 Precompile Upgrade](https://www.tally.xyz/gov/zksync/proposal/54063168049426383294336598998322383147338444177076559098597792110160570100155?govId=eip155:324:0x76705327e682F2d96943280D99464Ab61219e34f) is only available on the ZKSync mainnet (I don't think it's applied to the testnet), and it is not reproducible with the foundry fork test, so you need to test it directly on the mainnet.\n\nDeploy the following code to the ZKSync Era mainnet and run it to see the gas cost. If you put the correct `input` in the `verify` function and experiment with incrementing `pairingGas` from 120_000, you will see that at around 161_000, the signature verification succeeds with `success` and `out[0]` set to 1. This is consistent with ZKSync's `80000 * k (k = 2)`.\n\n```solidity\n// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\ncontract Test {\n\n    event Cost(uint256);\n    event Out(bool, uint256);\n\n    function test (uint256[12] memory input, uint256 pairingGas) public {\n        (uint256 gasBefore, uint256 gasAfter, bool success, uint256 out) = verify(input, pairingGas);\n        emit Cost(gasBefore - gasAfter);\n        emit Out(success, out);\n    }\n\n    function verify (uint256[12] memory input, uint256 pairingGas) public view returns (uint256, uint256, bool, uint256) {\n        uint256[1] memory out;\n        bool success;\n\n        uint256 gasBefore = gasleft();\n        // solium-disable-next-line security/no-inline-assembly\n        assembly {\n            success := staticcall(pairingGas, 8, input, mul(12, 0x20), out, 0x20)\n        }\n        uint256 gasAfter = gasleft();\n        return (gasBefore, gasAfter, success, out[0]);\n    }\n}\n```\n\nUse the following as the `input` parameter, created with the correct signature and key value. This is the value from the test code.\n\n```json\n[\n17542794946843030738197687269502130768488764040084025709702018229683082027107, 21243454333462907454433938848788936660904069824545612138480299027504168819393, 11559732032986387107991004021392285783925812861821192530917403151452391805634, 10857046999023057135944570762232829481370756359578518086990519993285655852781, 17805874995975841540914202342111839520379459829704422454583296818431106115052, 13392588948715843804641432497768002650278120570034223513918757245338268106653, 6152845192698230377440204073057238033424791113774748884801148069022325658846, 13760496706863554449593094343798996929546352261485265365831743695186162488392, 10168917783125035928329339378130255896597415372015030444874307897081997728948, 15338339620195733484325031668011173090672215643291231872576243132177438055881, 10104509023153927337647655231628382133731833653099790728025758502925918550767, 13448048280709447326318302930315758447948705837394892676501482696723894570897\n]\n```\n\n### Mitigation\n\nThe gas cost required to call Precompile may be changed in the future and can differ between chains. Therefore, instead of using `PAIRING_CHECK_GAS_LIMIT`, you should use a variable that can be set by an administrator.\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/symbioticfi/relay-contracts/pull/33/commits/cb0c4e22963b3bc5f532b6250e6891fa077c6069\n\n\n\n\n# Issue M-7: A malicious operator will control consensus without risking stake (stake-exit lag exploit) \n\nSource: https://github.com/sherlock-audit/2025-06-symbiotic-relay-judging/issues/446 \n\nThis issue has been acknowledged by the team but won't be fixed at this time.\n\n## Found by \n0x73696d616f, hunt1, montecristo\n\n### Summary\n\nThe non-atomic nature of `setSigVerifier` and `commitValSetHeader` will cause a potential loss of security guarantees for networks as a malicious operator can manipulate validator sets after unstaking their funds, avoiding slashing penalties.\n\n### Root Cause\n\nIn `Settlement` contract the design choice to separate `setSigVerifier` and `commitValSetHeader` functions (latter being public) is a mistake as it allows for a time gap between validator selection and header commitment. This creates a window where an operator can withdraw their stake while still maintaining their voting power.\n\nSpecifically, `commitValSetHeader` in `Settlement` is marked as public:\n\nhttps://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/main/middleware-sdk/src/contracts/modules/settlement/Settlement.sol#L292-L324\n\nWhile `setSigVerifier` is a separate function:\n\nhttps://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/main/middleware-sdk/src/contracts/modules/settlement/Settlement.sol#L267-L275\n\n### Attack Path\n\nEpoch 1:\n\n1. A malicious operator deposits a large amount of stake to their vault, ensuring it exceeds the `quorumThreshold` for voting power.\n2. The off-chain relay calculates voting powers, creates a `sigVerifier` using validators belonging to the malicious operator, and calls `setSigVerifier` to assign these validators for the next epoch.\n3. Near the end of Epoch, the operator calls `withdraw` on their vault to initiate the withdrawal of their stake.\n\nEpoch 2:\n\n1. The operator immediately calls `claim` on their vault to retrieve all their withdrawn stake, effectively removing their financial exposure.\n2. Despite having withdrawn their stake, their voting power is active in the system's state for a short time-span.\n3. The operator can craft their own proof using their validators' private keys, which will pass verification since they had enough voting power.\n4. When the operator submits this crafted proof to `commitValSetHeader`, it will be accepted by the system, allowing the operator to control consensus without any stake at risk.\n\n### Impact\n\nThe network suffers a complete compromise of its security model. The malicious operator can perform any validator action (such as approving invalid transactions or censoring valid ones) without having any stake at risk of being slashed. This fundamentally breaks the economic security assumptions of the protocol, which relies on validators having skin in the game to behave honestly.\n\n### Mitigation\n\nRedesign the `commitValSetHeader` function to be internal (`_commitValSetHeader`), and create a new public function that handles both setting the signature verifier (optional) and committing the header in a single atomic transaction.\n\nAlternatively apply `checkPermission` to `commitValSetHeader` so it can only be executed by the Network's relay service.\n\n\n# Issue Z-1: Malicious operator can alone, with any voting power smaller than quorum forge a proof \n\nSource: https://github.com/sherlock-audit/2025-06-symbiotic-relay-judging/issues/452 \n\n## Found by \n0x73696d616f, vinica\\_boy\n\n### Summary\n\nA malicious operator with any voting power (considering a minimum inclusion voting power if present) smaller than the quorum can forge a proof and bypass verification, setting the next epoch header to any value, taking over the network.\nFirstly, in `circuit.go`, operators with keys X, Y equal to 0 are not part of the validator set hash and are [skipped](https://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/main/middleware-sdk/pkg/proof/circuit.go#L94-L98) whenever they are last.\n```go\n\t\tvalsetHash = api.Select(\n\t\t\tapi.And(fieldFpApi.IsZero(&circuit.ValidatorData[i].Key.X), fieldFpApi.IsZero(&circuit.ValidatorData[i].Key.Y)),\n\t\t\tvalsetHash,\n\t\t\tvalsetHashTemp,\n\t\t)\n```\nThe reason the X = 0, Y = 0 operators need to be last to be skipped, is because the `valsetHash` takes the temp value when the key is not null. Hence, if we have an empty key (0,0), followed by a non empty key,  valsetHash will take the value of valsetHashTemp again, which includes the full mimc hash, which is cumulative:\n```go\n\t\thashAffineG1(&mimcApi, &circuit.ValidatorData[i].Key)\n\t\tmimcApi.Write(circuit.ValidatorData[i].VotingPower)\n\t\tvalsetHashTemp := mimcApi.Sum()\n```\nHence, for this to work, the operators must be sent as [OP1, ..., OPn, (0,0)], so `valsetHash` takes the hash of the set up until OPn. It needs to exclude the (0,0) key from the validator set because the MIMC hash is checked against the real hash, which doesn't contain this (0,0) key.\n\nNow, the (0,0) key is not a real operator, so their voting power contribution must be 0. However, it's actually possible to set any voting power (up until var size constraints), exceeding the quorum, of this fake (0,0) validator, and the proof still goes through.\n\nThe `IsNonSigner` flag is set to false of this (0,0) validator, so the voting power counts. As a result, their (0,0) key is also added to the signing aggregated key. However, the null (0,0) key point property is that its addition to the aggregate key has no effect, which means that effectively no signature is required from the null (0,0) key. Thus, having validators [OP1, ..., OPn, (0,0)], with an aggregated signature of validators 1 to n, will pass the signature verification.\n\nThis effectively means that any operator with any minimal voting power can add this (0,0) operator with a voting power that exceeds the quorum and lets the message go through. As a result, they can manipulate whatever data they want and take full control of the network, more precisely the valSetHeader for the next epoch in `Settlement.sol`, fully compromising the network.\n\n### Root Cause\n\nIn `circuit.go:101`, the voting power of an operator with null key must be null.\n\n### Internal Pre-conditions\n\nNone\n\n### External Pre-conditions\n\nNone\n\n### Attack Path\n\n1. Operator with 1 voting power (or any minimal amount) calls `Settlement::commitValSetHeader()` with a malicious header for next epoch to compromise the network. They send a proof with only them as signer, all other operators are non signers and add at the end a null operator (0,0) with voting power bigger than the quorum.\n\n### Impact\n\nNetwork is compromised and attacker can do whatever they want. Operator role is permissionless for some networks (depending on extensions) and even if it wasn't, they would still be able to completely bypass the quorum which is high severity.\n\n### PoC\n\nChange `proof_test.go` to:\n```go\nfunc genValset(numValidators int, nonSigners []int) []ValidatorData {\n\tvalset := make([]ValidatorData, numValidators)\n\tfor i := 0; i < numValidators; i++ {\n\t\tpk := big.NewInt(int64(i + 1000000000000000000))\n\t\tvalset[i].PrivateKey = pk\n\t\tvalset[i].Key = getPubkeyG1(pk)\n\t\tvalset[i].KeyG2 = getPubkeyG2(pk)\n\t\tvalset[i].VotingPower = big.NewInt(1) // this has to be the real voting power of the rest of the set but it doesn't really matter for this poc as they won't sign. Only validator i == 0 is signing.\n\t\tvalset[i].IsNonSigner = false\n\t\tif i != 0 {\n\t\t\tvalset[i].IsNonSigner = true\n\t\t}\n\t}\n\n\tfor _, nonSigner := range nonSigners {\n\t\tvalset[nonSigner].IsNonSigner = true\n\t}\n\n\treturn valset\n}\n```\n\nChange `helpers.go` to the following. Note that n is 11 (set has length 10) to add the null key (0,0).\n```go\nfunc NormalizeValset(valset []ValidatorData) []ValidatorData {\n\t// Sort validators by key in ascending order\n\tsort.Slice(valset, func(i, j int) bool {\n\t\t// Compare keys (lower first)\n\t\treturn valset[i].Key.X.Cmp(&valset[j].Key.X) < 0 || valset[i].Key.Y.Cmp(&valset[j].Key.Y) < 0\n\t})\n\tn := 11 //getOptimalN(len(valset))\n\tnormalizedValset := make([]ValidatorData, n)\n\tfor i := range n {\n\t\tif i < len(valset) {\n\t\t\tnormalizedValset[i] = valset[i]\n\t\t} else {\n\t\t\tzeroPoint := new(bn254.G1Affine)\n\t\t\tzeroPoint.SetInfinity()\n\t\t\tzeroPointG2 := new(bn254.G2Affine)\n\t\t\tzeroPointG2.SetInfinity()\n\t\t\tnormalizedValset[i] = ValidatorData{PrivateKey: big.NewInt(0), Key: *zeroPoint, KeyG2: *zeroPointG2, VotingPower: big.NewInt(30000000000000 * 10), IsNonSigner: false}\n\t\t}\n\t}\n\treturn normalizedValset\n}\n```\n\nIn `proof.go` set `MaxValidators = []int{11}`. If the 3 verifiers 10, 100, 1000 were used this wouldn't be needed but the current commit defaults to 10 only and it's easier to allow 11 for this POC.\n\nGo to `pkg/proof` and run `go test .`, it passes with 1 real voting power.\n\n### Mitigation\n\nIf the key is null, voting power must be null.\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/symbioticfi/relay-contracts/pull/33/commits/8d0d70bd47afa5029a0cf72ef82c49754fd2201c\n\n\n\n\n","is_legacy_contest":false,"context_questions":[{"question":"On what chains are the smart contracts going to be deployed?","answer":"Ethereum\nBSC\nTron\nBase\nArbitrum\nHyperliquid L1\nAvalanche\nPolygon\nBerachain\nUnichain\nSonic\nCronos\nBSquared\nBitlayer\nCORE\nOP Mainnet\nHemi\nTaiko\nGnosis\nMantle\nLinea\nBlast\nCelo\nZKsync Era\nScroll\nWorldChain\nSwellchain\nManta","order":1},{"question":"If you are integrating tokens, are you allowing only whitelisted tokens to work with the codebase or any complying with the standard? Are they assumed to have certain properties, e.g. be non-reentrant? Are there any types of [weird tokens](https://github.com/d-xo/weird-erc20) you want to integrate?","answer":"The system is permissionless; the only limitation is rebasing tokens","order":2},{"question":"Are there any limitations on values set by admins (or other roles) in the codebase, including restrictions on array lengths?","answer":"- Constructor inputs like NETWORK_REGISTRY, NETWORK_MIDDLEWARE_SERVICE and others mean Symbiotic core contracts deployment, and are considered to be correctly set up (e.g., using https://docs.symbiotic.fi/deployments/mainnet)\n- Regarding permissions \"inside\" the network: In general, the system allows a high granularity of roles across all the contracts. However, the most important part to mention here is the `Settlement.setGenesis()` function with a needed permission, which ideally should be called only once at the start, as it can affect the validator set (which represents the network) and, hence, the network's decisions. \n- Regarding permissions in the sense of external parties: Basically, all the permissions directly or indirectly belong to the network, and in most cases, different setups mean different security assumptions inside its system (e.g., possible configurations are 1) all the actions are performed based on the majority decision, 2) fully controlled by the protocol's (network's) team). There are a couple of exceptions, such as \"slasher\" and \"rewarder\" addresses in `BaseSlashing` and `BaseRewards`, which affect the stakers directly and should be thoroughly considered by them.","order":3},{"question":"Are there any limitations on values set by admins (or other roles) in protocols you integrate with, including restrictions on array lengths?","answer":"No, but it's important to mention that the code under the contest is an SDK, so the number of use-cases can be wide and, in the end, it is up to the user of this SDK (network/protocol) to decide on external trust assumptions (e.g., threre is shared vault with several stake consumers and with a custom route for slashed funds; it is up to the network if to consume the stake from this vault or not)","order":4},{"question":"Is the codebase expected to comply with any specific EIPs?","answer":"No, the codebase only uses some subjects (ERC20, precompiles) from EIPs, but doesn't implement any","order":5},{"question":"Are there any off-chain mechanisms involved in the protocol (e.g., keeper bots, arbitrage bots, etc.)? We assume these mechanisms will not misbehave, delay, or go offline unless otherwise specified.","answer":"There is an off-chain part of the Symbiotic Relay that implements the validator set usage itself, including message signing, maintenance of validator set headers (which represent the validator set itself) through epochs, and signature aggregations. Mainly, it consumes `VotingPowerProvider.getVotingPowers(At)()`, `KeyRegistry.getKeys(At)()`, all of the `ValSetDriver` functions for validator set derivation and futher interactions with `Settlement`, `EpochManager.getCurrentEpochStart()` (and other epochs-related functions), most functions from `Settlement` for synchronisations, commitments, and verifications. Example dependency on this off-chain part: if any validator set has committed a malicious header by its majority, it is acceptable from the on-chain side.","order":6},{"question":"What properties/invariants do you want to hold even if breaking them has a low/unknown impact?","answer":"No","order":7},{"question":"Please discuss any design choices you made.","answer":"- Each basic contract\u2019s storage is located in an unstructured way, which allows for more convenient utilisation of them when developing a complete system (with proxies, non-standard storage layout schemes, etc)\n- We aimed to find an ideal balance between development UX (e.g., through forcefully exposed to external environment functions), bytecode size limitations, and gas costs. Hence,\n    - Most of the functionality is virtual, so it can be overridden/reused easily\n    - In rare cases, we used external libraries to save bytecode, but implemented internal functions in the contracts which don\u2019t increase bytecode when not used\n- Most of the data regarding the middleware\u2019s state is historical. This simplifies the implementation of the off-chain part.","order":8},{"question":"Please provide links to previous audits (if any).","answer":"https://github.com/symbioticfi/middleware-sdk/blob/main/audits/Bailsec-RelaySmartContracts.pdf","order":9},{"question":"Please list any relevant protocol resources.","answer":"https://docs.symbiotic.fi/\n","order":10},{"question":"Additional audit information.","answer":"As is the default, a high severity issue is worth 5 times the value of a medium severity issue. To encourage placing more of an emphasis on the main scope, vulnerabilities found in the ZK and Network portions have a reduced weight. An issue in the ZK or Network portion will be worth 0.1x weight of a normal medium or high. This means a **Medium** issue in that area is worth a tenth of a Medium in the main scope.\n\nFor the [`circuit.go`](https://github.com/sherlock-audit/2025-06-symbiotic-relay/blob/main/middleware-sdk/pkg/proof/circuit.go) file, only the `func (circuit *Circuit) Define` is considered in-scope, while the function `func setCircuitData` is considered OOS and issues related to it will be considered invalid.\n\nIssues in the SDK view functions that are not used by the state-changing functions and don't pose a threat to the current scope, but may pose issues for protocols which will build on top of the SDK, will be considered valid in this contest. However, since we can't fully evaluate the impact (as it depends on what will be built on top of SDK), these issues will be considered flat Medium.\n","order":11}],"is_judging_enabled":false,"lead_senior_auditor_fixed_pay":37500}
