diff --git a/.gitmodules b/.gitmodules index eda39c33e..79ae70dae 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,15 +4,18 @@ [submodule "bolt-contracts/lib/openzeppelin-contracts"] path = bolt-contracts/lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts -[submodule "bolt-contracts/lib/core"] - path = bolt-contracts/lib/core - url = https://github.com/symbioticfi/core -[submodule "bolt-contracts/lib/eigenlayer-contracts"] - path = bolt-contracts/lib/eigenlayer-contracts - url = https://github.com/layr-labs/eigenlayer-contracts [submodule "bolt-contracts/lib/openzeppelin-contracts-upgradeable"] path = bolt-contracts/lib/openzeppelin-contracts-upgradeable url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable [submodule "bolt-contracts/lib/openzeppelin-foundry-upgrades"] path = bolt-contracts/lib/openzeppelin-foundry-upgrades url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades +[submodule "bolt-contracts/lib/eigenlayer-contracts"] + path = bolt-contracts/lib/eigenlayer-contracts + url = https://github.com/layr-labs/eigenlayer-contracts +[submodule "bolt-contracts/lib/core"] + path = bolt-contracts/lib/core + url = https://github.com/symbioticfi/core +[submodule "bolt-contracts/lib/eigenlayer-middleware"] + path = bolt-contracts/lib/eigenlayer-middleware + url = https://github.com/layr-labs/eigenlayer-middleware diff --git a/bolt-contracts/config/holesky/deployments.json b/bolt-contracts/config/holesky/deployments.json index 18e40f615..a5a41415d 100644 --- a/bolt-contracts/config/holesky/deployments.json +++ b/bolt-contracts/config/holesky/deployments.json @@ -1,18 +1,25 @@ { "bolt": { - "validators": "0x47D2DC1DE1eFEFA5e6944402f2eda3981D36a9c8" + "validators": "0x47D2DC1DE1eFEFA5e6944402f2eda3981D36a9c8", + "parameters": "0x20d1cf3A5BD5928dB3118b2CfEF54FDF9fda5c12", + "manager": "0x440202829b493F9FF43E730EB5e8379EEa3678CF" }, "symbiotic": { "network": "0xb017002D8024d8c8870A5CECeFCc63887650D2a4", - "operatorRegistry": "0xAdFC41729fF447974cE27DdFa358A0f2096c3F39", - "networkOptInService": "0xF5AFc9FA3Ca63a07E529DDbB6eae55C665cCa83E", - "vaultFactory": "0x18C659a269a7172eF78BBC19Fe47ad2237Be0590", - "networkRegistry": "0xac5acD8A105C8305fb980734a5AD920b5920106A", - "networkMiddlewareService": "0x683F470440964E353b389391CdDDf8df381C282f", + "operatorRegistry": "0x6F75a4ffF97326A00e52662d82EA4FdE86a2C548", + "networkOptInService": "0x58973d16FFA900D11fC22e5e2B6840d9f7e13401", + "vaultFactory": "0x407A039D94948484D356eFB765b3c74382A050B4", + "vaultConfigurator": "0xD2191FE92987171691d552C219b8caEf186eb9cA", + "networkRegistry": "0x7d03b7343BF8d5cEC7C0C27ecE084a20113D15C9", + "networkMiddlewareService": "0x62a1ddfD86b4c1636759d9286D3A0EC722D086e3", "middleware": "0x04f40d9CaE475E5BaA462acE53E5c58A0DD8D8e8", "supportedVaults": [ - "0x1df2fbfcD600ADd561013f44B2D055E2e974f605", - "0xf427d00c34609053d97167352061DD2F0F27F853" + "0xc79c533a77691641d52ebD5e87E51dCbCaeb0D78", + "0xe5708788c90e971f73D928b7c5A8FD09137010e0", + "0x11c5b9A9cd8269580aDDbeE38857eE451c1CFacd", + "0xC56Ba584929c6f381744fA2d7a028fA927817f2b", + "0xcDdeFfcD2bA579B8801af1d603812fF64c301462", + "0x91e84e12Bb65576C0a6614c5E6EbbB2eA595E10f" ] }, "eigenLayer": { diff --git a/bolt-contracts/config/holesky/vaults.json b/bolt-contracts/config/holesky/vaults.json new file mode 100644 index 000000000..9df2dc1f0 --- /dev/null +++ b/bolt-contracts/config/holesky/vaults.json @@ -0,0 +1,26 @@ +[ + { + "admin": "0x20A1305Ec6c13FFE8e5289f98ba1A18Aa262c15E", + "collateral": "0x8d09a4502Cc8Cf1547aD300E066060D043f6982D" + }, + { + "admin": "0x20A1305Ec6c13FFE8e5289f98ba1A18Aa262c15E", + "collateral": "0x7322c24752f79c05FFD1E2a6FCB97020C1C264F1" + }, + { + "admin": "0x20A1305Ec6c13FFE8e5289f98ba1A18Aa262c15E", + "collateral": "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034" + }, + { + "admin": "0x20A1305Ec6c13FFE8e5289f98ba1A18Aa262c15E", + "collateral": "0x94373a4919B3240D86eA41593D5eBa789FEF3848" + }, + { + "admin": "0x20A1305Ec6c13FFE8e5289f98ba1A18Aa262c15E", + "collateral": "0x8720095Fa5739Ab051799211B146a2EEE4Dd8B37" + }, + { + "admin": "0x20A1305Ec6c13FFE8e5289f98ba1A18Aa262c15E", + "collateral": "0xe3C063B1BEe9de02eb28352b55D49D85514C67FF" + } +] \ No newline at end of file diff --git a/bolt-contracts/docs/admin/deploying.md b/bolt-contracts/docs/admin/deploying.md index 56de63f22..215827083 100644 --- a/bolt-contracts/docs/admin/deploying.md +++ b/bolt-contracts/docs/admin/deploying.md @@ -34,19 +34,28 @@ export ADMIN_PRIVATE_KEY=0x... ### Pre-deployment -Register a Symbiotic network for Bolt with the Symbiotic `NetworkRegistry`. The private key with which the script is run will determine the network address. This private key will also need to be used later. +- Register a Symbiotic network for Bolt with the Symbiotic `NetworkRegistry`. The private key with which the script is run will determine the network address. This private key will also need to be used later. ```bash forge script script/holesky/admin/helpers/Symbiotic.s.sol --rpc-url $HOLESKY_RPC --private-key $NETWORK_PRIVATE_KEY --broadcast -vvvv --sig "run(string memory arg)" registerNetwork ``` -Make sure `deployments.json` contains the correct address for the Symbiotic network. +Make sure [`deployments.json`](../../config/holesky/deployments.json) contains the correct address for the Symbiotic network. + +- Deploy Bolt-specific Symbiotic Vaults. Vaults will be deployed from the [`vaults.json`](../../config/holesky/vaults.json) configuration file. + +```bash +forge script script/holesky/admin/helpers/DeployVaults.s.sol --rpc-url $HOLESKY_RPC --private-key $ADMIN_PRIVATE_KEY --verify --broadcast -vvvv +``` + +If vaults with the `(collateral, admin)` combination already exist, they won't be recreated. After these vaults have been created, copy their +addresses into the [`deployments.json`](../../config/holesky/deployments.json) file under `symbiotic.supportedVaults`. ### Deployment Run the following script to deploy Bolt V1: ```bash -forge script script/holesky/admin/Deploy.s.sol --rpc-url $HOLESKY_RPC --private-key $ADMIN_PRIVATE_KEY --broadcast -vvvv +forge script script/holesky/admin/Deploy.s.sol --rpc-url $HOLESKY_RPC --private-key $ADMIN_PRIVATE_KEY --verify --broadcast -vvvv ``` This will deploy all the contracts. The address corresponding to the private key will be the system admin. @@ -71,3 +80,11 @@ forge script script/holesky/admin/helpers/RegisterAVS.s.sol --rpc-url $HOLESKY_R > [!IMPORTANT] > After the `deployments.json` file has been fully updated with the correct contract addresses, push it to Github. + +### Other Scripts + +#### Modifying supported Symbiotic Vaults +This script will update supported vaults according to `deployments.json`, and remove any vaults that have been whitelisted but are no longer in the `symbiotic.supportedVaults` list. +```bash +forge script script/holesky/admin/helpers/UpdateSupportedVaults.s.sol --rpc-url $HOLESKY_RPC --private-key $ADMIN_PRIVATE_KEY --broadcast -vvv +``` \ No newline at end of file diff --git a/bolt-contracts/docs/admin/upgrading.md b/bolt-contracts/docs/admin/upgrading.md index 65466d91b..28de9192f 100644 --- a/bolt-contracts/docs/admin/upgrading.md +++ b/bolt-contracts/docs/admin/upgrading.md @@ -13,4 +13,47 @@ bytes memory initManager = abi.encodeCall(BoltManagerV2.initialize, (params)); Upgrades.upgradeProxy(proxy, "BoltManagerV2.sol", initManager, opts); ``` -Before an upgrade, update the [`Upgrade.s.sol`](../script/holesky/Upgrade.s.sol) script to include the correct contracts, references and configurations. \ No newline at end of file +Before an upgrade, update the [`Upgrade.s.sol`](../script/holesky/Upgrade.s.sol) script to include the correct contracts, references and configurations. + +## Unsafe +In order to run an unsafe upgrade, set `Options.unsafeSkipAllChecks` to `true`: +```solidity +Options memory opts; +opts.unsafeSkipAllChecks = true; +``` + +## Verifying Storage Layout +You can verify storage layouts using `forge inspect`. Example: + +```bash +forge inspect BoltSymbioticMiddlewareV2 storage-layout --pretty +``` + +This will output the following table: +| Name | Type | Slot | Offset | Bytes | Contract | +|------------------------|---------------------------------------|------|--------|-------|-----------------------------------------------------------------------| +| INSTANT_SLASHER_TYPE | uint256 | 0 | 0 | 32 | src/contracts/BoltSymbioticMiddlewareV2.sol:BoltSymbioticMiddlewareV2 | +| VETO_SLASHER_TYPE | uint256 | 1 | 0 | 32 | src/contracts/BoltSymbioticMiddlewareV2.sol:BoltSymbioticMiddlewareV2 | +| START_TIMESTAMP | uint48 | 2 | 0 | 6 | src/contracts/BoltSymbioticMiddlewareV2.sol:BoltSymbioticMiddlewareV2 | +| parameters | contract IBoltParametersV1 | 2 | 6 | 20 | src/contracts/BoltSymbioticMiddlewareV2.sol:BoltSymbioticMiddlewareV2 | +| manager | contract IBoltManagerV1 | 3 | 0 | 20 | src/contracts/BoltSymbioticMiddlewareV2.sol:BoltSymbioticMiddlewareV2 | +| vaults | struct EnumerableMap.AddressToUintMap | 4 | 0 | 96 | src/contracts/BoltSymbioticMiddlewareV2.sol:BoltSymbioticMiddlewareV2 | +| BOLT_SYMBIOTIC_NETWORK | address | 7 | 0 | 20 | src/contracts/BoltSymbioticMiddlewareV2.sol:BoltSymbioticMiddlewareV2 | +| OPERATOR_REGISTRY | address | 8 | 0 | 20 | src/contracts/BoltSymbioticMiddlewareV2.sol:BoltSymbioticMiddlewareV2 | +| VAULT_FACTORY | address | 9 | 0 | 20 | src/contracts/BoltSymbioticMiddlewareV2.sol:BoltSymbioticMiddlewareV2 | +| OPERATOR_NET_OPTIN | address | 10 | 0 | 20 | src/contracts/BoltSymbioticMiddlewareV2.sol:BoltSymbioticMiddlewareV2 | +| NAME_HASH | bytes32 | 11 | 0 | 32 | src/contracts/BoltSymbioticMiddlewareV2.sol:BoltSymbioticMiddlewareV2 | +| __gap | uint256[38] | 12 | 0 | 1216 | src/contracts/BoltSymbioticMiddlewareV2.sol:BoltSymbioticMiddlewareV2 | + +The last line indicating the `__gap` storage slot is what's most important. `__gap` has a total of 50 storage slots reserved. You **MUST** verify that the array length of `__gap`, in this case `38`, is equal to `50 - __gap.Slot`. In this case, the `Slot` column for `__gap` shows 12, so the layout is correct. + +## Reinitializers +In case you need to reinitialize your contract, you'll need to create a reinitializer. + +Let `x` = your version number. Add the following new initializer to the contract to be upgraded: + +```solidity +function initializeVx() public reinitializer(x) { ... } +``` + +For more info, check out https://docs.openzeppelin.com/contracts/5.x/api/proxy#Initializable. \ No newline at end of file diff --git a/bolt-contracts/foundry.toml b/bolt-contracts/foundry.toml index b19612e4a..8dd782afe 100644 --- a/bolt-contracts/foundry.toml +++ b/bolt-contracts/foundry.toml @@ -26,6 +26,7 @@ remappings = [ "@relic/=lib/relic-sdk/packages/contracts", "@symbiotic/=lib/core/src/", "@eigenlayer/=lib/eigenlayer-contracts/", + "@eigenlayer-middleware/=lib/eigenlayer-middleware/", "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", "@openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/", diff --git a/bolt-contracts/lib/core b/bolt-contracts/lib/core index 76bf70945..f38f1b16b 160000 --- a/bolt-contracts/lib/core +++ b/bolt-contracts/lib/core @@ -1 +1 @@ -Subproject commit 76bf709458410b2682f7bc20c6e3a90845bf4b51 +Subproject commit f38f1b16b8207dcff55d681a0d5ba28c66e785c8 diff --git a/bolt-contracts/lib/eigenlayer-contracts b/bolt-contracts/lib/eigenlayer-contracts index 00fc4b95e..898c3e07e 160000 --- a/bolt-contracts/lib/eigenlayer-contracts +++ b/bolt-contracts/lib/eigenlayer-contracts @@ -1 +1 @@ -Subproject commit 00fc4b95e9c1a5c4f370e41f56d01052d186da07 +Subproject commit 898c3e07ed52440876cedb03da07c8578e1da166 diff --git a/bolt-contracts/lib/eigenlayer-middleware b/bolt-contracts/lib/eigenlayer-middleware new file mode 160000 index 000000000..48e0aecae --- /dev/null +++ b/bolt-contracts/lib/eigenlayer-middleware @@ -0,0 +1 @@ +Subproject commit 48e0aecae3f778356a5009f912ade946b285fe9b diff --git a/bolt-contracts/script/holesky/admin/Upgrade.s.sol b/bolt-contracts/script/holesky/admin/Upgrade.s.sol index 6ca5b0518..9f75f7e08 100644 --- a/bolt-contracts/script/holesky/admin/Upgrade.s.sol +++ b/bolt-contracts/script/holesky/admin/Upgrade.s.sol @@ -4,19 +4,124 @@ pragma solidity 0.8.25; import {Script, console} from "forge-std/Script.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {Upgrades} from "@openzeppelin-foundry-upgrades/src/Upgrades.sol"; +import {Upgrades, Options} from "@openzeppelin-foundry-upgrades/src/Upgrades.sol"; import {BoltParametersV1} from "../../../src/contracts/BoltParametersV1.sol"; import {BoltValidatorsV1} from "../../../src/contracts/BoltValidatorsV1.sol"; import {BoltManagerV1} from "../../../src/contracts/BoltManagerV1.sol"; import {BoltEigenLayerMiddlewareV1} from "../../../src/contracts/BoltEigenLayerMiddlewareV1.sol"; +import {BoltEigenLayerMiddlewareV2} from "../../../src/contracts/BoltEigenLayerMiddlewareV2.sol"; import {BoltSymbioticMiddlewareV1} from "../../../src/contracts/BoltSymbioticMiddlewareV1.sol"; +import {BoltSymbioticMiddlewareV2} from "../../../src/contracts/BoltSymbioticMiddlewareV2.sol"; import {BoltConfig} from "../../../src/lib/Config.sol"; contract UpgradeBolt is Script { - function run() public { + struct Deployments { + address boltManager; + address boltParameters; + address symbioticNetwork; + address symbioticOperatorRegistry; + address symbioticOperatorNetOptIn; + address symbioticVaultFactory; + address symbioticMiddleware; + address[] supportedVaults; + address eigenLayerAVSDirectory; + address eigenLayerDelegationManager; + address eigenLayerStrategyManager; + address eigenLayerMiddleware; + address[] supportedStrategies; + } + + function upgradeSymbioticMiddleware() public { + address admin = msg.sender; + console.log("Upgrading Symbiotic middleware with admin", admin); // TODO: Validate upgrades with Upgrades.validateUpgrade + Options memory opts; + opts.unsafeSkipAllChecks = true; + opts.referenceContract = "BoltSymbioticMiddlewareV1.sol"; + + string memory upgradeTo = "BoltSymbioticMiddlewareV2.sol"; + + Deployments memory deployments = _readDeployments(); + + bytes memory initSymbioticMiddleware = abi.encodeCall( + BoltSymbioticMiddlewareV2.initializeV2, + ( + admin, + deployments.boltParameters, + deployments.boltManager, + deployments.symbioticNetwork, + deployments.symbioticOperatorRegistry, + deployments.symbioticOperatorNetOptIn, + deployments.symbioticVaultFactory + ) + ); + + vm.startBroadcast(admin); + + Upgrades.upgradeProxy(deployments.symbioticMiddleware, upgradeTo, initSymbioticMiddleware, opts); + + vm.stopBroadcast(); + + console.log("BoltSymbioticMiddleware proxy upgraded from %s to %s", opts.referenceContract, upgradeTo); + // TODO: Upgrade contracts with Upgrades.upgradeProxy } + + function upgradeEigenLayerMiddleware() public { + address admin = msg.sender; + console.log("Upgrading EigenLayer middleware with admin", admin); + // TODO: Validate upgrades with Upgrades.validateUpgrade + + Options memory opts; + opts.unsafeSkipAllChecks = true; + opts.referenceContract = "BoltEigenLayerMiddlewareV1.sol"; + + string memory upgradeTo = "BoltEigenLayerMiddlewareV2.sol"; + + Deployments memory deployments = _readDeployments(); + + bytes memory initEigenLayerMiddleware = abi.encodeCall( + BoltEigenLayerMiddlewareV2.initializeV2, + ( + admin, + deployments.boltParameters, + deployments.boltManager, + deployments.eigenLayerAVSDirectory, + deployments.eigenLayerDelegationManager, + deployments.eigenLayerStrategyManager + ) + ); + + vm.startBroadcast(admin); + + Upgrades.upgradeProxy(deployments.eigenLayerMiddleware, upgradeTo, initEigenLayerMiddleware, opts); + + vm.stopBroadcast(); + + console.log("BoltSymbioticMiddleware proxy upgraded from %s to %s", opts.referenceContract, upgradeTo); + } + + function _readDeployments() public view returns (Deployments memory) { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/config/holesky/deployments.json"); + string memory json = vm.readFile(path); + + return Deployments({ + boltParameters: vm.parseJsonAddress(json, ".bolt.parameters"), + boltManager: vm.parseJsonAddress(json, ".bolt.manager"), + symbioticNetwork: vm.parseJsonAddress(json, ".symbiotic.network"), + symbioticOperatorRegistry: vm.parseJsonAddress(json, ".symbiotic.operatorRegistry"), + symbioticOperatorNetOptIn: vm.parseJsonAddress(json, ".symbiotic.networkOptInService"), + symbioticVaultFactory: vm.parseJsonAddress(json, ".symbiotic.vaultFactory"), + supportedVaults: vm.parseJsonAddressArray(json, ".symbiotic.supportedVaults"), + symbioticMiddleware: vm.parseJsonAddress(json, ".symbiotic.middleware"), + eigenLayerAVSDirectory: vm.parseJsonAddress(json, ".eigenLayer.avsDirectory"), + eigenLayerDelegationManager: vm.parseJsonAddress(json, ".eigenLayer.delegationManager"), + eigenLayerStrategyManager: vm.parseJsonAddress(json, ".eigenLayer.strategyManager"), + eigenLayerMiddleware: vm.parseJsonAddress(json, ".eigenLayer.middleware"), + supportedStrategies: vm.parseJsonAddressArray(json, ".eigenLayer.supportedStrategies") + }); + } } diff --git a/bolt-contracts/script/holesky/admin/helpers/DeployVaults.s.sol b/bolt-contracts/script/holesky/admin/helpers/DeployVaults.s.sol new file mode 100644 index 000000000..c0352a3a2 --- /dev/null +++ b/bolt-contracts/script/holesky/admin/helpers/DeployVaults.s.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {Script, console} from "forge-std/Script.sol"; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +import {IVault} from "@symbiotic/interfaces/vault/IVault.sol"; +import {IOptInService} from "@symbiotic/interfaces/service/IOptInService.sol"; +import {IVaultConfigurator} from "@symbiotic/interfaces/IVaultConfigurator.sol"; +import {IBaseDelegator} from "@symbiotic/interfaces/delegator/IBaseDelegator.sol"; +import {IBaseSlasher} from "@symbiotic/interfaces/slasher/IBaseSlasher.sol"; +import {INetworkRestakeDelegator} from "@symbiotic/interfaces/delegator/INetworkRestakeDelegator.sol"; +import {IVetoSlasher} from "@symbiotic/interfaces/slasher/IVetoSlasher.sol"; +import {IMigratablesFactory} from "@symbiotic/interfaces/common/IMigratablesFactory.sol"; + +contract DeploySymbioticVaults is Script { + struct VaultConfig { + address admin; + address collateral; + } + + function run() public { + IVaultConfigurator vaultConfigurator = _readVaultConfigurator(); + VaultConfig[] memory configs = _readVaultConfigs(); + uint48 epochDuration = _readEpochDuration(); + + // TODO: Check if vaults for specific collateral are already deployed! + + vm.startBroadcast(); + + for (uint256 i; i < configs.length; ++i) { + VaultConfig memory config = configs[i]; + + IMigratablesFactory vaultFactory = IMigratablesFactory(vaultConfigurator.VAULT_FACTORY()); + + bool exists; + + // First check if the vault already exists. We do this by checking for the collateral, and the admin. + // If we need to check for more properties in the future (like version), we can add them here. + for (uint256 j; j < vaultFactory.totalEntities(); ++j) { + address existingVault = vaultFactory.entity(j); + + if ( + IVault(existingVault).collateral() == config.collateral + && OwnableUpgradeable(existingVault).owner() == config.admin + ) { + console.log( + "Vault for collateral %s already deployed with admin %s", config.collateral, config.admin + ); + console.log("Address:", existingVault); + exists = true; + + break; + } + } + + if (exists) { + continue; + } + + address[] memory adminRoleHolders = new address[](1); + adminRoleHolders[0] = config.admin; + + IVaultConfigurator.InitParams memory vaultConfiguratorInitParams = IVaultConfigurator.InitParams({ + // Use Version 1 for a standard vault (non-tokenized). + version: 1, + owner: config.admin, + vaultParams: abi.encode( + IVault.InitParams({ + collateral: config.collateral, + burner: address(0xdead), + epochDuration: epochDuration, + depositWhitelist: false, + isDepositLimit: false, + depositLimit: 0, + defaultAdminRoleHolder: config.admin, + depositWhitelistSetRoleHolder: config.admin, + depositorWhitelistRoleHolder: config.admin, + isDepositLimitSetRoleHolder: config.admin, + depositLimitSetRoleHolder: config.admin + }) + ), + delegatorIndex: 0, // Use NetworkRestakeDelegator + delegatorParams: abi.encode( + INetworkRestakeDelegator.InitParams({ + baseParams: IBaseDelegator.BaseParams({ + defaultAdminRoleHolder: config.admin, + hook: address(0), // we don't need a hook + hookSetRoleHolder: config.admin + }), + networkLimitSetRoleHolders: adminRoleHolders, + operatorNetworkSharesSetRoleHolders: adminRoleHolders + }) + ), + withSlasher: true, + slasherIndex: 1, // Use VetoSlasher + slasherParams: abi.encode( + IVetoSlasher.InitParams({ + baseParams: IBaseSlasher.BaseParams({ + isBurnerHook: false // ? + }), + // veto duration must be smaller than epoch duration + vetoDuration: uint48(12 hours), + resolverSetEpochsDelay: 3 + }) + ) + }); + + (address vault, address networkRestakeDelegator, address vetoSlasher) = + vaultConfigurator.create(vaultConfiguratorInitParams); + + console.log("Deployed vault with collateral:", config.collateral); + console.log("Vault address:", vault); + console.log("NetworkRestakeDelegator:", networkRestakeDelegator); + console.log("VetoSlasher:", vetoSlasher); + } + + vm.stopBroadcast(); + } + + function _readVaultConfigs() internal view returns (VaultConfig[] memory configs) { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/config/holesky/vaults.json"); + string memory json = vm.readFile(path); + + configs = abi.decode(vm.parseJson(json), (VaultConfig[])); + } + + function _readEpochDuration() internal view returns (uint48) { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/config/holesky/parameters.json"); + string memory json = vm.readFile(path); + + return uint48(vm.parseJsonUint(json, ".epochDuration")); + } + + function _readVaultConfigurator() internal view returns (IVaultConfigurator) { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/config/holesky/deployments.json"); + string memory json = vm.readFile(path); + + return IVaultConfigurator(vm.parseJsonAddress(json, ".symbiotic.vaultConfigurator")); + } +} diff --git a/bolt-contracts/script/holesky/admin/helpers/UpdateSupportedVaults.s.sol b/bolt-contracts/script/holesky/admin/helpers/UpdateSupportedVaults.s.sol new file mode 100644 index 000000000..14fdb19be --- /dev/null +++ b/bolt-contracts/script/holesky/admin/helpers/UpdateSupportedVaults.s.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {Script, console} from "forge-std/Script.sol"; + +import {INetworkRegistry} from "@symbiotic/interfaces/INetworkRegistry.sol"; +import {INetworkMiddlewareService} from "@symbiotic/interfaces/service/INetworkMiddlewareService.sol"; + +import {BoltSymbioticMiddlewareV1} from "../../../../src/contracts/BoltSymbioticMiddlewareV1.sol"; + +contract UpdateSupportedVaults is Script { + function run() public { + BoltSymbioticMiddlewareV1 middleware = _readSymbioticMiddleware(); + + address[] memory whitelisted = middleware.getWhitelistedVaults(); + address[] memory toWhitelist = _readVaultsToWhitelist(); + + vm.startBroadcast(); + + // Step 1: Whitelist new vaults + for (uint256 i = 0; i < toWhitelist.length; i++) { + address vault = toWhitelist[i]; + + bool isWhitelisted = false; + for (uint256 j = 0; j < whitelisted.length; j++) { + if (whitelisted[j] == vault) { + isWhitelisted = true; + break; + } + } + + if (!isWhitelisted) { + console.log("Whitelisting vault", vault); + middleware.registerVault(vault); + } + } + + // Step 2: Remove vaults from contract that are not in the supported vaults list + for (uint256 i = 0; i < whitelisted.length; i++) { + address vault = whitelisted[i]; + + bool isSupported = false; + for (uint256 j = 0; j < toWhitelist.length; j++) { + if (toWhitelist[j] == vault) { + isSupported = true; + break; + } + } + + if (!isSupported) { + console.log("Removing vault", vault); + middleware.deregisterVault(vault); + } + } + + vm.stopBroadcast(); + } + + function _readVaultsToWhitelist() public view returns (address[] memory) { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/config/holesky/deployments.json"); + string memory json = vm.readFile(path); + + return vm.parseJsonAddressArray(json, ".symbiotic.supportedVaults"); + } + + function _readSymbioticMiddleware() public view returns (BoltSymbioticMiddlewareV1) { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/config/holesky/deployments.json"); + string memory json = vm.readFile(path); + + return BoltSymbioticMiddlewareV1(vm.parseJsonAddress(json, ".symbiotic.middleware")); + } +} diff --git a/bolt-contracts/src/contracts/BoltEigenLayerMiddlewareV1.sol b/bolt-contracts/src/contracts/BoltEigenLayerMiddlewareV1.sol index 750501777..34d4b1b9a 100644 --- a/bolt-contracts/src/contracts/BoltEigenLayerMiddlewareV1.sol +++ b/bolt-contracts/src/contracts/BoltEigenLayerMiddlewareV1.sol @@ -23,7 +23,7 @@ import {AVSDirectoryStorage} from "@eigenlayer/src/contracts/core/AVSDirectorySt import {DelegationManagerStorage} from "@eigenlayer/src/contracts/core/DelegationManagerStorage.sol"; import {StrategyManagerStorage} from "@eigenlayer/src/contracts/core/StrategyManagerStorage.sol"; -/// @title Bolt Manager +/// @title Bolt EigenLayer Middleware contract. /// @notice This contract is responsible for interfacing with the EigenLayer restaking protocol. /// @dev This contract is upgradeable using the UUPSProxy pattern. Storage layout remains fixed across upgrades /// with the use of storage gaps. diff --git a/bolt-contracts/src/contracts/BoltEigenLayerMiddlewareV2.sol b/bolt-contracts/src/contracts/BoltEigenLayerMiddlewareV2.sol new file mode 100644 index 000000000..a73921f37 --- /dev/null +++ b/bolt-contracts/src/contracts/BoltEigenLayerMiddlewareV2.sol @@ -0,0 +1,417 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; +import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; + +import {MapWithTimeData} from "../lib/MapWithTimeData.sol"; +import {IBoltParametersV1} from "../interfaces/IBoltParametersV1.sol"; +import {IBoltValidatorsV1} from "../interfaces/IBoltValidatorsV1.sol"; +import {IBoltMiddlewareV1} from "../interfaces/IBoltMiddlewareV1.sol"; +import {IBoltManagerV1} from "../interfaces/IBoltManagerV1.sol"; + +import {IServiceManager} from "@eigenlayer-middleware/src/interfaces/IServiceManager.sol"; +import {IStrategyManager} from "@eigenlayer/src/contracts/interfaces/IStrategyManager.sol"; +import {IAVSDirectory} from "@eigenlayer/src/contracts/interfaces/IAVSDirectory.sol"; +import {IDelegationManager} from "@eigenlayer/src/contracts/interfaces/IDelegationManager.sol"; +import {ISignatureUtils} from "@eigenlayer/src/contracts/interfaces/ISignatureUtils.sol"; +import {IStrategy} from "@eigenlayer/src/contracts/interfaces/IStrategy.sol"; +import {AVSDirectoryStorage} from "@eigenlayer/src/contracts/core/AVSDirectoryStorage.sol"; +import {DelegationManagerStorage} from "@eigenlayer/src/contracts/core/DelegationManagerStorage.sol"; +import {StrategyManagerStorage} from "@eigenlayer/src/contracts/core/StrategyManagerStorage.sol"; + +/// @title Bolt EigenLayer Middleware contract. +/// @notice This contract is responsible for interfacing with the EigenLayer restaking protocol. +/// @dev This contract is upgradeable using the UUPSProxy pattern. Storage layout remains fixed across upgrades +/// with the use of storage gaps. +/// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps +/// To validate the storage layout, use the Openzeppelin Foundry Upgrades toolkit. +/// You can also validate manually with forge: forge inspect storage-layout --pretty +contract BoltEigenLayerMiddlewareV2 is IBoltMiddlewareV1, IServiceManager, OwnableUpgradeable, UUPSUpgradeable { + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableMap for EnumerableMap.AddressToUintMap; + using MapWithTimeData for EnumerableMap.AddressToUintMap; + + // ========= STORAGE ========= + + /// @notice Start timestamp of the first epoch. + uint48 public START_TIMESTAMP; + + /// @notice Bolt Parameters contract. + IBoltParametersV1 public parameters; + + /// @notice Validators registry, where validators are registered via their + /// BLS pubkey and are assigned a sequence number. + IBoltManagerV1 public manager; + + /// @notice Set of EigenLayer protocol strategies that are used in Bolt Protocol. + EnumerableMap.AddressToUintMap private strategies; + + /// @notice Address of the EigenLayer AVS Directory contract. + IAVSDirectory public AVS_DIRECTORY; + + /// @notice Address of the EigenLayer Delegation Manager contract. + DelegationManagerStorage public DELEGATION_MANAGER; + + /// @notice Address of the EigenLayer Strategy Manager contract. + StrategyManagerStorage public STRATEGY_MANAGER; + + /// @notice Name hash of the restaking protocol for identifying the instance of `IBoltMiddleware`. + bytes32 public NAME_HASH; + + // --> Storage layout marker: 9 slots + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + * This can be validated with the Openzeppelin Foundry Upgrades toolkit. + * + * Total storage slots: 50 + */ + uint256[41] private __gap; + + // ========= ERRORS ========= + + error StrategyNotAllowed(); + error OperatorAlreadyRegisteredToAVS(); + + // ========= INITIALIZER & PROXY FUNCTIONALITY ========= // + + /// @notice Constructor for the BoltEigenLayerMiddleware contract. + /// @param _parameters The address of the Bolt Parameters contract. + /// @param _manager The address of the Bolt Manager contract. + /// @param _eigenlayerAVSDirectory The address of the EigenLayer AVS Directory contract. + /// @param _eigenlayerDelegationManager The address of the EigenLayer Delegation Manager contract. + /// @param _eigenlayerStrategyManager The address of the EigenLayer Strategy Manager. + function initialize( + address _owner, + address _parameters, + address _manager, + address _eigenlayerAVSDirectory, + address _eigenlayerDelegationManager, + address _eigenlayerStrategyManager + ) public initializer { + __Ownable_init(_owner); + parameters = IBoltParametersV1(_parameters); + manager = IBoltManagerV1(_manager); + START_TIMESTAMP = Time.timestamp(); + + AVS_DIRECTORY = IAVSDirectory(_eigenlayerAVSDirectory); + DELEGATION_MANAGER = DelegationManagerStorage(_eigenlayerDelegationManager); + STRATEGY_MANAGER = StrategyManagerStorage(_eigenlayerStrategyManager); + NAME_HASH = keccak256("EIGENLAYER"); + } + + function initializeV2( + address _owner, + address _parameters, + address _manager, + address _eigenlayerAVSDirectory, + address _eigenlayerDelegationManager, + address _eigenlayerStrategyManager + ) public reinitializer(2) { + __Ownable_init(_owner); + parameters = IBoltParametersV1(_parameters); + manager = IBoltManagerV1(_manager); + START_TIMESTAMP = Time.timestamp(); + + AVS_DIRECTORY = IAVSDirectory(_eigenlayerAVSDirectory); + DELEGATION_MANAGER = DelegationManagerStorage(_eigenlayerDelegationManager); + STRATEGY_MANAGER = StrategyManagerStorage(_eigenlayerStrategyManager); + NAME_HASH = keccak256("EIGENLAYER"); + } + + function _authorizeUpgrade( + address newImplementation + ) internal override onlyOwner {} + + // ========= VIEW FUNCTIONS ========= + + /// @notice Get the start timestamp of an epoch. + function getEpochStartTs( + uint48 epoch + ) public view returns (uint48 timestamp) { + return START_TIMESTAMP + epoch * parameters.EPOCH_DURATION(); + } + + /// @notice Get the epoch at a given timestamp. + function getEpochAtTs( + uint48 timestamp + ) public view returns (uint48 epoch) { + return (timestamp - START_TIMESTAMP) / parameters.EPOCH_DURATION(); + } + + /// @notice Get the current epoch. + function getCurrentEpoch() public view returns (uint48 epoch) { + return getEpochAtTs(Time.timestamp()); + } + + function getWhitelistedStrategies() public view returns (address[] memory) { + return strategies.keys(); + } + + // ========= ADMIN FUNCTIONS ========= + /// @notice Register a strategy to work in Bolt Protocol. + /// @param strategy The EigenLayer strategy address + function registerStrategy( + address strategy + ) public onlyOwner { + if (strategies.contains(strategy)) { + revert AlreadyRegistered(); + } + + if (!STRATEGY_MANAGER.strategyIsWhitelistedForDeposit(IStrategy(strategy))) { + revert StrategyNotAllowed(); + } + + strategies.add(strategy); + strategies.enable(strategy); + } + + /// @notice Deregister a strategy from working in Bolt Protocol. + /// @param strategy The EigenLayer strategy address. + function deregisterStrategy( + address strategy + ) public onlyOwner { + if (!strategies.contains(strategy)) { + revert NotRegistered(); + } + + strategies.remove(strategy); + } + + // ========= EIGENLAYER MIDDLEWARE LOGIC ========= + + /// @notice Allow an operator to signal opt-in to Bolt Protocol. + /// @dev This requires calling the EigenLayer AVS Directory contract to register the operator. + /// EigenLayer internally contains a mapping from `msg.sender` (our AVS contract) to the operator. + /// The msg.sender of this call will be the operator address. + function registerOperator( + string calldata rpc, + ISignatureUtils.SignatureWithSaltAndExpiry calldata operatorSignature + ) public { + if (manager.isOperator(msg.sender)) { + revert AlreadyRegistered(); + } + + if (!DELEGATION_MANAGER.isOperator(msg.sender)) { + revert NotOperator(); + } + + registerOperatorToAVS(msg.sender, operatorSignature); + + // Register the operator in the manager + manager.registerOperator(msg.sender, rpc); + } + + /// @notice Deregister an EigenLayer operator from working in Bolt Protocol. + /// @dev This requires calling the EigenLayer AVS Directory contract to deregister the operator. + /// EigenLayer internally contains a mapping from `msg.sender` (our AVS contract) to the operator. + function deregisterOperator() public { + if (!manager.isOperator(msg.sender)) { + revert NotRegistered(); + } + + deregisterOperatorFromAVS(msg.sender); + + manager.deregisterOperator(msg.sender); + } + + /// @notice Allow an operator to signal indefinite opt-out from Bolt Protocol. + /// @dev Pausing activity does not prevent the operator from being slashable for + /// the current network epoch until the end of the slashing window. + function pauseOperator() public { + manager.pauseOperator(msg.sender); + } + + /// @notice Allow a disabled operator to signal opt-in to Bolt Protocol. + function unpauseOperator() public { + manager.unpauseOperator(msg.sender); + } + + /// @notice Allow a strategy to signal indefinite opt-out from Bolt Protocol. + function pauseStrategy() public { + if (!strategies.contains(msg.sender)) { + revert NotRegistered(); + } + + strategies.disable(msg.sender); + } + + /// @notice Allow a disabled strategy to signal opt-in to Bolt Protocol. + function unpauseStrategy() public { + if (!strategies.contains(msg.sender)) { + revert NotRegistered(); + } + + strategies.enable(msg.sender); + } + + /// @notice Check if a strategy is currently enabled to work in Bolt Protocol. + /// @param strategy The strategy address to check the enabled status for. + /// @return True if the strategy is enabled, false otherwise. + function isStrategyEnabled( + address strategy + ) public view returns (bool) { + (uint48 enabledTime, uint48 disabledTime) = strategies.getTimes(strategy); + return enabledTime != 0 && disabledTime == 0; + } + + /// @notice Get the collaterals and amounts staked by an operator across the supported strategies. + /// + /// @param operator The operator address to get the collaterals and amounts staked for. + /// @return collaterals The collaterals staked by the operator. + /// @dev Assumes that the operator is registered and enabled. + function getOperatorCollaterals( + address operator + ) public view returns (address[] memory, uint256[] memory) { + address[] memory collateralTokens = new address[](strategies.length()); + uint256[] memory amounts = new uint256[](strategies.length()); + + uint48 epochStartTs = getEpochStartTs(getEpochAtTs(Time.timestamp())); + + for (uint256 i = 0; i < strategies.length(); ++i) { + (address strategy, uint48 enabledTime, uint48 disabledTime) = strategies.atWithTimes(i); + + if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { + continue; + } + + IStrategy strategyImpl = IStrategy(strategy); + + address collateral = address(strategyImpl.underlyingToken()); + collateralTokens[i] = collateral; + + uint256 shares = DELEGATION_MANAGER.operatorShares(operator, strategyImpl); + amounts[i] = strategyImpl.sharesToUnderlyingView(shares); + } + + return (collateralTokens, amounts); + } + + /// @notice Get the amount of tokens delegated to an operator across the allowed strategies. + /// @param operator The operator address to get the stake for. + /// @param collateral The collateral address to get the stake for. + /// @return amount The amount of tokens delegated to the operator of the specified collateral. + function getOperatorStake(address operator, address collateral) public view returns (uint256 amount) { + uint48 timestamp = Time.timestamp(); + return getOperatorStakeAt(operator, collateral, timestamp); + } + + /// @notice Get the stake of an operator in EigenLayer protocol at a given timestamp. + /// @param operator The operator address to check the stake for. + /// @param collateral The collateral address to check the stake for. + /// @param timestamp The timestamp to check the stake at. + /// @return amount The stake of the operator at the given timestamp, in collateral token. + function getOperatorStakeAt( + address operator, + address collateral, + uint48 timestamp + ) public view returns (uint256 amount) { + if (timestamp > Time.timestamp() || timestamp < START_TIMESTAMP) { + revert InvalidQuery(); + } + + uint48 epochStartTs = getEpochStartTs(getEpochAtTs(timestamp)); + + for (uint256 i = 0; i < strategies.length(); i++) { + (address strategy, uint48 enabledTime, uint48 disabledTime) = strategies.atWithTimes(i); + + if (collateral != address(IStrategy(strategy).underlyingToken())) { + continue; + } + + if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { + continue; + } + + uint256 shares = DELEGATION_MANAGER.operatorShares(operator, IStrategy(strategy)); + amount += IStrategy(strategy).sharesToUnderlyingView(shares); + } + + return amount; + } + + // ========= EIGENLAYER AVS FUNCTIONS ========= + + /// @notice emits an `AVSMetadataURIUpdated` event indicating the information has updated. + /// @param metadataURI The URI for metadata associated with an avs + function updateAVSMetadataURI( + string calldata metadataURI + ) public onlyOwner { + AVS_DIRECTORY.updateAVSMetadataURI(metadataURI); + } + + // ========= HELPER FUNCTIONS ========= + + /// @notice Check if a map entry was active at a given timestamp. + /// @param enabledTime The enabled time of the map entry. + /// @param disabledTime The disabled time of the map entry. + /// @param timestamp The timestamp to check the map entry status at. + /// @return True if the map entry was active at the given timestamp, false otherwise. + function _wasEnabledAt(uint48 enabledTime, uint48 disabledTime, uint48 timestamp) private pure returns (bool) { + return enabledTime != 0 && enabledTime <= timestamp && (disabledTime == 0 || disabledTime >= timestamp); + } + + // ============== EIGENLAYER SERVICE MANAGER ================= // + // Cfr. https://docs.eigenlayer.xyz/developers/avs-dashboard-onboarding + // getOperatorRestakedStrategies and getRestakeableStrategies have reference implementations + // that read from RegistryCoordinator & StakeRegistry. These are middleware contracts that + // are not used in the EigenLayer operator CLI as of today (23 Oct 2024): https://github.com/Layr-Labs/eigensdk-go/blob/0042b1a0dd502bb03c6bf1da85fc096c5c8e8f1b/chainio/clients/elcontracts/writer.go#L158 + // + // So we'll just get that information from our own system for now. + + function registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature + ) public override { + // Register the operator to the AVS directory for this AVS + AVS_DIRECTORY.registerOperatorToAVS(operator, operatorSignature); + } + + function deregisterOperatorFromAVS( + address operator + ) public override { + // NOTE: need to do this check because these functions have to be public + if (msg.sender != operator) { + revert NotAllowed(); + } + + AVS_DIRECTORY.deregisterOperatorFromAVS(operator); + } + + function getOperatorRestakedStrategies( + address operator + ) external view override returns (address[] memory) { + address[] memory restakedStrategies = new address[](strategies.length()); + + uint48 epochStartTs = getEpochStartTs(getEpochAtTs(Time.timestamp())); + + for (uint256 i = 0; i < strategies.length(); ++i) { + (address strategy, uint48 enabledTime, uint48 disabledTime) = strategies.atWithTimes(i); + + if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { + continue; + } + + if (DELEGATION_MANAGER.operatorShares(operator, IStrategy(strategy)) > 0) { + restakedStrategies[restakedStrategies.length] = strategy; + } + } + + return restakedStrategies; + } + + function getRestakeableStrategies() external view override returns (address[] memory) { + return strategies.keys(); + } + + function avsDirectory() external view override returns (address) { + return address(AVS_DIRECTORY); + } +} diff --git a/bolt-contracts/src/contracts/BoltSymbioticMiddlewareV2.sol b/bolt-contracts/src/contracts/BoltSymbioticMiddlewareV2.sol new file mode 100644 index 000000000..ae0530fa6 --- /dev/null +++ b/bolt-contracts/src/contracts/BoltSymbioticMiddlewareV2.sol @@ -0,0 +1,436 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; +import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; + +import {IBaseDelegator} from "@symbiotic/interfaces/delegator/IBaseDelegator.sol"; +import {Subnetwork} from "@symbiotic/contracts/libraries/Subnetwork.sol"; +import {IVault} from "@symbiotic/interfaces/vault/IVault.sol"; +import {IRegistry} from "@symbiotic/interfaces/common/IRegistry.sol"; +import {IOptInService} from "@symbiotic/interfaces/service/IOptInService.sol"; +import {ISlasher} from "@symbiotic/interfaces/slasher/ISlasher.sol"; +import {IVetoSlasher} from "@symbiotic/interfaces/slasher/IVetoSlasher.sol"; +import {IEntity} from "@symbiotic/interfaces/common/IEntity.sol"; + +import {MapWithTimeData} from "../lib/MapWithTimeData.sol"; +import {IBoltValidatorsV1} from "../interfaces/IBoltValidatorsV1.sol"; +import {IBoltParametersV1} from "../interfaces/IBoltParametersV1.sol"; +import {IBoltMiddlewareV1} from "../interfaces/IBoltMiddlewareV1.sol"; +import {IBoltManagerV1} from "../interfaces/IBoltManagerV1.sol"; + +/// @title Bolt Symbiotic Middleware +/// @notice This contract is responsible for interfacing with the Symbiotic restaking protocol. +/// @dev This contract is upgradeable using the UUPSProxy pattern. Storage layout remains fixed across upgrades +/// with the use of storage gaps. +/// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps +/// To validate the storage layout, use the Openzeppelin Foundry Upgrades toolkit. +/// You can also validate manually with forge: forge inspect storage-layout --pretty +contract BoltSymbioticMiddlewareV2 is IBoltMiddlewareV1, OwnableUpgradeable, UUPSUpgradeable { + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableMap for EnumerableMap.AddressToUintMap; + using MapWithTimeData for EnumerableMap.AddressToUintMap; + using Subnetwork for address; + + // ========== CONSTANTS ============ // + /// @notice Slasher that can instantly slash operators without veto. + uint256 public INSTANT_SLASHER_TYPE = 0; + + /// @notice Slasher that can request a veto before actually slashing operators. + uint256 public VETO_SLASHER_TYPE = 1; + + // ========= STORAGE ========= // + + /// @notice Start timestamp of the first epoch. + uint48 public START_TIMESTAMP; + + /// @notice Bolt Parameters contract. + IBoltParametersV1 public parameters; + + /// @notice Validators registry, where validators are registered via their + /// BLS pubkey and are assigned a sequence number. + IBoltManagerV1 public manager; + + /// @notice Set of Symbiotic protocol vaults that are used in Bolt Protocol. + EnumerableMap.AddressToUintMap private vaults; + + /// @notice Address of the Bolt network in Symbiotic Protocol. + address public BOLT_SYMBIOTIC_NETWORK; + + /// @notice Address of the Symbiotic Operator Registry contract. + address public OPERATOR_REGISTRY; + + /// @notice Address of the Symbiotic Vault Factory contract. + address public VAULT_FACTORY; + + /// @notice Address of the Symbiotic Operator Network Opt-In contract. + address public OPERATOR_NET_OPTIN; + + bytes32 public NAME_HASH; + + // --> Storage layout marker: 12 slots + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + * This can be validated with the Openzeppelin Foundry Upgrades toolkit. + * + * Total storage slots: 50 + */ + uint256[38] private __gap; + + // ========= ERRORS ========= + + error NotVault(); + error SlashAmountTooHigh(); + error UnknownSlasherType(); + + // ========= CONSTRUCTOR ========= + + /// @notice Constructor for the BoltSymbioticMiddleware contract. + /// @param _parameters The address of the Bolt Parameters contract. + /// @param _manager The address of the Bolt Manager contract. + /// @param _symbioticNetwork The address of the Symbiotic network. + /// @param _symbioticOperatorRegistry The address of the Symbiotic operator registry. + /// @param _symbioticOperatorNetOptIn The address of the Symbiotic operator network opt-in contract. + /// @param _symbioticVaultFactory The address of the Symbiotic vault registry. + function initialize( + address _owner, + address _parameters, + address _manager, + address _symbioticNetwork, + address _symbioticOperatorRegistry, + address _symbioticOperatorNetOptIn, + address _symbioticVaultFactory + ) public initializer { + __Ownable_init(_owner); + parameters = IBoltParametersV1(_parameters); + manager = IBoltManagerV1(_manager); + START_TIMESTAMP = Time.timestamp(); + + BOLT_SYMBIOTIC_NETWORK = _symbioticNetwork; + OPERATOR_REGISTRY = _symbioticOperatorRegistry; + OPERATOR_NET_OPTIN = _symbioticOperatorNetOptIn; + VAULT_FACTORY = _symbioticVaultFactory; + NAME_HASH = keccak256("SYMBIOTIC"); + } + + function initializeV2( + address _owner, + address _parameters, + address _manager, + address _symbioticNetwork, + address _symbioticOperatorRegistry, + address _symbioticOperatorNetOptIn, + address _symbioticVaultFactory + ) public reinitializer(2) { + __Ownable_init(_owner); + parameters = IBoltParametersV1(_parameters); + manager = IBoltManagerV1(_manager); + START_TIMESTAMP = Time.timestamp(); + + BOLT_SYMBIOTIC_NETWORK = _symbioticNetwork; + OPERATOR_REGISTRY = _symbioticOperatorRegistry; + OPERATOR_NET_OPTIN = _symbioticOperatorNetOptIn; + VAULT_FACTORY = _symbioticVaultFactory; + NAME_HASH = keccak256("SYMBIOTIC"); + } + + function _authorizeUpgrade( + address newImplementation + ) internal override onlyOwner {} + + // ========= VIEW FUNCTIONS ========= + + /// @notice Get the start timestamp of an epoch. + function getEpochStartTs( + uint48 epoch + ) public view returns (uint48 timestamp) { + return START_TIMESTAMP + epoch * parameters.EPOCH_DURATION(); + } + + /// @notice Get the epoch at a given timestamp. + function getEpochAtTs( + uint48 timestamp + ) public view returns (uint48 epoch) { + return (timestamp - START_TIMESTAMP) / parameters.EPOCH_DURATION(); + } + + /// @notice Get the current epoch. + function getCurrentEpoch() public view returns (uint48 epoch) { + return getEpochAtTs(Time.timestamp()); + } + + /// @notice Get the whitelisted vaults. + function getWhitelistedVaults() public view returns (address[] memory) { + return vaults.keys(); + } + + // =========== ADMIN FUNCTIONS ============ // + + /// @notice Allow a vault to signal opt-in to Bolt Protocol. + /// @param vault The vault address to signal opt-in for. + function registerVault( + address vault + ) public onlyOwner { + if (vaults.contains(vault)) { + revert AlreadyRegistered(); + } + + if (!IRegistry(VAULT_FACTORY).isEntity(vault)) { + revert NotVault(); + } + + // TODO: check slashing conditions and veto duration + + vaults.add(vault); + vaults.enable(vault); + } + + /// @notice Deregister a vault from working in Bolt Protocol. + /// @param vault The vault address to deregister. + function deregisterVault( + address vault + ) public onlyOwner { + if (!vaults.contains(vault)) { + revert NotRegistered(); + } + + vaults.remove(vault); + } + + // ========= SYMBIOTIC MIDDLEWARE LOGIC ========= + + /// @notice Allow an operator to signal opt-in to Bolt Protocol. + /// msg.sender must be an operator in the Symbiotic network. + function registerOperator( + string calldata rpc + ) public { + if (manager.isOperator(msg.sender)) { + revert AlreadyRegistered(); + } + + if (!IRegistry(OPERATOR_REGISTRY).isEntity(msg.sender)) { + revert NotOperator(); + } + + if (!IOptInService(OPERATOR_NET_OPTIN).isOptedIn(msg.sender, BOLT_SYMBIOTIC_NETWORK)) { + revert OperatorNotOptedIn(); + } + + manager.registerOperator(msg.sender, rpc); + } + + /// @notice Deregister a Symbiotic operator from working in Bolt Protocol. + /// @dev This does NOT deregister the operator from the Symbiotic network. + function deregisterOperator() public { + if (!manager.isOperator(msg.sender)) { + revert NotRegistered(); + } + + manager.deregisterOperator(msg.sender); + } + + /// @notice Allow an operator to signal indefinite opt-out from Bolt Protocol. + /// @dev Pausing activity does not prevent the operator from being slashable for + /// the current network epoch until the end of the slashing window. + function pauseOperator() public { + manager.pauseOperator(msg.sender); + } + + /// @notice Allow a disabled operator to signal opt-in to Bolt Protocol. + function unpauseOperator() public { + manager.unpauseOperator(msg.sender); + } + + /// @notice Allow a vault to signal indefinite opt-out from Bolt Protocol. + function pauseVault() public { + if (!vaults.contains(msg.sender)) { + revert NotRegistered(); + } + + vaults.disable(msg.sender); + } + + /// @notice Allow a disabled vault to signal opt-in to Bolt Protocol. + function unpauseVault() public { + if (!vaults.contains(msg.sender)) { + revert NotRegistered(); + } + + vaults.enable(msg.sender); + } + + /// @notice Check if a vault is currently enabled to work in Bolt Protocol. + /// @param vault The vault address to check the enabled status for. + /// @return True if the vault is enabled, false otherwise. + function isVaultEnabled( + address vault + ) public view returns (bool) { + (uint48 enabledTime, uint48 disabledTime) = vaults.getTimes(vault); + return enabledTime != 0 && disabledTime == 0; + } + + /// @notice Get the collaterals and amounts staked by an operator across the supported strategies. + /// + /// @param operator The operator address to get the collaterals and amounts staked for. + /// @return collaterals The collaterals staked by the operator. + /// @dev Assumes that the operator is registered and enabled. + function getOperatorCollaterals( + address operator + ) public view returns (address[] memory, uint256[] memory) { + address[] memory collateralTokens = new address[](vaults.length()); + uint256[] memory amounts = new uint256[](vaults.length()); + + uint48 epochStartTs = getEpochStartTs(getEpochAtTs(Time.timestamp())); + + for (uint256 i = 0; i < vaults.length(); ++i) { + (address vault, uint48 enabledTime, uint48 disabledTime) = vaults.atWithTimes(i); + + if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { + continue; + } + + address collateral = IVault(vault).collateral(); + collateralTokens[i] = collateral; + + // in order to have stake in a network, the operator needs to be opted in to that vault. + // this authorization is fully handled in the Vault, we just need to read the stake. + amounts[i] = IBaseDelegator(IVault(vault).delegator()).stakeAt( + // The stake for each subnetwork is stored in the vault's delegator contract. + // stakeAt returns the stake of "operator" at "timestamp" for "network" (or subnetwork) + // bytes(0) is for hints, which we don't currently use. + BOLT_SYMBIOTIC_NETWORK.subnetwork(0), + operator, + epochStartTs, + new bytes(0) + ); + } + + return (collateralTokens, amounts); + } + + /// @notice Get the stake of an operator in Symbiotic protocol at the current timestamp. + /// @param operator The operator address to check the stake for. + /// @param collateral The collateral address to check the stake for. + /// @return amount The stake of the operator at the current timestamp, in collateral token. + function getOperatorStake(address operator, address collateral) public view returns (uint256 amount) { + uint48 timestamp = Time.timestamp(); + return getOperatorStakeAt(operator, collateral, timestamp); + } + + /// @notice Get the stake of an operator in Symbiotic protocol at a given timestamp. + /// @param operator The operator address to check the stake for. + /// @param collateral The collateral address to check the stake for. + /// @param timestamp The timestamp to check the stake at. + /// @return amount The stake of the operator at the given timestamp, in collateral token. + function getOperatorStakeAt( + address operator, + address collateral, + uint48 timestamp + ) public view returns (uint256 amount) { + if (timestamp > Time.timestamp() || timestamp < START_TIMESTAMP) { + revert InvalidQuery(); + } + + uint48 epochStartTs = getEpochStartTs(getEpochAtTs(timestamp)); + + for (uint256 i = 0; i < vaults.length(); ++i) { + (address vault, uint48 enabledTime, uint48 disabledTime) = vaults.atWithTimes(i); + + if (collateral != IVault(vault).collateral()) { + continue; + } + + if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { + continue; + } + + // in order to have stake in a network, the operator needs to be opted in to that vault. + // this authorization is fully handled in the Vault, we just need to read the stake. + amount += IBaseDelegator(IVault(vault).delegator()).stakeAt( + // The stake for each subnetwork is stored in the vault's delegator contract. + // stakeAt returns the stake of "operator" at "timestamp" for "network" (or subnetwork) + // bytes(0) is for hints, which we don't currently use. + BOLT_SYMBIOTIC_NETWORK.subnetwork(0), + operator, + epochStartTs, + new bytes(0) + ); + } + + return amount; + } + + /// @notice Slash a given operator for a given amount of collateral. + /// @param timestamp The timestamp of the slash event. + /// @param operator The operator address to slash. + /// @param collateral The collateral address to slash. + /// @param amount The amount of collateral to slash. + function slash(uint48 timestamp, address operator, address collateral, uint256 amount) public onlyOwner { + // TODO: remove onlyOwner modifier and gate the slashing logic behind the BoltChallenger + // fault proof mechanism to allow for permissionless slashing. + + uint48 epochStartTs = getEpochStartTs(getEpochAtTs(timestamp)); + + for (uint256 i = 0; i < vaults.length(); ++i) { + (address vault, uint48 enabledTime, uint48 disabledTime) = vaults.atWithTimes(i); + + if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { + continue; + } + + if (collateral != IVault(vault).collateral()) { + continue; + } + + uint256 operatorStake = getOperatorStakeAt(operator, collateral, epochStartTs); + + if (amount > operatorStake) { + revert SlashAmountTooHigh(); + } + + uint256 vaultStake = IBaseDelegator(IVault(vault).delegator()).stakeAt( + BOLT_SYMBIOTIC_NETWORK.subnetwork(0), operator, epochStartTs, new bytes(0) + ); + + // Slash the vault pro-rata. + _slashVault(epochStartTs, vault, operator, (amount * vaultStake) / operatorStake); + } + } + + // ========= HELPER FUNCTIONS ========= + + /// @notice Check if a map entry was active at a given timestamp. + /// @param enabledTime The enabled time of the map entry. + /// @param disabledTime The disabled time of the map entry. + /// @param timestamp The timestamp to check the map entry status at. + /// @return True if the map entry was active at the given timestamp, false otherwise. + function _wasEnabledAt(uint48 enabledTime, uint48 disabledTime, uint48 timestamp) private pure returns (bool) { + return enabledTime != 0 && enabledTime <= timestamp && (disabledTime == 0 || disabledTime >= timestamp); + } + + /// @notice Slash an operator for a given amount of collateral. + /// @param timestamp The timestamp of the slash event. + /// @param operator The operator address to slash. + /// @param amount The amount of collateral to slash. + function _slashVault(uint48 timestamp, address vault, address operator, uint256 amount) private { + address slasher = IVault(vault).slasher(); + uint256 slasherType = IEntity(slasher).TYPE(); + + if (slasherType == INSTANT_SLASHER_TYPE) { + ISlasher(slasher).slash(BOLT_SYMBIOTIC_NETWORK.subnetwork(0), operator, amount, timestamp, new bytes(0)); + } else if (slasherType == VETO_SLASHER_TYPE) { + IVetoSlasher(slasher).requestSlash( + BOLT_SYMBIOTIC_NETWORK.subnetwork(0), operator, amount, timestamp, new bytes(0) + ); + } else { + revert UnknownSlasherType(); + } + } +} diff --git a/bolt-contracts/test/BoltManager.EigenLayer.t.sol b/bolt-contracts/test/BoltManager.EigenLayer.t.sol index 4a815704f..0edfe0186 100644 --- a/bolt-contracts/test/BoltManager.EigenLayer.t.sol +++ b/bolt-contracts/test/BoltManager.EigenLayer.t.sol @@ -6,7 +6,7 @@ import {Test, console} from "forge-std/Test.sol"; import {BoltValidatorsV1} from "../src/contracts/BoltValidatorsV1.sol"; import {BoltManagerV1} from "../src/contracts/BoltManagerV1.sol"; import {BoltParametersV1} from "../src/contracts/BoltParametersV1.sol"; -import {BoltEigenLayerMiddlewareV1} from "../src/contracts/BoltEigenLayerMiddlewareV1.sol"; +import {BoltEigenLayerMiddlewareV2} from "../src/contracts/BoltEigenLayerMiddlewareV2.sol"; import {BoltConfig} from "../src/lib/Config.sol"; import {IBoltValidatorsV1} from "../src/interfaces/IBoltValidatorsV1.sol"; import {IBoltManagerV1} from "../src/interfaces/IBoltManagerV1.sol"; @@ -30,7 +30,7 @@ contract BoltManagerEigenLayerTest is Test { BoltValidatorsV1 public validators; BoltManagerV1 public manager; - BoltEigenLayerMiddlewareV1 public middleware; + BoltEigenLayerMiddlewareV2 public middleware; EigenLayerDeployer public eigenLayerDeployer; uint128 public constant PRECONF_MAX_GAS_LIMIT = 5_000_000; @@ -74,7 +74,7 @@ contract BoltManagerEigenLayerTest is Test { validators.initialize(admin, address(parameters)); manager = new BoltManagerV1(); manager.initialize(admin, address(parameters), address(validators)); - middleware = new BoltEigenLayerMiddlewareV1(); + middleware = new BoltEigenLayerMiddlewareV2(); middleware.initialize( address(admin), diff --git a/bolt-contracts/test/BoltManager.Symbiotic.t.sol b/bolt-contracts/test/BoltManager.Symbiotic.t.sol index d64492646..75b7a16ad 100644 --- a/bolt-contracts/test/BoltManager.Symbiotic.t.sol +++ b/bolt-contracts/test/BoltManager.Symbiotic.t.sol @@ -10,6 +10,7 @@ import {IVault} from "@symbiotic/interfaces/vault/IVault.sol"; import {IOptInService} from "@symbiotic/interfaces/service/IOptInService.sol"; import {IVaultConfigurator} from "@symbiotic/interfaces/IVaultConfigurator.sol"; import {IBaseDelegator} from "@symbiotic/interfaces/delegator/IBaseDelegator.sol"; +import {IBaseSlasher} from "@symbiotic/interfaces/slasher/IBaseSlasher.sol"; import {IMetadataService} from "@symbiotic/interfaces/service/IMetadataService.sol"; import {INetworkRestakeDelegator} from "@symbiotic/interfaces/delegator/INetworkRestakeDelegator.sol"; import {INetworkMiddlewareService} from "@symbiotic/interfaces/service/INetworkMiddlewareService.sol"; @@ -18,7 +19,6 @@ import {IVetoSlasher} from "@symbiotic/interfaces/slasher/IVetoSlasher.sol"; import {IDelegatorFactory} from "@symbiotic/interfaces/IDelegatorFactory.sol"; import {IMigratablesFactory} from "@symbiotic/interfaces/common/IMigratablesFactory.sol"; import {Subnetwork} from "@symbiotic/contracts/libraries/Subnetwork.sol"; -import {SimpleCollateral} from "@symbiotic/../test/mocks/SimpleCollateral.sol"; import {IBoltValidatorsV1} from "../src/interfaces/IBoltValidatorsV1.sol"; import {IBoltMiddlewareV1} from "../src/interfaces/IBoltMiddlewareV1.sol"; @@ -32,6 +32,7 @@ import {BoltConfig} from "../src/lib/Config.sol"; import {Utils} from "./Utils.sol"; import {SymbioticSetupFixture} from "./fixtures/SymbioticSetup.f.sol"; +import {Token} from "../test/mocks/Token.sol"; contract BoltManagerSymbioticTest is Test { using BLS12381 for BLS12381.G1Point; @@ -60,7 +61,7 @@ contract BoltManagerSymbioticTest is Test { IVault public vault; INetworkRestakeDelegator public networkRestakeDelegator; IVaultConfigurator public vaultConfigurator; - SimpleCollateral public collateral; + Token public collateral; address deployer = makeAddr("deployer"); address admin = makeAddr("admin"); @@ -102,21 +103,21 @@ contract BoltManagerSymbioticTest is Test { IVaultConfigurator.InitParams memory vaultConfiguratorInitParams = IVaultConfigurator.InitParams({ version: IMigratablesFactory(vaultConfigurator.VAULT_FACTORY()).lastVersion(), owner: vaultAdmin, - vaultParams: IVault.InitParams({ - collateral: address(collateral), - delegator: address(0), - slasher: address(0), - burner: address(0xdead), - epochDuration: EPOCH_DURATION, - depositWhitelist: false, - isDepositLimit: false, - depositLimit: 0, - defaultAdminRoleHolder: vaultAdmin, - depositWhitelistSetRoleHolder: vaultAdmin, - depositorWhitelistRoleHolder: vaultAdmin, - isDepositLimitSetRoleHolder: vaultAdmin, - depositLimitSetRoleHolder: vaultAdmin - }), + vaultParams: abi.encode( + IVault.InitParams({ + collateral: address(collateral), + burner: address(0xdead), + epochDuration: EPOCH_DURATION, + depositWhitelist: false, + isDepositLimit: false, + depositLimit: 0, + defaultAdminRoleHolder: vaultAdmin, + depositWhitelistSetRoleHolder: vaultAdmin, + depositorWhitelistRoleHolder: vaultAdmin, + isDepositLimitSetRoleHolder: vaultAdmin, + depositLimitSetRoleHolder: vaultAdmin + }) + ), delegatorIndex: 0, // Use NetworkRestakeDelegator delegatorParams: abi.encode( INetworkRestakeDelegator.InitParams({ @@ -133,6 +134,9 @@ contract BoltManagerSymbioticTest is Test { slasherIndex: 1, // Use VetoSlasher slasherParams: abi.encode( IVetoSlasher.InitParams({ + baseParams: IBaseSlasher.BaseParams({ + isBurnerHook: false // ? + }), // veto duration must be smaller than epoch duration vetoDuration: uint48(12 hours), resolverSetEpochsDelay: 3 @@ -245,12 +249,10 @@ contract BoltManagerSymbioticTest is Test { networkRestakeDelegator.setNetworkLimit(subnetwork, 2 ether); // --- Add stake to the Vault --- + deal(address(collateral), provider, 1 ether); vm.prank(provider); - SimpleCollateral(collateral).mint(1 ether); - - vm.prank(provider); - SimpleCollateral(collateral).approve(address(vault), 1 ether); + collateral.approve(address(vault), 1 ether); // deposit collateral from "provider" on behalf of "operator" vm.prank(provider); @@ -258,8 +260,8 @@ contract BoltManagerSymbioticTest is Test { assertEq(depositedAmount, 1 ether); assertEq(mintedShares, 1 ether); - assertEq(vault.balanceOf(operator), 1 ether); - assertEq(SimpleCollateral(collateral).balanceOf(address(vault)), 1 ether); + assertEq(vault.slashableBalanceOf(operator), 1 ether); + assertEq(collateral.balanceOf(address(vault)), 1 ether); } /// @notice Compute the hash of a BLS public key diff --git a/bolt-contracts/test/fixtures/SymbioticSetup.f.sol b/bolt-contracts/test/fixtures/SymbioticSetup.f.sol index e23fdafb7..c5afca2a4 100644 --- a/bolt-contracts/test/fixtures/SymbioticSetup.f.sol +++ b/bolt-contracts/test/fixtures/SymbioticSetup.f.sol @@ -19,7 +19,6 @@ import {Slasher} from "@symbiotic/contracts/slasher/Slasher.sol"; import {VetoSlasher} from "@symbiotic/contracts/slasher/VetoSlasher.sol"; import {VaultConfigurator} from "@symbiotic/contracts/VaultConfigurator.sol"; -import {SimpleCollateral} from "@symbiotic/../test/mocks/SimpleCollateral.sol"; import {Token} from "../mocks/Token.sol"; contract SymbioticSetupFixture is Test { @@ -40,7 +39,7 @@ contract SymbioticSetupFixture is Test { OptInService operatorVaultOptInService, OptInService operatorNetworkOptInService, VaultConfigurator vaultConfigurator, - SimpleCollateral collateral + Token collateral ) { vm.startPrank(deployer); @@ -53,9 +52,10 @@ contract SymbioticSetupFixture is Test { MetadataService operatorMetadataService_ = new MetadataService(address(operatorRegistry_)); MetadataService networkMetadataService_ = new MetadataService(address(networkRegistry_)); NetworkMiddlewareService networkMiddlewareService_ = new NetworkMiddlewareService(address(networkRegistry_)); - OptInService operatorVaultOptInService_ = new OptInService(address(operatorRegistry_), address(vaultFactory_)); + OptInService operatorVaultOptInService_ = + new OptInService(address(operatorRegistry_), address(vaultFactory_), "vaultOptIn"); OptInService operatorNetworkOptInService_ = - new OptInService(address(operatorRegistry_), address(networkRegistry_)); + new OptInService(address(operatorRegistry_), address(networkRegistry_), "networkOptIn"); Vault vault_ = new Vault(address(delegatorFactory_), address(slasherFactory_), address(vaultFactory_)); vaultFactory_.whitelist(address(vault_)); @@ -113,7 +113,6 @@ contract SymbioticSetupFixture is Test { slasherFactory_.transferOwnership(owner); Token token_ = new Token("Token"); - SimpleCollateral collateral_ = new SimpleCollateral(address(token_)); vm.stopPrank(); @@ -129,7 +128,7 @@ contract SymbioticSetupFixture is Test { operatorVaultOptInService_, operatorNetworkOptInService_, vaultConfigurator_, - collateral_ + token_ ); } }